fix(coding-agent): add entry renderers for session entries
This commit is contained in:
@@ -335,12 +335,15 @@ describe("extensions discovery", () => {
|
||||
expect(result.extensions[0].tools.has("parse_duration")).toBe(true);
|
||||
});
|
||||
|
||||
it("registers message renderers", async () => {
|
||||
it("registers message and entry renderers", async () => {
|
||||
const extCode = `
|
||||
export default function(pi) {
|
||||
pi.registerMessageRenderer("my-custom-type", (message, options, theme) => {
|
||||
return null; // Use default rendering
|
||||
});
|
||||
pi.registerEntryRenderer("my-entry-type", (entry, options, theme) => {
|
||||
return null;
|
||||
});
|
||||
}
|
||||
`;
|
||||
fs.writeFileSync(path.join(extensionsDir, "with-renderer.ts"), extCode);
|
||||
@@ -350,6 +353,7 @@ describe("extensions discovery", () => {
|
||||
expect(result.errors).toHaveLength(0);
|
||||
expect(result.extensions).toHaveLength(1);
|
||||
expect(result.extensions[0].messageRenderers.has("my-custom-type")).toBe(true);
|
||||
expect(result.extensions[0].entryRenderers?.has("my-entry-type")).toBe(true);
|
||||
});
|
||||
|
||||
it("reports error when extension throws during initialization", async () => {
|
||||
|
||||
@@ -560,7 +560,7 @@ describe("ExtensionRunner", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("message renderers", () => {
|
||||
describe("message and entry renderers", () => {
|
||||
it("gets message renderer by type", async () => {
|
||||
const extCode = `
|
||||
export default function(pi) {
|
||||
@@ -578,6 +578,21 @@ describe("ExtensionRunner", () => {
|
||||
const missing = runner.getMessageRenderer("not-exists");
|
||||
expect(missing).toBeUndefined();
|
||||
});
|
||||
|
||||
it("gets entry renderer by type", async () => {
|
||||
const extCode = `
|
||||
export default function(pi) {
|
||||
pi.registerEntryRenderer("my-entry", (entry, options, theme) => null);
|
||||
}
|
||||
`;
|
||||
fs.writeFileSync(path.join(extensionsDir, "entry-renderer.ts"), extCode);
|
||||
|
||||
const result = await discoverAndLoadExtensions([], tempDir, tempDir);
|
||||
const runner = new ExtensionRunner(result.extensions, result.runtime, tempDir, sessionManager, modelRegistry);
|
||||
|
||||
expect(runner.getEntryRenderer("my-entry")).toBeDefined();
|
||||
expect(runner.getEntryRenderer("not-exists")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("flags", () => {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
type BranchSummaryEntry,
|
||||
buildContextEntries,
|
||||
buildSessionContext,
|
||||
type CompactionEntry,
|
||||
type CustomEntry,
|
||||
type ModelChangeEntry,
|
||||
type SessionEntry,
|
||||
type SessionMessageEntry,
|
||||
@@ -52,6 +54,10 @@ function branchSummary(id: string, parentId: string | null, summary: string, fro
|
||||
return { type: "branch_summary", id, parentId, timestamp: "2025-01-01T00:00:00Z", summary, fromId };
|
||||
}
|
||||
|
||||
function custom(id: string, parentId: string | null, customType: string, data?: unknown): CustomEntry {
|
||||
return { type: "custom", id, parentId, timestamp: "2025-01-01T00:00:00Z", customType, data };
|
||||
}
|
||||
|
||||
function thinkingLevel(id: string, parentId: string | null, level: string): ThinkingLevelChangeEntry {
|
||||
return { type: "thinking_level_change", id, parentId, timestamp: "2025-01-01T00:00:00Z", thinkingLevel: level };
|
||||
}
|
||||
@@ -169,6 +175,37 @@ describe("buildSessionContext", () => {
|
||||
expect(ctx.messages).toHaveLength(4);
|
||||
expect((ctx.messages[0] as any).summary).toContain("Second summary");
|
||||
});
|
||||
|
||||
it("buildContextEntries returns compaction-aware entries including custom entries", () => {
|
||||
const entries: SessionEntry[] = [
|
||||
msg("1", null, "user", "first"),
|
||||
custom("2", "1", "old-state", { hidden: true }),
|
||||
msg("3", "2", "assistant", "response1"),
|
||||
custom("4", "3", "kept-card", { title: "Kept" }),
|
||||
msg("5", "4", "user", "second"),
|
||||
compaction("6", "5", "Summary", "4"),
|
||||
custom("7", "6", "after-card", { title: "After" }),
|
||||
msg("8", "7", "assistant", "response2"),
|
||||
];
|
||||
|
||||
expect(buildContextEntries(entries).map((entry) => entry.id)).toEqual(["6", "4", "5", "7", "8"]);
|
||||
const ctx = buildSessionContext(entries);
|
||||
expect(ctx.messages.map((message) => message.role)).toEqual(["compactionSummary", "user", "assistant"]);
|
||||
});
|
||||
|
||||
it("keeps settings from the full path after compaction", () => {
|
||||
const entries: SessionEntry[] = [
|
||||
msg("1", null, "user", "first"),
|
||||
thinkingLevel("2", "1", "high"),
|
||||
msg("3", "2", "assistant", "response1"),
|
||||
msg("4", "3", "user", "second"),
|
||||
compaction("5", "4", "Summary", "4"),
|
||||
];
|
||||
|
||||
const ctx = buildSessionContext(entries);
|
||||
expect(ctx.thinkingLevel).toBe("high");
|
||||
expect(ctx.messages.map((message) => message.role)).toEqual(["compactionSummary", "user"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with branches", () => {
|
||||
|
||||
+35
-19
@@ -3,7 +3,7 @@ import type { AssistantMessage, ToolResultMessage, Usage } from "@earendil-works
|
||||
import { Container, Text, type TUI } from "@earendil-works/pi-tui";
|
||||
import { beforeAll, describe, expect, test, vi } from "vitest";
|
||||
import type { AgentSessionEvent } from "../../../src/core/agent-session.ts";
|
||||
import type { SessionContext } from "../../../src/core/session-manager.ts";
|
||||
import type { SessionEntry } from "../../../src/core/session-manager.ts";
|
||||
import type { ToolExecutionComponent } from "../../../src/modes/interactive/components/tool-execution.ts";
|
||||
import { InteractiveMode } from "../../../src/modes/interactive/interactive-mode.ts";
|
||||
import { initTheme } from "../../../src/modes/interactive/theme/theme.ts";
|
||||
@@ -27,6 +27,12 @@ const EMPTY_USAGE: Usage = {
|
||||
},
|
||||
};
|
||||
|
||||
type RenderSessionItems = (
|
||||
this: RenderSessionContextThis,
|
||||
items: AgentMessage[],
|
||||
options?: { updateFooter?: boolean; populateHistory?: boolean },
|
||||
) => void;
|
||||
|
||||
type RenderSessionContextThis = {
|
||||
pendingTools: Map<string, ToolExecutionComponent>;
|
||||
chatContainer: Container;
|
||||
@@ -43,11 +49,12 @@ type RenderSessionContextThis = {
|
||||
updateEditorBorderColor(): void;
|
||||
getRegisteredToolDefinition(toolName: string): undefined;
|
||||
addMessageToChat(message: AgentMessage, options?: { populateHistory?: boolean }): void;
|
||||
renderSessionItems: RenderSessionItems;
|
||||
};
|
||||
|
||||
type RenderSessionContext = (
|
||||
type RenderSessionEntries = (
|
||||
this: RenderSessionContextThis,
|
||||
sessionContext: SessionContext,
|
||||
entries: SessionEntry[],
|
||||
options?: { updateFooter?: boolean; populateHistory?: boolean },
|
||||
) => void;
|
||||
|
||||
@@ -70,6 +77,8 @@ function createFakeInteractiveModeThis(): RenderSessionContextThis {
|
||||
isInitialized: true,
|
||||
updateEditorBorderColor: vi.fn(),
|
||||
getRegisteredToolDefinition: (_toolName: string) => undefined,
|
||||
renderSessionItems: (InteractiveMode.prototype as unknown as { renderSessionItems: RenderSessionItems })
|
||||
.renderSessionItems,
|
||||
addMessageToChat(message: AgentMessage) {
|
||||
chatContainer.addChild(new Text(message.role, 0, 0));
|
||||
},
|
||||
@@ -107,31 +116,38 @@ function createToolResultMessage(text: string): ToolResultMessage {
|
||||
};
|
||||
}
|
||||
|
||||
function createSessionContext(messages: AgentMessage[]): SessionContext {
|
||||
return {
|
||||
messages,
|
||||
thinkingLevel: "off",
|
||||
model: null,
|
||||
};
|
||||
function createSessionEntries(messages: AgentMessage[]): SessionEntry[] {
|
||||
let parentId: string | null = null;
|
||||
return messages.map((message, index) => {
|
||||
const entry: SessionEntry = {
|
||||
type: "message",
|
||||
id: `entry-${index}`,
|
||||
parentId,
|
||||
timestamp: new Date().toISOString(),
|
||||
message,
|
||||
};
|
||||
parentId = entry.id;
|
||||
return entry;
|
||||
});
|
||||
}
|
||||
|
||||
function renderChat(container: Container): string {
|
||||
return stripAnsi(container.render(120).join("\n"));
|
||||
}
|
||||
|
||||
describe("InteractiveMode.renderSessionContext", () => {
|
||||
describe("InteractiveMode.renderSessionEntries", () => {
|
||||
beforeAll(() => {
|
||||
initTheme("dark");
|
||||
});
|
||||
|
||||
test("keeps unresolved rendered tool calls registered for live completion events", async () => {
|
||||
const fakeThis = createFakeInteractiveModeThis();
|
||||
const renderSessionContext = (
|
||||
InteractiveMode.prototype as unknown as { renderSessionContext: RenderSessionContext }
|
||||
).renderSessionContext;
|
||||
const renderSessionEntries = (
|
||||
InteractiveMode.prototype as unknown as { renderSessionEntries: RenderSessionEntries }
|
||||
).renderSessionEntries;
|
||||
const handleEvent = (InteractiveMode.prototype as unknown as { handleEvent: HandleEvent }).handleEvent;
|
||||
|
||||
renderSessionContext.call(fakeThis, createSessionContext([createAssistantToolCallMessage()]));
|
||||
renderSessionEntries.call(fakeThis, createSessionEntries([createAssistantToolCallMessage()]));
|
||||
|
||||
expect(fakeThis.pendingTools.has(TOOL_CALL_ID)).toBe(true);
|
||||
|
||||
@@ -149,13 +165,13 @@ describe("InteractiveMode.renderSessionContext", () => {
|
||||
|
||||
test("does not keep completed historical tool calls registered as pending", () => {
|
||||
const fakeThis = createFakeInteractiveModeThis();
|
||||
const renderSessionContext = (
|
||||
InteractiveMode.prototype as unknown as { renderSessionContext: RenderSessionContext }
|
||||
).renderSessionContext;
|
||||
const renderSessionEntries = (
|
||||
InteractiveMode.prototype as unknown as { renderSessionEntries: RenderSessionEntries }
|
||||
).renderSessionEntries;
|
||||
|
||||
renderSessionContext.call(
|
||||
renderSessionEntries.call(
|
||||
fakeThis,
|
||||
createSessionContext([createAssistantToolCallMessage(), createToolResultMessage("HISTORICAL_RESULT")]),
|
||||
createSessionEntries([createAssistantToolCallMessage(), createToolResultMessage("HISTORICAL_RESULT")]),
|
||||
);
|
||||
|
||||
expect(fakeThis.pendingTools.size).toBe(0);
|
||||
|
||||
Reference in New Issue
Block a user