fix: 展示真实文件编辑事件

This commit is contained in:
root
2026-07-12 18:06:39 +02:00
parent b687d133b5
commit 89ef9cdbfb
5 changed files with 159 additions and 4 deletions
+13 -2
View File
@@ -653,7 +653,8 @@ export function mapAgentRunEvent(event, mapping = {}) {
outputSummary: output,
message: output || command || textPayload(payload, "tool call"),
outputBytes: typeof payload.outputBytes === "number" ? payload.outputBytes : payload.summary?.outputBytes,
outputTruncated: payload.outputTruncated === true || payload.summary?.outputTruncated === true
outputTruncated: payload.outputTruncated === true || payload.summary?.outputTruncated === true,
...(Array.isArray(payload.changes) ? { changes: payload.changes, fileCount: Number.isInteger(payload.fileCount) ? payload.fileCount : null } : {})
};
}
if (event.type === "command_output") {
@@ -661,7 +662,17 @@ export function mapAgentRunEvent(event, mapping = {}) {
return { ...base, type: "output", eventType: "status", status: "running", label: `agentrun:output:${stream}`, stream, message: textPayload(payload, ""), outputTruncated: payload.outputTruncated === true };
}
if (event.type === "diff") {
return { ...base, type: "diff", eventType: "status", status: "running", label: "agentrun:diff", message: textPayload(payload, "diff") };
return {
...base,
type: "diff",
eventType: "diff",
status: String(payload.status ?? "updated"),
label: "agentrun:diff",
message: String(payload.diff ?? textPayload(payload, "diff")),
diff: String(payload.diff ?? ""),
threadId: payload.threadId ?? null,
turnId: payload.turnId ?? null
};
}
if (event.type === "error") {
const failureKind = normalizedFailureKind(payload.failureKind ?? payload.errorCode ?? "backend") ?? "backend";
+37
View File
@@ -103,6 +103,43 @@ test("projects AgentRun assistant_message Kafka event into HWLAB trace event", (
assert.equal(projected.valuesPrinted, false);
});
test("projects AgentRun fileChange and diff events without dropping paths or unified diff", () => {
const changes = [
{ path: "src/example.ts", kind: "update", diff: "@@ -1 +1 @@\n-export const value = 1;\n+export const value = 2;" },
{ path: "docs/说明.md", kind: "add", diff: "@@ -0,0 +1 @@\n+# 文件编辑可见" }
];
const fileChange = projectAgentRunKafkaEventToHwlabEvent({
schema: "agentrun.event.v1",
eventType: "agentrun.event.committed",
eventId: "evt_file_change_completed",
traceId: "trc_file_change",
hwlabSessionId: "ses_file_change",
runId: "run_file_change",
commandId: "cmd_file_change",
event: { id: "evt_file_change_completed", seq: 21, type: "tool_call", payload: { method: "item/completed", itemId: "item_file_change", type: "fileChange", toolName: "fileChange", status: "completed", changes, fileCount: 2 } }
}, { source: "hwlab-test", sourceTopic: "agentrun.event.debug.v1", sourceOffset: "21" });
assert.equal(fileChange.event.type, "tool");
assert.equal(fileChange.event.toolName, "fileChange");
assert.equal(fileChange.event.itemId, "item_file_change");
assert.deepEqual(fileChange.event.changes, changes);
assert.equal(fileChange.event.fileCount, 2);
const diff = projectAgentRunKafkaEventToHwlabEvent({
schema: "agentrun.event.v1",
eventType: "agentrun.event.committed",
eventId: "evt_turn_diff_updated",
traceId: "trc_file_change",
hwlabSessionId: "ses_file_change",
runId: "run_file_change",
commandId: "cmd_file_change",
event: { id: "evt_turn_diff_updated", seq: 22, type: "diff", payload: { phase: "turn/diff/updated", threadId: "thread_file_change", turnId: "turn_file_change", status: "updated", diff: changes.map((change) => change.diff).join("\n") } }
}, { source: "hwlab-test", sourceTopic: "agentrun.event.debug.v1", sourceOffset: "22" });
assert.equal(diff.event.type, "diff");
assert.equal(diff.event.eventType, "diff");
assert.equal(diff.event.threadId, "thread_file_change");
assert.match(diff.event.diff, /docs\/\.md|/u);
});
test("projects AgentRun assistant_progress as an identity-preserving HWLAB assistant lifecycle event", () => {
const text = "p".repeat(500);
const projected = projectAgentRunKafkaEventToHwlabEvent({
+15
View File
@@ -1444,6 +1444,21 @@ function mapAgentRunSourceEventToHwlabEvent({ input, sourceEvent, payload, run,
commandTruncated: payload.commandTruncated === true,
outputBytes: integerValue(payload.outputBytes ?? payload.summary?.outputBytes),
outputTruncated: payload.outputTruncated === true || payload.summary?.outputTruncated === true,
...(Array.isArray(payload.changes) ? { changes: payload.changes, fileCount: integerValue(payload.fileCount) } : {}),
terminal: false
};
}
if (type === "diff") {
return {
...base,
type: "diff",
eventType: "diff",
status: firstText(payload.status) || "updated",
label: "agentrun:diff",
message: firstText(payload.diff) || "",
diff: firstText(payload.diff) || "",
threadId: firstText(payload.threadId),
turnId: firstText(payload.turnId),
terminal: false
};
}
+45
View File
@@ -517,6 +517,51 @@ test("shared tool summary model keeps status semantic and visible content status
assert.match(markdown, /aria-label="/u);
});
test("fileChange and turn diff render real paths and unified diff as valid Markdown cards", () => {
const fileDiff = "@@ -1 +1 @@\n-export const value = 1;\n+export const value = 2;";
const aggregateDiff = `${fileDiff}\n@@ -0,0 +1 @@\n+# 文件编辑可见`;
const rows = traceDisplayRows({ eventSource: "hwlab-kafka-sse" }, [
{
sourceSeq: 21,
sourceEventId: "evt_file_change_completed",
type: "tool_call",
eventType: "tool_call",
status: "completed",
label: "agentrun:tool:fileChange",
toolName: "fileChange",
toolType: "fileChange",
itemId: "item_file_change",
changes: [
{ path: "src/example.ts", kind: "update", diff: fileDiff },
{ path: "docs/说明.md", kind: "add", diff: "@@ -0,0 +1 @@\n+# 文件编辑可见" }
],
createdAt: "2026-07-12T15:00:01.000Z"
},
{
sourceSeq: 22,
sourceEventId: "evt_turn_diff_updated",
type: "diff",
eventType: "diff",
status: "updated",
label: "agentrun:diff",
diff: aggregateDiff,
message: aggregateDiff,
createdAt: "2026-07-12T15:00:02.000Z"
}
]);
const markdown = renderTraceRowsMarkdown(rows);
assert.equal(rows.length, 2);
assert.match(markdown, /fileChange src\/example\.ts, docs\/\.md/u);
assert.match(markdown, /path: src\/example\.ts/u);
assert.match(markdown, /path: docs\/\.md/u);
assert.match(markdown, /\+export const value = 2;/u);
assert.match(markdown, /turn\/diff\/updated/u);
assert.match(markdown, /\+# /u);
assert.equal((markdown.match(/<details>/gu) ?? []).length, 2);
assert.equal((markdown.match(/<\/details>/gu) ?? []).length, 2);
assert.equal((markdown.match(/```text/gu) ?? []).length, 2);
});
test("outer final response suppression follows formal assistant identity instead of equal text", () => {
const finalText = "Hi. What do you want to work on?";
const rows = traceDisplayRows({ eventSource: "hwlab-kafka-sse" }, [
+49 -2
View File
@@ -68,6 +68,10 @@ export function traceDisplayRows(trace: Record<string, unknown> = {}, events: Tr
rows.push(traceToolCallRow(event, displayOptions));
continue;
}
if (isDiffTraceEvent(event)) {
rows.push(traceDiffRow(event, displayOptions));
continue;
}
if (isRequestTraceEvent(event)) {
continue;
}
@@ -245,7 +249,7 @@ function traceToolCallRow(event: TraceEvent, options: ResolvedTraceDisplayRowsOp
const command = cleanShellCommand(event.command);
const toolState = traceToolState(event);
const toolName = traceToolName(event, command);
const body = traceToolCallBody(event, command);
const body = isFileChangeTraceEvent(event) ? traceFileChangeBody(event) : traceToolCallBody(event, command);
const eventKey = traceEventIdentityToken(event, options.sequenceAuthority);
return {
rowId: `tool:${event.itemId ?? eventKey ?? `${event.label ?? event.type ?? "tool"}:${event.createdAt ?? "unknown"}`}`,
@@ -256,7 +260,50 @@ function traceToolCallRow(event: TraceEvent, options: ResolvedTraceDisplayRowsOp
statusLabel: traceToolStatusLabel(toolState),
header: traceEventClock(event, options),
body,
preview: traceToolPreview(command, toolName),
preview: isFileChangeTraceEvent(event) ? traceFileChangePreview(event) : traceToolPreview(command, toolName),
bodyFormat: "text"
};
}
function isFileChangeTraceEvent(event: TraceEvent): boolean {
return nonEmptyString(event.toolName ?? event.toolType) === "fileChange";
}
function traceFileChangePreview(event: TraceEvent): string {
const paths = (Array.isArray(event.changes) ? event.changes : [])
.filter(isRecord)
.map((change) => nonEmptyString(change.path))
.filter((path): path is string => Boolean(path));
return compactTraceOneLine(paths.length > 0 ? `fileChange ${paths.join(", ")}` : "fileChange", 140);
}
function traceFileChangeBody(event: TraceEvent): string | null {
const changes = (Array.isArray(event.changes) ? event.changes : []).filter(isRecord);
if (changes.length === 0) return null;
return changes.map((change) => {
const path = nonEmptyString(change.path) ?? "(path missing)";
const kind = nonEmptyString(change.kind) ?? "update";
const diff = nonEmptyString(change.diff) ?? "";
return [`path: ${path}`, `kind: ${kind}`, "diff:", diff].filter(Boolean).join("\n");
}).join("\n\n");
}
function isDiffTraceEvent(event: TraceEvent): boolean {
return event.type === "diff" || event.eventType === "diff" || nonEmptyString(event.label) === "agentrun:diff";
}
function traceDiffRow(event: TraceEvent, options: ResolvedTraceDisplayRowsOptions): TraceEventRow {
const eventKey = traceEventIdentityToken(event, options.sequenceAuthority);
return {
rowId: `tool:diff:${eventKey ?? event.createdAt ?? "updated"}`,
seq: traceEventDisplaySeq(event, options.sequenceAuthority),
sequenceAuthority: options.sequenceAuthority,
tone: traceEventTone(event),
toolState: "success",
statusLabel: "文件 diff 已更新",
header: traceEventClock(event, options),
body: nonEmptyString(event.diff ?? event.message),
preview: "turn/diff/updated",
bodyFormat: "text"
};
}