Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions actions/setup/js/log_parser_shared.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 : {};
Expand Down Expand Up @@ -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;
Expand All @@ -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 }) }],
},
});
}
Expand Down
46 changes: 46 additions & 0 deletions actions/setup/js/parse_copilot_log.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] The regression test covers the happy path (command present on tool.execution_start), but misses the merge case: when both data.input (or data.parameters) and data.command are present and base.command is undefined. Adding a test for that branch would prevent a silent regression if the merge logic changes.

💡 Suggested additional test
it('merges data.command into existing data.input when base.command is absent', () => {
  const eventsLog = [
    '{"type":"user.message","data":{}}',
    '{"type":"tool.execution_start","data":{"toolName":"bash","mcpServerName":"","input":{"cwd":"/tmp"},"command":"ls"}}',
    '{"type":"tool.execution_complete","data":{"toolName":"bash","mcpServerName":"","success":true,"result":{"content":"out"}}}',
    '{"type":"assistant.message","data":{"content":"Done"}}',
  ].join('\n');

  const result = parseCopilotLog(eventsLog);
  expect(result.markdown).toContain('ls');
  expect(result.markdown).toContain('/tmp'); // cwd from input preserved
});

@copilot please address this.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 1538d63. Added regression coverage for the merge path where structured input is preserved and data.command is merged in when input.command is absent.

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":{}}',
Expand Down
Loading