From 64015cecfdf65e531ec2cc05175f8c0dea0e9fc2 Mon Sep 17 00:00:00 2001 From: su-fen <715041@qq.com> Date: Wed, 24 Jun 2026 18:42:42 +0800 Subject: [PATCH 1/2] fix(tools): stabilize Write tool argument handling --- .../src/lib/chat/runner/agentRunner.ts | 10 +- crates/agent-gui/src/lib/tools/fsTools.ts | 164 +++++++--- .../test/chat/markdown-image-policy.test.mjs | 14 + .../test/tools/path-and-system-tools.test.mjs | 301 +++++++++++++++++- 4 files changed, 436 insertions(+), 53 deletions(-) diff --git a/crates/agent-gui/src/lib/chat/runner/agentRunner.ts b/crates/agent-gui/src/lib/chat/runner/agentRunner.ts index 963e7861..ac19edfc 100644 --- a/crates/agent-gui/src/lib/chat/runner/agentRunner.ts +++ b/crates/agent-gui/src/lib/chat/runner/agentRunner.ts @@ -198,7 +198,8 @@ export function buildToolsSuffix( } if (canWrite) { lines.push( - "- Read a file at least once before Write or Edit. If the file changes after that Read, the next Write/Edit is rejected — Read it again, then retry.", + "- Existing files: Read the full file before Write or Edit; stale writes are rejected, so re-Read if needed.", + "- New files: call Write with a file path that includes the filename and the full content; parent directories are created automatically.", ); } if (has("Read")) { @@ -215,7 +216,7 @@ export function buildToolsSuffix( } if (has("Write")) { lines.push( - "- Write creates a new file or fully overwrites an existing one. There is no append mode — to add content, Read first, then either Write the full new content or use Edit to insert.", + "- Write fully creates or overwrites one text file. Do not set `mode`. The path must include the intended filename, not just a directory.", ); } if (has("Edit")) { @@ -239,6 +240,11 @@ export function buildToolsSuffix( if (has("Bash")) { const alts: string[] = []; if (hasReadFamily) alts.push("read / list / search via `cat`, `ls`, `find`, `grep`, or `rg`"); + if (has("Write")) { + alts.push( + "write / create files via heredocs, `tee`, `touch`, `cp`, or `mkdir` just to prepare a parent directory", + ); + } if (has("Delete")) alts.push("delete via `rm`, `rmdir`, `unlink`, or `find -delete`"); if (alts.length > 0) { lines.push( diff --git a/crates/agent-gui/src/lib/tools/fsTools.ts b/crates/agent-gui/src/lib/tools/fsTools.ts index 1714afaa..d803e79d 100644 --- a/crates/agent-gui/src/lib/tools/fsTools.ts +++ b/crates/agent-gui/src/lib/tools/fsTools.ts @@ -188,6 +188,11 @@ function previewSnippet(input: string, maxChars = 500) { return `${text.slice(0, maxChars)}\n...(${text.length} chars total)...`; } +function childDisplayPath(parent: string, fileName: string) { + const base = parent && parent !== "." ? parent.replace(/\/+$/g, "") : ""; + return base ? `${base}/${fileName}` : fileName; +} + function formatLineWindow(startLine: number, numLines: number, totalLines: number) { if (totalLines === 0 || numLines === 0) return "empty"; const endLine = startLine + numLines - 1; @@ -304,6 +309,53 @@ export function createFsTools(params: { return `Write requires a full-file Read first for existing files: ${formatResolvedTarget(resolved)}. Retry with Read using the same path before rewriting. Do not use Bash for workspace or Skill file operations.`; } + function buildWriteDirectoryPathMessage(resolved: ResolvedPath) { + const directoryPath = formatResolvedTarget(resolved); + const examplePath = childDisplayPath(directoryPath, "file.txt"); + return [ + `Write.path points to a directory, not a file: ${directoryPath}.`, + `Retry Write with the intended filename appended to that directory, for example path="${examplePath}" if file.txt is the file you mean.`, + "Write creates missing parent directories, but it does not choose a filename from a directory path.", + ].join(" "); + } + + type NormalizedWriteArgs = { + path: unknown; + content: string; + hasContentArgument: boolean; + mode: "rewrite"; + }; + + function normalizeWriteArgs(args: unknown): NormalizedWriteArgs { + const record = + args && typeof args === "object" && !Array.isArray(args) + ? (args as Record) + : {}; + assertKnownArguments("Write", record, ["path", "content", "mode"]); + const mode = record.mode; + if (mode !== undefined && mode !== null && mode !== "" && mode !== "rewrite") { + throw new Error("Write.mode is deprecated. Omit it, or pass rewrite for compatibility."); + } + if ("content" in record && typeof record.content !== "string") { + throw new Error("Write.content must be a string."); + } + return { + path: record.path, + content: typeof record.content === "string" ? record.content : "", + hasContentArgument: "content" in record, + mode: "rewrite", + }; + } + + async function resolveWriteTarget(writeArgs: NormalizedWriteArgs) { + const resolved = await pathResolver.resolvePath(writeArgs.path, { + label: "Write.path", + intent: "write", + required: true, + }); + return { resolved, path: backendPath(resolved) }; + } + async function readPathStatus(resolved: ResolvedPath, path: string) { return invokePathFileCommand({ toolName: "Write", @@ -316,16 +368,23 @@ export function createFsTools(params: { }); } - function completeWriteToolCallForPreflight(toolCall: ToolCall): ToolCall { - const args = toolCall.arguments && typeof toolCall.arguments === "object" - ? toolCall.arguments - : {}; - if (typeof args.content === "string") return toolCall; + function completeWriteToolCallForPreflight( + toolCall: ToolCall, + normalized?: { path?: string; content?: string }, + ): ToolCall { + const args = + toolCall.arguments && typeof toolCall.arguments === "object" ? toolCall.arguments : {}; return { ...toolCall, arguments: { ...args, - content: "", + ...(typeof normalized?.path === "string" ? { path: normalized.path } : {}), + content: + typeof normalized?.content === "string" + ? normalized.content + : typeof args.content === "string" + ? args.content + : "", }, }; } @@ -472,7 +531,7 @@ export function createFsTools(params: { const toolWrite: Tool = { name: "Write", description: - "Create a new text file or fully rewrite an existing workspace or enabled Skill file. There is no append mode — to add content, Read the file first, then either Write the full new content or use Edit to insert. Existing files must have been Read first so the tool can validate version metadata and reject stale rewrites.", + "Create a new text file or fully overwrite an existing workspace or enabled Skill text file. Pass only `path` and `content`; do not set `mode`. For new files, `path` must include the intended filename, for example `notes/todo.txt`; Write creates missing parent directories but does not choose filenames from directory paths. Existing files must have been fully Read first so the tool can reject stale rewrites. Use Edit for small changes.", parameters: strictToolParameters({ path: Type.String({ description: @@ -480,8 +539,9 @@ export function createFsTools(params: { }), content: Type.String({ description: "Entire text content to write" }), mode: Type.Optional( - Type.Literal("rewrite", { - description: "Only rewrite is supported; append is intentionally disabled", + Type.Union([Type.Literal("rewrite"), Type.Literal("")], { + description: + "Deprecated compatibility field. Omit this argument; empty string and rewrite are accepted as rewrite.", }), ), }), @@ -1355,18 +1415,10 @@ export function createFsTools(params: { async function execWrite(args: any, signal?: AbortSignal): Promise> { if (signal?.aborted) throw new Error("Cancelled"); - const resolved = await pathResolver.resolvePath(args?.path, { - label: "Write.path", - intent: "write", - required: true, - }); - const path = backendPath(resolved); + const writeArgs = normalizeWriteArgs(args); + const { resolved, path } = await resolveWriteTarget(writeArgs); if (!path) throw new Error("Write.path must identify a file"); - const content = typeof args?.content === "string" ? args.content : ""; - const mode = typeof args?.mode === "string" ? args.mode : "rewrite"; - if (mode !== "rewrite") { - throw new Error("Write.mode only supports rewrite"); - } + const content = writeArgs.content; const latest = fileState.getLatest(statePathKey(resolved)); if (latest?.kind === "text" && latest.isPartialView) { @@ -1374,19 +1426,27 @@ export function createFsTools(params: { } const fullSnapshot = fileState.getLatestFullText(statePathKey(resolved)); - const res = await invokePathFileCommand({ - toolName: "Write", - resolved, - command: "fs_write_text", - args: { - workdir: resolved.workdir, - path, - content, - mode: "rewrite", - expected_mtime_ms: fullSnapshot?.mtimeMs, - expected_content_hash: fullSnapshot?.contentHash, - }, - }); + let res: WriteCommandResponse; + try { + res = await invokePathFileCommand({ + toolName: "Write", + resolved, + command: "fs_write_text", + args: { + workdir: resolved.workdir, + path, + content, + mode: "rewrite", + expected_mtime_ms: fullSnapshot?.mtimeMs, + expected_content_hash: fullSnapshot?.contentHash, + }, + }); + } catch (error) { + if (/Cannot write to a directory path/i.test(asErrorMessage(error))) { + throw new Error(buildWriteDirectoryPathMessage(resolved)); + } + throw error; + } const details: WriteResultDetails = { kind: "write", @@ -1410,11 +1470,9 @@ export function createFsTools(params: { content: [ { type: "text", - text: - `Write: ${formatResolvedTarget(resolved)}\n` + - `mode=rewrite\n` + - `target=${details.existedBefore ? "existing" : "new"}\n` + - `bytesWritten=${details.bytesWritten}`, + text: details.existedBefore + ? `File updated successfully at: ${formatResolvedTarget(resolved)}` + : `File created successfully at: ${formatResolvedTarget(resolved)}`, }, ], details, @@ -1427,18 +1485,16 @@ export function createFsTools(params: { ): Promise { if (signal?.aborted) return null; - const rawPath = typeof toolCall.arguments?.path === "string" ? toolCall.arguments.path : ""; + const writeArgs = normalizeWriteArgs(toolCall.arguments); + const rawPath = typeof writeArgs.path === "string" ? writeArgs.path : ""; if (!rawPath.trim()) return null; - const resolved = await pathResolver.resolvePath(rawPath, { - label: "Write.path", - intent: "write", - required: true, - }); - const path = backendPath(resolved); + const { resolved, path } = await resolveWriteTarget(writeArgs); if (!path) { return { - toolCall: completeWriteToolCallForPreflight(toolCall), + toolCall: completeWriteToolCallForPreflight(toolCall, { + content: writeArgs.content, + }), toolResult: buildToolErrorResult(toolCall, "Write.path must identify a file"), }; } @@ -1452,7 +1508,10 @@ export function createFsTools(params: { const latest = fileState.getLatest(key); if (latest?.kind === "text" && latest.isPartialView) { return { - toolCall: completeWriteToolCallForPreflight(toolCall), + toolCall: completeWriteToolCallForPreflight(toolCall, { + path: resolved.displayPath, + content: writeArgs.content, + }), toolResult: buildToolErrorResult(toolCall, buildWriteRequiresFullReadMessage(resolved)), }; } @@ -1468,11 +1527,18 @@ export function createFsTools(params: { return null; } - const completedToolCall = completeWriteToolCallForPreflight(toolCall); + const completedToolCall = completeWriteToolCallForPreflight(toolCall, { + path: resolved.displayPath, + content: writeArgs.content, + }); if (status.kind === "dir") { + if (!writeArgs.hasContentArgument) return null; return { toolCall: completedToolCall, - toolResult: buildToolErrorResult(toolCall, "Cannot write to a directory path"), + toolResult: buildToolErrorResult( + toolCall, + buildWriteDirectoryPathMessage(resolved), + ), }; } diff --git a/crates/agent-gui/test/chat/markdown-image-policy.test.mjs b/crates/agent-gui/test/chat/markdown-image-policy.test.mjs index 9c729b6b..f359e777 100644 --- a/crates/agent-gui/test/chat/markdown-image-policy.test.mjs +++ b/crates/agent-gui/test/chat/markdown-image-policy.test.mjs @@ -222,6 +222,20 @@ test("agent tool rules keep local file discovery on file tools instead of Bash", assert.match(suffix, /Do not run Bash cat\/ls\/find\/grep/); }); +test("agent tool rules steer new files to concrete Write paths", () => { + const suffix = agentRunnerModule.buildToolsSuffix("/workspace", [ + "Read", + "Write", + "Edit", + "Bash", + ]); + assert.match(suffix, /New files: call Write with a file path that includes the filename and the full content/); + assert.match(suffix, /parent directories are created automatically/); + assert.match(suffix, /Do not set `mode`/); + assert.match(suffix, /path must include the intended filename, not just a directory/); + assert.match(suffix, /write \/ create files via heredocs, `tee`, `touch`, `cp`, or `mkdir`/); +}); + test("agent tool rules keep workspace and Skills deletion on Delete", () => { const suffix = agentRunnerModule.buildToolsSuffix("/workspace", [ "Delete", diff --git a/crates/agent-gui/test/tools/path-and-system-tools.test.mjs b/crates/agent-gui/test/tools/path-and-system-tools.test.mjs index 056b33e0..3203548d 100644 --- a/crates/agent-gui/test/tools/path-and-system-tools.test.mjs +++ b/crates/agent-gui/test/tools/path-and-system-tools.test.mjs @@ -1,5 +1,6 @@ import assert from "node:assert/strict"; import test from "node:test"; +import { validateToolArguments } from "@earendil-works/pi-ai"; import { createTsModuleLoader } from "../helpers/load-ts-module.mjs"; const loader = createTsModuleLoader(); @@ -483,8 +484,8 @@ test("file tools allow direct mutations inside enabled Skills when mutation is g }); assert.equal(result.isError, false); - assert.match(result.content[0].text, /Write: skill:\/\/demo\/SKILL\.md/); - assert.match(result.content[0].text, /mode=rewrite/); + assert.match(result.content[0].text, /File created successfully at: skill:\/\/demo\/SKILL\.md/); + assert.doesNotMatch(result.content[0].text, /mode=rewrite/); assert.deepEqual(invocations, [ { command: "fs_write_text", @@ -500,6 +501,36 @@ test("file tools allow direct mutations inside enabled Skills when mutation is g ]); }); +test("Write schema accepts legacy empty mode without exposing it as required behavior", async () => { + const fsLoader = createTsModuleLoader(); + const fsTools = fsLoader.loadModule("src/lib/tools/fsTools.ts"); + const fileToolState = fsLoader.loadModule("src/lib/tools/fileToolState.ts"); + const bundle = fsTools.createFsTools({ + workdir: "/workspace", + fileState: fileToolState.createFileToolState(), + }); + const writeTool = bundle.tools.find((tool) => tool.name === "Write"); + + assert.ok(writeTool); + assert.match(writeTool.description, /Pass only `path` and `content`; do not set `mode`/); + const args = validateToolArguments(writeTool, { + type: "toolCall", + id: "legacy-empty-mode", + name: "Write", + arguments: { + path: "test8/gomoku.html", + mode: "", + content: "", + }, + }); + + assert.deepEqual(args, { + path: "test8/gomoku.html", + mode: "", + content: "", + }); +}); + test("Write preflight blocks existing files before content streaming but allows new files", async () => { const invocations = []; const fsLoader = createTsModuleLoader({ @@ -565,6 +596,272 @@ test("Write preflight blocks existing files before content streaming but allows ]); }); +test("Write preflight gives generic filename guidance for directory paths", async () => { + const invocations = []; + const fsLoader = createTsModuleLoader({ + mocks: { + "@tauri-apps/api/core": { + async invoke(command, args) { + invocations.push({ command, args }); + assert.equal(command, "fs_path_status"); + return { + path: args.path, + exists: args.path === "output", + kind: args.path === "output" ? "dir" : null, + sizeBytes: args.path === "output" ? 96 : null, + mtimeMs: args.path === "output" ? 55 : null, + }; + }, + }, + }, + }); + const fsTools = fsLoader.loadModule("src/lib/tools/fsTools.ts"); + const fileToolState = fsLoader.loadModule("src/lib/tools/fileToolState.ts"); + const bundle = fsTools.createFsTools({ + workdir: "/workspace", + fileState: fileToolState.createFileToolState(), + }); + + const writeTool = bundle.tools.find((tool) => tool.name === "Write"); + assert.match(writeTool.description, /notes\/todo\.txt/); + assert.match(writeTool.description, /does not choose filenames from directory paths/); + assert.match(writeTool.description, /do not set `mode`/); + + const blocked = await bundle.preflightToolCall({ + type: "toolCall", + id: "stream-write-directory", + name: "Write", + arguments: { + path: "output", + content: "", + }, + }); + + assert.equal(blocked.toolResult.isError, true); + assert.match(blocked.toolResult.content[0].text, /directory, not a file: output/); + assert.match(blocked.toolResult.content[0].text, /path="output\/file\.txt"/); + assert.match(blocked.toolResult.content[0].text, /does not choose a filename/); + assert.equal(blocked.toolCall.arguments.content, ""); + assert.deepEqual(invocations, [ + { + command: "fs_path_status", + args: { + workdir: "/workspace", + path: "output", + }, + }, + ]); +}); + +test("Write does not infer filenames from content when path is a directory", async () => { + const invocations = []; + const fsLoader = createTsModuleLoader({ + mocks: { + "@tauri-apps/api/core": { + async invoke(command, args) { + invocations.push({ command, args }); + assert.equal(command, "fs_write_text"); + assert.equal(args.path, "test8"); + throw new Error("Cannot write to a directory path"); + }, + }, + }, + }); + const fsTools = fsLoader.loadModule("src/lib/tools/fsTools.ts"); + const fileToolState = fsLoader.loadModule("src/lib/tools/fileToolState.ts"); + const bundle = fsTools.createFsTools({ + workdir: "/workspace", + fileState: fileToolState.createFileToolState(), + }); + + const result = await bundle.executeToolCall({ + type: "toolCall", + id: "write-content-dir", + name: "Write", + arguments: { + path: "test8", + content: '{"ok":true}\n', + }, + }); + + assert.equal(result.isError, true); + assert.match(result.content[0].text, /directory, not a file: test8/); + assert.match(result.content[0].text, /path="test8\/file\.txt"/); + assert.doesNotMatch(result.content[0].text, /data\.json|index\.html/); + assert.deepEqual(invocations, [ + { + command: "fs_write_text", + args: { + workdir: "/workspace", + path: "test8", + content: '{"ok":true}\n', + mode: "rewrite", + expected_mtime_ms: undefined, + expected_content_hash: undefined, + }, + }, + ]); +}); + +test("Write preserves extensionless file paths instead of adding a content-derived extension", async () => { + const invocations = []; + const fsLoader = createTsModuleLoader({ + mocks: { + "@tauri-apps/api/core": { + async invoke(command, args) { + invocations.push({ command, args }); + assert.equal(command, "fs_write_text"); + return { + existedBefore: false, + bytesWritten: args.content.length, + mtimeMs: 125, + contentHash: "hash-index", + totalLines: 2, + }; + }, + }, + }, + }); + const fsTools = fsLoader.loadModule("src/lib/tools/fsTools.ts"); + const fileToolState = fsLoader.loadModule("src/lib/tools/fileToolState.ts"); + const bundle = fsTools.createFsTools({ + workdir: "/workspace", + fileState: fileToolState.createFileToolState(), + }); + + const result = await bundle.executeToolCall({ + type: "toolCall", + id: "write-extensionless", + name: "Write", + arguments: { + path: "scripts/run", + content: "#!/usr/bin/env bash\necho ok\n", + }, + }); + + assert.equal(result.isError, false); + assert.match(result.content[0].text, /File created successfully at: scripts\/run/); + assert.equal(result.details.path, "scripts/run"); + assert.deepEqual(invocations, [ + { + command: "fs_write_text", + args: { + workdir: "/workspace", + path: "scripts/run", + content: "#!/usr/bin/env bash\necho ok\n", + mode: "rewrite", + expected_mtime_ms: undefined, + expected_content_hash: undefined, + }, + }, + ]); +}); + +test("Write replays the Gomoku failure sequence with generic directory recovery guidance", async () => { + const invocations = []; + const fsLoader = createTsModuleLoader({ + mocks: { + "@tauri-apps/api/core": { + async invoke(command, args) { + invocations.push({ command, args }); + if (command === "fs_path_status") { + return { + path: args.path, + exists: args.path === "test8", + kind: args.path === "test8" ? "dir" : null, + sizeBytes: args.path === "test8" ? 96 : null, + mtimeMs: args.path === "test8" ? 55 : null, + }; + } + assert.equal(command, "fs_write_text"); + return { + existedBefore: false, + bytesWritten: args.content.length, + mtimeMs: 126, + contentHash: "hash-gomoku", + totalLines: 2, + }; + }, + }, + }, + }); + const fsTools = fsLoader.loadModule("src/lib/tools/fsTools.ts"); + const fileToolState = fsLoader.loadModule("src/lib/tools/fileToolState.ts"); + const bundle = fsTools.createFsTools({ + workdir: "/workspace", + fileState: fileToolState.createFileToolState(), + }); + const writeTool = bundle.tools.find((tool) => tool.name === "Write"); + + assert.deepEqual( + validateToolArguments(writeTool, { + type: "toolCall", + id: "gomoku-empty-mode", + name: "Write", + arguments: { + path: "test8/gomoku.html", + mode: "", + content: "", + }, + }), + { + path: "test8/gomoku.html", + mode: "", + content: "", + }, + ); + + const directoryBlocked = await bundle.preflightToolCall({ + type: "toolCall", + id: "gomoku-directory-empty", + name: "Write", + arguments: { + path: "test8", + content: "", + }, + }); + + assert.equal(directoryBlocked.toolResult.isError, true); + assert.match(directoryBlocked.toolResult.content[0].text, /directory, not a file: test8/); + assert.match(directoryBlocked.toolResult.content[0].text, /path="test8\/file\.txt"/); + assert.doesNotMatch(directoryBlocked.toolResult.content[0].text, /mode constant|index\.html/); + + const recovered = await bundle.executeToolCall({ + type: "toolCall", + id: "gomoku-directory-html", + name: "Write", + arguments: { + path: "test8/index.html", + content: "\n\n", + }, + }); + + assert.equal(recovered.isError, false); + assert.match(recovered.content[0].text, /File created successfully at: test8\/index\.html/); + assert.doesNotMatch(recovered.content[0].text, /mode=rewrite|target=/); + assert.equal(recovered.details.path, "test8/index.html"); + assert.deepEqual(invocations, [ + { + command: "fs_path_status", + args: { + workdir: "/workspace", + path: "test8", + }, + }, + { + command: "fs_write_text", + args: { + workdir: "/workspace", + path: "test8/index.html", + content: "\n\n", + mode: "rewrite", + expected_mtime_ms: undefined, + expected_content_hash: undefined, + }, + }, + ]); +}); + test("file tools block direct mutations inside built-in Skills", async () => { const invocations = []; const fsLoader = createTsModuleLoader({ From 5bc3cbd31540dfb85b8396bd2795df41762671ec Mon Sep 17 00:00:00 2001 From: su-fen <715041@qq.com> Date: Wed, 24 Jun 2026 19:13:57 +0800 Subject: [PATCH 2/2] fix(ui): improve dark mode selection contrast --- .../src/components/git/GitBranchSelector.tsx | 2 +- .../src/pages/settings/WorkdirPickerModal.tsx | 2 +- crates/agent-gateway/web/src/styles.css | 41 +++++++++++++++++++ .../src/components/git/GitBranchSelector.tsx | 2 +- 4 files changed, 44 insertions(+), 3 deletions(-) diff --git a/crates/agent-gateway/web/src/components/git/GitBranchSelector.tsx b/crates/agent-gateway/web/src/components/git/GitBranchSelector.tsx index 70500db7..17cb621b 100644 --- a/crates/agent-gateway/web/src/components/git/GitBranchSelector.tsx +++ b/crates/agent-gateway/web/src/components/git/GitBranchSelector.tsx @@ -483,7 +483,7 @@ export function GitBranchSelector(props: { "composer-reasoning-trigger inline-flex h-8 min-w-0 max-w-[13rem] items-center gap-1 rounded-full border px-2 text-xs font-medium outline-hidden transition-colors", noRepo ? "border-transparent bg-foreground/[0.04] text-muted-foreground" - : "border-emerald-300/25 bg-emerald-50/65 text-foreground hover:bg-emerald-50 dark:border-emerald-300/15 dark:bg-emerald-400/[0.08]", + : "border-emerald-300/25 bg-emerald-50/65 text-foreground hover:bg-emerald-50 dark:border-emerald-300/15 dark:bg-emerald-400/[0.08] dark:hover:bg-emerald-400/[0.13]", "disabled:pointer-events-none disabled:opacity-45", )} title={visibleError || (!canWrite ? disabledMessage : "") || label} diff --git a/crates/agent-gateway/web/src/pages/settings/WorkdirPickerModal.tsx b/crates/agent-gateway/web/src/pages/settings/WorkdirPickerModal.tsx index d4e4ed0d..967d07e1 100644 --- a/crates/agent-gateway/web/src/pages/settings/WorkdirPickerModal.tsx +++ b/crates/agent-gateway/web/src/pages/settings/WorkdirPickerModal.tsx @@ -571,7 +571,7 @@ export function WorkdirPickerModal(props: WorkdirPickerModalProps) { ) : null} -
+
item.data.label} diff --git a/crates/agent-gateway/web/src/styles.css b/crates/agent-gateway/web/src/styles.css index 56677c62..32c8a426 100644 --- a/crates/agent-gateway/web/src/styles.css +++ b/crates/agent-gateway/web/src/styles.css @@ -104,6 +104,47 @@ html[data-liveagent-webui="gateway"] .project-file-tree-panel-scroll { scroll-padding-bottom: max(16px, env(safe-area-inset-bottom)); } +html[data-liveagent-webui="gateway"] .workdir-picker-tree { + --rct-color-tree-bg: transparent; + --rct-color-tree-focus-outline: transparent; + --rct-color-focustree-item-selected-bg: hsl(var(--accent)); + --rct-color-focustree-item-selected-text: hsl(var(--accent-foreground)); + --rct-color-focustree-item-hover-bg: hsl(var(--muted) / 0.72); + --rct-color-focustree-item-hover-text: hsl(var(--foreground)); + --rct-color-focustree-item-active-bg: hsl(var(--muted)); + --rct-color-focustree-item-active-text: hsl(var(--foreground)); + --rct-color-focustree-item-focused-border: hsl(var(--ring) / 0.58); + --rct-color-focustree-item-draggingover-bg: hsl(var(--accent)); + --rct-color-focustree-item-draggingover-color: hsl(var(--accent-foreground)); + --rct-color-nonfocustree-item-selected-bg: hsl(var(--accent) / 0.78); + --rct-color-nonfocustree-item-selected-text: hsl(var(--accent-foreground)); + --rct-color-nonfocustree-item-focused-border: hsl(var(--border)); + --rct-color-search-highlight-bg: hsl(var(--primary) / 0.18); + --rct-color-arrow: hsl(var(--muted-foreground)); + --rct-bar-color: hsl(var(--ring)); + --rct-focus-outline: hsl(var(--ring)); +} + +html[data-liveagent-webui="gateway"] .workdir-picker-tree .rct-tree-root { + background: transparent; + color: hsl(var(--foreground)); + font-family: inherit; +} + +html[data-liveagent-webui="gateway"].dark .workdir-picker-tree { + --rct-color-focustree-item-selected-bg: hsl(var(--accent) / 0.92); + --rct-color-focustree-item-selected-text: hsl(var(--foreground)); + --rct-color-focustree-item-hover-bg: hsl(var(--muted) / 0.68); + --rct-color-focustree-item-active-bg: hsl(var(--muted) / 0.86); + --rct-color-nonfocustree-item-selected-bg: hsl(var(--accent) / 0.72); + --rct-color-nonfocustree-item-selected-text: hsl(var(--foreground)); + --rct-color-search-highlight-bg: hsl(var(--primary) / 0.24); +} + +html[data-liveagent-webui="gateway"] .workdir-picker-tree .rct-tree-item-button { + min-width: 0; +} + html[data-liveagent-webui="gateway"] .project-tools-panel-tabs::-webkit-scrollbar { display: none; width: 0; diff --git a/crates/agent-gui/src/components/git/GitBranchSelector.tsx b/crates/agent-gui/src/components/git/GitBranchSelector.tsx index b89e420f..dd794ac3 100644 --- a/crates/agent-gui/src/components/git/GitBranchSelector.tsx +++ b/crates/agent-gui/src/components/git/GitBranchSelector.tsx @@ -459,7 +459,7 @@ export function GitBranchSelector(props: { "composer-reasoning-trigger inline-flex h-8 min-w-0 max-w-[13rem] items-center gap-1 rounded-full border px-2 text-xs font-medium outline-hidden transition-colors", noRepo ? "border-transparent bg-foreground/[0.04] text-muted-foreground" - : "border-emerald-300/25 bg-emerald-50/65 text-foreground hover:bg-emerald-50 dark:border-emerald-300/15 dark:bg-emerald-400/[0.08]", + : "border-emerald-300/25 bg-emerald-50/65 text-foreground hover:bg-emerald-50 dark:border-emerald-300/15 dark:bg-emerald-400/[0.08] dark:hover:bg-emerald-400/[0.13]", "disabled:pointer-events-none disabled:opacity-45", )} title={visibleError || (!canWrite ? disabledMessage : "") || label}