diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index 5edba853..e716cbd0 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -77,7 +77,7 @@ async function runInteractiveMode( } }); - mode.renderInitialMessages(session.state); + mode.renderInitialMessages(); if (migratedProviders.length > 0) { mode.showWarning(`Migrated credentials to auth.json: ${migratedProviders.join(", ")}`); diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 6da1e0fd..86ca9837 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -31,6 +31,7 @@ import type { HookUIContext } from "../../core/hooks/index.js"; import { isBashExecutionMessage } from "../../core/messages.js"; import { getLatestCompactionEntry, + type SessionContext, SessionManager, SUMMARY_PREFIX, SUMMARY_SUFFIX, @@ -45,6 +46,7 @@ import { AssistantMessageComponent } from "./components/assistant-message.js"; import { BashExecutionComponent } from "./components/bash-execution.js"; import { CompactionComponent } from "./components/compaction.js"; import { CustomEditor } from "./components/custom-editor.js"; +import { CustomMessageComponent } from "./components/custom-message.js"; import { DynamicBorder } from "./components/dynamic-border.js"; import { FooterComponent } from "./components/footer.js"; import { HookInputComponent } from "./components/hook-input.js"; @@ -1020,13 +1022,13 @@ export class InteractiveMode { } /** - * Render messages to chat. Used for initial load and rebuild after compaction. - * @param messages Messages to render + * 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 renderMessages( - messages: readonly (Message | AppMessage)[], + private renderSessionContext( + sessionContext: SessionContext, options: { updateFooter?: boolean; populateHistory?: boolean } = {}, ): void { this.isFirstUserMessage = true; @@ -1038,13 +1040,25 @@ export class InteractiveMode { } const compactionEntry = getLatestCompactionEntry(this.sessionManager.getEntries()); + const entries = sessionContext.entries; + + for (let i = 0; i < sessionContext.messages.length; i++) { + const message = sessionContext.messages[i]; + const entry = entries?.[i]; - for (const message of messages) { if (isBashExecutionMessage(message)) { this.addMessageToChat(message); continue; } + // Check if this is a custom_message entry + if (entry?.type === "custom_message") { + if (entry.display) { + this.chatContainer.addChild(new CustomMessageComponent(entry)); + } + continue; + } + if (message.role === "user") { const textContent = this.getUserMessageText(message); if (textContent) { @@ -1103,12 +1117,17 @@ export class InteractiveMode { this.ui.requestRender(); } - renderInitialMessages(state: AgentState): void { - this.renderMessages(state.messages, { updateFooter: true, populateHistory: true }); + renderInitialMessages(): void { + // Get aligned messages and entries from session context + const context = this.sessionManager.buildSessionContext(); + this.renderSessionContext(context, { + updateFooter: true, + populateHistory: true, + }); // Show compaction info if session was compacted - const entries = this.sessionManager.getEntries(); - const compactionCount = entries.filter((e) => e.type === "compaction").length; + const allEntries = this.sessionManager.getEntries(); + const compactionCount = allEntries.filter((e) => e.type === "compaction").length; if (compactionCount > 0) { const times = compactionCount === 1 ? "1 time" : `${compactionCount} times`; this.showStatus(`Session compacted ${times}`); @@ -1125,7 +1144,8 @@ export class InteractiveMode { } private rebuildChatFromMessages(): void { - this.renderMessages(this.session.messages); + const context = this.sessionManager.buildSessionContext(); + this.renderSessionContext(context); } // ========================================================================= @@ -1500,7 +1520,7 @@ export class InteractiveMode { this.chatContainer.clear(); this.isFirstUserMessage = true; - this.renderInitialMessages(this.session.state); + this.renderInitialMessages(); this.editor.setText(result.selectedText); done(); this.showStatus("Branched to new session"); @@ -1554,7 +1574,7 @@ export class InteractiveMode { // Clear and re-render the chat this.chatContainer.clear(); this.isFirstUserMessage = true; - this.renderInitialMessages(this.session.state); + this.renderInitialMessages(); this.showStatus("Resumed session"); } diff --git a/packages/coding-agent/src/modes/interactive/theme/dark.json b/packages/coding-agent/src/modes/interactive/theme/dark.json index 51ad7749..25c61db6 100644 --- a/packages/coding-agent/src/modes/interactive/theme/dark.json +++ b/packages/coding-agent/src/modes/interactive/theme/dark.json @@ -14,7 +14,8 @@ "userMsgBg": "#343541", "toolPendingBg": "#282832", "toolSuccessBg": "#283228", - "toolErrorBg": "#3c2828" + "toolErrorBg": "#3c2828", + "customMsgBg": "#2d2838" }, "colors": { "accent": "accent", @@ -30,6 +31,9 @@ "userMessageBg": "userMsgBg", "userMessageText": "", + "customMessageBg": "customMsgBg", + "customMessageText": "", + "customMessageLabel": "#9575cd", "toolPendingBg": "toolPendingBg", "toolSuccessBg": "toolSuccessBg", "toolErrorBg": "toolErrorBg", diff --git a/packages/coding-agent/src/modes/interactive/theme/light.json b/packages/coding-agent/src/modes/interactive/theme/light.json index 57eb2643..36e9c763 100644 --- a/packages/coding-agent/src/modes/interactive/theme/light.json +++ b/packages/coding-agent/src/modes/interactive/theme/light.json @@ -13,7 +13,8 @@ "userMsgBg": "#e8e8e8", "toolPendingBg": "#e8e8f0", "toolSuccessBg": "#e8f0e8", - "toolErrorBg": "#f0e8e8" + "toolErrorBg": "#f0e8e8", + "customMsgBg": "#ede7f6" }, "colors": { "accent": "teal", @@ -29,6 +30,9 @@ "userMessageBg": "userMsgBg", "userMessageText": "", + "customMessageBg": "customMsgBg", + "customMessageText": "", + "customMessageLabel": "#7e57c2", "toolPendingBg": "toolPendingBg", "toolSuccessBg": "toolSuccessBg", "toolErrorBg": "toolErrorBg", diff --git a/packages/coding-agent/src/modes/interactive/theme/theme-schema.json b/packages/coding-agent/src/modes/interactive/theme/theme-schema.json index 7f060d23..f561ecb1 100644 --- a/packages/coding-agent/src/modes/interactive/theme/theme-schema.json +++ b/packages/coding-agent/src/modes/interactive/theme/theme-schema.json @@ -47,6 +47,9 @@ "text", "userMessageBg", "userMessageText", + "customMessageBg", + "customMessageText", + "customMessageLabel", "toolPendingBg", "toolSuccessBg", "toolErrorBg", @@ -122,6 +125,18 @@ "$ref": "#/$defs/colorValue", "description": "User message text color" }, + "customMessageBg": { + "$ref": "#/$defs/colorValue", + "description": "Custom message background (hook-injected messages)" + }, + "customMessageText": { + "$ref": "#/$defs/colorValue", + "description": "Custom message text color" + }, + "customMessageLabel": { + "$ref": "#/$defs/colorValue", + "description": "Custom message type label color" + }, "toolPendingBg": { "$ref": "#/$defs/colorValue", "description": "Tool execution box (pending state)" diff --git a/packages/coding-agent/src/modes/interactive/theme/theme.ts b/packages/coding-agent/src/modes/interactive/theme/theme.ts index 8f56b8e6..915182f0 100644 --- a/packages/coding-agent/src/modes/interactive/theme/theme.ts +++ b/packages/coding-agent/src/modes/interactive/theme/theme.ts @@ -34,9 +34,12 @@ const ThemeJsonSchema = Type.Object({ muted: ColorValueSchema, dim: ColorValueSchema, text: ColorValueSchema, - // Backgrounds & Content Text (7 colors) + // Backgrounds & Content Text (10 colors) userMessageBg: ColorValueSchema, userMessageText: ColorValueSchema, + customMessageBg: ColorValueSchema, + customMessageText: ColorValueSchema, + customMessageLabel: ColorValueSchema, toolPendingBg: ColorValueSchema, toolSuccessBg: ColorValueSchema, toolErrorBg: ColorValueSchema, @@ -95,6 +98,8 @@ export type ThemeColor = | "dim" | "text" | "userMessageText" + | "customMessageText" + | "customMessageLabel" | "toolTitle" | "toolOutput" | "mdHeading" @@ -127,7 +132,7 @@ export type ThemeColor = | "thinkingXhigh" | "bashMode"; -export type ThemeBg = "userMessageBg" | "toolPendingBg" | "toolSuccessBg" | "toolErrorBg"; +export type ThemeBg = "userMessageBg" | "customMessageBg" | "toolPendingBg" | "toolSuccessBg" | "toolErrorBg"; type ColorMode = "truecolor" | "256color"; @@ -482,7 +487,13 @@ function createTheme(themeJson: ThemeJson, mode?: ColorMode): Theme { const resolvedColors = resolveThemeColors(themeJson.colors, themeJson.vars); const fgColors: Record = {} as Record; const bgColors: Record = {} as Record; - const bgColorKeys: Set = new Set(["userMessageBg", "toolPendingBg", "toolSuccessBg", "toolErrorBg"]); + const bgColorKeys: Set = new Set([ + "userMessageBg", + "customMessageBg", + "toolPendingBg", + "toolSuccessBg", + "toolErrorBg", + ]); for (const [key, value] of Object.entries(resolvedColors)) { if (bgColorKeys.has(key)) { bgColors[key as ThemeBg] = value;