fix(coding-agent): add entry renderers for session entries
This commit is contained in:
@@ -86,7 +86,7 @@ import type { BashExecutionMessage, CustomMessage } from "./messages.ts";
|
||||
import type { ModelRegistry } from "./model-registry.ts";
|
||||
import { expandPromptTemplate, type PromptTemplate } from "./prompt-templates.ts";
|
||||
import type { ResourceExtensionPaths, ResourceLoader } from "./resource-loader.ts";
|
||||
import type { BranchSummaryEntry, CompactionEntry, SessionManager } from "./session-manager.ts";
|
||||
import type { BranchSummaryEntry, CompactionEntry, SessionEntry, SessionManager } from "./session-manager.ts";
|
||||
import { CURRENT_SESSION_VERSION, getLatestCompactionEntry, type SessionHeader } from "./session-manager.ts";
|
||||
import type { SettingsManager } from "./settings-manager.ts";
|
||||
import type { SlashCommandInfo } from "./slash-commands.ts";
|
||||
@@ -137,6 +137,7 @@ export type AgentSessionEvent =
|
||||
followUp: readonly string[];
|
||||
}
|
||||
| { type: "compaction_start"; reason: "manual" | "threshold" | "overflow" }
|
||||
| { type: "entry_appended"; entry: SessionEntry }
|
||||
| { type: "session_info_changed"; name: string | undefined }
|
||||
| { type: "thinking_level_changed"; level: ThinkingLevel }
|
||||
| {
|
||||
@@ -2262,7 +2263,11 @@ export class AgentSession {
|
||||
});
|
||||
},
|
||||
appendEntry: (customType, data) => {
|
||||
this.sessionManager.appendCustomEntry(customType, data);
|
||||
const entryId = this.sessionManager.appendCustomEntry(customType, data);
|
||||
const entry = this.sessionManager.getEntry(entryId);
|
||||
if (entry) {
|
||||
this._emit({ type: "entry_appended", entry });
|
||||
}
|
||||
},
|
||||
setSessionName: (name) => {
|
||||
this.setSessionName(name);
|
||||
|
||||
@@ -50,6 +50,9 @@ export type {
|
||||
EditorFactory,
|
||||
EditToolCallEvent,
|
||||
EditToolResultEvent,
|
||||
// Message and Entry Rendering
|
||||
EntryRenderer,
|
||||
EntryRenderOptions,
|
||||
ExecOptions,
|
||||
ExecResult,
|
||||
Extension,
|
||||
@@ -91,7 +94,6 @@ export type {
|
||||
LsToolResultEvent,
|
||||
// Events - Message
|
||||
MessageEndEvent,
|
||||
// Message Rendering
|
||||
MessageRenderer,
|
||||
MessageRenderOptions,
|
||||
MessageStartEvent,
|
||||
|
||||
@@ -30,6 +30,7 @@ import { execCommand } from "../exec.ts";
|
||||
import { createSyntheticSourceInfo } from "../source-info.ts";
|
||||
import { time } from "../timings.ts";
|
||||
import type {
|
||||
EntryRenderer,
|
||||
Extension,
|
||||
ExtensionAPI,
|
||||
ExtensionFactory,
|
||||
@@ -269,6 +270,12 @@ function createExtensionAPI(
|
||||
extension.messageRenderers.set(customType, renderer as MessageRenderer);
|
||||
},
|
||||
|
||||
registerEntryRenderer<T>(customType: string, renderer: EntryRenderer<T>): void {
|
||||
runtime.assertActive();
|
||||
extension.entryRenderers ??= new Map();
|
||||
extension.entryRenderers.set(customType, renderer as EntryRenderer);
|
||||
},
|
||||
|
||||
// Flag access - checks extension registered it, reads from runtime
|
||||
getFlag(name: string): boolean | string | undefined {
|
||||
runtime.assertActive();
|
||||
@@ -415,6 +422,7 @@ function createExtension(extensionPath: string, resolvedPath: string): Extension
|
||||
handlers: new Map(),
|
||||
tools: new Map(),
|
||||
messageRenderers: new Map(),
|
||||
entryRenderers: new Map(),
|
||||
commands: new Map(),
|
||||
flags: new Map(),
|
||||
shortcuts: new Map(),
|
||||
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
ContextEvent,
|
||||
ContextEventResult,
|
||||
ContextUsage,
|
||||
EntryRenderer,
|
||||
Extension,
|
||||
ExtensionActions,
|
||||
ExtensionCommandContext,
|
||||
@@ -553,6 +554,16 @@ export class ExtensionRunner {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
getEntryRenderer(customType: string): EntryRenderer | undefined {
|
||||
for (const ext of this.extensions) {
|
||||
const renderer = ext.entryRenderers?.get(customType);
|
||||
if (renderer) {
|
||||
return renderer;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private resolveRegisteredCommands(): ResolvedCommand[] {
|
||||
const commands: RegisteredCommand[] = [];
|
||||
const counts = new Map<string, number>();
|
||||
|
||||
@@ -52,6 +52,7 @@ import type { ModelRegistry } from "../model-registry.ts";
|
||||
import type {
|
||||
BranchSummaryEntry,
|
||||
CompactionEntry,
|
||||
CustomEntry,
|
||||
ReadonlySessionManager,
|
||||
SessionEntry,
|
||||
SessionManager,
|
||||
@@ -1093,19 +1094,29 @@ export interface SessionBeforeTreeResult {
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Message Rendering
|
||||
// Message and Entry Rendering
|
||||
// ============================================================================
|
||||
|
||||
export interface MessageRenderOptions {
|
||||
expanded: boolean;
|
||||
}
|
||||
|
||||
export interface EntryRenderOptions {
|
||||
expanded: boolean;
|
||||
}
|
||||
|
||||
export type MessageRenderer<T = unknown> = (
|
||||
message: CustomMessage<T>,
|
||||
options: MessageRenderOptions,
|
||||
theme: Theme,
|
||||
) => Component | undefined;
|
||||
|
||||
export type EntryRenderer<T = unknown> = (
|
||||
entry: CustomEntry<T>,
|
||||
options: EntryRenderOptions,
|
||||
theme: Theme,
|
||||
) => Component | undefined;
|
||||
|
||||
// ============================================================================
|
||||
// Command Registration
|
||||
// ============================================================================
|
||||
@@ -1224,6 +1235,9 @@ export interface ExtensionAPI {
|
||||
/** Register a custom renderer for CustomMessageEntry. */
|
||||
registerMessageRenderer<T = unknown>(customType: string, renderer: MessageRenderer<T>): void;
|
||||
|
||||
/** Register a custom renderer for CustomEntry. Custom entries do not participate in LLM context. */
|
||||
registerEntryRenderer<T = unknown>(customType: string, renderer: EntryRenderer<T>): void;
|
||||
|
||||
// =========================================================================
|
||||
// Actions
|
||||
// =========================================================================
|
||||
@@ -1598,6 +1612,7 @@ export interface Extension {
|
||||
handlers: Map<string, HandlerFn[]>;
|
||||
tools: Map<string, RegisteredTool>;
|
||||
messageRenderers: Map<string, MessageRenderer>;
|
||||
entryRenderers?: Map<string, EntryRenderer>;
|
||||
commands: Map<string, RegisteredCommand>;
|
||||
flags: Map<string, ExtensionFlag>;
|
||||
shortcuts: Map<KeyId, ExtensionShortcut>;
|
||||
|
||||
@@ -194,6 +194,7 @@ export type ReadonlySessionManager = Pick<
|
||||
| "getEntry"
|
||||
| "getLabel"
|
||||
| "getBranch"
|
||||
| "buildContextEntries"
|
||||
| "getHeader"
|
||||
| "getEntries"
|
||||
| "getTree"
|
||||
@@ -317,6 +318,126 @@ export function getLatestCompactionEntry(entries: SessionEntry[]): CompactionEnt
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildEntryIndex(entries: SessionEntry[], byId?: Map<string, SessionEntry>): Map<string, SessionEntry> {
|
||||
if (byId) return byId;
|
||||
const index = new Map<string, SessionEntry>();
|
||||
for (const entry of entries) {
|
||||
index.set(entry.id, entry);
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
function buildSessionPath(
|
||||
entries: SessionEntry[],
|
||||
leafId?: string | null,
|
||||
byId?: Map<string, SessionEntry>,
|
||||
): SessionEntry[] {
|
||||
const index = buildEntryIndex(entries, byId);
|
||||
let leaf: SessionEntry | undefined;
|
||||
if (leafId === null) {
|
||||
return [];
|
||||
}
|
||||
if (leafId) {
|
||||
leaf = index.get(leafId);
|
||||
}
|
||||
leaf ??= entries[entries.length - 1];
|
||||
if (!leaf) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const path: SessionEntry[] = [];
|
||||
let current: SessionEntry | undefined = leaf;
|
||||
while (current) {
|
||||
path.push(current);
|
||||
current = current.parentId ? index.get(current.parentId) : undefined;
|
||||
}
|
||||
path.reverse();
|
||||
return path;
|
||||
}
|
||||
|
||||
function getSessionContextSettings(path: SessionEntry[]): Pick<SessionContext, "thinkingLevel" | "model"> {
|
||||
let thinkingLevel = "off";
|
||||
let model: { provider: string; modelId: string } | null = null;
|
||||
|
||||
for (const entry of path) {
|
||||
if (entry.type === "thinking_level_change") {
|
||||
thinkingLevel = entry.thinkingLevel;
|
||||
} else if (entry.type === "model_change") {
|
||||
model = { provider: entry.provider, modelId: entry.modelId };
|
||||
} else if (entry.type === "message" && entry.message.role === "assistant") {
|
||||
model = { provider: entry.message.provider, modelId: entry.message.model };
|
||||
}
|
||||
}
|
||||
|
||||
return { thinkingLevel, model };
|
||||
}
|
||||
|
||||
/**
|
||||
* Project one selected session entry into LLM/runtime messages.
|
||||
* Plain custom entries are display/state entries and do not participate in context.
|
||||
*/
|
||||
export function sessionEntryToContextMessages(entry: SessionEntry): AgentMessage[] {
|
||||
if (entry.type === "message") {
|
||||
return [entry.message];
|
||||
}
|
||||
if (entry.type === "custom_message") {
|
||||
return [createCustomMessage(entry.customType, entry.content, entry.display, entry.details, entry.timestamp)];
|
||||
}
|
||||
if (entry.type === "branch_summary" && entry.summary) {
|
||||
return [createBranchSummaryMessage(entry.summary, entry.fromId, entry.timestamp)];
|
||||
}
|
||||
if (entry.type === "compaction") {
|
||||
return [createCompactionSummaryMessage(entry.summary, entry.tokensBefore, entry.timestamp)];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the active, compaction-aware session entry list.
|
||||
*
|
||||
* This follows the current leaf path. If the path contains compaction entries,
|
||||
* the latest compaction is represented by the compaction entry itself, followed
|
||||
* by the kept entries starting at firstKeptEntryId and all entries after the
|
||||
* compaction entry. Older summarized entries are omitted.
|
||||
*/
|
||||
export function buildContextEntries(
|
||||
entries: SessionEntry[],
|
||||
leafId?: string | null,
|
||||
byId?: Map<string, SessionEntry>,
|
||||
): SessionEntry[] {
|
||||
const path = buildSessionPath(entries, leafId, byId);
|
||||
let compaction: CompactionEntry | null = null;
|
||||
|
||||
for (const entry of path) {
|
||||
if (entry.type === "compaction") {
|
||||
compaction = entry;
|
||||
}
|
||||
}
|
||||
|
||||
if (!compaction) {
|
||||
return path;
|
||||
}
|
||||
|
||||
const compactionIdx = path.findIndex((entry) => entry.id === compaction.id);
|
||||
if (compactionIdx < 0) {
|
||||
return path;
|
||||
}
|
||||
|
||||
const contextEntries: SessionEntry[] = [compaction];
|
||||
let foundFirstKept = false;
|
||||
for (let i = 0; i < compactionIdx; i++) {
|
||||
const entry = path[i];
|
||||
if (entry.id === compaction.firstKeptEntryId) {
|
||||
foundFirstKept = true;
|
||||
}
|
||||
if (foundFirstKept) {
|
||||
contextEntries.push(entry);
|
||||
}
|
||||
}
|
||||
contextEntries.push(...path.slice(compactionIdx + 1));
|
||||
return contextEntries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the session context from entries using tree traversal.
|
||||
* If leafId is provided, walks from that entry to root.
|
||||
@@ -327,108 +448,9 @@ export function buildSessionContext(
|
||||
leafId?: string | null,
|
||||
byId?: Map<string, SessionEntry>,
|
||||
): SessionContext {
|
||||
// Build uuid index if not available
|
||||
if (!byId) {
|
||||
byId = new Map<string, SessionEntry>();
|
||||
for (const entry of entries) {
|
||||
byId.set(entry.id, entry);
|
||||
}
|
||||
}
|
||||
|
||||
// Find leaf
|
||||
let leaf: SessionEntry | undefined;
|
||||
if (leafId === null) {
|
||||
// Explicitly null - return no messages (navigated to before first entry)
|
||||
return { messages: [], thinkingLevel: "off", model: null };
|
||||
}
|
||||
if (leafId) {
|
||||
leaf = byId.get(leafId);
|
||||
}
|
||||
if (!leaf) {
|
||||
// Fallback to last entry (when leafId is undefined)
|
||||
leaf = entries[entries.length - 1];
|
||||
}
|
||||
|
||||
if (!leaf) {
|
||||
return { messages: [], thinkingLevel: "off", model: null };
|
||||
}
|
||||
|
||||
// Walk from leaf to root, collecting path
|
||||
const path: SessionEntry[] = [];
|
||||
let current: SessionEntry | undefined = leaf;
|
||||
while (current) {
|
||||
path.push(current);
|
||||
current = current.parentId ? byId.get(current.parentId) : undefined;
|
||||
}
|
||||
path.reverse();
|
||||
|
||||
// Extract settings and find compaction
|
||||
let thinkingLevel = "off";
|
||||
let model: { provider: string; modelId: string } | null = null;
|
||||
let compaction: CompactionEntry | null = null;
|
||||
|
||||
for (const entry of path) {
|
||||
if (entry.type === "thinking_level_change") {
|
||||
thinkingLevel = entry.thinkingLevel;
|
||||
} else if (entry.type === "model_change") {
|
||||
model = { provider: entry.provider, modelId: entry.modelId };
|
||||
} else if (entry.type === "message" && entry.message.role === "assistant") {
|
||||
model = { provider: entry.message.provider, modelId: entry.message.model };
|
||||
} else if (entry.type === "compaction") {
|
||||
compaction = entry;
|
||||
}
|
||||
}
|
||||
|
||||
// Build messages and collect corresponding entries
|
||||
// When there's a compaction, we need to:
|
||||
// 1. Emit summary first (entry = compaction)
|
||||
// 2. Emit kept messages (from firstKeptEntryId up to compaction)
|
||||
// 3. Emit messages after compaction
|
||||
const messages: AgentMessage[] = [];
|
||||
|
||||
const appendMessage = (entry: SessionEntry) => {
|
||||
if (entry.type === "message") {
|
||||
messages.push(entry.message);
|
||||
} else if (entry.type === "custom_message") {
|
||||
messages.push(
|
||||
createCustomMessage(entry.customType, entry.content, entry.display, entry.details, entry.timestamp),
|
||||
);
|
||||
} else if (entry.type === "branch_summary" && entry.summary) {
|
||||
messages.push(createBranchSummaryMessage(entry.summary, entry.fromId, entry.timestamp));
|
||||
}
|
||||
};
|
||||
|
||||
if (compaction) {
|
||||
// Emit summary first
|
||||
messages.push(createCompactionSummaryMessage(compaction.summary, compaction.tokensBefore, compaction.timestamp));
|
||||
|
||||
// Find compaction index in path
|
||||
const compactionIdx = path.findIndex((e) => e.type === "compaction" && e.id === compaction.id);
|
||||
|
||||
// Emit kept messages (before compaction, starting from firstKeptEntryId)
|
||||
let foundFirstKept = false;
|
||||
for (let i = 0; i < compactionIdx; i++) {
|
||||
const entry = path[i];
|
||||
if (entry.id === compaction.firstKeptEntryId) {
|
||||
foundFirstKept = true;
|
||||
}
|
||||
if (foundFirstKept) {
|
||||
appendMessage(entry);
|
||||
}
|
||||
}
|
||||
|
||||
// Emit messages after compaction
|
||||
for (let i = compactionIdx + 1; i < path.length; i++) {
|
||||
const entry = path[i];
|
||||
appendMessage(entry);
|
||||
}
|
||||
} else {
|
||||
// No compaction - emit all messages, handle branch summaries and custom messages
|
||||
for (const entry of path) {
|
||||
appendMessage(entry);
|
||||
}
|
||||
}
|
||||
|
||||
const path = buildSessionPath(entries, leafId, byId);
|
||||
const { thinkingLevel, model } = getSessionContextSettings(path);
|
||||
const messages = buildContextEntries(entries, leafId, byId).flatMap(sessionEntryToContextMessages);
|
||||
return { messages, thinkingLevel, model };
|
||||
}
|
||||
|
||||
@@ -1164,6 +1186,14 @@ export class SessionManager {
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the active, compaction-aware entry list for context/rendering.
|
||||
* Uses tree traversal from current leaf.
|
||||
*/
|
||||
buildContextEntries(): SessionEntry[] {
|
||||
return buildContextEntries(this.getEntries(), this.leafId, this.byId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the session context (what gets sent to the LLM).
|
||||
* Uses tree traversal from current leaf.
|
||||
|
||||
@@ -77,6 +77,8 @@ export type {
|
||||
ContextUsage,
|
||||
CustomToolCallEvent,
|
||||
EditToolCallEvent,
|
||||
EntryRenderer,
|
||||
EntryRenderOptions,
|
||||
ExecOptions,
|
||||
ExecResult,
|
||||
Extension,
|
||||
@@ -214,6 +216,7 @@ export {
|
||||
} from "./core/sdk.ts";
|
||||
export {
|
||||
type BranchSummaryEntry,
|
||||
buildContextEntries,
|
||||
buildSessionContext,
|
||||
type CompactionEntry,
|
||||
CURRENT_SESSION_VERSION,
|
||||
@@ -234,6 +237,7 @@ export {
|
||||
SessionManager,
|
||||
type SessionMessageEntry,
|
||||
type SessionTreeNode,
|
||||
sessionEntryToContextMessages,
|
||||
type ThinkingLevelChangeEntry,
|
||||
} from "./core/session-manager.ts";
|
||||
export {
|
||||
|
||||
@@ -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