Refine issue chat activity and message chrome

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
dotta 2026-04-06 11:00:12 -05:00
parent 3fea60c04c
commit f593e116c1
7 changed files with 446 additions and 167 deletions

View file

@ -85,6 +85,50 @@ describe("buildAssistantPartsFromTranscript", () => {
});
expect(result.notices).toEqual(["warn: noisy setup output"]);
});
it("preserves transcript ordering when text and tool activity are interleaved", () => {
const result = buildAssistantPartsFromTranscript([
{ kind: "assistant", ts: "2026-04-06T12:00:00.000Z", text: "First." },
{
kind: "tool_call",
ts: "2026-04-06T12:00:01.000Z",
name: "read_file",
toolUseId: "tool-1",
input: { path: "ui/src/components/IssueChatThread.tsx" },
},
{ kind: "assistant", ts: "2026-04-06T12:00:02.000Z", text: "Second." },
{
kind: "tool_result",
ts: "2026-04-06T12:00:03.000Z",
toolUseId: "tool-1",
content: "ok",
isError: false,
},
{ kind: "thinking", ts: "2026-04-06T12:00:04.000Z", text: "Need one more check." },
{
kind: "tool_call",
ts: "2026-04-06T12:00:05.000Z",
name: "write_file",
toolUseId: "tool-2",
input: { path: "ui/src/lib/issue-chat-messages.ts" },
},
{
kind: "tool_result",
ts: "2026-04-06T12:00:06.000Z",
toolUseId: "tool-2",
content: "saved",
isError: false,
},
]);
expect(result.parts).toMatchObject([
{ type: "text", text: "First." },
{ type: "tool-call", toolCallId: "tool-1", toolName: "read_file", result: "ok" },
{ type: "text", text: "Second." },
{ type: "reasoning", text: "Need one more check." },
{ type: "tool-call", toolCallId: "tool-2", toolName: "write_file", result: "saved" },
]);
});
});
describe("buildIssueChatMessages", () => {

View file

@ -268,58 +268,46 @@ function createHistoricalRunMessage(run: IssueChatLinkedRun, agentMap?: Map<stri
return message;
}
function mergeAdjacentTextParts(parts: Array<TextMessagePart | ReasoningMessagePart>) {
const merged: Array<TextMessagePart | ReasoningMessagePart> = [];
for (const part of parts) {
const previous = merged.at(-1);
if (previous && previous.type === part.type && previous.parentId === part.parentId) {
merged[merged.length - 1] = {
...previous,
text: `${previous.text}${part.text}`,
};
continue;
}
merged.push(part);
}
return merged;
}
export function buildAssistantPartsFromTranscript(entries: readonly IssueChatTranscriptEntry[]) {
const textLikeParts: Array<TextMessagePart | ReasoningMessagePart> = [];
const orderedParts: Array<TextMessagePart | ReasoningMessagePart | ToolCallMessagePart<JsonObject, unknown>> = [];
const toolParts = new Map<string, ToolCallMessagePart<JsonObject, unknown>>();
const toolOrder: string[] = [];
const toolIndices = new Map<string, number>();
const notices: string[] = [];
for (const [index, entry] of entries.entries()) {
if (entry.kind === "assistant" && entry.text) {
textLikeParts.push({ type: "text", text: entry.text });
orderedParts.push({ type: "text", text: entry.text });
continue;
}
if (entry.kind === "thinking" && entry.text) {
textLikeParts.push({ type: "reasoning", text: entry.text });
orderedParts.push({ type: "reasoning", text: entry.text });
continue;
}
if (entry.kind === "tool_call") {
const toolCallId = entry.toolUseId || `tool-${index}`;
if (!toolParts.has(toolCallId)) {
toolOrder.push(toolCallId);
}
toolParts.set(toolCallId, {
const nextPart: ToolCallMessagePart<JsonObject, unknown> = {
type: "tool-call",
toolCallId,
toolName: entry.name || "tool",
args: normalizeToolArgs(entry.input),
argsText: stringifyUnknown(entry.input),
});
};
if (!toolParts.has(toolCallId)) {
toolIndices.set(toolCallId, orderedParts.length);
orderedParts.push(nextPart);
} else {
const existingIndex = toolIndices.get(toolCallId);
if (existingIndex !== undefined) {
orderedParts[existingIndex] = nextPart;
}
}
toolParts.set(toolCallId, nextPart);
continue;
}
if (entry.kind === "tool_result") {
const toolCallId = entry.toolUseId || `tool-result-${index}`;
const existing = toolParts.get(toolCallId);
if (!existing) {
toolOrder.push(toolCallId);
}
toolParts.set(toolCallId, {
const nextPart: ToolCallMessagePart<JsonObject, unknown> = {
type: "tool-call",
toolCallId,
toolName: existing?.toolName || entry.toolName || "tool",
@ -327,7 +315,17 @@ export function buildAssistantPartsFromTranscript(entries: readonly IssueChatTra
argsText: existing?.argsText ?? "",
result: entry.content ?? "",
isError: entry.isError === true,
});
};
if (existing) {
const existingIndex = toolIndices.get(toolCallId);
if (existingIndex !== undefined) {
orderedParts[existingIndex] = nextPart;
}
} else {
toolIndices.set(toolCallId, orderedParts.length);
orderedParts.push(nextPart);
}
toolParts.set(toolCallId, nextPart);
continue;
}
if (entry.kind === "stderr" && entry.text) {
@ -347,13 +345,25 @@ export function buildAssistantPartsFromTranscript(entries: readonly IssueChatTra
}
}
const mergedParts: Array<TextMessagePart | ReasoningMessagePart | ToolCallMessagePart<JsonObject, unknown>> = [];
for (const part of orderedParts) {
if (part.type === "tool-call") {
mergedParts.push(part);
continue;
}
const previous = mergedParts.at(-1);
if (previous && previous.type === part.type && previous.parentId === part.parentId) {
mergedParts[mergedParts.length - 1] = {
...previous,
text: `${previous.text}${part.text}`,
};
continue;
}
mergedParts.push(part);
}
return {
parts: [
...mergeAdjacentTextParts(textLikeParts),
...toolOrder
.map((toolCallId) => toolParts.get(toolCallId))
.filter((part): part is ToolCallMessagePart<JsonObject, unknown> => Boolean(part)),
],
parts: mergedParts,
notices,
};
}