@oh-my-pi/pi-coding-agent 4.1.0 → 4.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +2 -1
  3. package/docs/sdk.md +0 -3
  4. package/package.json +6 -5
  5. package/src/config.ts +9 -0
  6. package/src/core/agent-session.ts +3 -3
  7. package/src/core/agent-storage.ts +450 -0
  8. package/src/core/auth-storage.ts +102 -183
  9. package/src/core/compaction/branch-summarization.ts +5 -4
  10. package/src/core/compaction/compaction.ts +7 -6
  11. package/src/core/compaction/utils.ts +6 -11
  12. package/src/core/custom-commands/bundled/review/index.ts +22 -94
  13. package/src/core/custom-share.ts +66 -0
  14. package/src/core/export-html/index.ts +1 -33
  15. package/src/core/history-storage.ts +15 -7
  16. package/src/core/prompt-templates.ts +271 -1
  17. package/src/core/sdk.ts +14 -3
  18. package/src/core/settings-manager.ts +100 -34
  19. package/src/core/slash-commands.ts +4 -1
  20. package/src/core/storage-migration.ts +215 -0
  21. package/src/core/system-prompt.ts +130 -290
  22. package/src/core/title-generator.ts +3 -2
  23. package/src/core/tools/ask.ts +2 -2
  24. package/src/core/tools/bash.ts +2 -1
  25. package/src/core/tools/calculator.ts +2 -1
  26. package/src/core/tools/complete.ts +5 -2
  27. package/src/core/tools/edit.ts +2 -1
  28. package/src/core/tools/find.ts +2 -1
  29. package/src/core/tools/gemini-image.ts +2 -1
  30. package/src/core/tools/git.ts +2 -2
  31. package/src/core/tools/grep.ts +2 -1
  32. package/src/core/tools/index.test.ts +0 -28
  33. package/src/core/tools/index.ts +0 -6
  34. package/src/core/tools/lsp/index.ts +2 -1
  35. package/src/core/tools/output.ts +2 -1
  36. package/src/core/tools/read.ts +4 -1
  37. package/src/core/tools/ssh.ts +4 -2
  38. package/src/core/tools/task/agents.ts +56 -30
  39. package/src/core/tools/task/commands.ts +5 -8
  40. package/src/core/tools/task/index.ts +7 -15
  41. package/src/core/tools/web-fetch.ts +2 -1
  42. package/src/core/tools/web-search/auth.ts +106 -16
  43. package/src/core/tools/web-search/index.ts +3 -2
  44. package/src/core/tools/web-search/providers/anthropic.ts +44 -6
  45. package/src/core/tools/write.ts +2 -1
  46. package/src/core/voice.ts +3 -1
  47. package/src/discovery/builtin.ts +9 -54
  48. package/src/discovery/claude.ts +16 -69
  49. package/src/discovery/codex.ts +11 -36
  50. package/src/discovery/helpers.ts +52 -1
  51. package/src/main.ts +1 -1
  52. package/src/migrations.ts +20 -20
  53. package/src/modes/interactive/controllers/command-controller.ts +527 -0
  54. package/src/modes/interactive/controllers/event-controller.ts +340 -0
  55. package/src/modes/interactive/controllers/extension-ui-controller.ts +600 -0
  56. package/src/modes/interactive/controllers/input-controller.ts +585 -0
  57. package/src/modes/interactive/controllers/selector-controller.ts +585 -0
  58. package/src/modes/interactive/interactive-mode.ts +363 -3139
  59. package/src/modes/interactive/theme/theme.ts +5 -5
  60. package/src/modes/interactive/types.ts +189 -0
  61. package/src/modes/interactive/utils/ui-helpers.ts +449 -0
  62. package/src/modes/interactive/utils/voice-manager.ts +96 -0
  63. package/src/prompts/{explore.md → agents/explore.md} +7 -5
  64. package/src/prompts/agents/frontmatter.md +7 -0
  65. package/src/prompts/{plan.md → agents/plan.md} +3 -3
  66. package/src/prompts/agents/planner.md +112 -0
  67. package/src/prompts/agents/task.md +15 -0
  68. package/src/prompts/review-request.md +44 -8
  69. package/src/prompts/system/custom-system-prompt.md +80 -0
  70. package/src/prompts/system/file-operations.md +12 -0
  71. package/src/prompts/system/system-prompt.md +237 -0
  72. package/src/prompts/system/title-system.md +2 -0
  73. package/src/prompts/tools/bash.md +1 -1
  74. package/src/prompts/tools/read.md +1 -1
  75. package/src/prompts/tools/task.md +34 -22
  76. package/src/core/tools/rulebook.ts +0 -132
  77. package/src/prompts/architect-plan.md +0 -10
  78. package/src/prompts/implement-with-critic.md +0 -11
  79. package/src/prompts/implement.md +0 -11
  80. package/src/prompts/system-prompt.md +0 -43
  81. package/src/prompts/task.md +0 -14
  82. package/src/prompts/title-system.md +0 -8
  83. /package/src/prompts/{init.md → agents/init.md} +0 -0
  84. /package/src/prompts/{reviewer.md → agents/reviewer.md} +0 -0
  85. /package/src/prompts/{branch-summary-preamble.md → compaction/branch-summary-preamble.md} +0 -0
  86. /package/src/prompts/{branch-summary.md → compaction/branch-summary.md} +0 -0
  87. /package/src/prompts/{compaction-summary.md → compaction/compaction-summary.md} +0 -0
  88. /package/src/prompts/{compaction-turn-prefix.md → compaction/compaction-turn-prefix.md} +0 -0
  89. /package/src/prompts/{compaction-update-summary.md → compaction/compaction-update-summary.md} +0 -0
  90. /package/src/prompts/{summarization-system.md → system/summarization-system.md} +0 -0
@@ -2008,11 +2008,11 @@ export function getThemeExportColors(themeName?: string): {
2008
2008
  const resolve = (value: string | number | undefined): string | undefined => {
2009
2009
  if (value === undefined) return undefined;
2010
2010
  if (typeof value === "number") return ansi256ToHex(value);
2011
- if (value.startsWith("$")) {
2012
- const resolved = vars[value];
2013
- if (resolved === undefined) return undefined;
2014
- if (typeof resolved === "number") return ansi256ToHex(resolved);
2015
- return resolved;
2011
+ if (value === "" || value.startsWith("#")) return value;
2012
+ const varName = value.startsWith("$") ? value.slice(1) : value;
2013
+ if (varName in vars) {
2014
+ const resolved = resolveVarRefs(varName, vars);
2015
+ return typeof resolved === "number" ? ansi256ToHex(resolved) : resolved;
2016
2016
  }
2017
2017
  return value;
2018
2018
  };
@@ -0,0 +1,189 @@
1
+ import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
2
+ import type { AssistantMessage, ImageContent, Message } from "@oh-my-pi/pi-ai";
3
+ import type { Component, Container, Loader, Spacer, Text, TUI } from "@oh-my-pi/pi-tui";
4
+ import type { AgentSession, AgentSessionEvent } from "../../core/agent-session";
5
+ import type { ExtensionUIContext } from "../../core/extensions/index";
6
+ import type { HistoryStorage } from "../../core/history-storage";
7
+ import type { KeybindingsManager } from "../../core/keybindings";
8
+ import type { SessionContext, SessionManager } from "../../core/session-manager";
9
+ import type { SettingsManager } from "../../core/settings-manager";
10
+ import type { VoiceSupervisor } from "../../core/voice-supervisor";
11
+ import type { AssistantMessageComponent } from "./components/assistant-message";
12
+ import type { BashExecutionComponent } from "./components/bash-execution";
13
+ import type { CustomEditor } from "./components/custom-editor";
14
+ import type { HookEditorComponent } from "./components/hook-editor";
15
+ import type { HookInputComponent } from "./components/hook-input";
16
+ import type { HookSelectorComponent } from "./components/hook-selector";
17
+ import type { StatusLineComponent } from "./components/status-line";
18
+ import type { ToolExecutionComponent } from "./components/tool-execution";
19
+ import type { Theme } from "./theme/theme";
20
+
21
+ export type CompactionQueuedMessage = {
22
+ text: string;
23
+ mode: "steer" | "followUp";
24
+ };
25
+
26
+ export interface InteractiveModeContext {
27
+ // UI access
28
+ ui: TUI;
29
+ chatContainer: Container;
30
+ pendingMessagesContainer: Container;
31
+ statusContainer: Container;
32
+ editor: CustomEditor;
33
+ editorContainer: Container;
34
+ statusLine: StatusLineComponent;
35
+
36
+ // Session access
37
+ session: AgentSession;
38
+ sessionManager: SessionManager;
39
+ settingsManager: SettingsManager;
40
+ agent: AgentSession["agent"];
41
+ voiceSupervisor: VoiceSupervisor;
42
+ historyStorage?: HistoryStorage;
43
+
44
+ // State
45
+ isInitialized: boolean;
46
+ isBackgrounded: boolean;
47
+ isBashMode: boolean;
48
+ toolOutputExpanded: boolean;
49
+ hideThinkingBlock: boolean;
50
+ pendingImages: ImageContent[];
51
+ compactionQueuedMessages: CompactionQueuedMessage[];
52
+ pendingTools: Map<string, ToolExecutionComponent>;
53
+ pendingBashComponents: BashExecutionComponent[];
54
+ bashComponent: BashExecutionComponent | undefined;
55
+ streamingComponent: AssistantMessageComponent | undefined;
56
+ streamingMessage: AssistantMessage | undefined;
57
+ loadingAnimation: Loader | undefined;
58
+ autoCompactionLoader: Loader | undefined;
59
+ retryLoader: Loader | undefined;
60
+ autoCompactionEscapeHandler?: () => void;
61
+ retryEscapeHandler?: () => void;
62
+ unsubscribe?: () => void;
63
+ onInputCallback?: (input: { text: string; images?: ImageContent[] }) => void;
64
+ lastSigintTime: number;
65
+ lastEscapeTime: number;
66
+ lastVoiceInterruptAt: number;
67
+ voiceAutoModeEnabled: boolean;
68
+ voiceProgressTimer: ReturnType<typeof setTimeout> | undefined;
69
+ voiceProgressSpoken: boolean;
70
+ voiceProgressLastLength: number;
71
+ hookSelector: HookSelectorComponent | undefined;
72
+ hookInput: HookInputComponent | undefined;
73
+ hookEditor: HookEditorComponent | undefined;
74
+ lastStatusSpacer: Spacer | undefined;
75
+ lastStatusText: Text | undefined;
76
+ fileSlashCommands: Set<string>;
77
+
78
+ // Lifecycle
79
+ init(): Promise<void>;
80
+ shutdown(): Promise<void>;
81
+
82
+ // Extension UI integration
83
+ setToolUIContext(uiContext: ExtensionUIContext, hasUI: boolean): void;
84
+ initializeHookRunner(uiContext: ExtensionUIContext, hasUI: boolean): void;
85
+ createBackgroundUiContext(): ExtensionUIContext;
86
+
87
+ // Event handling
88
+ handleBackgroundEvent(event: AgentSessionEvent): Promise<void>;
89
+
90
+ // UI helpers
91
+ showStatus(message: string, options?: { dim?: boolean }): void;
92
+ showError(message: string): void;
93
+ showWarning(message: string): void;
94
+ showNewVersionNotification(newVersion: string): void;
95
+ clearEditor(): void;
96
+ updatePendingMessagesDisplay(): void;
97
+ queueCompactionMessage(text: string, mode: "steer" | "followUp"): void;
98
+ flushCompactionQueue(options?: { willRetry?: boolean }): Promise<void>;
99
+ flushPendingBashComponents(): void;
100
+ isKnownSlashCommand(text: string): boolean;
101
+ addMessageToChat(message: AgentMessage, options?: { populateHistory?: boolean }): void;
102
+ renderSessionContext(
103
+ sessionContext: SessionContext,
104
+ options?: { updateFooter?: boolean; populateHistory?: boolean },
105
+ ): void;
106
+ renderInitialMessages(): void;
107
+ getUserMessageText(message: Message): string;
108
+ findLastAssistantMessage(): AssistantMessage | undefined;
109
+ extractAssistantText(message: AssistantMessage): string;
110
+ updateEditorTopBorder(): void;
111
+ updateEditorBorderColor(): void;
112
+ rebuildChatFromMessages(): void;
113
+
114
+ // Command handling
115
+ handleExportCommand(text: string): Promise<void>;
116
+ handleShareCommand(): Promise<void>;
117
+ handleCopyCommand(): Promise<void>;
118
+ handleSessionCommand(): void;
119
+ handleChangelogCommand(): void;
120
+ handleHotkeysCommand(): void;
121
+ handleDumpCommand(): Promise<void>;
122
+ handleClearCommand(): Promise<void>;
123
+ handleDebugCommand(): void;
124
+ handleArminSaysHi(): void;
125
+ handleBashCommand(command: string, excludeFromContext?: boolean): Promise<void>;
126
+ handleCompactCommand(customInstructions?: string): Promise<void>;
127
+ executeCompaction(customInstructions?: string, isAuto?: boolean): Promise<void>;
128
+ openInBrowser(urlOrPath: string): void;
129
+
130
+ // Selector handling
131
+ showSettingsSelector(): void;
132
+ showHistorySearch(): void;
133
+ showExtensionsDashboard(): void;
134
+ showModelSelector(options?: { temporaryOnly?: boolean }): void;
135
+ showUserMessageSelector(): void;
136
+ showTreeSelector(): void;
137
+ showSessionSelector(): void;
138
+ handleResumeSession(sessionPath: string): Promise<void>;
139
+ showOAuthSelector(mode: "login" | "logout"): Promise<void>;
140
+ showHookConfirm(title: string, message: string): Promise<boolean>;
141
+
142
+ // Input handling
143
+ handleCtrlC(): void;
144
+ handleCtrlD(): void;
145
+ handleCtrlZ(): void;
146
+ handleDequeue(): void;
147
+ handleBackgroundCommand(): void;
148
+ handleImagePaste(): Promise<boolean>;
149
+ cycleThinkingLevel(): void;
150
+ cycleRoleModel(options?: { temporary?: boolean }): Promise<void>;
151
+ toggleToolOutputExpansion(): void;
152
+ toggleThinkingBlockVisibility(): void;
153
+ openExternalEditor(): void;
154
+ registerExtensionShortcuts(): void;
155
+
156
+ // Voice handling
157
+ setVoiceStatus(text: string | undefined): void;
158
+ handleVoiceInterrupt(reason?: string): Promise<void>;
159
+ startVoiceProgressTimer(): void;
160
+ stopVoiceProgressTimer(): void;
161
+ maybeSpeakProgress(): Promise<void>;
162
+ submitVoiceText(text: string): Promise<void>;
163
+
164
+ // Hook UI methods
165
+ initHooksAndCustomTools(): Promise<void>;
166
+ emitCustomToolSessionEvent(
167
+ reason: "start" | "switch" | "branch" | "tree" | "shutdown",
168
+ previousSessionFile?: string,
169
+ ): Promise<void>;
170
+ setHookWidget(key: string, content: unknown): void;
171
+ setHookStatus(key: string, text: string | undefined): void;
172
+ showHookSelector(title: string, options: string[]): Promise<string | undefined>;
173
+ hideHookSelector(): void;
174
+ showHookInput(title: string, placeholder?: string): Promise<string | undefined>;
175
+ hideHookInput(): void;
176
+ showHookEditor(title: string, prefill?: string): Promise<string | undefined>;
177
+ hideHookEditor(): void;
178
+ showHookNotify(message: string, type?: "info" | "warning" | "error"): void;
179
+ showHookCustom<T>(
180
+ factory: (
181
+ tui: TUI,
182
+ theme: Theme,
183
+ keybindings: KeybindingsManager,
184
+ done: (result: T) => void,
185
+ ) => (Component & { dispose?(): void }) | Promise<Component & { dispose?(): void }>,
186
+ ): Promise<T>;
187
+ showExtensionError(extensionPath: string, error: string): void;
188
+ showToolError(toolName: string, error: string): void;
189
+ }
@@ -0,0 +1,449 @@
1
+ import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
2
+ import type { AssistantMessage, Message } from "@oh-my-pi/pi-ai";
3
+ import { Spacer, Text, TruncatedText } from "@oh-my-pi/pi-tui";
4
+ import type { CustomMessage } from "../../../core/messages";
5
+ import type { SessionContext } from "../../../core/session-manager";
6
+ import type { TruncationResult } from "../../../core/tools/truncate";
7
+ import { AssistantMessageComponent } from "../components/assistant-message";
8
+ import { BashExecutionComponent } from "../components/bash-execution";
9
+ import { BranchSummaryMessageComponent } from "../components/branch-summary-message";
10
+ import { CompactionSummaryMessageComponent } from "../components/compaction-summary-message";
11
+ import { CustomMessageComponent } from "../components/custom-message";
12
+ import { DynamicBorder } from "../components/dynamic-border";
13
+ import { ToolExecutionComponent } from "../components/tool-execution";
14
+ import { UserMessageComponent } from "../components/user-message";
15
+ import { theme } from "../theme/theme";
16
+ import type { CompactionQueuedMessage, InteractiveModeContext } from "../types";
17
+
18
+ type TextBlock = { type: "text"; text: string };
19
+
20
+ type QueuedMessages = {
21
+ steering: string[];
22
+ followUp: string[];
23
+ };
24
+
25
+ export class UiHelpers {
26
+ constructor(private ctx: InteractiveModeContext) {}
27
+
28
+ /** Extract text content from a user message */
29
+ getUserMessageText(message: Message): string {
30
+ if (message.role !== "user") return "";
31
+ const textBlocks =
32
+ typeof message.content === "string"
33
+ ? [{ type: "text", text: message.content }]
34
+ : message.content.filter((content): content is TextBlock => content.type === "text");
35
+ return textBlocks.map((block) => block.text).join("");
36
+ }
37
+
38
+ /**
39
+ * Show a status message in the chat.
40
+ *
41
+ * If multiple status messages are emitted back-to-back (without anything else being added to the chat),
42
+ * we update the previous status line instead of appending new ones to avoid log spam.
43
+ */
44
+ showStatus(message: string, options?: { dim?: boolean }): void {
45
+ if (this.ctx.isBackgrounded) {
46
+ return;
47
+ }
48
+ const children = this.ctx.chatContainer.children;
49
+ const last = children.length > 0 ? children[children.length - 1] : undefined;
50
+ const secondLast = children.length > 1 ? children[children.length - 2] : undefined;
51
+ const useDim = options?.dim ?? true;
52
+ const rendered = useDim ? theme.fg("dim", message) : message;
53
+
54
+ if (last && secondLast && last === this.ctx.lastStatusText && secondLast === this.ctx.lastStatusSpacer) {
55
+ this.ctx.lastStatusText.setText(rendered);
56
+ this.ctx.ui.requestRender();
57
+ return;
58
+ }
59
+
60
+ const spacer = new Spacer(1);
61
+ const text = new Text(rendered, 1, 0);
62
+ this.ctx.chatContainer.addChild(spacer);
63
+ this.ctx.chatContainer.addChild(text);
64
+ this.ctx.lastStatusSpacer = spacer;
65
+ this.ctx.lastStatusText = text;
66
+ this.ctx.ui.requestRender();
67
+ }
68
+
69
+ addMessageToChat(message: AgentMessage, options?: { populateHistory?: boolean }): void {
70
+ switch (message.role) {
71
+ case "bashExecution": {
72
+ const component = new BashExecutionComponent(message.command, this.ctx.ui, message.excludeFromContext);
73
+ if (message.output) {
74
+ component.appendOutput(message.output);
75
+ }
76
+ component.setComplete(
77
+ message.exitCode,
78
+ message.cancelled,
79
+ message.truncated ? ({ truncated: true } as TruncationResult) : undefined,
80
+ message.fullOutputPath,
81
+ );
82
+ this.ctx.chatContainer.addChild(component);
83
+ break;
84
+ }
85
+ case "hookMessage":
86
+ case "custom": {
87
+ if (message.display) {
88
+ const renderer = this.ctx.session.extensionRunner?.getMessageRenderer(message.customType);
89
+ // Both HookMessage and CustomMessage have the same structure, cast for compatibility
90
+ this.ctx.chatContainer.addChild(new CustomMessageComponent(message as CustomMessage<unknown>, renderer));
91
+ }
92
+ break;
93
+ }
94
+ case "compactionSummary": {
95
+ this.ctx.chatContainer.addChild(new Spacer(1));
96
+ const component = new CompactionSummaryMessageComponent(message);
97
+ component.setExpanded(this.ctx.toolOutputExpanded);
98
+ this.ctx.chatContainer.addChild(component);
99
+ break;
100
+ }
101
+ case "branchSummary": {
102
+ this.ctx.chatContainer.addChild(new Spacer(1));
103
+ const component = new BranchSummaryMessageComponent(message);
104
+ component.setExpanded(this.ctx.toolOutputExpanded);
105
+ this.ctx.chatContainer.addChild(component);
106
+ break;
107
+ }
108
+ case "fileMention": {
109
+ // Render compact file mention display
110
+ for (const file of message.files) {
111
+ const text = `${theme.fg("dim", `${theme.tree.last} `)}${theme.fg("muted", "Read")} ${theme.fg(
112
+ "accent",
113
+ file.path,
114
+ )} ${theme.fg("dim", `(${file.lineCount} lines)`)}`;
115
+ this.ctx.chatContainer.addChild(new Text(text, 0, 0));
116
+ }
117
+ break;
118
+ }
119
+ case "user": {
120
+ const textContent = this.ctx.getUserMessageText(message);
121
+ if (textContent) {
122
+ const userComponent = new UserMessageComponent(textContent);
123
+ this.ctx.chatContainer.addChild(userComponent);
124
+ if (options?.populateHistory) {
125
+ this.ctx.editor.addToHistory(textContent);
126
+ }
127
+ }
128
+ break;
129
+ }
130
+ case "assistant": {
131
+ const assistantComponent = new AssistantMessageComponent(message, this.ctx.hideThinkingBlock);
132
+ this.ctx.chatContainer.addChild(assistantComponent);
133
+ break;
134
+ }
135
+ case "toolResult": {
136
+ // Tool results are rendered inline with tool calls, handled separately
137
+ break;
138
+ }
139
+ default: {
140
+ const _exhaustive: never = message;
141
+ }
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Render session context to chat. Used for initial load and rebuild after compaction.
147
+ * @param sessionContext Session context to render
148
+ * @param options.updateFooter Update footer state
149
+ * @param options.populateHistory Add user messages to editor history
150
+ */
151
+ renderSessionContext(
152
+ sessionContext: SessionContext,
153
+ options: { updateFooter?: boolean; populateHistory?: boolean } = {},
154
+ ): void {
155
+ this.ctx.pendingTools.clear();
156
+
157
+ if (options.updateFooter) {
158
+ this.ctx.statusLine.invalidate();
159
+ this.ctx.updateEditorBorderColor();
160
+ }
161
+
162
+ for (const message of sessionContext.messages) {
163
+ // Assistant messages need special handling for tool calls
164
+ if (message.role === "assistant") {
165
+ this.ctx.addMessageToChat(message);
166
+ // Render tool call components
167
+ for (const content of message.content) {
168
+ if (content.type === "toolCall") {
169
+ const tool = this.ctx.session.getToolByName(content.name);
170
+ const component = new ToolExecutionComponent(
171
+ content.name,
172
+ content.arguments,
173
+ { showImages: this.ctx.settingsManager.getShowImages() },
174
+ tool,
175
+ this.ctx.ui,
176
+ this.ctx.sessionManager.getCwd(),
177
+ );
178
+ component.setExpanded(this.ctx.toolOutputExpanded);
179
+ this.ctx.chatContainer.addChild(component);
180
+
181
+ if (message.stopReason === "aborted" || message.stopReason === "error") {
182
+ let errorMessage: string;
183
+ if (message.stopReason === "aborted") {
184
+ const retryAttempt = this.ctx.session.retryAttempt;
185
+ errorMessage =
186
+ retryAttempt > 0
187
+ ? `Aborted after ${retryAttempt} retry attempt${retryAttempt > 1 ? "s" : ""}`
188
+ : "Operation aborted";
189
+ } else {
190
+ errorMessage = message.errorMessage || "Error";
191
+ }
192
+ component.updateResult({ content: [{ type: "text", text: errorMessage }], isError: true });
193
+ } else {
194
+ this.ctx.pendingTools.set(content.id, component);
195
+ }
196
+ }
197
+ }
198
+ } else if (message.role === "toolResult") {
199
+ // Match tool results to pending tool components
200
+ const component = this.ctx.pendingTools.get(message.toolCallId);
201
+ if (component) {
202
+ component.updateResult(message);
203
+ this.ctx.pendingTools.delete(message.toolCallId);
204
+ }
205
+ } else {
206
+ // All other messages use standard rendering
207
+ this.ctx.addMessageToChat(message, options);
208
+ }
209
+ }
210
+
211
+ this.ctx.pendingTools.clear();
212
+ this.ctx.ui.requestRender();
213
+ }
214
+
215
+ renderInitialMessages(): void {
216
+ // Get aligned messages and entries from session context
217
+ const context = this.ctx.sessionManager.buildSessionContext();
218
+ this.ctx.renderSessionContext(context, {
219
+ updateFooter: true,
220
+ populateHistory: true,
221
+ });
222
+
223
+ // Show compaction info if session was compacted
224
+ const allEntries = this.ctx.sessionManager.getEntries();
225
+ let compactionCount = 0;
226
+ for (const entry of allEntries) {
227
+ if (entry.type === "compaction") {
228
+ compactionCount++;
229
+ }
230
+ }
231
+ if (compactionCount > 0) {
232
+ const times = compactionCount === 1 ? "1 time" : `${compactionCount} times`;
233
+ this.ctx.showStatus(`Session compacted ${times}`);
234
+ }
235
+ }
236
+
237
+ clearEditor(): void {
238
+ if (this.ctx.isBackgrounded) {
239
+ return;
240
+ }
241
+ this.ctx.editor.setText("");
242
+ this.ctx.pendingImages = [];
243
+ this.ctx.ui.requestRender();
244
+ }
245
+
246
+ showError(errorMessage: string): void {
247
+ if (this.ctx.isBackgrounded) {
248
+ process.stderr.write(`Error: ${errorMessage}\n`);
249
+ return;
250
+ }
251
+ this.ctx.chatContainer.addChild(new Spacer(1));
252
+ this.ctx.chatContainer.addChild(new Text(theme.fg("error", `Error: ${errorMessage}`), 1, 0));
253
+ this.ctx.ui.requestRender();
254
+ }
255
+
256
+ showWarning(warningMessage: string): void {
257
+ if (this.ctx.isBackgrounded) {
258
+ process.stderr.write(`Warning: ${warningMessage}\n`);
259
+ return;
260
+ }
261
+ this.ctx.chatContainer.addChild(new Spacer(1));
262
+ this.ctx.chatContainer.addChild(new Text(theme.fg("warning", `Warning: ${warningMessage}`), 1, 0));
263
+ this.ctx.ui.requestRender();
264
+ }
265
+
266
+ showNewVersionNotification(newVersion: string): void {
267
+ this.ctx.chatContainer.addChild(new Spacer(1));
268
+ this.ctx.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
269
+ this.ctx.chatContainer.addChild(
270
+ new Text(
271
+ theme.bold(theme.fg("warning", "Update Available")) +
272
+ "\n" +
273
+ theme.fg("muted", `New version ${newVersion} is available. Run: `) +
274
+ theme.fg("accent", "omp update"),
275
+ 1,
276
+ 0,
277
+ ),
278
+ );
279
+ this.ctx.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
280
+ this.ctx.ui.requestRender();
281
+ }
282
+
283
+ updatePendingMessagesDisplay(): void {
284
+ this.ctx.pendingMessagesContainer.clear();
285
+ const queuedMessages = this.ctx.session.getQueuedMessages() as QueuedMessages;
286
+
287
+ const steeringMessages: Array<{ message: string; label: string }> = [];
288
+ for (const message of queuedMessages.steering) {
289
+ steeringMessages.push({ message, label: "Steer" });
290
+ }
291
+ for (const entry of this.ctx.compactionQueuedMessages as CompactionQueuedMessage[]) {
292
+ if (entry.mode === "steer") {
293
+ steeringMessages.push({ message: entry.text, label: "Steer" });
294
+ }
295
+ }
296
+
297
+ const followUpMessages: Array<{ message: string; label: string }> = [];
298
+ for (const message of queuedMessages.followUp) {
299
+ followUpMessages.push({ message, label: "Follow-up" });
300
+ }
301
+ for (const entry of this.ctx.compactionQueuedMessages as CompactionQueuedMessage[]) {
302
+ if (entry.mode === "followUp") {
303
+ followUpMessages.push({ message: entry.text, label: "Follow-up" });
304
+ }
305
+ }
306
+
307
+ const allMessages = [...steeringMessages, ...followUpMessages];
308
+ if (allMessages.length > 0) {
309
+ this.ctx.pendingMessagesContainer.addChild(new Spacer(1));
310
+ for (const entry of allMessages) {
311
+ const queuedText = theme.fg("dim", `${entry.label}: ${entry.message}`);
312
+ this.ctx.pendingMessagesContainer.addChild(new TruncatedText(queuedText, 1, 0));
313
+ }
314
+ }
315
+ }
316
+
317
+ queueCompactionMessage(text: string, mode: "steer" | "followUp"): void {
318
+ this.ctx.compactionQueuedMessages.push({ text, mode } as CompactionQueuedMessage);
319
+ this.ctx.editor.addToHistory(text);
320
+ this.ctx.editor.setText("");
321
+ this.ctx.updatePendingMessagesDisplay();
322
+ this.ctx.showStatus("Queued message for after compaction");
323
+ }
324
+
325
+ isKnownSlashCommand(text: string): boolean {
326
+ if (!text.startsWith("/")) return false;
327
+ const spaceIndex = text.indexOf(" ");
328
+ const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
329
+ if (!commandName) return false;
330
+
331
+ if (this.ctx.session.extensionRunner?.getCommand(commandName)) {
332
+ return true;
333
+ }
334
+
335
+ for (const command of this.ctx.session.customCommands) {
336
+ if (command.command.name === commandName) {
337
+ return true;
338
+ }
339
+ }
340
+
341
+ return this.ctx.fileSlashCommands.has(commandName);
342
+ }
343
+
344
+ async flushCompactionQueue(options?: { willRetry?: boolean }): Promise<void> {
345
+ if (this.ctx.compactionQueuedMessages.length === 0) {
346
+ return;
347
+ }
348
+
349
+ const queuedMessages = [...(this.ctx.compactionQueuedMessages as CompactionQueuedMessage[])];
350
+ this.ctx.compactionQueuedMessages = [] as CompactionQueuedMessage[];
351
+ this.ctx.updatePendingMessagesDisplay();
352
+
353
+ const restoreQueue = (error: unknown) => {
354
+ this.ctx.session.clearQueue();
355
+ this.ctx.compactionQueuedMessages = queuedMessages;
356
+ this.ctx.updatePendingMessagesDisplay();
357
+ this.ctx.showError(
358
+ `Failed to send queued message${queuedMessages.length > 1 ? "s" : ""}: ${
359
+ error instanceof Error ? error.message : String(error)
360
+ }`,
361
+ );
362
+ };
363
+
364
+ try {
365
+ if (options?.willRetry) {
366
+ for (const message of queuedMessages) {
367
+ if (this.ctx.isKnownSlashCommand(message.text)) {
368
+ await this.ctx.session.prompt(message.text);
369
+ } else if (message.mode === "followUp") {
370
+ await this.ctx.session.followUp(message.text);
371
+ } else {
372
+ await this.ctx.session.steer(message.text);
373
+ }
374
+ }
375
+ this.ctx.updatePendingMessagesDisplay();
376
+ return;
377
+ }
378
+
379
+ let firstPromptIndex = -1;
380
+ for (let i = 0; i < queuedMessages.length; i++) {
381
+ if (!this.ctx.isKnownSlashCommand(queuedMessages[i].text)) {
382
+ firstPromptIndex = i;
383
+ break;
384
+ }
385
+ }
386
+ if (firstPromptIndex === -1) {
387
+ for (const message of queuedMessages) {
388
+ await this.ctx.session.prompt(message.text);
389
+ }
390
+ return;
391
+ }
392
+
393
+ const preCommands = queuedMessages.slice(0, firstPromptIndex);
394
+ const firstPrompt = queuedMessages[firstPromptIndex];
395
+ const rest = queuedMessages.slice(firstPromptIndex + 1);
396
+
397
+ for (const message of preCommands) {
398
+ await this.ctx.session.prompt(message.text);
399
+ }
400
+
401
+ const promptPromise = this.ctx.session.prompt(firstPrompt.text).catch((error: unknown) => {
402
+ restoreQueue(error);
403
+ });
404
+
405
+ for (const message of rest) {
406
+ if (this.ctx.isKnownSlashCommand(message.text)) {
407
+ await this.ctx.session.prompt(message.text);
408
+ } else if (message.mode === "followUp") {
409
+ await this.ctx.session.followUp(message.text);
410
+ } else {
411
+ await this.ctx.session.steer(message.text);
412
+ }
413
+ }
414
+ this.ctx.updatePendingMessagesDisplay();
415
+ void promptPromise;
416
+ } catch (error) {
417
+ restoreQueue(error);
418
+ }
419
+ }
420
+
421
+ /** Move pending bash components from pending area to chat */
422
+ flushPendingBashComponents(): void {
423
+ for (const component of this.ctx.pendingBashComponents) {
424
+ this.ctx.pendingMessagesContainer.removeChild(component);
425
+ this.ctx.chatContainer.addChild(component);
426
+ }
427
+ this.ctx.pendingBashComponents = [];
428
+ }
429
+
430
+ findLastAssistantMessage(): AssistantMessage | undefined {
431
+ for (let i = this.ctx.session.messages.length - 1; i >= 0; i--) {
432
+ const message = this.ctx.session.messages[i];
433
+ if (message?.role === "assistant") {
434
+ return message as AssistantMessage;
435
+ }
436
+ }
437
+ return undefined;
438
+ }
439
+
440
+ extractAssistantText(message: AssistantMessage): string {
441
+ let text = "";
442
+ for (const content of message.content) {
443
+ if (content.type === "text") {
444
+ text += content.text;
445
+ }
446
+ }
447
+ return text.trim();
448
+ }
449
+ }