mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-16 21:03:42 +00:00
Use exhaustive switch on message.role throughout coding-agent
- addMessageToChat: exhaustive switch for all AgentMessage roles - renderSessionContext: delegates to addMessageToChat, special handling for assistant tool calls and tool results - export-html formatMessage: exhaustive switch for all AgentMessage roles - Removed isHookMessage, isBashExecutionMessage type guards in favor of role checks - Fixed imports and removed unused getLatestCompactionEntry
This commit is contained in:
parent
ecef601d19
commit
b921298af7
11 changed files with 442 additions and 376 deletions
|
|
@ -1,20 +1,19 @@
|
|||
import { Container, Markdown, Spacer, Text } from "@mariozechner/pi-tui";
|
||||
import type { CompactionSummaryMessage } from "packages/coding-agent/src/core/messages.js";
|
||||
import { getMarkdownTheme, theme } from "../theme/theme.js";
|
||||
|
||||
/**
|
||||
* Component that renders a compaction indicator with collapsed/expanded state.
|
||||
* Component that renders a compaction message with collapsed/expanded state.
|
||||
* Collapsed: shows "Context compacted from X tokens"
|
||||
* Expanded: shows the full summary rendered as markdown (like a user message)
|
||||
*/
|
||||
export class CompactionComponent extends Container {
|
||||
export class CompactionSummaryMessageComponent extends Container {
|
||||
private expanded = false;
|
||||
private tokensBefore: number;
|
||||
private summary: string;
|
||||
private message: CompactionSummaryMessage;
|
||||
|
||||
constructor(tokensBefore: number, summary: string) {
|
||||
constructor(message: CompactionSummaryMessage) {
|
||||
super();
|
||||
this.tokensBefore = tokensBefore;
|
||||
this.summary = summary;
|
||||
this.message = message;
|
||||
this.updateDisplay();
|
||||
}
|
||||
|
||||
|
|
@ -29,9 +28,9 @@ export class CompactionComponent extends Container {
|
|||
if (this.expanded) {
|
||||
// Show header + summary as markdown (like user message)
|
||||
this.addChild(new Spacer(1));
|
||||
const header = `**Context compacted from ${this.tokensBefore.toLocaleString()} tokens**\n\n`;
|
||||
const header = `**Context compacted from ${this.message.tokensBefore.toLocaleString()} tokens**\n\n`;
|
||||
this.addChild(
|
||||
new Markdown(header + this.summary, 1, 1, getMarkdownTheme(), {
|
||||
new Markdown(header + this.message.summary, 1, 1, getMarkdownTheme(), {
|
||||
bgColor: (text: string) => theme.bg("userMessageBg", text),
|
||||
color: (text: string) => theme.fg("userMessageText", text),
|
||||
}),
|
||||
|
|
@ -39,7 +38,7 @@ export class CompactionComponent extends Container {
|
|||
this.addChild(new Spacer(1));
|
||||
} else {
|
||||
// Collapsed: simple text in warning color with token count
|
||||
const tokenStr = this.tokensBefore.toLocaleString();
|
||||
const tokenStr = this.message.tokensBefore.toLocaleString();
|
||||
this.addChild(
|
||||
new Text(
|
||||
theme.fg("warning", `Earlier messages compacted from ${tokenStr} tokens (ctrl+o to expand)`),
|
||||
|
|
@ -5,13 +5,9 @@ import { getMarkdownTheme, theme } from "../theme/theme.js";
|
|||
* Component that renders a user message
|
||||
*/
|
||||
export class UserMessageComponent extends Container {
|
||||
constructor(text: string, isFirst: boolean) {
|
||||
constructor(text: string) {
|
||||
super();
|
||||
|
||||
// Add spacer before user message (except first one)
|
||||
if (!isFirst) {
|
||||
this.addChild(new Spacer(1));
|
||||
}
|
||||
this.addChild(new Spacer(1));
|
||||
this.addChild(
|
||||
new Markdown(text, 1, 1, getMarkdownTheme(), {
|
||||
bgColor: (text: string) => theme.bg("userMessageBg", text),
|
||||
|
|
|
|||
|
|
@ -28,14 +28,8 @@ import { APP_NAME, getAuthPath, getDebugLogPath } from "../../config.js";
|
|||
import type { AgentSession, AgentSessionEvent } from "../../core/agent-session.js";
|
||||
import type { LoadedCustomTool, SessionEvent as ToolSessionEvent } from "../../core/custom-tools/index.js";
|
||||
import type { HookUIContext } from "../../core/hooks/index.js";
|
||||
import { isBashExecutionMessage, isHookMessage } from "../../core/messages.js";
|
||||
import {
|
||||
getLatestCompactionEntry,
|
||||
type SessionContext,
|
||||
SessionManager,
|
||||
SUMMARY_PREFIX,
|
||||
SUMMARY_SUFFIX,
|
||||
} from "../../core/session-manager.js";
|
||||
import { createCompactionSummaryMessage } from "../../core/messages.js";
|
||||
import { type SessionContext, SessionManager } from "../../core/session-manager.js";
|
||||
import { loadSkills } from "../../core/skills.js";
|
||||
import { loadProjectContextFiles } from "../../core/system-prompt.js";
|
||||
import type { TruncationResult } from "../../core/tools/truncate.js";
|
||||
|
|
@ -44,7 +38,7 @@ import { copyToClipboard } from "../../utils/clipboard.js";
|
|||
import { ArminComponent } from "./components/armin.js";
|
||||
import { AssistantMessageComponent } from "./components/assistant-message.js";
|
||||
import { BashExecutionComponent } from "./components/bash-execution.js";
|
||||
import { CompactionComponent } from "./components/compaction.js";
|
||||
import { CompactionSummaryMessageComponent } from "./components/compaction-summary-message.js";
|
||||
import { CustomEditor } from "./components/custom-editor.js";
|
||||
import { DynamicBorder } from "./components/dynamic-border.js";
|
||||
import { FooterComponent } from "./components/footer.js";
|
||||
|
|
@ -84,9 +78,6 @@ export class InteractiveMode {
|
|||
// Tool execution tracking: toolCallId -> component
|
||||
private pendingTools = new Map<string, ToolExecutionComponent>();
|
||||
|
||||
// Track if this is the first user message (to skip spacer)
|
||||
private isFirstUserMessage = true;
|
||||
|
||||
// Tool output expansion state
|
||||
private toolOutputExpanded = false;
|
||||
|
||||
|
|
@ -817,7 +808,7 @@ export class InteractiveMode {
|
|||
break;
|
||||
|
||||
case "message_start":
|
||||
if (isHookMessage(event.message)) {
|
||||
if (event.message.role === "hookMessage") {
|
||||
this.addMessageToChat(event.message);
|
||||
this.ui.requestRender();
|
||||
} else if (event.message.role === "user") {
|
||||
|
|
@ -828,7 +819,7 @@ export class InteractiveMode {
|
|||
} else if (event.message.role === "assistant") {
|
||||
this.streamingComponent = new AssistantMessageComponent(undefined, this.hideThinkingBlock);
|
||||
this.chatContainer.addChild(this.streamingComponent);
|
||||
this.streamingComponent.updateContent(event.message as AssistantMessage);
|
||||
this.streamingComponent.updateContent(event.message);
|
||||
this.ui.requestRender();
|
||||
}
|
||||
break;
|
||||
|
|
@ -983,7 +974,12 @@ export class InteractiveMode {
|
|||
this.chatContainer.clear();
|
||||
this.rebuildChatFromMessages();
|
||||
// Add compaction component (same as manual /compact)
|
||||
const compactionComponent = new CompactionComponent(event.result.tokensBefore, event.result.summary);
|
||||
const compactionComponent = new CompactionSummaryMessageComponent({
|
||||
role: "compactionSummary",
|
||||
tokensBefore: event.result.tokensBefore,
|
||||
summary: event.result.summary,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
compactionComponent.setExpanded(this.toolOutputExpanded);
|
||||
this.chatContainer.addChild(compactionComponent);
|
||||
this.footer.updateState(this.session.state);
|
||||
|
|
@ -1051,38 +1047,70 @@ export class InteractiveMode {
|
|||
this.ui.requestRender();
|
||||
}
|
||||
|
||||
private addMessageToChat(message: AgentMessage): void {
|
||||
if (isBashExecutionMessage(message)) {
|
||||
const component = new BashExecutionComponent(message.command, this.ui);
|
||||
if (message.output) {
|
||||
component.appendOutput(message.output);
|
||||
private addMessageToChat(message: AgentMessage, options?: { populateHistory?: boolean }): void {
|
||||
switch (message.role) {
|
||||
case "bashExecution": {
|
||||
const component = new BashExecutionComponent(message.command, this.ui);
|
||||
if (message.output) {
|
||||
component.appendOutput(message.output);
|
||||
}
|
||||
component.setComplete(
|
||||
message.exitCode,
|
||||
message.cancelled,
|
||||
message.truncated ? ({ truncated: true } as TruncationResult) : undefined,
|
||||
message.fullOutputPath,
|
||||
);
|
||||
this.chatContainer.addChild(component);
|
||||
break;
|
||||
}
|
||||
component.setComplete(
|
||||
message.exitCode,
|
||||
message.cancelled,
|
||||
message.truncated ? ({ truncated: true } as TruncationResult) : undefined,
|
||||
message.fullOutputPath,
|
||||
);
|
||||
this.chatContainer.addChild(component);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isHookMessage(message)) {
|
||||
// Render as custom message if display is true
|
||||
if (message.display) {
|
||||
const renderer = this.session.hookRunner?.getMessageRenderer(message.customType);
|
||||
this.chatContainer.addChild(new HookMessageComponent(message, renderer));
|
||||
case "hookMessage": {
|
||||
if (message.display) {
|
||||
const renderer = this.session.hookRunner?.getMessageRenderer(message.customType);
|
||||
this.chatContainer.addChild(new HookMessageComponent(message, renderer));
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else if (message.role === "user") {
|
||||
const textContent = this.getUserMessageText(message);
|
||||
if (textContent) {
|
||||
const userComponent = new UserMessageComponent(textContent, this.isFirstUserMessage);
|
||||
this.chatContainer.addChild(userComponent);
|
||||
this.isFirstUserMessage = false;
|
||||
case "compactionSummary": {
|
||||
const component = new CompactionSummaryMessageComponent(message);
|
||||
component.setExpanded(this.toolOutputExpanded);
|
||||
this.chatContainer.addChild(component);
|
||||
break;
|
||||
}
|
||||
case "branchSummary": {
|
||||
// Branch summaries are rendered as compaction summaries
|
||||
const component = new CompactionSummaryMessageComponent({
|
||||
role: "compactionSummary",
|
||||
summary: message.summary,
|
||||
tokensBefore: 0,
|
||||
timestamp: message.timestamp,
|
||||
});
|
||||
component.setExpanded(this.toolOutputExpanded);
|
||||
this.chatContainer.addChild(component);
|
||||
break;
|
||||
}
|
||||
case "user": {
|
||||
const textContent = this.getUserMessageText(message);
|
||||
if (textContent) {
|
||||
const userComponent = new UserMessageComponent(textContent);
|
||||
this.chatContainer.addChild(userComponent);
|
||||
if (options?.populateHistory) {
|
||||
this.editor.addToHistory(textContent);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "assistant": {
|
||||
const assistantComponent = new AssistantMessageComponent(message, this.hideThinkingBlock);
|
||||
this.chatContainer.addChild(assistantComponent);
|
||||
break;
|
||||
}
|
||||
case "toolResult": {
|
||||
// Tool results are rendered inline with tool calls, handled separately
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const _exhaustive: never = message;
|
||||
}
|
||||
} else if (message.role === "assistant") {
|
||||
const assistantComponent = new AssistantMessageComponent(message as AssistantMessage, this.hideThinkingBlock);
|
||||
this.chatContainer.addChild(assistantComponent);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1096,7 +1124,6 @@ export class InteractiveMode {
|
|||
sessionContext: SessionContext,
|
||||
options: { updateFooter?: boolean; populateHistory?: boolean } = {},
|
||||
): void {
|
||||
this.isFirstUserMessage = true;
|
||||
this.pendingTools.clear();
|
||||
|
||||
if (options.updateFooter) {
|
||||
|
|
@ -1104,65 +1131,25 @@ export class InteractiveMode {
|
|||
this.updateEditorBorderColor();
|
||||
}
|
||||
|
||||
const compactionEntry = getLatestCompactionEntry(this.sessionManager.getEntries());
|
||||
|
||||
for (let i = 0; i < sessionContext.messages.length; i++) {
|
||||
const message = sessionContext.messages[i];
|
||||
|
||||
if (isBashExecutionMessage(message)) {
|
||||
for (const message of sessionContext.messages) {
|
||||
// Assistant messages need special handling for tool calls
|
||||
if (message.role === "assistant") {
|
||||
this.addMessageToChat(message);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if this is a custom_message entry
|
||||
if (isHookMessage(message)) {
|
||||
if (message.display) {
|
||||
const renderer = this.session.hookRunner?.getMessageRenderer(message.customType);
|
||||
this.chatContainer.addChild(new HookMessageComponent(message, renderer));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (message.role === "user") {
|
||||
const textContent = this.getUserMessageText(message);
|
||||
if (textContent) {
|
||||
if (textContent.startsWith(SUMMARY_PREFIX) && compactionEntry) {
|
||||
const summary = textContent.slice(SUMMARY_PREFIX.length, -SUMMARY_SUFFIX.length);
|
||||
const component = new CompactionComponent(compactionEntry.tokensBefore, summary);
|
||||
component.setExpanded(this.toolOutputExpanded);
|
||||
this.chatContainer.addChild(component);
|
||||
} else {
|
||||
const userComponent = new UserMessageComponent(textContent, this.isFirstUserMessage);
|
||||
this.chatContainer.addChild(userComponent);
|
||||
this.isFirstUserMessage = false;
|
||||
if (options.populateHistory) {
|
||||
this.editor.addToHistory(textContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (message.role === "assistant") {
|
||||
const assistantMsg = message as AssistantMessage;
|
||||
const assistantComponent = new AssistantMessageComponent(assistantMsg, this.hideThinkingBlock);
|
||||
this.chatContainer.addChild(assistantComponent);
|
||||
|
||||
for (const content of assistantMsg.content) {
|
||||
// Render tool call components
|
||||
for (const content of message.content) {
|
||||
if (content.type === "toolCall") {
|
||||
const component = new ToolExecutionComponent(
|
||||
content.name,
|
||||
content.arguments,
|
||||
{
|
||||
showImages: this.settingsManager.getShowImages(),
|
||||
},
|
||||
{ showImages: this.settingsManager.getShowImages() },
|
||||
this.customTools.get(content.name)?.tool,
|
||||
this.ui,
|
||||
);
|
||||
this.chatContainer.addChild(component);
|
||||
|
||||
if (assistantMsg.stopReason === "aborted" || assistantMsg.stopReason === "error") {
|
||||
if (message.stopReason === "aborted" || message.stopReason === "error") {
|
||||
const errorMessage =
|
||||
assistantMsg.stopReason === "aborted"
|
||||
? "Operation aborted"
|
||||
: assistantMsg.errorMessage || "Error";
|
||||
message.stopReason === "aborted" ? "Operation aborted" : message.errorMessage || "Error";
|
||||
component.updateResult({ content: [{ type: "text", text: errorMessage }], isError: true });
|
||||
} else {
|
||||
this.pendingTools.set(content.id, component);
|
||||
|
|
@ -1170,13 +1157,18 @@ export class InteractiveMode {
|
|||
}
|
||||
}
|
||||
} else if (message.role === "toolResult") {
|
||||
// Match tool results to pending tool components
|
||||
const component = this.pendingTools.get(message.toolCallId);
|
||||
if (component) {
|
||||
component.updateResult(message);
|
||||
this.pendingTools.delete(message.toolCallId);
|
||||
}
|
||||
} else {
|
||||
// All other messages use standard rendering
|
||||
this.addMessageToChat(message, options);
|
||||
}
|
||||
}
|
||||
|
||||
this.pendingTools.clear();
|
||||
this.ui.requestRender();
|
||||
}
|
||||
|
|
@ -1308,7 +1300,7 @@ export class InteractiveMode {
|
|||
for (const child of this.chatContainer.children) {
|
||||
if (child instanceof ToolExecutionComponent) {
|
||||
child.setExpanded(this.toolOutputExpanded);
|
||||
} else if (child instanceof CompactionComponent) {
|
||||
} else if (child instanceof CompactionSummaryMessageComponent) {
|
||||
child.setExpanded(this.toolOutputExpanded);
|
||||
} else if (child instanceof BashExecutionComponent) {
|
||||
child.setExpanded(this.toolOutputExpanded);
|
||||
|
|
@ -1584,7 +1576,6 @@ export class InteractiveMode {
|
|||
}
|
||||
|
||||
this.chatContainer.clear();
|
||||
this.isFirstUserMessage = true;
|
||||
this.renderInitialMessages();
|
||||
this.editor.setText(result.selectedText);
|
||||
done();
|
||||
|
|
@ -1638,7 +1629,6 @@ export class InteractiveMode {
|
|||
|
||||
// Clear and re-render the chat
|
||||
this.chatContainer.clear();
|
||||
this.isFirstUserMessage = true;
|
||||
this.renderInitialMessages();
|
||||
this.showStatus("Resumed session");
|
||||
}
|
||||
|
|
@ -1899,7 +1889,6 @@ export class InteractiveMode {
|
|||
this.pendingMessagesContainer.clear();
|
||||
this.streamingComponent = null;
|
||||
this.pendingTools.clear();
|
||||
this.isFirstUserMessage = true;
|
||||
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new Text(`${theme.fg("accent", "✓ New session started")}`, 1, 1));
|
||||
|
|
@ -2027,11 +2016,11 @@ export class InteractiveMode {
|
|||
const result = await this.session.compact(customInstructions);
|
||||
|
||||
// Rebuild UI
|
||||
this.chatContainer.clear();
|
||||
this.rebuildChatFromMessages();
|
||||
|
||||
// Add compaction component
|
||||
const compactionComponent = new CompactionComponent(result.tokensBefore, result.summary);
|
||||
const msg = createCompactionSummaryMessage(result.summary, result.tokensBefore, new Date().toISOString());
|
||||
const compactionComponent = new CompactionSummaryMessageComponent(msg);
|
||||
compactionComponent.setExpanded(this.toolOutputExpanded);
|
||||
this.chatContainer.addChild(compactionComponent);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue