From 842404bf6b43c48d15a97a68e0b07f850d0c92d7 Mon Sep 17 00:00:00 2001 From: sandyzzx Date: Wed, 7 Oct 2026 08:39:10 +0800 Subject: [PATCH] fix: render concise task feedback --- plugins/codex-zcode-bridge/dist/bridge.mjs | 36 +++++++++++++++-- src/feedback/renderer.ts | 47 ++++++++++++++++++++-- test/task-feedback.test.ts | 26 +++++++++++- 3 files changed, 100 insertions(+), 9 deletions(-) diff --git a/plugins/codex-zcode-bridge/dist/bridge.mjs b/plugins/codex-zcode-bridge/dist/bridge.mjs index 52a449e..ea2fc44 100644 --- a/plugins/codex-zcode-bridge/dist/bridge.mjs +++ b/plugins/codex-zcode-bridge/dist/bridge.mjs @@ -26069,6 +26069,29 @@ function displayText(value, limit) { function oneLine(value, limit) { return displayText(value.replace(/\s+/gu, " "), limit); } +function truncatedOneLine(value, limit) { + const text = oneLine(value, limit + 1); + return text.length > limit ? `${text.slice(0, limit - 1).trimEnd()}\u2026` : text; +} +function testCommandLabel(value) { + let command = oneLine(value, 2e3); + const segments = command.split(/\s*&&\s*/u); + if (segments.length > 1) command = segments.at(-1) ?? command; + command = command.replace(/^['"]+|['"]+$/gu, "").trim(); + const dotnet = command.match(/\bdotnet\s+test\s+([^\s]+)(.*)$/iu); + if (dotnet) { + const project = (dotnet[1] ?? "").split(/[\\/]/u).at(-1)?.replace(/\.csproj$/iu, "") ?? "project"; + const filterTail = dotnet[2]?.match(/--filter(?:=|\s+)(.+)$/iu)?.[1]?.replace(/["']+$/gu, "").trim(); + const filter = filterTail?.includes("~") ? filterTail.slice(filterTail.lastIndexOf("~") + 1) : filterTail; + return `dotnet test ${project}${filter ? ` \xB7 filter ${oneLine(filter, 64)}` : ""}`; + } + const packageScript = command.match(/\b(npm|pnpm|yarn)\s+(?:(run)\s+)?([A-Za-z0-9:_-]+)\b/iu); + if (packageScript) { + return `${packageScript[1]} ${packageScript[2] ? "run " : ""}${packageScript[3]}`; + } + const tokens = command.match(/(?:"[^"]*"|'[^']*'|[^\s]+)/gu) ?? []; + return tokens.slice(0, 2).map((token) => token.replace(/^['"]|['"]$/gu, "")).join(" ").slice(0, 96); +} function renderTaskFeedback(snapshot) { const terminalLabel = snapshot.status === "waiting_for_master" ? "WAITING_FOR_MASTER" : ["completed", "failed", "cancelled"].includes(snapshot.status) ? snapshot.status.toUpperCase() : null; const lines = [`\u25A3 ZCode \xB7 ${snapshot.task_id}${terminalLabel ? ` \xB7 ${terminalLabel}` : ""}`]; @@ -26101,14 +26124,19 @@ function renderTaskFeedback(snapshot) { const result = snapshot.result; if (result) { lines.push("", "Agent report:"); - const summary = oneLine(result.summary, 800); + const summary = truncatedOneLine(result.summary, 220); if (summary) lines.push(summary); - lines.push(`Changed: ${result.files_changed.length} files`); + const changedCount = result.files_changed.length; + lines.push(`Changed: ${changedCount} ${changedCount === 1 ? "file" : "files"}`); for (const test of result.tests.slice(0, 3)) { - lines.push(`Tests: ${oneLine(test.command, 240)} \xB7 reported ${test.status}`); + lines.push(`Tests: ${testCommandLabel(test.command)} \xB7 reported ${test.status}`); } if (result.tests.length > 3) lines.push(`Tests: ${result.tests.length - 3} more reported`); - if (result.issues.length) lines.push(`Issues: ${result.issues.length} reported`); + if (result.issues.length) { + const issue2 = truncatedOneLine(result.issues[0] ?? "", 140); + lines.push(`! ${result.issues.length} reported ${result.issues.length === 1 ? "issue" : "issues"}${issue2 ? ` \xB7 ${issue2}` : ""}`); + if (result.issues.length > 1) lines.push(`! ${result.issues.length - 1} more reported issues`); + } if (result.duration_ms !== null) lines.push(`Duration: ${formatDuration(result.duration_ms)}`); } return lines.join("\n"); diff --git a/src/feedback/renderer.ts b/src/feedback/renderer.ts index f7f38fa..39540de 100644 --- a/src/feedback/renderer.ts +++ b/src/feedback/renderer.ts @@ -11,6 +11,40 @@ function oneLine(value: string, limit: number): string { return displayText(value.replace(/\s+/gu, " "), limit); } +function truncatedOneLine(value: string, limit: number): string { + const text = oneLine(value, limit + 1); + return text.length > limit ? `${text.slice(0, limit - 1).trimEnd()}…` : text; +} + +function testCommandLabel(value: string): string { + let command = oneLine(value, 2_000); + + // Host shells often wrap the actual test command with environment setup. + // Keep only the final command segment so HOME/TEMP assignments and wrapper + // flags do not dominate the native transcript. + const segments = command.split(/\s*&&\s*/u); + if (segments.length > 1) command = segments.at(-1) ?? command; + command = command.replace(/^['"]+|['"]+$/gu, "").trim(); + + const dotnet = command.match(/\bdotnet\s+test\s+([^\s]+)(.*)$/iu); + if (dotnet) { + const project = (dotnet[1] ?? "").split(/[\\/]/u).at(-1)?.replace(/\.csproj$/iu, "") ?? "project"; + const filterTail = dotnet[2]?.match(/--filter(?:=|\s+)(.+)$/iu)?.[1]?.replace(/["']+$/gu, "").trim(); + const filter = filterTail?.includes("~") ? filterTail.slice(filterTail.lastIndexOf("~") + 1) : filterTail; + return `dotnet test ${project}${filter ? ` · filter ${oneLine(filter, 64)}` : ""}`; + } + + const packageScript = command.match(/\b(npm|pnpm|yarn)\s+(?:(run)\s+)?([A-Za-z0-9:_-]+)\b/iu); + if (packageScript) { + return `${packageScript[1]} ${packageScript[2] ? "run " : ""}${packageScript[3]}`; + } + + // Unknown commands are represented by their executable and first operand; + // avoid echoing arbitrary shell arguments into the user-facing renderer. + const tokens = command.match(/(?:"[^"]*"|'[^']*'|[^\s]+)/gu) ?? []; + return tokens.slice(0, 2).map((token) => token.replace(/^['"]|['"]$/gu, "")).join(" ").slice(0, 96); +} + /** Render the v0.1 snapshot as concise native transcript text. */ export function renderTaskFeedback(snapshot: TaskFeedbackSnapshotV01): string { const terminalLabel = snapshot.status === "waiting_for_master" @@ -50,14 +84,19 @@ export function renderTaskFeedback(snapshot: TaskFeedbackSnapshotV01): string { const result = snapshot.result; if (result) { lines.push("", "Agent report:"); - const summary = oneLine(result.summary, 800); + const summary = truncatedOneLine(result.summary, 220); if (summary) lines.push(summary); - lines.push(`Changed: ${result.files_changed.length} files`); + const changedCount = result.files_changed.length; + lines.push(`Changed: ${changedCount} ${changedCount === 1 ? "file" : "files"}`); for (const test of result.tests.slice(0, 3)) { - lines.push(`Tests: ${oneLine(test.command, 240)} · reported ${test.status}`); + lines.push(`Tests: ${testCommandLabel(test.command)} · reported ${test.status}`); } if (result.tests.length > 3) lines.push(`Tests: ${result.tests.length - 3} more reported`); - if (result.issues.length) lines.push(`Issues: ${result.issues.length} reported`); + if (result.issues.length) { + const issue = truncatedOneLine(result.issues[0] ?? "", 140); + lines.push(`! ${result.issues.length} reported ${result.issues.length === 1 ? "issue" : "issues"}${issue ? ` · ${issue}` : ""}`); + if (result.issues.length > 1) lines.push(`! ${result.issues.length - 1} more reported issues`); + } if (result.duration_ms !== null) lines.push(`Duration: ${formatDuration(result.duration_ms)}`); } diff --git a/test/task-feedback.test.ts b/test/task-feedback.test.ts index 2a44b95..9df5f9e 100644 --- a/test/task-feedback.test.ts +++ b/test/task-feedback.test.ts @@ -125,12 +125,36 @@ test("renderer labels report claims and avoids unsupported activity or verificat const output = renderTaskFeedback(snapshot); assert.match(output, /Bridge task completed/); assert.match(output, /Agent report:/); - assert.match(output, /Changed: 1 files/); + assert.match(output, /Changed: 1 file/u); assert.match(output, /npm test · reported passed/); assert.match(output, /Duration: 3m 42s/); assert.doesNotMatch(output, /\bverified\b|\baccepted\b|host acceptance|review pass|must not leak/iu); }); +test("renderer shortens wrapped test commands and long report text", () => { + const longSummary = "Monotonic cleanup deadline implemented. ".repeat(12); + const snapshot = buildTaskFeedbackSnapshotV01({ + status: status({ status: "completed", finished_at: FINISH }), + events: [event("report_ready")], + result: result({ + summary: longSummary, + tests: [{ + command: 'cmd.exe /d /s /c "set HOME=C:\\Users\\Sandy&& set TEMP=C:\\Temp&& dotnet test tests\\LumeCAE.RadianceAdapter.Tests\\LumeCAE.RadianceAdapter.Tests.csproj --no-restore --filter FullyQualifiedName~RadianceDeadlineTests"', + status: "passed", + details: "9/9 passed", + }], + issues: ["Git Bash rewrites cmd.exe switches; the command was rerun with MSYS_NO_PATHCONV=1."], + }), + }); + + const output = renderTaskFeedback(snapshot); + assert.match(output, /dotnet test LumeCAE\.RadianceAdapter\.Tests · filter RadianceDeadlineTests · reported passed/u); + assert.match(output, /! 1 reported issue · Git Bash rewrites cmd\.exe switches/u); + assert.match(output, /…/u); + assert.doesNotMatch(output, /cmd\.exe \/d|HOME=|TEMP=|9\/9 passed/u); + assert.ok(output.length < 600, `expected concise output, got ${output.length} characters`); +}); + test("renderer has Bridge-derived text for every task lifecycle status", () => { const cases = [ ["queued", "○ Queued"],