Add TUI rendering for CustomMessageEntry

- Add CustomMessageComponent with purple-tinted styling
- Add theme colors: customMessageBg, customMessageText, customMessageLabel
- Rename renderMessages to renderSessionContext taking SessionContext directly
- renderInitialMessages now gets context from sessionManager
- Skip rendering for display: false entries
This commit is contained in:
Mario Zechner 2025-12-26 23:21:04 +01:00
parent beb804cda0
commit 7b94ddf36b
6 changed files with 72 additions and 18 deletions

View file

@ -77,7 +77,7 @@ async function runInteractiveMode(
} }
}); });
mode.renderInitialMessages(session.state); mode.renderInitialMessages();
if (migratedProviders.length > 0) { if (migratedProviders.length > 0) {
mode.showWarning(`Migrated credentials to auth.json: ${migratedProviders.join(", ")}`); mode.showWarning(`Migrated credentials to auth.json: ${migratedProviders.join(", ")}`);

View file

@ -31,6 +31,7 @@ import type { HookUIContext } from "../../core/hooks/index.js";
import { isBashExecutionMessage } from "../../core/messages.js"; import { isBashExecutionMessage } from "../../core/messages.js";
import { import {
getLatestCompactionEntry, getLatestCompactionEntry,
type SessionContext,
SessionManager, SessionManager,
SUMMARY_PREFIX, SUMMARY_PREFIX,
SUMMARY_SUFFIX, SUMMARY_SUFFIX,
@ -45,6 +46,7 @@ import { AssistantMessageComponent } from "./components/assistant-message.js";
import { BashExecutionComponent } from "./components/bash-execution.js"; import { BashExecutionComponent } from "./components/bash-execution.js";
import { CompactionComponent } from "./components/compaction.js"; import { CompactionComponent } from "./components/compaction.js";
import { CustomEditor } from "./components/custom-editor.js"; import { CustomEditor } from "./components/custom-editor.js";
import { CustomMessageComponent } from "./components/custom-message.js";
import { DynamicBorder } from "./components/dynamic-border.js"; import { DynamicBorder } from "./components/dynamic-border.js";
import { FooterComponent } from "./components/footer.js"; import { FooterComponent } from "./components/footer.js";
import { HookInputComponent } from "./components/hook-input.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. * Render session context to chat. Used for initial load and rebuild after compaction.
* @param messages Messages to render * @param sessionContext Session context to render
* @param options.updateFooter Update footer state * @param options.updateFooter Update footer state
* @param options.populateHistory Add user messages to editor history * @param options.populateHistory Add user messages to editor history
*/ */
private renderMessages( private renderSessionContext(
messages: readonly (Message | AppMessage)[], sessionContext: SessionContext,
options: { updateFooter?: boolean; populateHistory?: boolean } = {}, options: { updateFooter?: boolean; populateHistory?: boolean } = {},
): void { ): void {
this.isFirstUserMessage = true; this.isFirstUserMessage = true;
@ -1038,13 +1040,25 @@ export class InteractiveMode {
} }
const compactionEntry = getLatestCompactionEntry(this.sessionManager.getEntries()); 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)) { if (isBashExecutionMessage(message)) {
this.addMessageToChat(message); this.addMessageToChat(message);
continue; 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") { if (message.role === "user") {
const textContent = this.getUserMessageText(message); const textContent = this.getUserMessageText(message);
if (textContent) { if (textContent) {
@ -1103,12 +1117,17 @@ export class InteractiveMode {
this.ui.requestRender(); this.ui.requestRender();
} }
renderInitialMessages(state: AgentState): void { renderInitialMessages(): void {
this.renderMessages(state.messages, { updateFooter: true, populateHistory: true }); // 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 // Show compaction info if session was compacted
const entries = this.sessionManager.getEntries(); const allEntries = this.sessionManager.getEntries();
const compactionCount = entries.filter((e) => e.type === "compaction").length; const compactionCount = allEntries.filter((e) => e.type === "compaction").length;
if (compactionCount > 0) { if (compactionCount > 0) {
const times = compactionCount === 1 ? "1 time" : `${compactionCount} times`; const times = compactionCount === 1 ? "1 time" : `${compactionCount} times`;
this.showStatus(`Session compacted ${times}`); this.showStatus(`Session compacted ${times}`);
@ -1125,7 +1144,8 @@ export class InteractiveMode {
} }
private rebuildChatFromMessages(): void { 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.chatContainer.clear();
this.isFirstUserMessage = true; this.isFirstUserMessage = true;
this.renderInitialMessages(this.session.state); this.renderInitialMessages();
this.editor.setText(result.selectedText); this.editor.setText(result.selectedText);
done(); done();
this.showStatus("Branched to new session"); this.showStatus("Branched to new session");
@ -1554,7 +1574,7 @@ export class InteractiveMode {
// Clear and re-render the chat // Clear and re-render the chat
this.chatContainer.clear(); this.chatContainer.clear();
this.isFirstUserMessage = true; this.isFirstUserMessage = true;
this.renderInitialMessages(this.session.state); this.renderInitialMessages();
this.showStatus("Resumed session"); this.showStatus("Resumed session");
} }

View file

@ -14,7 +14,8 @@
"userMsgBg": "#343541", "userMsgBg": "#343541",
"toolPendingBg": "#282832", "toolPendingBg": "#282832",
"toolSuccessBg": "#283228", "toolSuccessBg": "#283228",
"toolErrorBg": "#3c2828" "toolErrorBg": "#3c2828",
"customMsgBg": "#2d2838"
}, },
"colors": { "colors": {
"accent": "accent", "accent": "accent",
@ -30,6 +31,9 @@
"userMessageBg": "userMsgBg", "userMessageBg": "userMsgBg",
"userMessageText": "", "userMessageText": "",
"customMessageBg": "customMsgBg",
"customMessageText": "",
"customMessageLabel": "#9575cd",
"toolPendingBg": "toolPendingBg", "toolPendingBg": "toolPendingBg",
"toolSuccessBg": "toolSuccessBg", "toolSuccessBg": "toolSuccessBg",
"toolErrorBg": "toolErrorBg", "toolErrorBg": "toolErrorBg",

View file

@ -13,7 +13,8 @@
"userMsgBg": "#e8e8e8", "userMsgBg": "#e8e8e8",
"toolPendingBg": "#e8e8f0", "toolPendingBg": "#e8e8f0",
"toolSuccessBg": "#e8f0e8", "toolSuccessBg": "#e8f0e8",
"toolErrorBg": "#f0e8e8" "toolErrorBg": "#f0e8e8",
"customMsgBg": "#ede7f6"
}, },
"colors": { "colors": {
"accent": "teal", "accent": "teal",
@ -29,6 +30,9 @@
"userMessageBg": "userMsgBg", "userMessageBg": "userMsgBg",
"userMessageText": "", "userMessageText": "",
"customMessageBg": "customMsgBg",
"customMessageText": "",
"customMessageLabel": "#7e57c2",
"toolPendingBg": "toolPendingBg", "toolPendingBg": "toolPendingBg",
"toolSuccessBg": "toolSuccessBg", "toolSuccessBg": "toolSuccessBg",
"toolErrorBg": "toolErrorBg", "toolErrorBg": "toolErrorBg",

View file

@ -47,6 +47,9 @@
"text", "text",
"userMessageBg", "userMessageBg",
"userMessageText", "userMessageText",
"customMessageBg",
"customMessageText",
"customMessageLabel",
"toolPendingBg", "toolPendingBg",
"toolSuccessBg", "toolSuccessBg",
"toolErrorBg", "toolErrorBg",
@ -122,6 +125,18 @@
"$ref": "#/$defs/colorValue", "$ref": "#/$defs/colorValue",
"description": "User message text color" "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": { "toolPendingBg": {
"$ref": "#/$defs/colorValue", "$ref": "#/$defs/colorValue",
"description": "Tool execution box (pending state)" "description": "Tool execution box (pending state)"

View file

@ -34,9 +34,12 @@ const ThemeJsonSchema = Type.Object({
muted: ColorValueSchema, muted: ColorValueSchema,
dim: ColorValueSchema, dim: ColorValueSchema,
text: ColorValueSchema, text: ColorValueSchema,
// Backgrounds & Content Text (7 colors) // Backgrounds & Content Text (10 colors)
userMessageBg: ColorValueSchema, userMessageBg: ColorValueSchema,
userMessageText: ColorValueSchema, userMessageText: ColorValueSchema,
customMessageBg: ColorValueSchema,
customMessageText: ColorValueSchema,
customMessageLabel: ColorValueSchema,
toolPendingBg: ColorValueSchema, toolPendingBg: ColorValueSchema,
toolSuccessBg: ColorValueSchema, toolSuccessBg: ColorValueSchema,
toolErrorBg: ColorValueSchema, toolErrorBg: ColorValueSchema,
@ -95,6 +98,8 @@ export type ThemeColor =
| "dim" | "dim"
| "text" | "text"
| "userMessageText" | "userMessageText"
| "customMessageText"
| "customMessageLabel"
| "toolTitle" | "toolTitle"
| "toolOutput" | "toolOutput"
| "mdHeading" | "mdHeading"
@ -127,7 +132,7 @@ export type ThemeColor =
| "thinkingXhigh" | "thinkingXhigh"
| "bashMode"; | "bashMode";
export type ThemeBg = "userMessageBg" | "toolPendingBg" | "toolSuccessBg" | "toolErrorBg"; export type ThemeBg = "userMessageBg" | "customMessageBg" | "toolPendingBg" | "toolSuccessBg" | "toolErrorBg";
type ColorMode = "truecolor" | "256color"; type ColorMode = "truecolor" | "256color";
@ -482,7 +487,13 @@ function createTheme(themeJson: ThemeJson, mode?: ColorMode): Theme {
const resolvedColors = resolveThemeColors(themeJson.colors, themeJson.vars); const resolvedColors = resolveThemeColors(themeJson.colors, themeJson.vars);
const fgColors: Record<ThemeColor, string | number> = {} as Record<ThemeColor, string | number>; const fgColors: Record<ThemeColor, string | number> = {} as Record<ThemeColor, string | number>;
const bgColors: Record<ThemeBg, string | number> = {} as Record<ThemeBg, string | number>; const bgColors: Record<ThemeBg, string | number> = {} as Record<ThemeBg, string | number>;
const bgColorKeys: Set<string> = new Set(["userMessageBg", "toolPendingBg", "toolSuccessBg", "toolErrorBg"]); const bgColorKeys: Set<string> = new Set([
"userMessageBg",
"customMessageBg",
"toolPendingBg",
"toolSuccessBg",
"toolErrorBg",
]);
for (const [key, value] of Object.entries(resolvedColors)) { for (const [key, value] of Object.entries(resolvedColors)) {
if (bgColorKeys.has(key)) { if (bgColorKeys.has(key)) {
bgColors[key as ThemeBg] = value; bgColors[key as ThemeBg] = value;