mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
feat(coding-agent): add messageId and timestamp to markdown transformer context
* feat(coding-agent): add messageId and timestamp to markdown transformer context * fix(coding-agent): update compaction render test for messageMeta argument --------- Co-authored-by: Petr Houška <houskape@gmail.com>
This commit is contained in:
parent
9359adf0c6
commit
64627838b2
7 changed files with 90 additions and 7 deletions
|
|
@ -1542,6 +1542,10 @@ export interface MarkdownTransformContext {
|
|||
messageType: "user" | "assistant" | "assistant-thinking";
|
||||
isStreaming: boolean;
|
||||
availableWidth: number;
|
||||
/** Persisted session-entry id. Undefined for live messages or live rendering; available when rendering from persisted session entries. */
|
||||
messageId?: string;
|
||||
/** Persisted session-entry ISO timestamp. Undefined for live messages or live rendering; available when rendering from persisted session entries. */
|
||||
timestamp?: string;
|
||||
}
|
||||
|
||||
export type MarkdownTransformer = (markdown: string, context: MarkdownTransformContext) => string;
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ export class AssistantMessageComponent extends Container {
|
|||
private lastMessage?: AssistantMessage;
|
||||
private hasToolCalls = false;
|
||||
private isStreaming = false;
|
||||
private messageId?: string;
|
||||
private timestamp?: string;
|
||||
private thinkingVisibilityOverrides = new Map<number, boolean>();
|
||||
|
||||
constructor(
|
||||
|
|
@ -30,6 +32,8 @@ export class AssistantMessageComponent extends Container {
|
|||
hiddenThinkingLabel = "Thinking...",
|
||||
outputPad = 1,
|
||||
markdownTransformers: readonly MarkdownTransformer[] = [],
|
||||
messageId?: string,
|
||||
timestamp?: string,
|
||||
) {
|
||||
super();
|
||||
|
||||
|
|
@ -38,6 +42,8 @@ export class AssistantMessageComponent extends Container {
|
|||
this.hiddenThinkingLabel = hiddenThinkingLabel;
|
||||
this.outputPad = outputPad;
|
||||
this.markdownTransformers = markdownTransformers;
|
||||
this.messageId = messageId;
|
||||
this.timestamp = timestamp;
|
||||
|
||||
// Container for text/thinking content
|
||||
this.contentContainer = new Container();
|
||||
|
|
@ -112,7 +118,13 @@ export class AssistantMessageComponent extends Container {
|
|||
// Set paddingY=0 to avoid extra spacing before tool executions
|
||||
this.contentContainer.addChild(
|
||||
new Markdown(content.text.trim(), this.outputPad, 0, this.markdownTheme, undefined, {
|
||||
transform: createMarkdownTransform("assistant", this.isStreaming, this.markdownTransformers),
|
||||
transform: createMarkdownTransform(
|
||||
"assistant",
|
||||
this.isStreaming,
|
||||
this.markdownTransformers,
|
||||
this.messageId,
|
||||
this.timestamp,
|
||||
),
|
||||
}),
|
||||
);
|
||||
} else if (content.type === "thinking") {
|
||||
|
|
@ -157,6 +169,8 @@ export class AssistantMessageComponent extends Container {
|
|||
"assistant-thinking",
|
||||
this.isStreaming,
|
||||
this.markdownTransformers,
|
||||
this.messageId,
|
||||
this.timestamp,
|
||||
),
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,9 +4,15 @@ export function createMarkdownTransform(
|
|||
messageType: MarkdownTransformContext["messageType"],
|
||||
isStreaming: boolean,
|
||||
transformers: readonly MarkdownTransformer[],
|
||||
messageId?: string,
|
||||
timestamp?: string,
|
||||
): (markdown: string, availableWidth: number) => string {
|
||||
return (markdown, availableWidth) =>
|
||||
applyMarkdownTransformers(markdown, { messageType, isStreaming, availableWidth }, transformers);
|
||||
applyMarkdownTransformers(
|
||||
markdown,
|
||||
{ messageType, isStreaming, availableWidth, messageId, timestamp },
|
||||
transformers,
|
||||
);
|
||||
}
|
||||
|
||||
function applyMarkdownTransformers(
|
||||
|
|
|
|||
|
|
@ -15,18 +15,24 @@ export class UserMessageComponent extends Container {
|
|||
private markdownTheme: MarkdownTheme;
|
||||
private outputPad: number;
|
||||
private markdownTransformers: readonly MarkdownTransformer[];
|
||||
private messageId?: string;
|
||||
private timestamp?: string;
|
||||
|
||||
constructor(
|
||||
text: string,
|
||||
markdownTheme: MarkdownTheme = getMarkdownTheme(),
|
||||
outputPad = 1,
|
||||
markdownTransformers: readonly MarkdownTransformer[] = [],
|
||||
messageId?: string,
|
||||
timestamp?: string,
|
||||
) {
|
||||
super();
|
||||
this.text = text;
|
||||
this.markdownTheme = markdownTheme;
|
||||
this.outputPad = outputPad;
|
||||
this.markdownTransformers = markdownTransformers;
|
||||
this.messageId = messageId;
|
||||
this.timestamp = timestamp;
|
||||
this.rebuild();
|
||||
}
|
||||
|
||||
|
|
@ -52,7 +58,13 @@ export class UserMessageComponent extends Container {
|
|||
{
|
||||
preserveOrderedListMarkers: true,
|
||||
preserveBackslashEscapes: true,
|
||||
transform: createMarkdownTransform("user", false, this.markdownTransformers),
|
||||
transform: createMarkdownTransform(
|
||||
"user",
|
||||
false,
|
||||
this.markdownTransformers,
|
||||
this.messageId,
|
||||
this.timestamp,
|
||||
),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3834,7 +3834,10 @@ export class InteractiveMode {
|
|||
this.chatContainer.addChild(component);
|
||||
}
|
||||
|
||||
private addMessageToChat(message: AgentMessage, options?: { populateHistory?: boolean }): void {
|
||||
private addMessageToChat(
|
||||
message: AgentMessage,
|
||||
options?: { populateHistory?: boolean; messageId?: string; timestamp?: string },
|
||||
): void {
|
||||
switch (message.role) {
|
||||
case "bashExecution": {
|
||||
const component = new BashExecutionComponent(
|
||||
|
|
@ -3917,6 +3920,8 @@ export class InteractiveMode {
|
|||
this.getMarkdownThemeWithSettings(),
|
||||
this.outputPad,
|
||||
this.getMarkdownTransformers(),
|
||||
options?.messageId,
|
||||
options?.timestamp,
|
||||
);
|
||||
this.chatContainer.addChild(userComponent);
|
||||
}
|
||||
|
|
@ -3926,6 +3931,8 @@ export class InteractiveMode {
|
|||
this.getMarkdownThemeWithSettings(),
|
||||
this.outputPad,
|
||||
this.getMarkdownTransformers(),
|
||||
options?.messageId,
|
||||
options?.timestamp,
|
||||
);
|
||||
this.chatContainer.addChild(userComponent);
|
||||
}
|
||||
|
|
@ -3943,6 +3950,8 @@ export class InteractiveMode {
|
|||
this.hiddenThinkingLabel,
|
||||
this.outputPad,
|
||||
this.getMarkdownTransformers(),
|
||||
options?.messageId,
|
||||
options?.timestamp,
|
||||
);
|
||||
this.chatContainer.addChild(assistantComponent);
|
||||
break;
|
||||
|
|
@ -3959,6 +3968,7 @@ export class InteractiveMode {
|
|||
|
||||
private renderSessionItems(
|
||||
items: readonly RenderSessionItem[],
|
||||
messageMeta?: Map<AgentMessage, { id: string; timestamp: string }>,
|
||||
options: { updateFooter?: boolean; populateHistory?: boolean } = {},
|
||||
): void {
|
||||
this.pendingTools.clear();
|
||||
|
|
@ -3991,7 +4001,11 @@ export class InteractiveMode {
|
|||
const message = item;
|
||||
// Assistant messages need special handling for tool calls
|
||||
if (message.role === "assistant") {
|
||||
this.addMessageToChat(message);
|
||||
const meta = messageMeta?.get(message);
|
||||
this.addMessageToChat(message, {
|
||||
messageId: meta?.id,
|
||||
timestamp: meta?.timestamp,
|
||||
});
|
||||
// Render tool call components
|
||||
for (const content of message.content) {
|
||||
if (content.type === "toolCall") {
|
||||
|
|
@ -4041,7 +4055,12 @@ export class InteractiveMode {
|
|||
}
|
||||
} else {
|
||||
// All other messages use standard rendering
|
||||
this.addMessageToChat(message, options);
|
||||
const meta = messageMeta?.get(message);
|
||||
this.addMessageToChat(message, {
|
||||
...options,
|
||||
messageId: meta?.id,
|
||||
timestamp: meta?.timestamp,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -4063,17 +4082,21 @@ export class InteractiveMode {
|
|||
): void {
|
||||
// Selection coordinates point into the transcript being replaced (#9311).
|
||||
if (this.renderer instanceof TuiAltScreen) this.renderer.resetTextSelection();
|
||||
const messageMeta = new Map<AgentMessage, { id: string; timestamp: string }>();
|
||||
const items = entries.flatMap((entry): RenderSessionItem[] => {
|
||||
if (entry.type === "custom" || (entry.type === "usage" && entry.kind === "cache_warm")) {
|
||||
return [entry];
|
||||
}
|
||||
const messages = sessionEntryToContextMessages(entry);
|
||||
for (const msg of messages) {
|
||||
messageMeta.set(msg, { id: entry.id, timestamp: entry.timestamp });
|
||||
}
|
||||
if ((entry.type === "compaction" || entry.type === "branch_summary") && entry.usage && messages.length > 0) {
|
||||
return [...messages, { type: "compaction_cost", kind: entry.type, usage: entry.usage }];
|
||||
}
|
||||
return messages;
|
||||
});
|
||||
this.renderSessionItems(items, options);
|
||||
this.renderSessionItems(items, messageMeta, options);
|
||||
}
|
||||
|
||||
private addCacheWarmingUsage(entry: UsageEntry): void {
|
||||
|
|
|
|||
|
|
@ -102,8 +102,15 @@ describe("InteractiveMode compaction events", () => {
|
|||
expect.objectContaining({ role: "compactionSummary", summary: "previous summary" }),
|
||||
{ type: "compaction_cost", kind: "compaction", usage: previousUsage },
|
||||
],
|
||||
expect.any(Map),
|
||||
{},
|
||||
);
|
||||
const [items, messageMeta] = fakeThis.renderSessionItems.mock.calls[0] as [
|
||||
unknown[],
|
||||
Map<unknown, { id: string; timestamp: string }>,
|
||||
];
|
||||
expect(messageMeta.get(items[0])).toEqual({ id: "current", timestamp: "2025-01-02T00:00:00Z" });
|
||||
expect(messageMeta.get(items[2])).toEqual({ id: "previous", timestamp: "2025-01-01T00:00:00Z" });
|
||||
});
|
||||
|
||||
test("renders retained entries and appends the latest summary cost at the bottom", async () => {
|
||||
|
|
|
|||
|
|
@ -55,4 +55,21 @@ describe("UserMessageComponent", () => {
|
|||
|
||||
expect(stripAnsi(component.render(80).join("\n"))).toContain("Message after");
|
||||
});
|
||||
|
||||
test("passes persisted entry identity in transformer context; live leaves it undefined", () => {
|
||||
initTheme("dark");
|
||||
const captured: Array<{ messageId?: string; timestamp?: string }> = [];
|
||||
const capture = (_markdown: string, context: { messageId?: string; timestamp?: string }) => {
|
||||
captured.push({ messageId: context.messageId, timestamp: context.timestamp });
|
||||
return _markdown;
|
||||
};
|
||||
|
||||
new UserMessageComponent("restored", undefined, 1, [capture], "entry-abc", "2025-01-15T10:30:00.000Z").render(80);
|
||||
new UserMessageComponent("live", undefined, 1, [capture]).render(80);
|
||||
|
||||
expect(captured).toEqual([
|
||||
{ messageId: "entry-abc", timestamp: "2025-01-15T10:30:00.000Z" },
|
||||
{ messageId: undefined, timestamp: undefined },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue