diff --git a/actions/setup/js/log_parser_shared.cjs b/actions/setup/js/log_parser_shared.cjs index e07d4cc6bc4..ab93a9cc7a6 100644 --- a/actions/setup/js/log_parser_shared.cjs +++ b/actions/setup/js/log_parser_shared.cjs @@ -840,6 +840,28 @@ function convertCopilotEventsToLegacyLogEntries(logEntries) { return ""; }; + // Builds the tool_use `input` object for a Copilot SDK tool event. + // Copilot SDK bash events carry the executed command as a top-level `data.command` + // field rather than nesting it inside `data.input`/`data.parameters`, so fall back + // to it (and merge it in when structured input lacks a command) to avoid dropping + // the command from the rendered summary. + // Pass { includeCommand: false } when normalizing orphaned completion events that + // may still carry structured input but cannot reliably recover the original command. + const buildToolInput = (data, options = {}) => { + const { includeCommand = true } = options; + const base = data.input || data.parameters; + if (base && typeof base === "object" && !Array.isArray(base)) { + if (includeCommand && base.command === undefined && typeof data.command === "string") { + return { ...base, command: data.command }; + } + return base; + } + if (includeCommand && typeof data.command === "string") { + return { command: data.command }; + } + return {}; + }; + for (const entry of logEntries) { if (!entry || typeof entry !== "object") continue; const data = entry.data && typeof entry.data === "object" ? entry.data : {}; @@ -900,7 +922,7 @@ function convertCopilotEventsToLegacyLogEntries(logEntries) { normalizedEntries.push({ type: "assistant", message: { - content: [{ type: "tool_use", id: resolvedToolId, name: toolName, input: data.input || data.parameters || {} }], + content: [{ type: "tool_use", id: resolvedToolId, name: toolName, input: buildToolInput(data) }], }, }); break; @@ -926,7 +948,9 @@ function convertCopilotEventsToLegacyLogEntries(logEntries) { normalizedEntries.push({ type: "assistant", message: { - content: [{ type: "tool_use", id: resolvedToolId, name: toolName, input: data.input || data.parameters || {} }], + // Orphaned completion events have no corresponding start event, so keep + // structured input but do not synthesize a command from completion data. + content: [{ type: "tool_use", id: resolvedToolId, name: toolName, input: buildToolInput(data, { includeCommand: false }) }], }, }); } diff --git a/actions/setup/js/parse_copilot_log.test.cjs b/actions/setup/js/parse_copilot_log.test.cjs index 89f5c991d1b..e03da170ce8 100644 --- a/actions/setup/js/parse_copilot_log.test.cjs +++ b/actions/setup/js/parse_copilot_log.test.cjs @@ -138,6 +138,52 @@ describe("parse_copilot_log.cjs", () => { expect(result.markdown).toContain("file1.txt"); }); + it("renders the bash command from data.command in Copilot CLI events.jsonl", () => { + // Real Copilot SDK events carry the executed command as a top-level data.command + // field on tool.execution_start, not nested inside data.input/data.parameters. + const eventsLog = [ + '{"type":"user.message","timestamp":"2026-06-05T00:44:01.367Z","data":{}}', + '{"type":"tool.execution_start","timestamp":"2026-06-05T00:44:04.520Z","data":{"toolName":"bash","mcpServerName":"","command":"cat /tmp/gh-aw/agent/candidates.txt"}}', + '{"type":"tool.execution_complete","timestamp":"2026-06-05T00:44:04.700Z","data":{"toolName":"bash","mcpServerName":"","success":true,"result":{"content":"candidate-list-output"}}}', + '{"type":"assistant.message","timestamp":"2026-06-05T00:44:59.769Z","data":{"content":"Done"}}', + ].join("\n"); + + const result = parseCopilotLog(eventsLog); + + expect(result.markdown).toContain("bash"); + expect(result.markdown).toContain("cat /tmp/gh-aw/agent/candidates.txt"); + expect(result.markdown).toContain("candidate-list-output"); + }); + + it("merges data.command into existing data.input when command is absent there", () => { + const eventsLog = [ + '{"type":"user.message","timestamp":"2026-06-05T00:44:01.367Z","data":{}}', + '{"type":"tool.execution_start","timestamp":"2026-06-05T00:44:04.520Z","data":{"toolName":"bash","mcpServerName":"","input":{"cwd":"/tmp"},"command":"ls"}}', + '{"type":"tool.execution_complete","timestamp":"2026-06-05T00:44:04.700Z","data":{"toolName":"bash","mcpServerName":"","success":true,"result":{"content":"file1.txt\\nfile2.txt"}}}', + '{"type":"assistant.message","timestamp":"2026-06-05T00:44:59.769Z","data":{"content":"Done"}}', + ].join("\n"); + + const result = parseCopilotLog(eventsLog); + + expect(result.markdown).toContain("ls"); + expect(result.markdown).toContain("/tmp"); + expect(result.markdown).toContain("file1.txt"); + }); + + it("preserves structured input for orphaned completion events without inventing a command", () => { + const eventsLog = [ + '{"type":"user.message","timestamp":"2026-06-05T00:44:01.367Z","data":{}}', + '{"type":"tool.execution_complete","timestamp":"2026-06-05T00:44:04.700Z","data":{"toolName":"bash","mcpServerName":"","input":{"cwd":"/tmp"},"success":true,"result":{"content":"file1.txt\\nfile2.txt"}}}', + '{"type":"assistant.message","timestamp":"2026-06-05T00:44:59.769Z","data":{"content":"Done"}}', + ].join("\n"); + + const result = parseCopilotLog(eventsLog); + + expect(result.markdown).toContain("bash"); + expect(result.markdown).toContain("/tmp"); + expect(result.markdown).toContain("file1.txt"); + }); + it("renders tool output preview from array-based result.content in Copilot CLI events.jsonl", () => { const eventsLog = [ '{"type":"user.message","timestamp":"2026-06-05T00:44:01.367Z","data":{}}',