fix: relativize web console tool paths
This commit is contained in:
parent
c7c838b4e5
commit
1ff02151b8
|
|
@ -2,7 +2,7 @@
|
|||
title: 'Backend runtime経由の操作をprotocol transportへ統一しTUI同等にする'
|
||||
state: 'planning'
|
||||
created_at: '2026-07-21T09:20:07Z'
|
||||
updated_at: '2026-07-22T07:08:39Z'
|
||||
updated_at: '2026-07-22T07:23:32Z'
|
||||
assignee: null
|
||||
readiness: 'draft'
|
||||
---
|
||||
|
|
|
|||
|
|
@ -414,3 +414,52 @@ Verification:
|
|||
Note: `deno fmt --check` on the touched files still reports existing formatting changes outside this patch scope, so it was not used as the gating check for this small change.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: comment author: assistant at: 2026-07-22T07:12:46Z -->
|
||||
|
||||
## Comment
|
||||
|
||||
Requested Web Console tool-path display change before implementation:
|
||||
|
||||
- Use the Worker's protocol `snapshot.greeting.cwd` as the display base.
|
||||
- For known display portions of edit-related tools plus `Read`, `Grep`, and `Glob`, show absolute paths relative to cwd.
|
||||
- Do not globally replace arbitrary tool output text; only rewrite known path-bearing parts of known tool display formats.
|
||||
|
||||
Implementation plan:
|
||||
- Carry `greeting.cwd` through the Web Console projection state.
|
||||
- Add a focused display normalizer for known tool output formats:
|
||||
- `Read ... from <path>` / read error path forms if present.
|
||||
- grep result path prefixes such as `<path>:line:...` and summary lines like matching lines in files.
|
||||
- glob found-path listing entries.
|
||||
- edit/write-style success/error lines that explicitly include a file path.
|
||||
- Add focused model tests for the supported formats and keep transcript/protocol payloads unchanged.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: assistant at: 2026-07-22T07:23:32Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Implementation report for Web Console known tool-path display relativization:
|
||||
|
||||
- Web Console projection now carries `snapshot.greeting.cwd` in `ConsoleProjection`.
|
||||
- Each `ToolCallView` captures the current cwd so later rendering can use the correct display base without changing protocol payloads or stored tool output.
|
||||
- Added path-display normalization scoped to known tool display formats only:
|
||||
- `Read`: `file_path` display and `Read ... from <path>` summary/detail path.
|
||||
- `Write`: `file_path` display and known `Created` / `Overwrote` / `Wrote` / related result-line path prefixes.
|
||||
- `Edit`: `file_path` display and known `Edited <path> (...)` result-line path prefix.
|
||||
- `Glob`: line-start cwd paths in result preview.
|
||||
- `Grep`: line-start cwd paths in result preview, including `path:line:...` output.
|
||||
- Paths outside cwd are left absolute.
|
||||
- Arbitrary default tool output, Bash output, and argument JSON/details are not globally rewritten.
|
||||
|
||||
Tests:
|
||||
- Added a focused Web model regression test with `snapshot.greeting.cwd = /repo` covering Read/Write/Edit/Glob/Grep display output.
|
||||
- The test asserts cwd-contained paths become relative while `/outside/...` paths remain absolute.
|
||||
|
||||
Verification:
|
||||
- `cd web/workspace && deno test --allow-read=src --allow-env=VSCODE_TEXTMATE_DEBUG src/lib/workspace/console/model.test.ts`
|
||||
- `cd web/workspace && deno task check` passed with 0 errors; it still reports the existing 2 a11y warnings in `+page.svelte`.
|
||||
- `git diff --check`
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -36,6 +36,27 @@ function consoleLine(id: string, kind: ConsoleLine["kind"]): ConsoleLine {
|
|||
};
|
||||
}
|
||||
|
||||
function snapshotEvent(cwd: string): Event {
|
||||
return {
|
||||
event: "snapshot",
|
||||
data: {
|
||||
entries: [],
|
||||
greeting: {
|
||||
worker_name: "Worker",
|
||||
cwd,
|
||||
provider: "provider",
|
||||
model: "model",
|
||||
scope_summary: "bounded",
|
||||
tools: [],
|
||||
context_window: 100,
|
||||
context_tokens: 20,
|
||||
},
|
||||
status: "idle",
|
||||
in_flight: { blocks: [] },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Deno.test("workerConsoleHref encodes runtime and worker target authority", () => {
|
||||
assert(
|
||||
workerConsoleHref({
|
||||
|
|
@ -886,3 +907,156 @@ Deno.test("selectConsoleTimelineLines keeps all users and only last assistant pe
|
|||
["0:u1", "3:a2", "4:u2", "6:a4"],
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("projectConsole relativizes known tool path displays from snapshot cwd", () => {
|
||||
const projection = projectConsole([
|
||||
{ eventId: "cwd-0", event: snapshotEvent("/repo") },
|
||||
{
|
||||
eventId: "cwd-1",
|
||||
event: {
|
||||
event: "tool_call_done",
|
||||
data: {
|
||||
id: "read-rel",
|
||||
name: "Read",
|
||||
arguments: JSON.stringify({ file_path: "/repo/src/main.rs" }),
|
||||
},
|
||||
} satisfies Event,
|
||||
},
|
||||
{
|
||||
eventId: "cwd-2",
|
||||
event: {
|
||||
event: "tool_result",
|
||||
data: {
|
||||
id: "read-rel",
|
||||
summary: "Read 4 line(s) [1..4] of 20 from /repo/src/main.rs",
|
||||
is_error: false,
|
||||
},
|
||||
} satisfies Event,
|
||||
},
|
||||
{
|
||||
eventId: "cwd-3",
|
||||
event: {
|
||||
event: "tool_call_done",
|
||||
data: {
|
||||
id: "write-rel",
|
||||
name: "Write",
|
||||
arguments: JSON.stringify({ file_path: "/repo/out.txt", content: "ok" }),
|
||||
},
|
||||
} satisfies Event,
|
||||
},
|
||||
{
|
||||
eventId: "cwd-4",
|
||||
event: {
|
||||
event: "tool_result",
|
||||
data: {
|
||||
id: "write-rel",
|
||||
summary: "Wrote /repo/out.txt",
|
||||
output: "Wrote /repo/out.txt",
|
||||
is_error: false,
|
||||
},
|
||||
} satisfies Event,
|
||||
},
|
||||
{
|
||||
eventId: "cwd-5",
|
||||
event: {
|
||||
event: "tool_call_done",
|
||||
data: {
|
||||
id: "edit-rel",
|
||||
name: "Edit",
|
||||
arguments: JSON.stringify({
|
||||
file_path: "/repo/src/main.rs",
|
||||
old_string: "a",
|
||||
new_string: "b",
|
||||
}),
|
||||
},
|
||||
} satisfies Event,
|
||||
},
|
||||
{
|
||||
eventId: "cwd-6",
|
||||
event: {
|
||||
event: "tool_result",
|
||||
data: {
|
||||
id: "edit-rel",
|
||||
summary: "Edited /repo/src/main.rs (1 replacement)",
|
||||
output: "Edited /repo/src/main.rs (1 replacement)",
|
||||
is_error: false,
|
||||
},
|
||||
} satisfies Event,
|
||||
},
|
||||
{
|
||||
eventId: "cwd-7",
|
||||
event: {
|
||||
event: "tool_call_done",
|
||||
data: {
|
||||
id: "glob-rel",
|
||||
name: "Glob",
|
||||
arguments: JSON.stringify({ path: "/repo", pattern: "**/*.rs" }),
|
||||
},
|
||||
} satisfies Event,
|
||||
},
|
||||
{
|
||||
eventId: "cwd-8",
|
||||
event: {
|
||||
event: "tool_result",
|
||||
data: {
|
||||
id: "glob-rel",
|
||||
summary: "Found 2 file(s) matching **/*.rs",
|
||||
output: "Found 2 file(s) matching **/*.rs\n/repo/src/main.rs\n/outside/lib.rs",
|
||||
is_error: false,
|
||||
},
|
||||
} satisfies Event,
|
||||
},
|
||||
{
|
||||
eventId: "cwd-9",
|
||||
event: {
|
||||
event: "tool_call_done",
|
||||
data: {
|
||||
id: "grep-rel",
|
||||
name: "Grep",
|
||||
arguments: JSON.stringify({ pattern: "needle", path: "/repo" }),
|
||||
},
|
||||
} satisfies Event,
|
||||
},
|
||||
{
|
||||
eventId: "cwd-10",
|
||||
event: {
|
||||
event: "tool_result",
|
||||
data: {
|
||||
id: "grep-rel",
|
||||
summary: "2 matching line(s) in 2 file(s)",
|
||||
output: "/repo/src/main.rs:12:needle\n/outside/lib.rs:1:needle",
|
||||
is_error: false,
|
||||
},
|
||||
} satisfies Event,
|
||||
},
|
||||
]);
|
||||
|
||||
const bodies = projection.lines.filter((line) => line.kind === "tool").map((line) => line.body);
|
||||
assertEquals(bodies[0], "Read — 1 file read\n src/main.rs");
|
||||
assert(
|
||||
projection.lines[0].detail?.includes("from src/main.rs"),
|
||||
"Read summary detail path should be relative",
|
||||
);
|
||||
assert(
|
||||
bodies.some((body) => body.includes("Write — out.txt") && body.includes("Wrote out.txt")),
|
||||
"Write header and known result path should be relative",
|
||||
);
|
||||
assert(
|
||||
bodies.some((body) =>
|
||||
body.includes("Edit — src/main.rs") && body.includes("Edited src/main.rs")
|
||||
),
|
||||
"Edit header and known result path should be relative",
|
||||
);
|
||||
assert(
|
||||
bodies.some((body) =>
|
||||
body.includes("src/main.rs") && body.includes("/outside/lib.rs")
|
||||
),
|
||||
"Glob should relativize cwd paths and keep outside paths absolute",
|
||||
);
|
||||
assert(
|
||||
bodies.some((body) =>
|
||||
body.includes("src/main.rs:12:needle") && body.includes("/outside/lib.rs:1:needle")
|
||||
),
|
||||
"Grep should relativize line-start cwd paths and keep outside paths absolute",
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ type ToolCallView = {
|
|||
summary?: string;
|
||||
output?: string | null;
|
||||
isError?: boolean;
|
||||
cwd?: string | null;
|
||||
};
|
||||
|
||||
export type ConsoleDiffLine = {
|
||||
|
|
@ -61,6 +62,7 @@ export type ConsoleProjection = {
|
|||
lines: ConsoleLine[];
|
||||
status: string | null;
|
||||
usage: string | null;
|
||||
cwd: string | null;
|
||||
lastEventId: string | null;
|
||||
};
|
||||
|
||||
|
|
@ -138,6 +140,7 @@ export function emptyConsoleProjection(): ConsoleProjection {
|
|||
lines: [],
|
||||
status: null,
|
||||
usage: null,
|
||||
cwd: null,
|
||||
lastEventId: null,
|
||||
};
|
||||
}
|
||||
|
|
@ -183,6 +186,7 @@ export function applyProtocolEvent(
|
|||
lines: [...projection.lines],
|
||||
status: projection.status,
|
||||
usage: projection.usage,
|
||||
cwd: projection.cwd,
|
||||
lastEventId: envelope.eventId,
|
||||
};
|
||||
const event = envelope.event;
|
||||
|
|
@ -284,16 +288,21 @@ export function applyProtocolEvent(
|
|||
break;
|
||||
case "snapshot":
|
||||
next.status = event.data.status;
|
||||
next.lines = snapshotLinesFromEntries(envelope.eventId, event.data.entries);
|
||||
next.cwd = event.data.greeting.cwd;
|
||||
next.lines = snapshotLinesFromEntries(
|
||||
envelope.eventId,
|
||||
event.data.entries,
|
||||
next.cwd,
|
||||
);
|
||||
for (const block of event.data.in_flight?.blocks ?? []) {
|
||||
next.lines.push(inFlightLine(envelope.eventId, block));
|
||||
next.lines.push(inFlightLine(envelope.eventId, block, next.cwd));
|
||||
}
|
||||
break;
|
||||
case "status":
|
||||
next.status = event.data.status;
|
||||
break;
|
||||
case "segment_rotated":
|
||||
next.lines = snapshotLinesFromEntries(envelope.eventId, [event.data.entry]);
|
||||
next.lines = snapshotLinesFromEntries(envelope.eventId, [event.data.entry], next.cwd);
|
||||
break;
|
||||
case "invoke_start":
|
||||
case "turn_start":
|
||||
|
|
@ -504,6 +513,7 @@ function upsertToolCall(
|
|||
summary: update.summary,
|
||||
output: update.output,
|
||||
isError: update.isError,
|
||||
cwd: update.cwd ?? projection.cwd,
|
||||
});
|
||||
projection.lines.push(created);
|
||||
return created;
|
||||
|
|
@ -518,6 +528,7 @@ function upsertToolCall(
|
|||
...update,
|
||||
id,
|
||||
name: update.name ?? existing.toolCall!.name,
|
||||
cwd: update.cwd ?? existing.toolCall!.cwd ?? projection.cwd,
|
||||
},
|
||||
});
|
||||
projection.lines[index] = updated;
|
||||
|
|
@ -537,6 +548,7 @@ function appendToolArgs(
|
|||
name: "Tool",
|
||||
argsStream: delta,
|
||||
state: "streaming_args",
|
||||
cwd: projection.cwd,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
|
@ -567,6 +579,7 @@ function attachToolResult(
|
|||
name: "Tool",
|
||||
argsStream: "",
|
||||
state: result.isError ? "error" : "done",
|
||||
cwd: projection.cwd,
|
||||
...result,
|
||||
}),
|
||||
title: result.isError ? "Call · Tool result error" : "Call · Tool result",
|
||||
|
|
@ -703,6 +716,10 @@ function readAggregateLine(group: ConsoleLine[]): ConsoleLine {
|
|||
}
|
||||
|
||||
function readPath(toolCall: ToolCallView): string {
|
||||
return displayPath(readRawPath(toolCall), toolCall.cwd);
|
||||
}
|
||||
|
||||
function readRawPath(toolCall: ToolCallView): string {
|
||||
const args = parsedArgs(toolCall);
|
||||
return stringField(args, "file_path") ?? "?";
|
||||
}
|
||||
|
|
@ -712,37 +729,37 @@ function readDetail(toolCall: ToolCallView): string {
|
|||
`id: ${toolCall.id}`,
|
||||
`state: ${stateSuffix(toolCall.state)}`,
|
||||
`path: ${readPath(toolCall)}`,
|
||||
toolCall.summary ? `summary: ${toolCall.summary}` : undefined,
|
||||
toolCall.summary
|
||||
? `summary: ${normalizeKnownToolResult(toolCall.name, toolCall.summary, toolCall.cwd)}`
|
||||
: undefined,
|
||||
]);
|
||||
}
|
||||
|
||||
function renderReadTool(toolCall: ToolCallView): string {
|
||||
const args = parsedArgs(toolCall);
|
||||
const path = stringField(args, "file_path") ?? "?";
|
||||
return `Read — ${path} (${stateSuffix(toolCall.state)})`;
|
||||
return `Read — ${readPath(toolCall)} (${stateSuffix(toolCall.state)})`;
|
||||
}
|
||||
|
||||
function renderWriteTool(toolCall: ToolCallView): string {
|
||||
const args = parsedArgs(toolCall);
|
||||
const path = stringField(args, "file_path") ?? "?";
|
||||
const path = displayPath(stringField(args, "file_path") ?? "?", toolCall.cwd);
|
||||
const content = stringField(args, "content");
|
||||
return compactLines([
|
||||
`Write — ${path} (${stateSuffix(toolCall.state)})`,
|
||||
cappedSection(content, 5),
|
||||
resultText(toolCall),
|
||||
knownToolResultText(toolCall),
|
||||
]);
|
||||
}
|
||||
|
||||
function renderEditTool(toolCall: ToolCallView): string {
|
||||
const args = parsedArgs(toolCall);
|
||||
const path = stringField(args, "file_path") ?? "?";
|
||||
const path = displayPath(stringField(args, "file_path") ?? "?", toolCall.cwd);
|
||||
const diff = editDiff(toolCall) ?? [];
|
||||
const removes = diff.filter((line) => line.kind === "remove").length;
|
||||
const adds = diff.filter((line) => line.kind === "add").length;
|
||||
return compactLines([
|
||||
`Edit — ${path} (${stateSuffix(toolCall.state)})`,
|
||||
diff.length > 0 ? `diff: -${removes} +${adds}` : undefined,
|
||||
resultText(toolCall),
|
||||
knownToolResultText(toolCall),
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -829,7 +846,7 @@ function renderSearchTool(toolCall: ToolCallView): string {
|
|||
const summary = toolCall.summary?.trim();
|
||||
return compactLines([
|
||||
`${toolCall.name} — ${toolHeaderSuffix(toolCall, summary)}`,
|
||||
resultText(toolCall),
|
||||
knownToolResultText(toolCall),
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -838,7 +855,7 @@ function renderGrepTool(toolCall: ToolCallView): string {
|
|||
return compactLines([
|
||||
`Grep — ${toolHeaderSuffix(toolCall, summary)}`,
|
||||
grepQueryText(toolCall),
|
||||
cappedResultSection(resultText(toolCall), 5),
|
||||
cappedResultSection(knownToolResultText(toolCall), 5),
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -884,7 +901,9 @@ function toolCallDetail(toolCall: ToolCallView): string {
|
|||
return compactLines([
|
||||
`id: ${toolCall.id}`,
|
||||
`state: ${stateSuffix(toolCall.state)}`,
|
||||
toolCall.summary ? `summary: ${toolCall.summary}` : undefined,
|
||||
toolCall.summary
|
||||
? `summary: ${normalizeKnownToolResult(toolCall.name, toolCall.summary, toolCall.cwd)}`
|
||||
: undefined,
|
||||
argsText(toolCall) ? `arguments:\n${argsText(toolCall)}` : undefined,
|
||||
]);
|
||||
}
|
||||
|
|
@ -896,6 +915,84 @@ function resultText(toolCall: ToolCallView): string | undefined {
|
|||
return toolCall.summary;
|
||||
}
|
||||
|
||||
function knownToolResultText(toolCall: ToolCallView): string | undefined {
|
||||
const text = resultText(toolCall);
|
||||
if (!text) return undefined;
|
||||
return normalizeKnownToolResult(toolCall.name, text, toolCall.cwd);
|
||||
}
|
||||
|
||||
function normalizeKnownToolResult(
|
||||
toolName: string,
|
||||
text: string,
|
||||
cwd: string | null | undefined,
|
||||
): string {
|
||||
if (!cwd) return text;
|
||||
switch (toolName) {
|
||||
case "Read":
|
||||
return normalizeReadResultPaths(text, cwd);
|
||||
case "Grep":
|
||||
return normalizeLineStartPaths(text, cwd);
|
||||
case "Glob":
|
||||
return normalizeLineStartPaths(text, cwd);
|
||||
case "Edit":
|
||||
case "Write":
|
||||
return normalizeEditResultPaths(text, cwd);
|
||||
default:
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
function displayPath(path: string, cwd: string | null | undefined): string {
|
||||
if (!cwd || !path.startsWith("/")) return path;
|
||||
const normalizedCwd = cwd.endsWith("/") ? cwd.slice(0, -1) : cwd;
|
||||
if (path === normalizedCwd) return ".";
|
||||
const prefix = `${normalizedCwd}/`;
|
||||
return path.startsWith(prefix) ? path.slice(prefix.length) : path;
|
||||
}
|
||||
|
||||
function normalizeReadResultPaths(text: string, cwd: string): string {
|
||||
return text.split("\n").map((line) => {
|
||||
const readMatch = line.match(
|
||||
/^(Read \d+ line\(s\)(?: \[[^\]]+\])?(?: of \d+)? from )(.+)$/,
|
||||
);
|
||||
if (readMatch) {
|
||||
return `${readMatch[1]}${displayPath(readMatch[2], cwd)}`;
|
||||
}
|
||||
return normalizeKnownPathSuffix(line, cwd);
|
||||
}).join("\n");
|
||||
}
|
||||
|
||||
function normalizeEditResultPaths(text: string, cwd: string): string {
|
||||
return text.split("\n").map((line) => {
|
||||
const simpleMatch = line.match(
|
||||
/^(Edited |Wrote |Created |Overwrote |Deleted )(.+?)( \(.+\))?$/,
|
||||
);
|
||||
if (simpleMatch) {
|
||||
return `${simpleMatch[1]}${displayPath(simpleMatch[2], cwd)}${
|
||||
simpleMatch[3] ?? ""
|
||||
}`;
|
||||
}
|
||||
return normalizeKnownPathSuffix(line, cwd);
|
||||
}).join("\n");
|
||||
}
|
||||
|
||||
function normalizeKnownPathSuffix(line: string, cwd: string): string {
|
||||
const match = line.match(
|
||||
/^(.*\b(?:from|path|file not found|not a directory): )(.+)$/,
|
||||
);
|
||||
if (!match) return line;
|
||||
return `${match[1]}${displayPath(match[2], cwd)}`;
|
||||
}
|
||||
|
||||
function normalizeLineStartPaths(text: string, cwd: string): string {
|
||||
const normalizedCwd = cwd.endsWith("/") ? cwd.slice(0, -1) : cwd;
|
||||
const prefix = `${normalizedCwd}/`;
|
||||
return text.split("\n").map((line) => {
|
||||
if (line === normalizedCwd) return ".";
|
||||
return line.startsWith(prefix) ? line.slice(prefix.length) : line;
|
||||
}).join("\n");
|
||||
}
|
||||
|
||||
function argsText(toolCall: ToolCallView): string {
|
||||
const raw = toolCall.arguments ?? toolCall.argsStream;
|
||||
if (!raw.trim()) {
|
||||
|
|
@ -992,14 +1089,21 @@ function usageText(
|
|||
} · cache ${data.cache_read_input_tokens ?? "unknown"}`;
|
||||
}
|
||||
|
||||
function snapshotLinesFromEntries(eventId: string, entries: unknown[]): ConsoleLine[] {
|
||||
function snapshotLinesFromEntries(
|
||||
eventId: string,
|
||||
entries: unknown[],
|
||||
cwd: string | null,
|
||||
): ConsoleLine[] {
|
||||
const projection: ConsoleProjection = {
|
||||
lines: [],
|
||||
status: null,
|
||||
usage: null,
|
||||
cwd,
|
||||
lastEventId: eventId,
|
||||
};
|
||||
entries.forEach((entry, index) => applyLogEntry(projection, `${eventId}-snapshot-${index}`, entry));
|
||||
entries.forEach((entry, index) =>
|
||||
applyLogEntry(projection, `${eventId}-snapshot-${index}`, entry)
|
||||
);
|
||||
return projection.lines;
|
||||
}
|
||||
|
||||
|
|
@ -1143,7 +1247,11 @@ function loggedContentText(parts: unknown[]): string {
|
|||
.join("\n");
|
||||
}
|
||||
|
||||
function inFlightLine(eventId: string, block: InFlightBlock): ConsoleLine {
|
||||
function inFlightLine(
|
||||
eventId: string,
|
||||
block: InFlightBlock,
|
||||
cwd: string | null,
|
||||
): ConsoleLine {
|
||||
switch (block.kind) {
|
||||
case "text":
|
||||
return line(
|
||||
|
|
@ -1170,6 +1278,7 @@ function inFlightLine(eventId: string, block: InFlightBlock): ConsoleLine {
|
|||
argsStream: block.args,
|
||||
arguments: block.state === "done" ? block.args : undefined,
|
||||
state: inFlightToolState(block.state),
|
||||
cwd,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user