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) {
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 {
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");
}

View file

@ -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",

View file

@ -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",

View file

@ -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)"

View file

@ -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<ThemeColor, string | number> = {} as Record<ThemeColor, 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)) {
if (bgColorKeys.has(key)) {
bgColors[key as ThemeBg] = value;