fix(coding-agent): add entry renderers for session entries
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
import type { Component } from "@earendil-works/pi-tui";
|
||||
import { Box, Container, Spacer, Text } from "@earendil-works/pi-tui";
|
||||
import type { EntryRenderer } from "../../../core/extensions/types.ts";
|
||||
import type { CustomEntry } from "../../../core/session-manager.ts";
|
||||
import { theme } from "../theme/theme.ts";
|
||||
|
||||
/**
|
||||
* Component that renders a custom session entry from extensions.
|
||||
* The host owns transcript spacing; renderer output should provide only its content.
|
||||
*/
|
||||
export class CustomEntryComponent extends Container {
|
||||
private entry: CustomEntry<unknown>;
|
||||
private renderer: EntryRenderer;
|
||||
private customComponent?: Component;
|
||||
private _expanded = false;
|
||||
|
||||
constructor(entry: CustomEntry<unknown>, renderer: EntryRenderer) {
|
||||
super();
|
||||
this.entry = entry;
|
||||
this.renderer = renderer;
|
||||
this.rebuild();
|
||||
}
|
||||
|
||||
hasContent(): boolean {
|
||||
return this.customComponent !== undefined;
|
||||
}
|
||||
|
||||
setExpanded(expanded: boolean): void {
|
||||
if (this._expanded !== expanded) {
|
||||
this._expanded = expanded;
|
||||
this.rebuild();
|
||||
}
|
||||
}
|
||||
|
||||
override invalidate(): void {
|
||||
super.invalidate();
|
||||
this.rebuild();
|
||||
}
|
||||
|
||||
private rebuild(): void {
|
||||
this.clear();
|
||||
this.customComponent = undefined;
|
||||
|
||||
let component: Component | undefined;
|
||||
try {
|
||||
component = this.renderer(this.entry, { expanded: this._expanded }, theme);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
|
||||
box.addChild(new Text(theme.fg("error", `[${this.entry.customType}] renderer failed: ${message}`), 0, 0));
|
||||
component = box;
|
||||
}
|
||||
|
||||
if (!component) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.customComponent = component;
|
||||
this.addChild(new Spacer(1));
|
||||
this.addChild(component);
|
||||
}
|
||||
}
|
||||
@@ -81,7 +81,7 @@ import { DefaultPackageManager } from "../../core/package-manager.ts";
|
||||
import { BUILT_IN_PROVIDER_DISPLAY_NAMES } from "../../core/provider-display-names.ts";
|
||||
import type { ResourceDiagnostic } from "../../core/resource-loader.ts";
|
||||
import { formatMissingSessionCwdPrompt, MissingSessionCwdError } from "../../core/session-cwd.ts";
|
||||
import { type SessionContext, SessionManager } from "../../core/session-manager.ts";
|
||||
import { type SessionEntry, SessionManager, sessionEntryToContextMessages } from "../../core/session-manager.ts";
|
||||
import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.ts";
|
||||
import type { SourceInfo } from "../../core/source-info.ts";
|
||||
import { isInstallTelemetryEnabled } from "../../core/telemetry.ts";
|
||||
@@ -103,6 +103,7 @@ import { BorderedLoader } from "./components/bordered-loader.ts";
|
||||
import { BranchSummaryMessageComponent } from "./components/branch-summary-message.ts";
|
||||
import { CompactionSummaryMessageComponent } from "./components/compaction-summary-message.ts";
|
||||
import { CustomEditor } from "./components/custom-editor.ts";
|
||||
import { CustomEntryComponent } from "./components/custom-entry.ts";
|
||||
import { CustomMessageComponent } from "./components/custom-message.ts";
|
||||
import { DaxnutsComponent } from "./components/daxnuts.ts";
|
||||
import { DynamicBorder } from "./components/dynamic-border.ts";
|
||||
@@ -183,6 +184,12 @@ type CompactionQueuedMessage = {
|
||||
mode: "steer" | "followUp";
|
||||
};
|
||||
|
||||
type RenderSessionItem = AgentMessage | Extract<SessionEntry, { type: "custom" }>;
|
||||
|
||||
function isCustomSessionEntry(item: RenderSessionItem): item is Extract<SessionEntry, { type: "custom" }> {
|
||||
return "type" in item && item.type === "custom";
|
||||
}
|
||||
|
||||
const DEAD_TERMINAL_ERROR_CODES = new Set(["EIO", "EPIPE", "ENOTCONN"]);
|
||||
|
||||
function isDeadTerminalError(error: unknown): boolean {
|
||||
@@ -2781,6 +2788,13 @@ export class InteractiveMode {
|
||||
this.ui.requestRender();
|
||||
break;
|
||||
|
||||
case "entry_appended":
|
||||
if (event.entry.type === "custom") {
|
||||
this.addCustomEntryToChat(event.entry);
|
||||
this.ui.requestRender();
|
||||
}
|
||||
break;
|
||||
|
||||
case "session_info_changed":
|
||||
this.updateTerminalTitle();
|
||||
this.footer.invalidate();
|
||||
@@ -3068,6 +3082,28 @@ export class InteractiveMode {
|
||||
this.ui.requestRender();
|
||||
}
|
||||
|
||||
private addCustomEntryToChat(entry: Extract<SessionEntry, { type: "custom" }>): void {
|
||||
const renderer = this.session.extensionRunner.getEntryRenderer(entry.customType);
|
||||
if (!renderer) {
|
||||
return;
|
||||
}
|
||||
const component = new CustomEntryComponent(entry, renderer);
|
||||
component.setExpanded(this.toolOutputExpanded);
|
||||
if (!component.hasContent()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.streamingComponent) {
|
||||
const streamingIndex = this.chatContainer.children.indexOf(this.streamingComponent);
|
||||
if (streamingIndex >= 0) {
|
||||
this.chatContainer.children.splice(streamingIndex, 0, component);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.chatContainer.addChild(component);
|
||||
}
|
||||
|
||||
private addMessageToChat(message: AgentMessage, options?: { populateHistory?: boolean }): void {
|
||||
switch (message.role) {
|
||||
case "bashExecution": {
|
||||
@@ -3167,14 +3203,8 @@ export class InteractiveMode {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render session context to chat. Used for initial load and rebuild after compaction.
|
||||
* @param sessionContext Session context to render
|
||||
* @param options.updateFooter Update footer state
|
||||
* @param options.populateHistory Add user messages to editor history
|
||||
*/
|
||||
private renderSessionContext(
|
||||
sessionContext: SessionContext,
|
||||
private renderSessionItems(
|
||||
items: readonly RenderSessionItem[],
|
||||
options: { updateFooter?: boolean; populateHistory?: boolean } = {},
|
||||
): void {
|
||||
this.pendingTools.clear();
|
||||
@@ -3185,7 +3215,13 @@ export class InteractiveMode {
|
||||
this.updateEditorBorderColor();
|
||||
}
|
||||
|
||||
for (const message of sessionContext.messages) {
|
||||
for (const item of items) {
|
||||
if (isCustomSessionEntry(item)) {
|
||||
this.addCustomEntryToChat(item);
|
||||
continue;
|
||||
}
|
||||
|
||||
const message = item;
|
||||
// Assistant messages need special handling for tool calls
|
||||
if (message.role === "assistant") {
|
||||
this.addMessageToChat(message);
|
||||
@@ -3243,10 +3279,28 @@ export class InteractiveMode {
|
||||
this.ui.requestRender();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render session entries to chat. Used for initial load and rebuild after compaction.
|
||||
* @param entries Compaction-aware session entries to render
|
||||
* @param options.updateFooter Update footer state
|
||||
* @param options.populateHistory Add user messages to editor history
|
||||
*/
|
||||
private renderSessionEntries(
|
||||
entries: SessionEntry[],
|
||||
options: { updateFooter?: boolean; populateHistory?: boolean } = {},
|
||||
): void {
|
||||
const items = entries.flatMap((entry): RenderSessionItem[] => {
|
||||
if (entry.type === "custom") {
|
||||
return [entry];
|
||||
}
|
||||
return sessionEntryToContextMessages(entry);
|
||||
});
|
||||
this.renderSessionItems(items, options);
|
||||
}
|
||||
|
||||
renderInitialMessages(): void {
|
||||
// Get aligned messages and entries from session context
|
||||
const context = this.sessionManager.buildSessionContext();
|
||||
this.renderSessionContext(context, {
|
||||
const entries = this.sessionManager.buildContextEntries();
|
||||
this.renderSessionEntries(entries, {
|
||||
updateFooter: true,
|
||||
populateHistory: true,
|
||||
});
|
||||
@@ -3297,8 +3351,7 @@ export class InteractiveMode {
|
||||
|
||||
private rebuildChatFromMessages(): void {
|
||||
this.chatContainer.clear();
|
||||
const context = this.sessionManager.buildSessionContext();
|
||||
this.renderSessionContext(context);
|
||||
this.renderSessionEntries(this.sessionManager.buildContextEntries());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
|
||||
Reference in New Issue
Block a user