From 03746a67419d69bb5400017b0cbdddf3f62ee88a Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 17 Apr 2026 16:02:41 -0700 Subject: [PATCH 1/4] Add Claude Opus 4.5 to built-in Claude models (#2143) --- .../src/provider/Layers/ClaudeProvider.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/apps/server/src/provider/Layers/ClaudeProvider.ts b/apps/server/src/provider/Layers/ClaudeProvider.ts index 3a2c4f97638..bb5e74865b1 100644 --- a/apps/server/src/provider/Layers/ClaudeProvider.ts +++ b/apps/server/src/provider/Layers/ClaudeProvider.ts @@ -87,6 +87,23 @@ const BUILT_IN_MODELS: ReadonlyArray = [ promptInjectedEffortLevels: ["ultrathink"], } satisfies ModelCapabilities, }, + { + slug: "claude-opus-4-5", + name: "Opus 4.5", + isCustom: false, + capabilities: { + reasoningEffortLevels: [ + { value: "low", label: "Low" }, + { value: "medium", label: "Medium" }, + { value: "high", label: "High", isDefault: true }, + { value: "max", label: "Max" }, + ], + supportsFastMode: true, + supportsThinkingToggle: false, + contextWindowOptions: [], + promptInjectedEffortLevels: [], + } satisfies ModelCapabilities, + }, { slug: "claude-sonnet-4-6", name: "Sonnet 4.6", From af03160c40c6db2c79054f58860d4ca899aeec82 Mon Sep 17 00:00:00 2001 From: Utkarsh Patil <73941998+UtkarshUsername@users.noreply.github.com> Date: Tue, 14 Apr 2026 00:56:41 +0530 Subject: [PATCH 2/4] feat: show full thread title in a tooltip when hovering sidebar thread names (#1994) --- apps/web/src/components/ChatView.browser.tsx | 31 +++++++++++++++++++- apps/web/src/components/Sidebar.tsx | 16 +++++++++- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/ChatView.browser.tsx b/apps/web/src/components/ChatView.browser.tsx index 47069777ce2..423a7026969 100644 --- a/apps/web/src/components/ChatView.browser.tsx +++ b/apps/web/src/components/ChatView.browser.tsx @@ -59,6 +59,7 @@ vi.mock("../lib/gitStatusState", () => ({ })); const THREAD_ID = "thread-browser-test" as ThreadId; +const THREAD_TITLE = "Browser test thread"; const ARCHIVED_SECONDARY_THREAD_ID = "thread-secondary-project-archived" as ThreadId; const PROJECT_ID = "project-1" as ProjectId; const SECOND_PROJECT_ID = "project-2" as ProjectId; @@ -320,7 +321,7 @@ function createSnapshotForTargetUser(options: { { id: THREAD_ID, projectId: PROJECT_ID, - title: "Browser test thread", + title: THREAD_TITLE, modelSelection: { provider: "codex", model: "gpt-5", @@ -3304,6 +3305,34 @@ describe("ChatView timeline estimator parity (full app)", () => { } }); + it("exposes the full thread title on the sidebar row tooltip", async () => { + const mounted = await mountChatView({ + viewport: DEFAULT_VIEWPORT, + snapshot: createSnapshotForTargetUser({ + targetMessageId: "msg-user-thread-tooltip-target" as MessageId, + targetText: "thread tooltip target", + }), + }); + + try { + const threadTitle = page.getByTestId(`thread-title-${THREAD_ID}`); + + await expect.element(threadTitle).toBeInTheDocument(); + await threadTitle.hover(); + + await vi.waitFor( + () => { + const tooltip = document.querySelector('[data-slot="tooltip-popup"]'); + expect(tooltip).not.toBeNull(); + expect(tooltip?.textContent).toContain(THREAD_TITLE); + }, + { timeout: 8_000, interval: 16 }, + ); + } finally { + await mounted.cleanup(); + } + }); + it("shows the confirm archive action after clicking the archive button", async () => { localStorage.setItem( "marcode:client-settings:v1", diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index fd872819d5a..9f35b2ec332 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -594,7 +594,21 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThreadRowP onClick={handleRenameInputClick} /> ) : ( - {thread.title} + + + {thread.title} + + } + /> + + {thread.title} + + )}
From 08a37d0a73efda02ad043ef7f5fdd018841e56b8 Mon Sep 17 00:00:00 2001 From: Andrei <12261380+akarabach@users.noreply.github.com> Date: Wed, 15 Apr 2026 23:26:36 +0200 Subject: [PATCH 3/4] feat: add Launch Args setting for Claude provider (#1971) Co-authored-by: Julius Marminge --- .../src/provider/Layers/ClaudeAdapter.ts | 3 + apps/server/src/serverSettings.test.ts | 2 + .../components/KeybindingsToast.browser.tsx | 2 +- .../components/settings/SettingsPanels.tsx | 34 ++++- packages/contracts/src/settings.ts | 2 + packages/shared/package.json | 4 + packages/shared/src/cliArgs.test.ts | 134 ++++++++++++++++++ packages/shared/src/cliArgs.ts | 76 ++++++++++ .../update-release-package-versions.test.ts | 71 ++++++++++ scripts/update-release-package-versions.ts | 51 +++---- 10 files changed, 345 insertions(+), 34 deletions(-) create mode 100644 packages/shared/src/cliArgs.test.ts create mode 100644 packages/shared/src/cliArgs.ts create mode 100644 scripts/update-release-package-versions.test.ts diff --git a/apps/server/src/provider/Layers/ClaudeAdapter.ts b/apps/server/src/provider/Layers/ClaudeAdapter.ts index a4d9ef9a402..5cc704d0f1a 100644 --- a/apps/server/src/provider/Layers/ClaudeAdapter.ts +++ b/apps/server/src/provider/Layers/ClaudeAdapter.ts @@ -19,6 +19,7 @@ import { type SDKUserMessage, ModelUsage, } from "@anthropic-ai/claude-agent-sdk"; +import { parseCliArgs } from "@marcode/shared/cliArgs"; import { ApprovalRequestId, type CanonicalItemType, @@ -2992,6 +2993,7 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( ), ); const claudeBinaryPath = claudeSettings.binaryPath; + const extraArgs = parseCliArgs(claudeSettings.launchArgs).flags; const modelSelection = input.modelSelection?.provider === "claudeAgent" ? input.modelSelection : undefined; const caps = getClaudeModelCapabilities(modelSelection?.model); @@ -3040,6 +3042,7 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( ...(computedAdditionalDirs.length > 0 ? { additionalDirectories: computedAdditionalDirs } : {}), + ...(Object.keys(extraArgs).length > 0 ? { extraArgs } : {}), }; const queryRuntime = yield* Effect.try({ diff --git a/apps/server/src/serverSettings.test.ts b/apps/server/src/serverSettings.test.ts index 3d4a997cbc4..3e5cff1e521 100644 --- a/apps/server/src/serverSettings.test.ts +++ b/apps/server/src/serverSettings.test.ts @@ -92,6 +92,7 @@ it.layer(NodeServices.layer)("server settings", (it) => { enabled: true, binaryPath: "/usr/local/bin/claude", customModels: ["claude-custom"], + launchArgs: "", }); assert.deepEqual(next.textGenerationModelSelection, { provider: "codex", @@ -167,6 +168,7 @@ it.layer(NodeServices.layer)("server settings", (it) => { enabled: true, binaryPath: "/opt/homebrew/bin/claude", customModels: [], + launchArgs: "", }); }).pipe(Effect.provide(makeServerSettingsLayer())), ); diff --git a/apps/web/src/components/KeybindingsToast.browser.tsx b/apps/web/src/components/KeybindingsToast.browser.tsx index 327fb4fbe86..6925a699e6c 100644 --- a/apps/web/src/components/KeybindingsToast.browser.tsx +++ b/apps/web/src/components/KeybindingsToast.browser.tsx @@ -98,7 +98,7 @@ function createBaseServerConfig(): ServerConfig { textGenerationModelSelection: { provider: "codex" as const, model: "gpt-5.4-mini" }, providers: { codex: { enabled: true, binaryPath: "", homePath: "", customModels: [] }, - claudeAgent: { enabled: true, binaryPath: "", customModels: [] }, + claudeAgent: { enabled: true, binaryPath: "", customModels: [], launchArgs: "" }, }, }, }; diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx index 789ed7c76d0..b8a98a27899 100644 --- a/apps/web/src/components/settings/SettingsPanels.tsx +++ b/apps/web/src/components/settings/SettingsPanels.tsx @@ -939,7 +939,8 @@ export function GeneralSettingsPanel() { claudeAgent: Boolean( settings.providers.claudeAgent.binaryPath !== DEFAULT_UNIFIED_SETTINGS.providers.claudeAgent.binaryPath || - settings.providers.claudeAgent.customModels.length > 0, + settings.providers.claudeAgent.customModels.length > 0 || + settings.providers.claudeAgent.launchArgs !== "", ), }); const [customModelInputByProvider, setCustomModelInputByProvider] = useState< @@ -1692,6 +1693,37 @@ export function GeneralSettingsPanel() {
) : null} + {providerCard.provider === "claudeAgent" ? ( +
+ +
+ ) : null} +
Models
diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index 8422ec930de..07027cbe9e8 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -139,6 +139,7 @@ export const ClaudeSettings = Schema.Struct({ enabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), binaryPath: makeBinaryPathSetting("claude"), customModels: Schema.Array(Schema.String).pipe(Schema.withDecodingDefault(Effect.succeed([]))), + launchArgs: Schema.String.pipe(Schema.withDecodingDefault(Effect.succeed(""))), }); export type ClaudeSettings = typeof ClaudeSettings.Type; @@ -233,6 +234,7 @@ const ClaudeSettingsPatch = Schema.Struct({ enabled: Schema.optionalKey(Schema.Boolean), binaryPath: Schema.optionalKey(Schema.String), customModels: Schema.optionalKey(Schema.Array(Schema.String)), + launchArgs: Schema.optionalKey(Schema.String), }); export const ServerSettingsPatch = Schema.Struct({ diff --git a/packages/shared/package.json b/packages/shared/package.json index e716a574337..7863dca2cc6 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -63,6 +63,10 @@ "./path": { "types": "./src/path.ts", "import": "./src/path.ts" + }, + "./cliArgs": { + "types": "./src/cliArgs.ts", + "import": "./src/cliArgs.ts" } }, "scripts": { diff --git a/packages/shared/src/cliArgs.test.ts b/packages/shared/src/cliArgs.test.ts new file mode 100644 index 00000000000..02c0b48805b --- /dev/null +++ b/packages/shared/src/cliArgs.test.ts @@ -0,0 +1,134 @@ +import { describe, expect, it } from "vitest"; + +import { parseCliArgs } from "./cliArgs"; + +describe("parseCliArgs", () => { + it("returns empty result for empty string", () => { + expect(parseCliArgs("")).toEqual({ flags: {}, positionals: [] }); + }); + + it("returns empty result for whitespace-only string", () => { + expect(parseCliArgs(" ")).toEqual({ flags: {}, positionals: [] }); + }); + + it("returns empty result for empty array", () => { + expect(parseCliArgs([])).toEqual({ flags: {}, positionals: [] }); + }); + + it("parses --chrome boolean flag", () => { + expect(parseCliArgs("--chrome")).toEqual({ + flags: { chrome: null }, + positionals: [], + }); + }); + + it("parses --chrome with --verbose", () => { + expect(parseCliArgs("--chrome --verbose")).toEqual({ + flags: { chrome: null, verbose: null }, + positionals: [], + }); + }); + + it("parses --effort with a value", () => { + expect(parseCliArgs("--effort high")).toEqual({ + flags: { effort: "high" }, + positionals: [], + }); + }); + + it("parses --chrome --effort high --debug", () => { + expect(parseCliArgs("--chrome --effort high --debug")).toEqual({ + flags: { chrome: null, effort: "high", debug: null }, + positionals: [], + }); + }); + + it("parses --model with full model name", () => { + expect(parseCliArgs("--model claude-sonnet-4-6")).toEqual({ + flags: { model: "claude-sonnet-4-6" }, + positionals: [], + }); + }); + + it("parses --append-system-prompt with value and --chrome", () => { + expect(parseCliArgs("--append-system-prompt always-think-step-by-step --chrome")).toEqual({ + flags: { "append-system-prompt": "always-think-step-by-step", chrome: null }, + positionals: [], + }); + }); + + it("parses --max-budget-usd with numeric value", () => { + expect(parseCliArgs("--chrome --max-budget-usd 5.00")).toEqual({ + flags: { chrome: null, "max-budget-usd": "5.00" }, + positionals: [], + }); + }); + + it("parses --effort=high syntax", () => { + expect(parseCliArgs("--effort=high")).toEqual({ + flags: { effort: "high" }, + positionals: [], + }); + }); + + it("parses --key=value mixed with boolean flags", () => { + expect(parseCliArgs("--chrome --model=claude-sonnet-4-6 --debug")).toEqual({ + flags: { chrome: null, model: "claude-sonnet-4-6", debug: null }, + positionals: [], + }); + }); + + it("collects positional arguments", () => { + expect(parseCliArgs("1.2.3")).toEqual({ + flags: {}, + positionals: ["1.2.3"], + }); + }); + + it("collects positionals mixed with flags (argv array)", () => { + expect(parseCliArgs(["1.2.3", "--root", "/path", "--github-output"])).toEqual({ + flags: { root: "/path", "github-output": null }, + positionals: ["1.2.3"], + }); + }); + + it("handles extra whitespace between tokens", () => { + expect(parseCliArgs(" --chrome --verbose ")).toEqual({ + flags: { chrome: null, verbose: null }, + positionals: [], + }); + }); + + it("ignores bare -- with no flag name", () => { + expect(parseCliArgs("--")).toEqual({ flags: {}, positionals: [] }); + }); + + it("boolean flag does not consume next token as value", () => { + expect(parseCliArgs(["--github-output", "1.2.3"], { booleanFlags: ["github-output"] })).toEqual( + { + flags: { "github-output": null }, + positionals: ["1.2.3"], + }, + ); + }); + + it("non-boolean flag still consumes next token", () => { + expect(parseCliArgs(["--root", "/path", "1.2.3"], { booleanFlags: ["github-output"] })).toEqual( + { + flags: { root: "/path" }, + positionals: ["1.2.3"], + }, + ); + }); + + it("mixes boolean and value flags with positionals", () => { + expect( + parseCliArgs(["--github-output", "--root", "/path", "1.2.3"], { + booleanFlags: ["github-output"], + }), + ).toEqual({ + flags: { "github-output": null, root: "/path" }, + positionals: ["1.2.3"], + }); + }); +}); diff --git a/packages/shared/src/cliArgs.ts b/packages/shared/src/cliArgs.ts new file mode 100644 index 00000000000..20920093302 --- /dev/null +++ b/packages/shared/src/cliArgs.ts @@ -0,0 +1,76 @@ +export interface ParsedCliArgs { + readonly flags: Record; + readonly positionals: string[]; +} + +export interface ParseCliArgsOptions { + readonly booleanFlags?: readonly string[]; +} + +/** + * Parse CLI-style arguments into flags and positionals. + * + * Accepts a string (split by whitespace) or a pre-split argv array. + * Supports `--key value`, `--key=value`, and `--flag` (boolean) syntax. + * + * parseCliArgs("") + * → { flags: {}, positionals: [] } + * + * parseCliArgs("--chrome") + * → { flags: { chrome: null }, positionals: [] } + * + * parseCliArgs("--chrome --effort high") + * → { flags: { chrome: null, effort: "high" }, positionals: [] } + * + * parseCliArgs("--effort=high") + * → { flags: { effort: "high" }, positionals: [] } + * + * parseCliArgs(["1.2.3", "--root", "/path", "--github-output"], { booleanFlags: ["github-output"] }) + * → { flags: { root: "/path", "github-output": null }, positionals: ["1.2.3"] } + */ +export function parseCliArgs( + args: string | readonly string[], + options?: ParseCliArgsOptions, +): ParsedCliArgs { + const tokens = + typeof args === "string" ? args.trim().split(/\s+/).filter(Boolean) : Array.from(args); + const booleanSet = options?.booleanFlags ? new Set(options.booleanFlags) : undefined; + + const flags: Record = {}; + const positionals: string[] = []; + + for (let i = 0; i < tokens.length; i++) { + const token = tokens[i]!; + + if (token.startsWith("--")) { + const rest = token.slice(2); + if (!rest) continue; + + // Handle --key=value syntax + const eqIndex = rest.indexOf("="); + if (eqIndex !== -1) { + flags[rest.slice(0, eqIndex)] = rest.slice(eqIndex + 1); + continue; + } + + // Known boolean flag — never consumes next token + if (booleanSet?.has(rest)) { + flags[rest] = null; + continue; + } + + // Handle --key value or --flag (boolean) + const next = tokens[i + 1]; + if (next !== undefined && !next.startsWith("--")) { + flags[rest] = next; + i++; + } else { + flags[rest] = null; + } + } else { + positionals.push(token); + } + } + + return { flags, positionals }; +} diff --git a/scripts/update-release-package-versions.test.ts b/scripts/update-release-package-versions.test.ts new file mode 100644 index 00000000000..9e31c7675b3 --- /dev/null +++ b/scripts/update-release-package-versions.test.ts @@ -0,0 +1,71 @@ +import { describe, expect, it } from "vitest"; + +import { parseArgs } from "./update-release-package-versions.ts"; + +describe("parseArgs", () => { + it("parses version only", () => { + expect(parseArgs(["1.2.3"])).toEqual({ + version: "1.2.3", + rootDir: undefined, + writeGithubOutput: false, + }); + }); + + it("parses version with --root", () => { + expect(parseArgs(["1.2.3", "--root", "/path"])).toEqual({ + version: "1.2.3", + rootDir: "/path", + writeGithubOutput: false, + }); + }); + + it("parses version with --github-output", () => { + expect(parseArgs(["1.2.3", "--github-output"])).toEqual({ + version: "1.2.3", + rootDir: undefined, + writeGithubOutput: true, + }); + }); + + it("parses version with --root and --github-output", () => { + expect(parseArgs(["1.2.3", "--root", "/path", "--github-output"])).toEqual({ + version: "1.2.3", + rootDir: "/path", + writeGithubOutput: true, + }); + }); + + it("accepts flags before the version positional", () => { + expect(parseArgs(["--github-output", "--root", "/path", "1.2.3"])).toEqual({ + version: "1.2.3", + rootDir: "/path", + writeGithubOutput: true, + }); + }); + + it("throws on missing version", () => { + expect(() => parseArgs([])).toThrow("Usage:"); + }); + + it("throws on duplicate version", () => { + expect(() => parseArgs(["1.2.3", "2.0.0"])).toThrow( + "Only one release version can be provided.", + ); + }); + + it("throws on unknown flag", () => { + expect(() => parseArgs(["1.2.3", "--unknown"])).toThrow("Unknown argument: --unknown"); + }); + + it("throws on --root without value", () => { + expect(() => parseArgs(["1.2.3", "--root"])).toThrow("Missing value for --root."); + }); + + it("does not consume version as --github-output value", () => { + expect(parseArgs(["--github-output", "1.2.3"])).toEqual({ + version: "1.2.3", + rootDir: undefined, + writeGithubOutput: true, + }); + }); +}); diff --git a/scripts/update-release-package-versions.ts b/scripts/update-release-package-versions.ts index b860b85e8e8..20214c20ac6 100644 --- a/scripts/update-release-package-versions.ts +++ b/scripts/update-release-package-versions.ts @@ -2,6 +2,8 @@ import { appendFileSync, readFileSync, writeFileSync } from "node:fs"; import { resolve } from "node:path"; import { fileURLToPath } from "node:url"; +import { parseCliArgs } from "@marcode/shared/cliArgs"; + export const releasePackageFiles = [ "apps/server/package.json", "apps/desktop/package.json", @@ -40,52 +42,37 @@ export function updateReleasePackageVersions( return { changed }; } -function parseArgs(argv: ReadonlyArray): { +export function parseArgs(argv: ReadonlyArray): { version: string; rootDir: string | undefined; writeGithubOutput: boolean; } { - let version: string | undefined; - let rootDir: string | undefined; - let writeGithubOutput = false; - - for (let index = 0; index < argv.length; index += 1) { - const argument = argv[index]; - if (argument === undefined) { - continue; - } - - if (argument === "--github-output") { - writeGithubOutput = true; - continue; - } + const { flags, positionals } = parseCliArgs(argv, { booleanFlags: ["github-output"] }); - if (argument === "--root") { - rootDir = argv[index + 1]; - if (!rootDir) { - throw new Error("Missing value for --root."); - } - index += 1; - continue; - } + const unknownFlags = Object.keys(flags).filter((k) => k !== "github-output" && k !== "root"); + if (unknownFlags.length > 0) { + throw new Error(`Unknown argument: --${unknownFlags[0]}`); + } - if (argument.startsWith("--")) { - throw new Error(`Unknown argument: ${argument}`); - } + if ("root" in flags && flags.root === null) { + throw new Error("Missing value for --root."); + } - if (version !== undefined) { - throw new Error("Only one release version can be provided."); - } - version = argument; + if (positionals.length > 1) { + throw new Error("Only one release version can be provided."); } - if (!version) { + if (positionals.length !== 1 || !positionals[0]) { throw new Error( "Usage: node scripts/update-release-package-versions.ts [--root ] [--github-output]", ); } - return { version, rootDir, writeGithubOutput }; + return { + version: positionals[0], + rootDir: flags.root ?? undefined, + writeGithubOutput: "github-output" in flags, + }; } const isMain = From a4fd01171e2cf895bd178df366ed38028deeec6f Mon Sep 17 00:00:00 2001 From: tyulyukov Date: Tue, 21 Apr 2026 15:04:28 +0300 Subject: [PATCH 4/4] ci: install deps before release-smoke so workspace imports resolve The Launch Args port added `@marcode/shared/cliArgs` as a workspace import in scripts/update-release-package-versions.ts. The release_smoke job runs that script via `node` without ever installing dependencies, so node's module resolver can't find the @marcode namespace. Match upstream's CI and run `bun install --frozen-lockfile` before the smoke script. Verified locally: `node scripts/release-smoke.ts` now prints "Release smoke checks passed." Co-Authored-By: Claude Opus 4.7 --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e1e139c90db..8edf9eef4fd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -92,5 +92,8 @@ jobs: with: node-version-file: package.json + - name: Install dependencies + run: bun install --frozen-lockfile + - name: Exercise release-only workflow steps run: node scripts/release-smoke.ts