authorgravatar for 12261380+akarabach@users.noreply.github.comAndrei <12261380+akarabach@users.noreply.github.com> 2026-04-15 23:26:36+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-04-15 21:26:36+00:00
log5e1dd56d2c534d90ba3363f97b1e1e23412507c0
tree1724a1a96434558cc5bd400332faa583d2fe2396
parent68061af0420ba6acd9c1a4c2f77eeddfff0e8bd2
signature Signed by PGP key B5690EEEBB952194

feat: add Launch Args setting for Claude provider (#1971)

Co-authored-by: Julius Marminge <julius0216@outlook.com>

10 files changed, 345 insertions(+), 34 deletions(-)

apps/server/src/provider/Layers/ClaudeAdapter.ts+3
...@@ -20,6 +20,7 @@ import {...@@ -20,6 +20,7 @@ import {
20 ModelUsage,20 ModelUsage,
21 NonNullableUsage,21 NonNullableUsage,
22} from "@anthropic-ai/claude-agent-sdk";22} from "@anthropic-ai/claude-agent-sdk";
23import { parseCliArgs } from "@t3tools/shared/cliArgs";
23import {24import {
24 ApprovalRequestId,25 ApprovalRequestId,
25 type CanonicalItemType,26 type CanonicalItemType,
...@@ -2742,6 +2743,7 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (...@@ -2742,6 +2743,7 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
2742 ),2743 ),
2743 );2744 );
2744 const claudeBinaryPath = claudeSettings.binaryPath;2745 const claudeBinaryPath = claudeSettings.binaryPath;
2746 const extraArgs = parseCliArgs(claudeSettings.launchArgs).flags;
2745 const modelSelection =2747 const modelSelection =
2746 input.modelSelection?.provider === "claudeAgent" ? input.modelSelection : undefined;2748 input.modelSelection?.provider === "claudeAgent" ? input.modelSelection : undefined;
2747 const caps = getClaudeModelCapabilities(modelSelection?.model);2749 const caps = getClaudeModelCapabilities(modelSelection?.model);
...@@ -2781,6 +2783,7 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (...@@ -2781,6 +2783,7 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
2781 canUseTool,2783 canUseTool,
2782 env: process.env,2784 env: process.env,
2783 ...(input.cwd ? { additionalDirectories: [input.cwd] } : {}),2785 ...(input.cwd ? { additionalDirectories: [input.cwd] } : {}),
2786 ...(Object.keys(extraArgs).length > 0 ? { extraArgs } : {}),
2784 };2787 };
27852788
2786 const queryRuntime = yield* Effect.try({2789 const queryRuntime = yield* Effect.try({
apps/server/src/serverSettings.test.ts+2
...@@ -92,6 +92,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -92,6 +92,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
92 enabled: true,92 enabled: true,
93 binaryPath: "/usr/local/bin/claude",93 binaryPath: "/usr/local/bin/claude",
94 customModels: ["claude-custom"],94 customModels: ["claude-custom"],
95 launchArgs: "",
95 });96 });
96 assert.deepEqual(next.textGenerationModelSelection, {97 assert.deepEqual(next.textGenerationModelSelection, {
97 provider: "codex",98 provider: "codex",
...@@ -167,6 +168,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -167,6 +168,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
167 enabled: true,168 enabled: true,
168 binaryPath: "/opt/homebrew/bin/claude",169 binaryPath: "/opt/homebrew/bin/claude",
169 customModels: [],170 customModels: [],
171 launchArgs: "",
170 });172 });
171 }).pipe(Effect.provide(makeServerSettingsLayer())),173 }).pipe(Effect.provide(makeServerSettingsLayer())),
172 );174 );
apps/web/src/components/KeybindingsToast.browser.tsx+1-1
...@@ -98,7 +98,7 @@ function createBaseServerConfig(): ServerConfig {...@@ -98,7 +98,7 @@ function createBaseServerConfig(): ServerConfig {
98 textGenerationModelSelection: { provider: "codex" as const, model: "gpt-5.4-mini" },98 textGenerationModelSelection: { provider: "codex" as const, model: "gpt-5.4-mini" },
99 providers: {99 providers: {
100 codex: { enabled: true, binaryPath: "", homePath: "", customModels: [] },100 codex: { enabled: true, binaryPath: "", homePath: "", customModels: [] },
101 claudeAgent: { enabled: true, binaryPath: "", customModels: [] },101 claudeAgent: { enabled: true, binaryPath: "", customModels: [], launchArgs: "" },
102 },102 },
103 },103 },
104 };104 };
apps/web/src/components/settings/SettingsPanels.tsx+33-1
...@@ -511,7 +511,8 @@ export function GeneralSettingsPanel() {...@@ -511,7 +511,8 @@ export function GeneralSettingsPanel() {
511 claudeAgent: Boolean(511 claudeAgent: Boolean(
512 settings.providers.claudeAgent.binaryPath !==512 settings.providers.claudeAgent.binaryPath !==
513 DEFAULT_UNIFIED_SETTINGS.providers.claudeAgent.binaryPath ||513 DEFAULT_UNIFIED_SETTINGS.providers.claudeAgent.binaryPath ||
514 settings.providers.claudeAgent.customModels.length > 0,514 settings.providers.claudeAgent.customModels.length > 0 ||
515 settings.providers.claudeAgent.launchArgs !== "",
515 ),516 ),
516 });517 });
517 const [customModelInputByProvider, setCustomModelInputByProvider] = useState<518 const [customModelInputByProvider, setCustomModelInputByProvider] = useState<
...@@ -1278,6 +1279,37 @@ export function GeneralSettingsPanel() {...@@ -1278,6 +1279,37 @@ export function GeneralSettingsPanel() {
1278 </div>1279 </div>
1279 ) : null}1280 ) : null}
12801281
1282 {providerCard.provider === "claudeAgent" ? (
1283 <div className="border-t border-border/60 px-4 py-3 sm:px-5">
1284 <label htmlFor="provider-install-claudeAgent-launch-args" className="block">
1285 <span className="text-xs font-medium text-foreground">
1286 Launch arguments
1287 </span>
1288 <Input
1289 id="provider-install-claudeAgent-launch-args"
1290 className="mt-1.5"
1291 value={settings.providers.claudeAgent.launchArgs}
1292 onChange={(event) =>
1293 updateSettings({
1294 providers: {
1295 ...settings.providers,
1296 claudeAgent: {
1297 ...settings.providers.claudeAgent,
1298 launchArgs: event.target.value,
1299 },
1300 },
1301 })
1302 }
1303 placeholder="e.g. --chrome"
1304 spellCheck={false}
1305 />
1306 <span className="mt-1 block text-xs text-muted-foreground">
1307 Additional CLI arguments passed to Claude Code on session start.
1308 </span>
1309 </label>
1310 </div>
1311 ) : null}
1312
1281 <div className="border-t border-border/60 px-4 py-3 sm:px-5">1313 <div className="border-t border-border/60 px-4 py-3 sm:px-5">
1282 <div className="text-xs font-medium text-foreground">Models</div>1314 <div className="text-xs font-medium text-foreground">Models</div>
1283 <div className="mt-1 text-xs text-muted-foreground">1315 <div className="mt-1 text-xs text-muted-foreground">
packages/contracts/src/settings.ts+2
...@@ -70,6 +70,7 @@ export const ClaudeSettings = Schema.Struct({...@@ -70,6 +70,7 @@ export const ClaudeSettings = Schema.Struct({
70 enabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),70 enabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),
71 binaryPath: makeBinaryPathSetting("claude"),71 binaryPath: makeBinaryPathSetting("claude"),
72 customModels: Schema.Array(Schema.String).pipe(Schema.withDecodingDefault(Effect.succeed([]))),72 customModels: Schema.Array(Schema.String).pipe(Schema.withDecodingDefault(Effect.succeed([]))),
73 launchArgs: Schema.String.pipe(Schema.withDecodingDefault(Effect.succeed(""))),
73});74});
74export type ClaudeSettings = typeof ClaudeSettings.Type;75export type ClaudeSettings = typeof ClaudeSettings.Type;
7576
...@@ -164,6 +165,7 @@ const ClaudeSettingsPatch = Schema.Struct({...@@ -164,6 +165,7 @@ const ClaudeSettingsPatch = Schema.Struct({
164 enabled: Schema.optionalKey(Schema.Boolean),165 enabled: Schema.optionalKey(Schema.Boolean),
165 binaryPath: Schema.optionalKey(Schema.String),166 binaryPath: Schema.optionalKey(Schema.String),
166 customModels: Schema.optionalKey(Schema.Array(Schema.String)),167 customModels: Schema.optionalKey(Schema.Array(Schema.String)),
168 launchArgs: Schema.optionalKey(Schema.String),
167});169});
168170
169export const ServerSettingsPatch = Schema.Struct({171export const ServerSettingsPatch = Schema.Struct({
packages/shared/package.json+4
...@@ -60,6 +60,10 @@...@@ -60,6 +60,10 @@
60 "types": "./src/qrCode.ts",60 "types": "./src/qrCode.ts",
61 "import": "./src/qrCode.ts"61 "import": "./src/qrCode.ts"
62 },62 },
63 "./cliArgs": {
64 "types": "./src/cliArgs.ts",
65 "import": "./src/cliArgs.ts"
66 },
63 "./path": {67 "./path": {
64 "types": "./src/path.ts",68 "types": "./src/path.ts",
65 "import": "./src/path.ts"69 "import": "./src/path.ts"
packages/shared/src/cliArgs.test.ts created+134
...@@ -0,0 +1,134 @@
1import { describe, expect, it } from "vitest";
2
3import { parseCliArgs } from "./cliArgs";
4
5describe("parseCliArgs", () => {
6 it("returns empty result for empty string", () => {
7 expect(parseCliArgs("")).toEqual({ flags: {}, positionals: [] });
8 });
9
10 it("returns empty result for whitespace-only string", () => {
11 expect(parseCliArgs(" ")).toEqual({ flags: {}, positionals: [] });
12 });
13
14 it("returns empty result for empty array", () => {
15 expect(parseCliArgs([])).toEqual({ flags: {}, positionals: [] });
16 });
17
18 it("parses --chrome boolean flag", () => {
19 expect(parseCliArgs("--chrome")).toEqual({
20 flags: { chrome: null },
21 positionals: [],
22 });
23 });
24
25 it("parses --chrome with --verbose", () => {
26 expect(parseCliArgs("--chrome --verbose")).toEqual({
27 flags: { chrome: null, verbose: null },
28 positionals: [],
29 });
30 });
31
32 it("parses --effort with a value", () => {
33 expect(parseCliArgs("--effort high")).toEqual({
34 flags: { effort: "high" },
35 positionals: [],
36 });
37 });
38
39 it("parses --chrome --effort high --debug", () => {
40 expect(parseCliArgs("--chrome --effort high --debug")).toEqual({
41 flags: { chrome: null, effort: "high", debug: null },
42 positionals: [],
43 });
44 });
45
46 it("parses --model with full model name", () => {
47 expect(parseCliArgs("--model claude-sonnet-4-6")).toEqual({
48 flags: { model: "claude-sonnet-4-6" },
49 positionals: [],
50 });
51 });
52
53 it("parses --append-system-prompt with value and --chrome", () => {
54 expect(parseCliArgs("--append-system-prompt always-think-step-by-step --chrome")).toEqual({
55 flags: { "append-system-prompt": "always-think-step-by-step", chrome: null },
56 positionals: [],
57 });
58 });
59
60 it("parses --max-budget-usd with numeric value", () => {
61 expect(parseCliArgs("--chrome --max-budget-usd 5.00")).toEqual({
62 flags: { chrome: null, "max-budget-usd": "5.00" },
63 positionals: [],
64 });
65 });
66
67 it("parses --effort=high syntax", () => {
68 expect(parseCliArgs("--effort=high")).toEqual({
69 flags: { effort: "high" },
70 positionals: [],
71 });
72 });
73
74 it("parses --key=value mixed with boolean flags", () => {
75 expect(parseCliArgs("--chrome --model=claude-sonnet-4-6 --debug")).toEqual({
76 flags: { chrome: null, model: "claude-sonnet-4-6", debug: null },
77 positionals: [],
78 });
79 });
80
81 it("collects positional arguments", () => {
82 expect(parseCliArgs("1.2.3")).toEqual({
83 flags: {},
84 positionals: ["1.2.3"],
85 });
86 });
87
88 it("collects positionals mixed with flags (argv array)", () => {
89 expect(parseCliArgs(["1.2.3", "--root", "/path", "--github-output"])).toEqual({
90 flags: { root: "/path", "github-output": null },
91 positionals: ["1.2.3"],
92 });
93 });
94
95 it("handles extra whitespace between tokens", () => {
96 expect(parseCliArgs(" --chrome --verbose ")).toEqual({
97 flags: { chrome: null, verbose: null },
98 positionals: [],
99 });
100 });
101
102 it("ignores bare -- with no flag name", () => {
103 expect(parseCliArgs("--")).toEqual({ flags: {}, positionals: [] });
104 });
105
106 it("boolean flag does not consume next token as value", () => {
107 expect(parseCliArgs(["--github-output", "1.2.3"], { booleanFlags: ["github-output"] })).toEqual(
108 {
109 flags: { "github-output": null },
110 positionals: ["1.2.3"],
111 },
112 );
113 });
114
115 it("non-boolean flag still consumes next token", () => {
116 expect(parseCliArgs(["--root", "/path", "1.2.3"], { booleanFlags: ["github-output"] })).toEqual(
117 {
118 flags: { root: "/path" },
119 positionals: ["1.2.3"],
120 },
121 );
122 });
123
124 it("mixes boolean and value flags with positionals", () => {
125 expect(
126 parseCliArgs(["--github-output", "--root", "/path", "1.2.3"], {
127 booleanFlags: ["github-output"],
128 }),
129 ).toEqual({
130 flags: { "github-output": null, root: "/path" },
131 positionals: ["1.2.3"],
132 });
133 });
134});
packages/shared/src/cliArgs.ts created+76
...@@ -0,0 +1,76 @@
1export interface ParsedCliArgs {
2 readonly flags: Record<string, string | null>;
3 readonly positionals: string[];
4}
5
6export interface ParseCliArgsOptions {
7 readonly booleanFlags?: readonly string[];
8}
9
10/**
11 * Parse CLI-style arguments into flags and positionals.
12 *
13 * Accepts a string (split by whitespace) or a pre-split argv array.
14 * Supports `--key value`, `--key=value`, and `--flag` (boolean) syntax.
15 *
16 * parseCliArgs("")
17 * → { flags: {}, positionals: [] }
18 *
19 * parseCliArgs("--chrome")
20 * → { flags: { chrome: null }, positionals: [] }
21 *
22 * parseCliArgs("--chrome --effort high")
23 * → { flags: { chrome: null, effort: "high" }, positionals: [] }
24 *
25 * parseCliArgs("--effort=high")
26 * → { flags: { effort: "high" }, positionals: [] }
27 *
28 * parseCliArgs(["1.2.3", "--root", "/path", "--github-output"], { booleanFlags: ["github-output"] })
29 * → { flags: { root: "/path", "github-output": null }, positionals: ["1.2.3"] }
30 */
31export function parseCliArgs(
32 args: string | readonly string[],
33 options?: ParseCliArgsOptions,
34): ParsedCliArgs {
35 const tokens =
36 typeof args === "string" ? args.trim().split(/\s+/).filter(Boolean) : Array.from(args);
37 const booleanSet = options?.booleanFlags ? new Set(options.booleanFlags) : undefined;
38
39 const flags: Record<string, string | null> = {};
40 const positionals: string[] = [];
41
42 for (let i = 0; i < tokens.length; i++) {
43 const token = tokens[i]!;
44
45 if (token.startsWith("--")) {
46 const rest = token.slice(2);
47 if (!rest) continue;
48
49 // Handle --key=value syntax
50 const eqIndex = rest.indexOf("=");
51 if (eqIndex !== -1) {
52 flags[rest.slice(0, eqIndex)] = rest.slice(eqIndex + 1);
53 continue;
54 }
55
56 // Known boolean flag — never consumes next token
57 if (booleanSet?.has(rest)) {
58 flags[rest] = null;
59 continue;
60 }
61
62 // Handle --key value or --flag (boolean)
63 const next = tokens[i + 1];
64 if (next !== undefined && !next.startsWith("--")) {
65 flags[rest] = next;
66 i++;
67 } else {
68 flags[rest] = null;
69 }
70 } else {
71 positionals.push(token);
72 }
73 }
74
75 return { flags, positionals };
76}
scripts/update-release-package-versions.test.ts created+71
...@@ -0,0 +1,71 @@
1import { describe, expect, it } from "vitest";
2
3import { parseArgs } from "./update-release-package-versions.ts";
4
5describe("parseArgs", () => {
6 it("parses version only", () => {
7 expect(parseArgs(["1.2.3"])).toEqual({
8 version: "1.2.3",
9 rootDir: undefined,
10 writeGithubOutput: false,
11 });
12 });
13
14 it("parses version with --root", () => {
15 expect(parseArgs(["1.2.3", "--root", "/path"])).toEqual({
16 version: "1.2.3",
17 rootDir: "/path",
18 writeGithubOutput: false,
19 });
20 });
21
22 it("parses version with --github-output", () => {
23 expect(parseArgs(["1.2.3", "--github-output"])).toEqual({
24 version: "1.2.3",
25 rootDir: undefined,
26 writeGithubOutput: true,
27 });
28 });
29
30 it("parses version with --root and --github-output", () => {
31 expect(parseArgs(["1.2.3", "--root", "/path", "--github-output"])).toEqual({
32 version: "1.2.3",
33 rootDir: "/path",
34 writeGithubOutput: true,
35 });
36 });
37
38 it("accepts flags before the version positional", () => {
39 expect(parseArgs(["--github-output", "--root", "/path", "1.2.3"])).toEqual({
40 version: "1.2.3",
41 rootDir: "/path",
42 writeGithubOutput: true,
43 });
44 });
45
46 it("throws on missing version", () => {
47 expect(() => parseArgs([])).toThrow("Usage:");
48 });
49
50 it("throws on duplicate version", () => {
51 expect(() => parseArgs(["1.2.3", "2.0.0"])).toThrow(
52 "Only one release version can be provided.",
53 );
54 });
55
56 it("throws on unknown flag", () => {
57 expect(() => parseArgs(["1.2.3", "--unknown"])).toThrow("Unknown argument: --unknown");
58 });
59
60 it("throws on --root without value", () => {
61 expect(() => parseArgs(["1.2.3", "--root"])).toThrow("Missing value for --root.");
62 });
63
64 it("does not consume version as --github-output value", () => {
65 expect(parseArgs(["--github-output", "1.2.3"])).toEqual({
66 version: "1.2.3",
67 rootDir: undefined,
68 writeGithubOutput: true,
69 });
70 });
71});
scripts/update-release-package-versions.ts+19-32
...@@ -2,6 +2,8 @@ import { appendFileSync, readFileSync, writeFileSync } from "node:fs";...@@ -2,6 +2,8 @@ import { appendFileSync, readFileSync, writeFileSync } from "node:fs";
2import { resolve } from "node:path";2import { resolve } from "node:path";
3import { fileURLToPath } from "node:url";3import { fileURLToPath } from "node:url";
44
5import { parseCliArgs } from "@t3tools/shared/cliArgs";
6
5export const releasePackageFiles = [7export const releasePackageFiles = [
6 "apps/server/package.json",8 "apps/server/package.json",
7 "apps/desktop/package.json",9 "apps/desktop/package.json",
...@@ -40,52 +42,37 @@ export function updateReleasePackageVersions(...@@ -40,52 +42,37 @@ export function updateReleasePackageVersions(
40 return { changed };42 return { changed };
41}43}
4244
43function parseArgs(argv: ReadonlyArray<string>): {45export function parseArgs(argv: ReadonlyArray<string>): {
44 version: string;46 version: string;
45 rootDir: string | undefined;47 rootDir: string | undefined;
46 writeGithubOutput: boolean;48 writeGithubOutput: boolean;
47} {49} {
48 let version: string | undefined;50 const { flags, positionals } = parseCliArgs(argv, { booleanFlags: ["github-output"] });
49 let rootDir: string | undefined;
50 let writeGithubOutput = false;
51
52 for (let index = 0; index < argv.length; index += 1) {
53 const argument = argv[index];
54 if (argument === undefined) {
55 continue;
56 }
57
58 if (argument === "--github-output") {
59 writeGithubOutput = true;
60 continue;
61 }
6251
63 if (argument === "--root") {52 const unknownFlags = Object.keys(flags).filter((k) => k !== "github-output" && k !== "root");
64 rootDir = argv[index + 1];53 if (unknownFlags.length > 0) {
65 if (!rootDir) {54 throw new Error(`Unknown argument: --${unknownFlags[0]}`);
66 throw new Error("Missing value for --root.");55 }
67 }
68 index += 1;
69 continue;
70 }
7156
72 if (argument.startsWith("--")) {57 if ("root" in flags && flags.root === null) {
73 throw new Error(`Unknown argument: ${argument}`);58 throw new Error("Missing value for --root.");
74 }59 }
7560
76 if (version !== undefined) {61 if (positionals.length > 1) {
77 throw new Error("Only one release version can be provided.");62 throw new Error("Only one release version can be provided.");
78 }
79 version = argument;
80 }63 }
8164
82 if (!version) {65 if (positionals.length !== 1 || !positionals[0]) {
83 throw new Error(66 throw new Error(
84 "Usage: node scripts/update-release-package-versions.ts <version> [--root <path>] [--github-output]",67 "Usage: node scripts/update-release-package-versions.ts <version> [--root <path>] [--github-output]",
85 );68 );
86 }69 }
8770
88 return { version, rootDir, writeGithubOutput };71 return {
72 version: positionals[0],
73 rootDir: flags.root ?? undefined,
74 writeGithubOutput: "github-output" in flags,
75 };
89}76}
9077
91const isMain =78const isMain =