@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.
- package/CHANGELOG.md +66 -0
- package/README.md +2 -1
- package/docs/sdk.md +0 -3
- package/package.json +6 -5
- package/src/config.ts +9 -0
- package/src/core/agent-session.ts +3 -3
- package/src/core/agent-storage.ts +450 -0
- package/src/core/auth-storage.ts +102 -183
- package/src/core/compaction/branch-summarization.ts +5 -4
- package/src/core/compaction/compaction.ts +7 -6
- package/src/core/compaction/utils.ts +6 -11
- package/src/core/custom-commands/bundled/review/index.ts +22 -94
- package/src/core/custom-share.ts +66 -0
- package/src/core/export-html/index.ts +1 -33
- package/src/core/history-storage.ts +15 -7
- package/src/core/prompt-templates.ts +271 -1
- package/src/core/sdk.ts +14 -3
- package/src/core/settings-manager.ts +100 -34
- package/src/core/slash-commands.ts +4 -1
- package/src/core/storage-migration.ts +215 -0
- package/src/core/system-prompt.ts +130 -290
- package/src/core/title-generator.ts +3 -2
- package/src/core/tools/ask.ts +2 -2
- package/src/core/tools/bash.ts +2 -1
- package/src/core/tools/calculator.ts +2 -1
- package/src/core/tools/complete.ts +5 -2
- package/src/core/tools/edit.ts +2 -1
- package/src/core/tools/find.ts +2 -1
- package/src/core/tools/gemini-image.ts +2 -1
- package/src/core/tools/git.ts +2 -2
- package/src/core/tools/grep.ts +2 -1
- package/src/core/tools/index.test.ts +0 -28
- package/src/core/tools/index.ts +0 -6
- package/src/core/tools/lsp/index.ts +2 -1
- package/src/core/tools/output.ts +2 -1
- package/src/core/tools/read.ts +4 -1
- package/src/core/tools/ssh.ts +4 -2
- package/src/core/tools/task/agents.ts +56 -30
- package/src/core/tools/task/commands.ts +5 -8
- package/src/core/tools/task/index.ts +7 -15
- package/src/core/tools/web-fetch.ts +2 -1
- package/src/core/tools/web-search/auth.ts +106 -16
- package/src/core/tools/web-search/index.ts +3 -2
- package/src/core/tools/web-search/providers/anthropic.ts +44 -6
- package/src/core/tools/write.ts +2 -1
- package/src/core/voice.ts +3 -1
- package/src/discovery/builtin.ts +9 -54
- package/src/discovery/claude.ts +16 -69
- package/src/discovery/codex.ts +11 -36
- package/src/discovery/helpers.ts +52 -1
- package/src/main.ts +1 -1
- package/src/migrations.ts +20 -20
- package/src/modes/interactive/controllers/command-controller.ts +527 -0
- package/src/modes/interactive/controllers/event-controller.ts +340 -0
- package/src/modes/interactive/controllers/extension-ui-controller.ts +600 -0
- package/src/modes/interactive/controllers/input-controller.ts +585 -0
- package/src/modes/interactive/controllers/selector-controller.ts +585 -0
- package/src/modes/interactive/interactive-mode.ts +363 -3139
- package/src/modes/interactive/theme/theme.ts +5 -5
- package/src/modes/interactive/types.ts +189 -0
- package/src/modes/interactive/utils/ui-helpers.ts +449 -0
- package/src/modes/interactive/utils/voice-manager.ts +96 -0
- package/src/prompts/{explore.md → agents/explore.md} +7 -5
- package/src/prompts/agents/frontmatter.md +7 -0
- package/src/prompts/{plan.md → agents/plan.md} +3 -3
- package/src/prompts/agents/planner.md +112 -0
- package/src/prompts/agents/task.md +15 -0
- package/src/prompts/review-request.md +44 -8
- package/src/prompts/system/custom-system-prompt.md +80 -0
- package/src/prompts/system/file-operations.md +12 -0
- package/src/prompts/system/system-prompt.md +237 -0
- package/src/prompts/system/title-system.md +2 -0
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/read.md +1 -1
- package/src/prompts/tools/task.md +34 -22
- package/src/core/tools/rulebook.ts +0 -132
- package/src/prompts/architect-plan.md +0 -10
- package/src/prompts/implement-with-critic.md +0 -11
- package/src/prompts/implement.md +0 -11
- package/src/prompts/system-prompt.md +0 -43
- package/src/prompts/task.md +0 -14
- package/src/prompts/title-system.md +0 -8
- /package/src/prompts/{init.md → agents/init.md} +0 -0
- /package/src/prompts/{reviewer.md → agents/reviewer.md} +0 -0
- /package/src/prompts/{branch-summary-preamble.md → compaction/branch-summary-preamble.md} +0 -0
- /package/src/prompts/{branch-summary.md → compaction/branch-summary.md} +0 -0
- /package/src/prompts/{compaction-summary.md → compaction/compaction-summary.md} +0 -0
- /package/src/prompts/{compaction-turn-prefix.md → compaction/compaction-turn-prefix.md} +0 -0
- /package/src/prompts/{compaction-update-summary.md → compaction/compaction-update-summary.md} +0 -0
- /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
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
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
|
+
}
|