@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
|
@@ -3,84 +3,51 @@
|
|
|
3
3
|
* Handles TUI rendering and user interaction, delegating business logic to AgentSession.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import * as fs from "node:fs";
|
|
7
|
-
import * as os from "node:os";
|
|
8
6
|
import * as path from "node:path";
|
|
9
|
-
import type { AgentMessage
|
|
10
|
-
import type { AssistantMessage, ImageContent, Message
|
|
11
|
-
import type { SlashCommand } from "@oh-my-pi/pi-tui";
|
|
7
|
+
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
8
|
+
import type { AssistantMessage, ImageContent, Message } from "@oh-my-pi/pi-ai";
|
|
9
|
+
import type { Component, Loader, SlashCommand } from "@oh-my-pi/pi-tui";
|
|
12
10
|
import {
|
|
13
11
|
CombinedAutocompleteProvider,
|
|
14
|
-
type Component,
|
|
15
12
|
Container,
|
|
16
|
-
Input,
|
|
17
|
-
Loader,
|
|
18
13
|
Markdown,
|
|
19
14
|
ProcessTerminal,
|
|
20
15
|
Spacer,
|
|
21
16
|
Text,
|
|
22
|
-
TruncatedText,
|
|
23
17
|
TUI,
|
|
24
|
-
visibleWidth,
|
|
25
18
|
} from "@oh-my-pi/pi-tui";
|
|
26
|
-
import { nanoid } from "nanoid";
|
|
27
|
-
import { getAuthPath, getDebugLogPath } from "../../config";
|
|
28
19
|
import type { AgentSession, AgentSessionEvent } from "../../core/agent-session";
|
|
29
20
|
import type { ExtensionUIContext } from "../../core/extensions/index";
|
|
30
21
|
import { HistoryStorage } from "../../core/history-storage";
|
|
31
|
-
import { KeybindingsManager } from "../../core/keybindings";
|
|
22
|
+
import type { KeybindingsManager } from "../../core/keybindings";
|
|
32
23
|
import { logger } from "../../core/logger";
|
|
33
|
-
import {
|
|
34
|
-
import { getRecentSessions
|
|
24
|
+
import type { SessionContext, SessionManager } from "../../core/session-manager";
|
|
25
|
+
import { getRecentSessions } from "../../core/session-manager";
|
|
26
|
+
import type { SettingsManager } from "../../core/settings-manager";
|
|
35
27
|
import { loadSlashCommands } from "../../core/slash-commands";
|
|
36
|
-
import {
|
|
37
|
-
import { generateSessionTitle, setTerminalTitle } from "../../core/title-generator";
|
|
38
|
-
import { setPreferredImageProvider, setPreferredWebSearchProvider } from "../../core/tools/index";
|
|
39
|
-
import type { TruncationResult } from "../../core/tools/truncate";
|
|
28
|
+
import { setTerminalTitle } from "../../core/title-generator";
|
|
40
29
|
import { VoiceSupervisor } from "../../core/voice-supervisor";
|
|
41
|
-
import { disableProvider, enableProvider } from "../../discovery";
|
|
42
|
-
import { getChangelogPath, parseChangelog } from "../../utils/changelog";
|
|
43
|
-
import { copyToClipboard, readImageFromClipboard } from "../../utils/clipboard";
|
|
44
|
-
import { resizeImage } from "../../utils/image-resize";
|
|
45
30
|
import { registerAsyncCleanup } from "../cleanup";
|
|
46
|
-
import {
|
|
47
|
-
import {
|
|
48
|
-
import { BashExecutionComponent } from "./components/bash-execution";
|
|
49
|
-
import { BorderedLoader } from "./components/bordered-loader";
|
|
50
|
-
import { BranchSummaryMessageComponent } from "./components/branch-summary-message";
|
|
51
|
-
import { CompactionSummaryMessageComponent } from "./components/compaction-summary-message";
|
|
31
|
+
import type { AssistantMessageComponent } from "./components/assistant-message";
|
|
32
|
+
import type { BashExecutionComponent } from "./components/bash-execution";
|
|
52
33
|
import { CustomEditor } from "./components/custom-editor";
|
|
53
|
-
import { CustomMessageComponent } from "./components/custom-message";
|
|
54
34
|
import { DynamicBorder } from "./components/dynamic-border";
|
|
55
|
-
import {
|
|
56
|
-
import {
|
|
57
|
-
import {
|
|
58
|
-
import { HookInputComponent } from "./components/hook-input";
|
|
59
|
-
import { HookSelectorComponent } from "./components/hook-selector";
|
|
60
|
-
import { ModelSelectorComponent } from "./components/model-selector";
|
|
61
|
-
import { OAuthSelectorComponent } from "./components/oauth-selector";
|
|
62
|
-
import { SessionSelectorComponent } from "./components/session-selector";
|
|
63
|
-
import { SettingsSelectorComponent } from "./components/settings-selector";
|
|
35
|
+
import type { HookEditorComponent } from "./components/hook-editor";
|
|
36
|
+
import type { HookInputComponent } from "./components/hook-input";
|
|
37
|
+
import type { HookSelectorComponent } from "./components/hook-selector";
|
|
64
38
|
import { StatusLineComponent } from "./components/status-line";
|
|
65
|
-
import { ToolExecutionComponent } from "./components/tool-execution";
|
|
66
|
-
import { TreeSelectorComponent } from "./components/tree-selector";
|
|
67
|
-
import { TtsrNotificationComponent } from "./components/ttsr-notification";
|
|
68
|
-
import { UserMessageComponent } from "./components/user-message";
|
|
69
|
-
import { UserMessageSelectorComponent } from "./components/user-message-selector";
|
|
39
|
+
import type { ToolExecutionComponent } from "./components/tool-execution";
|
|
70
40
|
import { WelcomeComponent } from "./components/welcome";
|
|
71
|
-
import {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
type Theme,
|
|
82
|
-
theme,
|
|
83
|
-
} from "./theme/theme";
|
|
41
|
+
import { CommandController } from "./controllers/command-controller";
|
|
42
|
+
import { EventController } from "./controllers/event-controller";
|
|
43
|
+
import { ExtensionUiController } from "./controllers/extension-ui-controller";
|
|
44
|
+
import { InputController } from "./controllers/input-controller";
|
|
45
|
+
import { SelectorController } from "./controllers/selector-controller";
|
|
46
|
+
import type { Theme } from "./theme/theme";
|
|
47
|
+
import { getEditorTheme, getMarkdownTheme, onThemeChange, theme } from "./theme/theme";
|
|
48
|
+
import type { CompactionQueuedMessage, InteractiveModeContext } from "./types";
|
|
49
|
+
import { UiHelpers } from "./utils/ui-helpers";
|
|
50
|
+
import { VoiceManager } from "./utils/voice-manager";
|
|
84
51
|
|
|
85
52
|
/** Options for creating an InteractiveMode instance (for future API use) */
|
|
86
53
|
export interface InteractiveModeOptions {
|
|
@@ -96,138 +63,95 @@ export interface InteractiveModeOptions {
|
|
|
96
63
|
initialMessages?: string[];
|
|
97
64
|
}
|
|
98
65
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
66
|
+
export class InteractiveMode implements InteractiveModeContext {
|
|
67
|
+
public session: AgentSession;
|
|
68
|
+
public sessionManager: SessionManager;
|
|
69
|
+
public settingsManager: SettingsManager;
|
|
70
|
+
public agent: AgentSession["agent"];
|
|
71
|
+
public voiceSupervisor: VoiceSupervisor;
|
|
72
|
+
public historyStorage?: HistoryStorage;
|
|
73
|
+
|
|
74
|
+
public ui: TUI;
|
|
75
|
+
public chatContainer: Container;
|
|
76
|
+
public pendingMessagesContainer: Container;
|
|
77
|
+
public statusContainer: Container;
|
|
78
|
+
public editor: CustomEditor;
|
|
79
|
+
public editorContainer: Container;
|
|
80
|
+
public statusLine: StatusLineComponent;
|
|
81
|
+
|
|
82
|
+
public isInitialized = false;
|
|
83
|
+
public isBackgrounded = false;
|
|
84
|
+
public isBashMode = false;
|
|
85
|
+
public toolOutputExpanded = false;
|
|
86
|
+
public hideThinkingBlock = false;
|
|
87
|
+
public pendingImages: ImageContent[] = [];
|
|
88
|
+
public compactionQueuedMessages: CompactionQueuedMessage[] = [];
|
|
89
|
+
public pendingTools = new Map<string, ToolExecutionComponent>();
|
|
90
|
+
public pendingBashComponents: BashExecutionComponent[] = [];
|
|
91
|
+
public bashComponent: BashExecutionComponent | undefined = undefined;
|
|
92
|
+
public streamingComponent: AssistantMessageComponent | undefined = undefined;
|
|
93
|
+
public streamingMessage: AssistantMessage | undefined = undefined;
|
|
94
|
+
public loadingAnimation: Loader | undefined = undefined;
|
|
95
|
+
public autoCompactionLoader: Loader | undefined = undefined;
|
|
96
|
+
public retryLoader: Loader | undefined = undefined;
|
|
97
|
+
public autoCompactionEscapeHandler?: () => void;
|
|
98
|
+
public retryEscapeHandler?: () => void;
|
|
99
|
+
public unsubscribe?: () => void;
|
|
100
|
+
public onInputCallback?: (input: { text: string; images?: ImageContent[] }) => void;
|
|
101
|
+
public lastSigintTime = 0;
|
|
102
|
+
public lastEscapeTime = 0;
|
|
103
|
+
public lastVoiceInterruptAt = 0;
|
|
104
|
+
public voiceAutoModeEnabled = false;
|
|
105
|
+
public voiceProgressTimer: ReturnType<typeof setTimeout> | undefined = undefined;
|
|
106
|
+
public voiceProgressSpoken = false;
|
|
107
|
+
public voiceProgressLastLength = 0;
|
|
108
|
+
public hookSelector: HookSelectorComponent | undefined = undefined;
|
|
109
|
+
public hookInput: HookInputComponent | undefined = undefined;
|
|
110
|
+
public hookEditor: HookEditorComponent | undefined = undefined;
|
|
111
|
+
public lastStatusSpacer: Spacer | undefined = undefined;
|
|
112
|
+
public lastStatusText: Text | undefined = undefined;
|
|
113
|
+
public fileSlashCommands: Set<string> = new Set();
|
|
107
114
|
|
|
108
|
-
type CompactionQueuedMessage = {
|
|
109
|
-
text: string;
|
|
110
|
-
mode: "steer" | "followUp";
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
const VOICE_PROGRESS_DELAY_MS = 15000;
|
|
114
|
-
const VOICE_PROGRESS_MIN_CHARS = 160;
|
|
115
|
-
const VOICE_PROGRESS_DELTA_CHARS = 120;
|
|
116
|
-
|
|
117
|
-
export class InteractiveMode {
|
|
118
|
-
private session: AgentSession;
|
|
119
|
-
private ui: TUI;
|
|
120
|
-
private chatContainer: Container;
|
|
121
|
-
private pendingMessagesContainer: Container;
|
|
122
|
-
private statusContainer: Container;
|
|
123
|
-
private editor: CustomEditor;
|
|
124
|
-
private editorContainer: Container;
|
|
125
|
-
private statusLine: StatusLineComponent;
|
|
126
|
-
private version: string;
|
|
127
|
-
private isInitialized = false;
|
|
128
|
-
private onInputCallback?: (input: { text: string; images?: ImageContent[] }) => void;
|
|
129
|
-
private loadingAnimation: Loader | undefined = undefined;
|
|
130
|
-
|
|
131
|
-
private lastSigintTime = 0;
|
|
132
|
-
private lastEscapeTime = 0;
|
|
133
|
-
private changelogMarkdown: string | undefined = undefined;
|
|
134
|
-
|
|
135
|
-
// Status line tracking (for mutating immediately-sequential status updates)
|
|
136
|
-
private lastStatusSpacer: Spacer | undefined = undefined;
|
|
137
|
-
private lastStatusText: Text | undefined = undefined;
|
|
138
|
-
|
|
139
|
-
// Streaming message tracking
|
|
140
|
-
private streamingComponent: AssistantMessageComponent | undefined = undefined;
|
|
141
|
-
private streamingMessage: AssistantMessage | undefined = undefined;
|
|
142
|
-
|
|
143
|
-
// Tool execution tracking: toolCallId -> component
|
|
144
|
-
private pendingTools = new Map<string, ToolExecutionComponent>();
|
|
145
|
-
|
|
146
|
-
// Tool output expansion state
|
|
147
|
-
private toolOutputExpanded = false;
|
|
148
|
-
|
|
149
|
-
// Thinking block visibility state
|
|
150
|
-
private hideThinkingBlock = false;
|
|
151
|
-
|
|
152
|
-
// Background mode flag (no UI, no interactive prompts)
|
|
153
|
-
private isBackgrounded = false;
|
|
154
|
-
|
|
155
|
-
// Agent subscription unsubscribe function
|
|
156
|
-
private unsubscribe?: () => void;
|
|
157
|
-
|
|
158
|
-
// Signal cleanup unsubscribe function (for SIGINT/SIGTERM flush)
|
|
159
115
|
private cleanupUnsubscribe?: () => void;
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
private
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
private
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
private
|
|
172
|
-
|
|
173
|
-
// Slash commands loaded from files (for compaction queue handling)
|
|
174
|
-
private fileSlashCommands = new Set<string>();
|
|
175
|
-
|
|
176
|
-
private historyStorage?: HistoryStorage;
|
|
177
|
-
|
|
178
|
-
// Voice mode state
|
|
179
|
-
private voiceSupervisor: VoiceSupervisor;
|
|
180
|
-
private voiceAutoModeEnabled = false;
|
|
181
|
-
private voiceProgressTimer: ReturnType<typeof setTimeout> | undefined = undefined;
|
|
182
|
-
private voiceProgressSpoken = false;
|
|
183
|
-
private voiceProgressLastLength = 0;
|
|
184
|
-
private lastVoiceInterruptAt = 0;
|
|
185
|
-
|
|
186
|
-
// Auto-compaction state
|
|
187
|
-
private autoCompactionLoader: Loader | undefined = undefined;
|
|
188
|
-
private autoCompactionEscapeHandler?: () => void;
|
|
189
|
-
|
|
190
|
-
// Messages queued while compaction is running
|
|
191
|
-
private compactionQueuedMessages: CompactionQueuedMessage[] = [];
|
|
192
|
-
|
|
193
|
-
// Auto-retry state
|
|
194
|
-
private retryLoader: Loader | undefined = undefined;
|
|
195
|
-
private retryEscapeHandler?: () => void;
|
|
196
|
-
|
|
197
|
-
// Hook UI state
|
|
198
|
-
private hookSelector: HookSelectorComponent | undefined = undefined;
|
|
199
|
-
private hookInput: HookInputComponent | undefined = undefined;
|
|
200
|
-
private hookEditor: HookEditorComponent | undefined = undefined;
|
|
201
|
-
|
|
202
|
-
// Convenience accessors
|
|
203
|
-
private get agent() {
|
|
204
|
-
return this.session.agent;
|
|
205
|
-
}
|
|
206
|
-
private get sessionManager() {
|
|
207
|
-
return this.session.sessionManager;
|
|
208
|
-
}
|
|
209
|
-
private get settingsManager() {
|
|
210
|
-
return this.session.settingsManager;
|
|
211
|
-
}
|
|
116
|
+
private readonly version: string;
|
|
117
|
+
private readonly changelogMarkdown: string | undefined;
|
|
118
|
+
private readonly lspServers: Array<{ name: string; status: "ready" | "error"; fileTypes: string[] }> | undefined =
|
|
119
|
+
undefined;
|
|
120
|
+
private readonly toolUiContextSetter: (uiContext: ExtensionUIContext, hasUI: boolean) => void;
|
|
121
|
+
|
|
122
|
+
private readonly commandController: CommandController;
|
|
123
|
+
private readonly eventController: EventController;
|
|
124
|
+
private readonly extensionUiController: ExtensionUiController;
|
|
125
|
+
private readonly inputController: InputController;
|
|
126
|
+
private readonly selectorController: SelectorController;
|
|
127
|
+
private readonly uiHelpers: UiHelpers;
|
|
128
|
+
private readonly voiceManager: VoiceManager;
|
|
212
129
|
|
|
213
130
|
constructor(
|
|
214
131
|
session: AgentSession,
|
|
215
132
|
version: string,
|
|
216
133
|
changelogMarkdown: string | undefined = undefined,
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
| Array<{ name: string; status: "ready" | "error"; fileTypes: string[] }>
|
|
220
|
-
| undefined = undefined,
|
|
134
|
+
setToolUIContext: (uiContext: ExtensionUIContext, hasUI: boolean) => void = () => {},
|
|
135
|
+
lspServers: Array<{ name: string; status: "ready" | "error"; fileTypes: string[] }> | undefined = undefined,
|
|
221
136
|
) {
|
|
222
137
|
this.session = session;
|
|
138
|
+
this.sessionManager = session.sessionManager;
|
|
139
|
+
this.settingsManager = session.settingsManager;
|
|
140
|
+
this.agent = session.agent;
|
|
223
141
|
this.version = version;
|
|
224
142
|
this.changelogMarkdown = changelogMarkdown;
|
|
143
|
+
this.toolUiContextSetter = setToolUIContext;
|
|
144
|
+
this.lspServers = lspServers;
|
|
145
|
+
|
|
225
146
|
this.ui = new TUI(new ProcessTerminal());
|
|
226
147
|
this.chatContainer = new Container();
|
|
227
148
|
this.pendingMessagesContainer = new Container();
|
|
228
149
|
this.statusContainer = new Container();
|
|
229
150
|
this.editor = new CustomEditor(getEditorTheme());
|
|
230
151
|
this.editor.setUseTerminalCursor(true);
|
|
152
|
+
this.editor.onAutocompleteCancel = () => {
|
|
153
|
+
this.ui.requestRender(true);
|
|
154
|
+
};
|
|
231
155
|
try {
|
|
232
156
|
this.historyStorage = HistoryStorage.open();
|
|
233
157
|
this.editor.setHistoryStorage(this.historyStorage);
|
|
@@ -259,11 +183,14 @@ export class InteractiveMode {
|
|
|
259
183
|
},
|
|
260
184
|
});
|
|
261
185
|
|
|
186
|
+
this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
|
|
187
|
+
|
|
262
188
|
// Define slash commands for autocomplete
|
|
263
189
|
const slashCommands: SlashCommand[] = [
|
|
264
190
|
{ name: "settings", description: "Open settings menu" },
|
|
265
191
|
{ name: "model", description: "Select model (opens selector UI)" },
|
|
266
|
-
{ name: "export", description: "Export session to HTML file
|
|
192
|
+
{ name: "export", description: "Export session to HTML file" },
|
|
193
|
+
{ name: "dump", description: "Copy session transcript to clipboard" },
|
|
267
194
|
{ name: "share", description: "Share session as a secret GitHub gist" },
|
|
268
195
|
{ name: "copy", description: "Copy last agent message to clipboard" },
|
|
269
196
|
{ name: "session", description: "Show session info and stats" },
|
|
@@ -283,9 +210,6 @@ export class InteractiveMode {
|
|
|
283
210
|
{ name: "exit", description: "Exit the application" },
|
|
284
211
|
];
|
|
285
212
|
|
|
286
|
-
// Load hide thinking block setting
|
|
287
|
-
this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
|
|
288
|
-
|
|
289
213
|
// Load and convert file commands to SlashCommand format
|
|
290
214
|
const fileCommands = loadSlashCommands({ cwd: process.cwd() });
|
|
291
215
|
this.fileSlashCommands = new Set(fileCommands.map((cmd) => cmd.name));
|
|
@@ -312,6 +236,14 @@ export class InteractiveMode {
|
|
|
312
236
|
process.cwd(),
|
|
313
237
|
);
|
|
314
238
|
this.editor.setAutocompleteProvider(autocompleteProvider);
|
|
239
|
+
|
|
240
|
+
this.uiHelpers = new UiHelpers(this);
|
|
241
|
+
this.voiceManager = new VoiceManager(this);
|
|
242
|
+
this.extensionUiController = new ExtensionUiController(this);
|
|
243
|
+
this.eventController = new EventController(this);
|
|
244
|
+
this.commandController = new CommandController(this);
|
|
245
|
+
this.selectorController = new SelectorController(this);
|
|
246
|
+
this.inputController = new InputController(this);
|
|
315
247
|
}
|
|
316
248
|
|
|
317
249
|
async init(): Promise<void> {
|
|
@@ -377,8 +309,8 @@ export class InteractiveMode {
|
|
|
377
309
|
this.ui.addChild(this.statusLine); // Only renders hook statuses (main status in editor border)
|
|
378
310
|
this.ui.setFocus(this.editor);
|
|
379
311
|
|
|
380
|
-
this.setupKeyHandlers();
|
|
381
|
-
this.setupEditorSubmitHandler();
|
|
312
|
+
this.inputController.setupKeyHandlers();
|
|
313
|
+
this.inputController.setupEditorSubmitHandler();
|
|
382
314
|
|
|
383
315
|
// Start the UI
|
|
384
316
|
this.ui.start();
|
|
@@ -411,3104 +343,396 @@ export class InteractiveMode {
|
|
|
411
343
|
this.updateEditorTopBorder();
|
|
412
344
|
}
|
|
413
345
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
*/
|
|
421
|
-
private async initHooksAndCustomTools(): Promise<void> {
|
|
422
|
-
// Create and set hook & tool UI context
|
|
423
|
-
const uiContext: ExtensionUIContext = {
|
|
424
|
-
select: (title, options, _dialogOptions) => this.showHookSelector(title, options),
|
|
425
|
-
confirm: (title, message, _dialogOptions) => this.showHookConfirm(title, message),
|
|
426
|
-
input: (title, placeholder, _dialogOptions) => this.showHookInput(title, placeholder),
|
|
427
|
-
notify: (message, type) => this.showHookNotify(message, type),
|
|
428
|
-
setStatus: (key, text) => this.setHookStatus(key, text),
|
|
429
|
-
setWidget: (key, content) => this.setHookWidget(key, content),
|
|
430
|
-
setTitle: (title) => setTerminalTitle(title),
|
|
431
|
-
custom: (factory, _options) => this.showHookCustom(factory),
|
|
432
|
-
setEditorText: (text) => this.editor.setText(text),
|
|
433
|
-
getEditorText: () => this.editor.getText(),
|
|
434
|
-
editor: (title, prefill) => this.showHookEditor(title, prefill),
|
|
435
|
-
get theme() {
|
|
436
|
-
return theme;
|
|
437
|
-
},
|
|
438
|
-
getAllThemes: () => getAvailableThemesWithPaths().map((t) => ({ name: t.name, path: t.path })),
|
|
439
|
-
getTheme: (name) => getThemeByName(name),
|
|
440
|
-
setTheme: (themeArg) => {
|
|
441
|
-
if (typeof themeArg === "string") {
|
|
442
|
-
return setTheme(themeArg, true);
|
|
443
|
-
}
|
|
444
|
-
// Theme object passed directly - not supported in current implementation
|
|
445
|
-
return { success: false, error: "Direct theme object not supported" };
|
|
446
|
-
},
|
|
447
|
-
setFooter: () => {},
|
|
448
|
-
setHeader: () => {},
|
|
449
|
-
setEditorComponent: () => {},
|
|
450
|
-
};
|
|
451
|
-
this.setToolUIContext(uiContext, true);
|
|
452
|
-
|
|
453
|
-
const extensionRunner = this.session.extensionRunner;
|
|
454
|
-
if (!extensionRunner) {
|
|
455
|
-
return; // No hooks loaded
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
extensionRunner.initialize(
|
|
459
|
-
// ExtensionActions - for pi.* API
|
|
460
|
-
{
|
|
461
|
-
sendMessage: (message, options) => {
|
|
462
|
-
const wasStreaming = this.session.isStreaming;
|
|
463
|
-
this.session
|
|
464
|
-
.sendCustomMessage(message, options)
|
|
465
|
-
.then(() => {
|
|
466
|
-
// For non-streaming cases with display=true, update UI
|
|
467
|
-
// (streaming cases update via message_end event)
|
|
468
|
-
if (!this.isBackgrounded && !wasStreaming && message.display) {
|
|
469
|
-
this.rebuildChatFromMessages();
|
|
470
|
-
}
|
|
471
|
-
})
|
|
472
|
-
.catch((err) => {
|
|
473
|
-
this.showError(
|
|
474
|
-
`Extension sendMessage failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
475
|
-
);
|
|
476
|
-
});
|
|
477
|
-
},
|
|
478
|
-
sendUserMessage: (content, options) => {
|
|
479
|
-
this.session.sendUserMessage(content, options).catch((err) => {
|
|
480
|
-
this.showError(
|
|
481
|
-
`Extension sendUserMessage failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
482
|
-
);
|
|
483
|
-
});
|
|
484
|
-
},
|
|
485
|
-
appendEntry: (customType, data) => {
|
|
486
|
-
this.sessionManager.appendCustomEntry(customType, data);
|
|
487
|
-
},
|
|
488
|
-
getActiveTools: () => this.session.getActiveToolNames(),
|
|
489
|
-
getAllTools: () => this.session.getAllToolNames(),
|
|
490
|
-
setActiveTools: (toolNames) => this.session.setActiveToolsByName(toolNames),
|
|
491
|
-
setModel: async (model) => {
|
|
492
|
-
const key = await this.session.modelRegistry.getApiKey(model);
|
|
493
|
-
if (!key) return false;
|
|
494
|
-
await this.session.setModel(model);
|
|
495
|
-
return true;
|
|
496
|
-
},
|
|
497
|
-
getThinkingLevel: () => this.session.thinkingLevel,
|
|
498
|
-
setThinkingLevel: (level) => this.session.setThinkingLevel(level),
|
|
499
|
-
},
|
|
500
|
-
// ExtensionContextActions - for ctx.* in event handlers
|
|
501
|
-
{
|
|
502
|
-
getModel: () => this.session.model,
|
|
503
|
-
isIdle: () => !this.session.isStreaming,
|
|
504
|
-
abort: () => this.session.abort(),
|
|
505
|
-
hasPendingMessages: () => this.session.queuedMessageCount > 0,
|
|
506
|
-
shutdown: () => {
|
|
507
|
-
// Signal shutdown request (will be handled by main loop)
|
|
508
|
-
},
|
|
509
|
-
},
|
|
510
|
-
// ExtensionCommandContextActions - for ctx.* in command handlers
|
|
511
|
-
{
|
|
512
|
-
waitForIdle: () => this.session.agent.waitForIdle(),
|
|
513
|
-
newSession: async (options) => {
|
|
514
|
-
// Stop any loading animation
|
|
515
|
-
if (this.loadingAnimation) {
|
|
516
|
-
this.loadingAnimation.stop();
|
|
517
|
-
this.loadingAnimation = undefined;
|
|
518
|
-
}
|
|
519
|
-
this.statusContainer.clear();
|
|
520
|
-
|
|
521
|
-
// Create new session
|
|
522
|
-
const success = await this.session.newSession({ parentSession: options?.parentSession });
|
|
523
|
-
if (!success) {
|
|
524
|
-
return { cancelled: true };
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
// Call setup callback if provided
|
|
528
|
-
if (options?.setup) {
|
|
529
|
-
await options.setup(this.sessionManager);
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
// Clear UI state
|
|
533
|
-
this.chatContainer.clear();
|
|
534
|
-
this.pendingMessagesContainer.clear();
|
|
535
|
-
this.compactionQueuedMessages = [];
|
|
536
|
-
this.streamingComponent = undefined;
|
|
537
|
-
this.streamingMessage = undefined;
|
|
538
|
-
this.pendingTools.clear();
|
|
539
|
-
|
|
540
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
541
|
-
this.chatContainer.addChild(
|
|
542
|
-
new Text(`${theme.fg("accent", `${theme.status.success} New session started`)}`, 1, 1),
|
|
543
|
-
);
|
|
544
|
-
this.ui.requestRender();
|
|
545
|
-
|
|
546
|
-
return { cancelled: false };
|
|
547
|
-
},
|
|
548
|
-
branch: async (entryId) => {
|
|
549
|
-
const result = await this.session.branch(entryId);
|
|
550
|
-
if (result.cancelled) {
|
|
551
|
-
return { cancelled: true };
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
// Update UI
|
|
555
|
-
this.chatContainer.clear();
|
|
556
|
-
this.renderInitialMessages();
|
|
557
|
-
this.editor.setText(result.selectedText);
|
|
558
|
-
this.showStatus("Branched to new session");
|
|
559
|
-
|
|
560
|
-
return { cancelled: false };
|
|
561
|
-
},
|
|
562
|
-
navigateTree: async (targetId, options) => {
|
|
563
|
-
const result = await this.session.navigateTree(targetId, { summarize: options?.summarize });
|
|
564
|
-
if (result.cancelled) {
|
|
565
|
-
return { cancelled: true };
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
// Update UI
|
|
569
|
-
this.chatContainer.clear();
|
|
570
|
-
this.renderInitialMessages();
|
|
571
|
-
if (result.editorText) {
|
|
572
|
-
this.editor.setText(result.editorText);
|
|
573
|
-
}
|
|
574
|
-
this.showStatus("Navigated to selected point");
|
|
575
|
-
|
|
576
|
-
return { cancelled: false };
|
|
577
|
-
},
|
|
578
|
-
},
|
|
579
|
-
// ExtensionUIContext
|
|
580
|
-
uiContext,
|
|
581
|
-
);
|
|
582
|
-
|
|
583
|
-
// Subscribe to extension errors
|
|
584
|
-
extensionRunner.onError((error) => {
|
|
585
|
-
this.showExtensionError(error.extensionPath, error.error);
|
|
586
|
-
});
|
|
587
|
-
|
|
588
|
-
// Emit session_start event
|
|
589
|
-
await extensionRunner.emit({
|
|
590
|
-
type: "session_start",
|
|
346
|
+
async getUserInput(): Promise<{ text: string; images?: ImageContent[] }> {
|
|
347
|
+
return new Promise((resolve) => {
|
|
348
|
+
this.onInputCallback = (input) => {
|
|
349
|
+
this.onInputCallback = undefined;
|
|
350
|
+
resolve(input);
|
|
351
|
+
};
|
|
591
352
|
});
|
|
592
353
|
}
|
|
593
354
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
355
|
+
updateEditorBorderColor(): void {
|
|
356
|
+
if (this.isBashMode) {
|
|
357
|
+
this.editor.borderColor = theme.getBashModeBorderColor();
|
|
358
|
+
} else {
|
|
359
|
+
const level = this.session.thinkingLevel || "off";
|
|
360
|
+
this.editor.borderColor = theme.getThinkingBorderColor(level);
|
|
361
|
+
}
|
|
362
|
+
this.updateEditorTopBorder();
|
|
599
363
|
this.ui.requestRender();
|
|
600
364
|
}
|
|
601
365
|
|
|
602
|
-
|
|
603
|
-
const
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
extensionRunner.initialize(
|
|
609
|
-
// ExtensionActions - for pi.* API
|
|
610
|
-
{
|
|
611
|
-
sendMessage: (message, options) => {
|
|
612
|
-
const wasStreaming = this.session.isStreaming;
|
|
613
|
-
this.session
|
|
614
|
-
.sendCustomMessage(message, options)
|
|
615
|
-
.then(() => {
|
|
616
|
-
// For non-streaming cases with display=true, update UI
|
|
617
|
-
// (streaming cases update via message_end event)
|
|
618
|
-
if (!this.isBackgrounded && !wasStreaming && message.display) {
|
|
619
|
-
this.rebuildChatFromMessages();
|
|
620
|
-
}
|
|
621
|
-
})
|
|
622
|
-
.catch((err: Error) => {
|
|
623
|
-
const errorText = `Extension sendMessage failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
624
|
-
if (this.isBackgrounded) {
|
|
625
|
-
console.error(errorText);
|
|
626
|
-
return;
|
|
627
|
-
}
|
|
628
|
-
this.showError(errorText);
|
|
629
|
-
});
|
|
630
|
-
},
|
|
631
|
-
sendUserMessage: (content, options) => {
|
|
632
|
-
this.session.sendUserMessage(content, options).catch((err) => {
|
|
633
|
-
this.showError(
|
|
634
|
-
`Extension sendUserMessage failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
635
|
-
);
|
|
636
|
-
});
|
|
637
|
-
},
|
|
638
|
-
appendEntry: (customType, data) => {
|
|
639
|
-
this.sessionManager.appendCustomEntry(customType, data);
|
|
640
|
-
},
|
|
641
|
-
getActiveTools: () => this.session.getActiveToolNames(),
|
|
642
|
-
getAllTools: () => this.session.getAllToolNames(),
|
|
643
|
-
setActiveTools: (toolNames: string[]) => this.session.setActiveToolsByName(toolNames),
|
|
644
|
-
setModel: async (model) => {
|
|
645
|
-
const key = await this.session.modelRegistry.getApiKey(model);
|
|
646
|
-
if (!key) return false;
|
|
647
|
-
await this.session.setModel(model);
|
|
648
|
-
return true;
|
|
649
|
-
},
|
|
650
|
-
getThinkingLevel: () => this.session.thinkingLevel,
|
|
651
|
-
setThinkingLevel: (level) => this.session.setThinkingLevel(level),
|
|
652
|
-
},
|
|
653
|
-
// ExtensionContextActions - for ctx.* in event handlers
|
|
654
|
-
{
|
|
655
|
-
getModel: () => this.session.model,
|
|
656
|
-
isIdle: () => !this.session.isStreaming,
|
|
657
|
-
abort: () => this.session.abort(),
|
|
658
|
-
hasPendingMessages: () => this.session.queuedMessageCount > 0,
|
|
659
|
-
shutdown: () => {
|
|
660
|
-
// Signal shutdown request (will be handled by main loop)
|
|
661
|
-
},
|
|
662
|
-
},
|
|
663
|
-
// ExtensionCommandContextActions - for ctx.* in command handlers
|
|
664
|
-
{
|
|
665
|
-
waitForIdle: () => this.session.agent.waitForIdle(),
|
|
666
|
-
newSession: async (options) => {
|
|
667
|
-
if (this.isBackgrounded) {
|
|
668
|
-
return { cancelled: true };
|
|
669
|
-
}
|
|
670
|
-
// Stop any loading animation
|
|
671
|
-
if (this.loadingAnimation) {
|
|
672
|
-
this.loadingAnimation.stop();
|
|
673
|
-
this.loadingAnimation = undefined;
|
|
674
|
-
}
|
|
675
|
-
this.statusContainer.clear();
|
|
676
|
-
|
|
677
|
-
// Create new session
|
|
678
|
-
const success = await this.session.newSession({ parentSession: options?.parentSession });
|
|
679
|
-
if (!success) {
|
|
680
|
-
return { cancelled: true };
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
// Call setup callback if provided
|
|
684
|
-
if (options?.setup) {
|
|
685
|
-
await options.setup(this.sessionManager);
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
// Clear UI state
|
|
689
|
-
this.chatContainer.clear();
|
|
690
|
-
this.pendingMessagesContainer.clear();
|
|
691
|
-
this.compactionQueuedMessages = [];
|
|
692
|
-
this.streamingComponent = undefined;
|
|
693
|
-
this.streamingMessage = undefined;
|
|
694
|
-
this.pendingTools.clear();
|
|
695
|
-
|
|
696
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
697
|
-
this.chatContainer.addChild(
|
|
698
|
-
new Text(`${theme.fg("accent", `${theme.status.success} New session started`)}`, 1, 1),
|
|
699
|
-
);
|
|
700
|
-
this.ui.requestRender();
|
|
701
|
-
|
|
702
|
-
return { cancelled: false };
|
|
703
|
-
},
|
|
704
|
-
branch: async (entryId) => {
|
|
705
|
-
if (this.isBackgrounded) {
|
|
706
|
-
return { cancelled: true };
|
|
707
|
-
}
|
|
708
|
-
const result = await this.session.branch(entryId);
|
|
709
|
-
if (result.cancelled) {
|
|
710
|
-
return { cancelled: true };
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
// Update UI
|
|
714
|
-
this.chatContainer.clear();
|
|
715
|
-
this.renderInitialMessages();
|
|
716
|
-
this.editor.setText(result.selectedText);
|
|
717
|
-
this.showStatus("Branched to new session");
|
|
718
|
-
|
|
719
|
-
return { cancelled: false };
|
|
720
|
-
},
|
|
721
|
-
navigateTree: async (targetId, options) => {
|
|
722
|
-
if (this.isBackgrounded) {
|
|
723
|
-
return { cancelled: true };
|
|
724
|
-
}
|
|
725
|
-
const result = await this.session.navigateTree(targetId, { summarize: options?.summarize });
|
|
726
|
-
if (result.cancelled) {
|
|
727
|
-
return { cancelled: true };
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
// Update UI
|
|
731
|
-
this.chatContainer.clear();
|
|
732
|
-
this.renderInitialMessages();
|
|
733
|
-
if (result.editorText) {
|
|
734
|
-
this.editor.setText(result.editorText);
|
|
735
|
-
}
|
|
736
|
-
this.showStatus("Navigated to selected point");
|
|
737
|
-
|
|
738
|
-
return { cancelled: false };
|
|
739
|
-
},
|
|
740
|
-
},
|
|
741
|
-
uiContext,
|
|
742
|
-
);
|
|
366
|
+
updateEditorTopBorder(): void {
|
|
367
|
+
const width = this.ui.getWidth();
|
|
368
|
+
const topBorder = this.statusLine.getTopBorder(width);
|
|
369
|
+
this.editor.setTopBorder(topBorder);
|
|
743
370
|
}
|
|
744
371
|
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
input: async (_title: string, _placeholder?: string, _dialogOptions?: unknown) => undefined,
|
|
750
|
-
notify: () => {},
|
|
751
|
-
setStatus: () => {},
|
|
752
|
-
setWidget: () => {},
|
|
753
|
-
setTitle: () => {},
|
|
754
|
-
custom: async () => undefined as never,
|
|
755
|
-
setEditorText: () => {},
|
|
756
|
-
getEditorText: () => "",
|
|
757
|
-
editor: async () => undefined,
|
|
758
|
-
get theme() {
|
|
759
|
-
return theme;
|
|
760
|
-
},
|
|
761
|
-
getAllThemes: () => [],
|
|
762
|
-
getTheme: () => undefined,
|
|
763
|
-
setTheme: () => ({ success: false, error: "Background mode" }),
|
|
764
|
-
setFooter: () => {},
|
|
765
|
-
setHeader: () => {},
|
|
766
|
-
setEditorComponent: () => {},
|
|
767
|
-
};
|
|
372
|
+
rebuildChatFromMessages(): void {
|
|
373
|
+
this.chatContainer.clear();
|
|
374
|
+
const context = this.sessionManager.buildSessionContext();
|
|
375
|
+
this.renderSessionContext(context);
|
|
768
376
|
}
|
|
769
377
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
reason: "start" | "switch" | "branch" | "tree" | "shutdown",
|
|
775
|
-
previousSessionFile?: string,
|
|
776
|
-
): Promise<void> {
|
|
777
|
-
const event = { reason, previousSessionFile };
|
|
778
|
-
const uiContext = this.session.extensionRunner?.getUIContext();
|
|
779
|
-
if (!uiContext) {
|
|
780
|
-
return;
|
|
378
|
+
stop(): void {
|
|
379
|
+
if (this.loadingAnimation) {
|
|
380
|
+
this.loadingAnimation.stop();
|
|
381
|
+
this.loadingAnimation = undefined;
|
|
781
382
|
}
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
isIdle: () => !this.session.isStreaming,
|
|
793
|
-
hasPendingMessages: () => this.session.queuedMessageCount > 0,
|
|
794
|
-
hasQueuedMessages: () => this.session.queuedMessageCount > 0,
|
|
795
|
-
abort: () => {
|
|
796
|
-
this.session.abort();
|
|
797
|
-
},
|
|
798
|
-
shutdown: () => {
|
|
799
|
-
// Signal shutdown request
|
|
800
|
-
},
|
|
801
|
-
});
|
|
802
|
-
} catch (err) {
|
|
803
|
-
this.showToolError(registeredTool.definition.name, err instanceof Error ? err.message : String(err));
|
|
804
|
-
}
|
|
805
|
-
}
|
|
383
|
+
this.statusLine.dispose();
|
|
384
|
+
if (this.unsubscribe) {
|
|
385
|
+
this.unsubscribe();
|
|
386
|
+
}
|
|
387
|
+
if (this.cleanupUnsubscribe) {
|
|
388
|
+
this.cleanupUnsubscribe();
|
|
389
|
+
}
|
|
390
|
+
if (this.isInitialized) {
|
|
391
|
+
this.ui.stop();
|
|
392
|
+
this.isInitialized = false;
|
|
806
393
|
}
|
|
807
394
|
}
|
|
808
395
|
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
this.
|
|
396
|
+
async shutdown(): Promise<void> {
|
|
397
|
+
this.voiceAutoModeEnabled = false;
|
|
398
|
+
await this.voiceSupervisor.stop();
|
|
399
|
+
|
|
400
|
+
// Flush pending session writes before shutdown
|
|
401
|
+
await this.sessionManager.flush();
|
|
402
|
+
|
|
403
|
+
// Emit shutdown event to hooks
|
|
404
|
+
await this.session.emitCustomToolSessionEvent("shutdown");
|
|
405
|
+
|
|
406
|
+
this.stop();
|
|
407
|
+
process.exit(0);
|
|
820
408
|
}
|
|
821
409
|
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
private setHookStatus(key: string, text: string | undefined): void {
|
|
826
|
-
if (this.isBackgrounded) {
|
|
827
|
-
return;
|
|
828
|
-
}
|
|
829
|
-
this.statusLine.setHookStatus(key, text);
|
|
830
|
-
this.ui.requestRender();
|
|
410
|
+
// Extension UI integration
|
|
411
|
+
setToolUIContext(uiContext: ExtensionUIContext, hasUI: boolean): void {
|
|
412
|
+
this.toolUiContextSetter(uiContext, hasUI);
|
|
831
413
|
}
|
|
832
414
|
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
*/
|
|
836
|
-
private showHookSelector(title: string, options: string[]): Promise<string | undefined> {
|
|
837
|
-
return new Promise((resolve) => {
|
|
838
|
-
this.hookSelector = new HookSelectorComponent(
|
|
839
|
-
title,
|
|
840
|
-
options,
|
|
841
|
-
(option) => {
|
|
842
|
-
this.hideHookSelector();
|
|
843
|
-
resolve(option);
|
|
844
|
-
},
|
|
845
|
-
() => {
|
|
846
|
-
this.hideHookSelector();
|
|
847
|
-
resolve(undefined);
|
|
848
|
-
},
|
|
849
|
-
);
|
|
850
|
-
|
|
851
|
-
this.editorContainer.clear();
|
|
852
|
-
this.editorContainer.addChild(this.hookSelector);
|
|
853
|
-
this.ui.setFocus(this.hookSelector);
|
|
854
|
-
this.ui.requestRender();
|
|
855
|
-
});
|
|
415
|
+
initializeHookRunner(uiContext: ExtensionUIContext, hasUI: boolean): void {
|
|
416
|
+
this.extensionUiController.initializeHookRunner(uiContext, hasUI);
|
|
856
417
|
}
|
|
857
418
|
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
*/
|
|
861
|
-
private hideHookSelector(): void {
|
|
862
|
-
this.editorContainer.clear();
|
|
863
|
-
this.editorContainer.addChild(this.editor);
|
|
864
|
-
this.hookSelector = undefined;
|
|
865
|
-
this.ui.setFocus(this.editor);
|
|
866
|
-
this.ui.requestRender();
|
|
419
|
+
createBackgroundUiContext(): ExtensionUIContext {
|
|
420
|
+
return this.extensionUiController.createBackgroundUiContext();
|
|
867
421
|
}
|
|
868
422
|
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
private async showHookConfirm(title: string, message: string): Promise<boolean> {
|
|
873
|
-
const result = await this.showHookSelector(`${title}\n${message}`, ["Yes", "No"]);
|
|
874
|
-
return result === "Yes";
|
|
423
|
+
// Event handling
|
|
424
|
+
async handleBackgroundEvent(event: AgentSessionEvent): Promise<void> {
|
|
425
|
+
await this.eventController.handleBackgroundEvent(event);
|
|
875
426
|
}
|
|
876
427
|
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
private showHookInput(title: string, placeholder?: string): Promise<string | undefined> {
|
|
881
|
-
return new Promise((resolve) => {
|
|
882
|
-
this.hookInput = new HookInputComponent(
|
|
883
|
-
title,
|
|
884
|
-
placeholder,
|
|
885
|
-
(value) => {
|
|
886
|
-
this.hideHookInput();
|
|
887
|
-
resolve(value);
|
|
888
|
-
},
|
|
889
|
-
() => {
|
|
890
|
-
this.hideHookInput();
|
|
891
|
-
resolve(undefined);
|
|
892
|
-
},
|
|
893
|
-
);
|
|
894
|
-
|
|
895
|
-
this.editorContainer.clear();
|
|
896
|
-
this.editorContainer.addChild(this.hookInput);
|
|
897
|
-
this.ui.setFocus(this.hookInput);
|
|
898
|
-
this.ui.requestRender();
|
|
899
|
-
});
|
|
428
|
+
// UI helpers
|
|
429
|
+
showStatus(message: string, options?: { dim?: boolean }): void {
|
|
430
|
+
this.uiHelpers.showStatus(message, options);
|
|
900
431
|
}
|
|
901
432
|
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
*/
|
|
905
|
-
private hideHookInput(): void {
|
|
906
|
-
this.editorContainer.clear();
|
|
907
|
-
this.editorContainer.addChild(this.editor);
|
|
908
|
-
this.hookInput = undefined;
|
|
909
|
-
this.ui.setFocus(this.editor);
|
|
910
|
-
this.ui.requestRender();
|
|
433
|
+
showError(message: string): void {
|
|
434
|
+
this.uiHelpers.showError(message);
|
|
911
435
|
}
|
|
912
436
|
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
*/
|
|
916
|
-
private showHookEditor(title: string, prefill?: string): Promise<string | undefined> {
|
|
917
|
-
return new Promise((resolve) => {
|
|
918
|
-
this.hookEditor = new HookEditorComponent(
|
|
919
|
-
this.ui,
|
|
920
|
-
title,
|
|
921
|
-
prefill,
|
|
922
|
-
(value) => {
|
|
923
|
-
this.hideHookEditor();
|
|
924
|
-
resolve(value);
|
|
925
|
-
},
|
|
926
|
-
() => {
|
|
927
|
-
this.hideHookEditor();
|
|
928
|
-
resolve(undefined);
|
|
929
|
-
},
|
|
930
|
-
);
|
|
931
|
-
|
|
932
|
-
this.editorContainer.clear();
|
|
933
|
-
this.editorContainer.addChild(this.hookEditor);
|
|
934
|
-
this.ui.setFocus(this.hookEditor);
|
|
935
|
-
this.ui.requestRender();
|
|
936
|
-
});
|
|
437
|
+
showWarning(message: string): void {
|
|
438
|
+
this.uiHelpers.showWarning(message);
|
|
937
439
|
}
|
|
938
440
|
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
*/
|
|
942
|
-
private hideHookEditor(): void {
|
|
943
|
-
this.editorContainer.clear();
|
|
944
|
-
this.editorContainer.addChild(this.editor);
|
|
945
|
-
this.hookEditor = undefined;
|
|
946
|
-
this.ui.setFocus(this.editor);
|
|
947
|
-
this.ui.requestRender();
|
|
441
|
+
showNewVersionNotification(newVersion: string): void {
|
|
442
|
+
this.uiHelpers.showNewVersionNotification(newVersion);
|
|
948
443
|
}
|
|
949
444
|
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
*/
|
|
953
|
-
private showHookNotify(message: string, type?: "info" | "warning" | "error"): void {
|
|
954
|
-
if (type === "error") {
|
|
955
|
-
this.showError(message);
|
|
956
|
-
} else if (type === "warning") {
|
|
957
|
-
this.showWarning(message);
|
|
958
|
-
} else {
|
|
959
|
-
this.showStatus(message);
|
|
960
|
-
}
|
|
445
|
+
clearEditor(): void {
|
|
446
|
+
this.uiHelpers.clearEditor();
|
|
961
447
|
}
|
|
962
448
|
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
private async showHookCustom<T>(
|
|
967
|
-
factory: (
|
|
968
|
-
tui: TUI,
|
|
969
|
-
theme: Theme,
|
|
970
|
-
keybindings: KeybindingsManager,
|
|
971
|
-
done: (result: T) => void,
|
|
972
|
-
) => (Component & { dispose?(): void }) | Promise<Component & { dispose?(): void }>,
|
|
973
|
-
): Promise<T> {
|
|
974
|
-
const savedText = this.editor.getText();
|
|
975
|
-
const keybindings = KeybindingsManager.inMemory();
|
|
449
|
+
updatePendingMessagesDisplay(): void {
|
|
450
|
+
this.uiHelpers.updatePendingMessagesDisplay();
|
|
451
|
+
}
|
|
976
452
|
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
const close = (result: T) => {
|
|
981
|
-
component.dispose?.();
|
|
982
|
-
this.editorContainer.clear();
|
|
983
|
-
this.editorContainer.addChild(this.editor);
|
|
984
|
-
this.editor.setText(savedText);
|
|
985
|
-
this.ui.setFocus(this.editor);
|
|
986
|
-
this.ui.requestRender();
|
|
987
|
-
resolve(result);
|
|
988
|
-
};
|
|
453
|
+
queueCompactionMessage(text: string, mode: "steer" | "followUp"): void {
|
|
454
|
+
this.uiHelpers.queueCompactionMessage(text, mode);
|
|
455
|
+
}
|
|
989
456
|
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
this.editorContainer.clear();
|
|
993
|
-
this.editorContainer.addChild(component);
|
|
994
|
-
this.ui.setFocus(component);
|
|
995
|
-
this.ui.requestRender();
|
|
996
|
-
});
|
|
997
|
-
});
|
|
457
|
+
flushCompactionQueue(options?: { willRetry?: boolean }): Promise<void> {
|
|
458
|
+
return this.uiHelpers.flushCompactionQueue(options);
|
|
998
459
|
}
|
|
999
460
|
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
*/
|
|
1003
|
-
private showExtensionError(extensionPath: string, error: string): void {
|
|
1004
|
-
const errorText = new Text(theme.fg("error", `Extension "${extensionPath}" error: ${error}`), 1, 0);
|
|
1005
|
-
this.chatContainer.addChild(errorText);
|
|
1006
|
-
this.ui.requestRender();
|
|
461
|
+
flushPendingBashComponents(): void {
|
|
462
|
+
this.uiHelpers.flushPendingBashComponents();
|
|
1007
463
|
}
|
|
1008
464
|
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
*/
|
|
1013
|
-
// =========================================================================
|
|
1014
|
-
// Key Handlers
|
|
1015
|
-
// =========================================================================
|
|
1016
|
-
|
|
1017
|
-
private setupKeyHandlers(): void {
|
|
1018
|
-
this.editor.onEscape = () => {
|
|
1019
|
-
if (this.loadingAnimation) {
|
|
1020
|
-
// Abort and restore queued messages to editor
|
|
1021
|
-
const queuedMessages = this.session.clearQueue();
|
|
1022
|
-
const queuedText = [...queuedMessages.steering, ...queuedMessages.followUp].join("\n\n");
|
|
1023
|
-
const currentText = this.editor.getText();
|
|
1024
|
-
const combinedText = [queuedText, currentText].filter((t) => t.trim()).join("\n\n");
|
|
1025
|
-
this.editor.setText(combinedText);
|
|
1026
|
-
this.updatePendingMessagesDisplay();
|
|
1027
|
-
this.agent.abort();
|
|
1028
|
-
} else if (this.session.isBashRunning) {
|
|
1029
|
-
this.session.abortBash();
|
|
1030
|
-
} else if (this.isBashMode) {
|
|
1031
|
-
this.editor.setText("");
|
|
1032
|
-
this.isBashMode = false;
|
|
1033
|
-
this.updateEditorBorderColor();
|
|
1034
|
-
} else if (!this.editor.getText().trim()) {
|
|
1035
|
-
// Double-escape with empty editor triggers /tree or /branch based on setting
|
|
1036
|
-
const now = Date.now();
|
|
1037
|
-
if (now - this.lastEscapeTime < 500) {
|
|
1038
|
-
if (this.settingsManager.getDoubleEscapeAction() === "tree") {
|
|
1039
|
-
this.showTreeSelector();
|
|
1040
|
-
} else {
|
|
1041
|
-
this.showUserMessageSelector();
|
|
1042
|
-
}
|
|
1043
|
-
this.lastEscapeTime = 0;
|
|
1044
|
-
} else {
|
|
1045
|
-
this.lastEscapeTime = now;
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1048
|
-
};
|
|
465
|
+
isKnownSlashCommand(text: string): boolean {
|
|
466
|
+
return this.uiHelpers.isKnownSlashCommand(text);
|
|
467
|
+
}
|
|
1049
468
|
|
|
1050
|
-
|
|
1051
|
-
this.
|
|
1052
|
-
|
|
1053
|
-
this.editor.onShiftTab = () => this.cycleThinkingLevel();
|
|
1054
|
-
this.editor.onCtrlP = () => this.cycleRoleModel();
|
|
1055
|
-
this.editor.onShiftCtrlP = () => this.cycleRoleModel({ temporary: true });
|
|
1056
|
-
this.editor.onCtrlY = () => this.showModelSelector({ temporaryOnly: true });
|
|
1057
|
-
|
|
1058
|
-
// Global debug handler on TUI (works regardless of focus)
|
|
1059
|
-
this.ui.onDebug = () => this.handleDebugCommand();
|
|
1060
|
-
this.editor.onCtrlL = () => this.showModelSelector();
|
|
1061
|
-
this.editor.onCtrlR = () => this.showHistorySearch();
|
|
1062
|
-
this.editor.onCtrlO = () => this.toggleToolOutputExpansion();
|
|
1063
|
-
this.editor.onCtrlT = () => this.toggleThinkingBlockVisibility();
|
|
1064
|
-
this.editor.onCtrlG = () => this.openExternalEditor();
|
|
1065
|
-
this.editor.onQuestionMark = () => this.handleHotkeysCommand();
|
|
1066
|
-
this.editor.onCtrlV = () => this.handleImagePaste();
|
|
1067
|
-
this.editor.onAltUp = () => this.handleDequeue();
|
|
1068
|
-
|
|
1069
|
-
// Wire up extension shortcuts
|
|
1070
|
-
this.registerExtensionShortcuts();
|
|
1071
|
-
|
|
1072
|
-
this.editor.onChange = (text: string) => {
|
|
1073
|
-
const wasBashMode = this.isBashMode;
|
|
1074
|
-
this.isBashMode = text.trimStart().startsWith("!");
|
|
1075
|
-
if (wasBashMode !== this.isBashMode) {
|
|
1076
|
-
this.updateEditorBorderColor();
|
|
1077
|
-
}
|
|
1078
|
-
};
|
|
469
|
+
addMessageToChat(message: AgentMessage, options?: { populateHistory?: boolean }): void {
|
|
470
|
+
this.uiHelpers.addMessageToChat(message, options);
|
|
471
|
+
}
|
|
1079
472
|
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
473
|
+
renderSessionContext(
|
|
474
|
+
sessionContext: SessionContext,
|
|
475
|
+
options?: { updateFooter?: boolean; populateHistory?: boolean },
|
|
476
|
+
): void {
|
|
477
|
+
this.uiHelpers.renderSessionContext(sessionContext, options);
|
|
478
|
+
}
|
|
1083
479
|
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
return;
|
|
1088
|
-
}
|
|
480
|
+
renderInitialMessages(): void {
|
|
481
|
+
this.uiHelpers.renderInitialMessages();
|
|
482
|
+
}
|
|
1089
483
|
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
if (this.session.isStreaming) {
|
|
1093
|
-
this.editor.addToHistory(text);
|
|
1094
|
-
this.editor.setText("");
|
|
1095
|
-
await this.session.prompt(text, { streamingBehavior: "followUp" });
|
|
1096
|
-
this.updatePendingMessagesDisplay();
|
|
1097
|
-
this.ui.requestRender();
|
|
1098
|
-
}
|
|
1099
|
-
// If not streaming, Alt+Enter acts like regular Enter (trigger onSubmit)
|
|
1100
|
-
else if (this.editor.onSubmit) {
|
|
1101
|
-
this.editor.onSubmit(text);
|
|
1102
|
-
}
|
|
1103
|
-
};
|
|
484
|
+
getUserMessageText(message: Message): string {
|
|
485
|
+
return this.uiHelpers.getUserMessageText(message);
|
|
1104
486
|
}
|
|
1105
487
|
|
|
1106
|
-
|
|
1107
|
-
this.
|
|
1108
|
-
|
|
488
|
+
findLastAssistantMessage(): AssistantMessage | undefined {
|
|
489
|
+
return this.uiHelpers.findLastAssistantMessage();
|
|
490
|
+
}
|
|
1109
491
|
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
await this.session.abort();
|
|
1114
|
-
return;
|
|
1115
|
-
}
|
|
492
|
+
extractAssistantText(message: AssistantMessage): string {
|
|
493
|
+
return this.uiHelpers.extractAssistantText(message);
|
|
494
|
+
}
|
|
1116
495
|
|
|
1117
|
-
|
|
496
|
+
// Command handling
|
|
497
|
+
handleExportCommand(text: string): Promise<void> {
|
|
498
|
+
return this.commandController.handleExportCommand(text);
|
|
499
|
+
}
|
|
1118
500
|
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
this.editor.setText("");
|
|
1123
|
-
return;
|
|
1124
|
-
}
|
|
1125
|
-
if (text === "/model") {
|
|
1126
|
-
this.showModelSelector();
|
|
1127
|
-
this.editor.setText("");
|
|
1128
|
-
return;
|
|
1129
|
-
}
|
|
1130
|
-
if (text.startsWith("/export")) {
|
|
1131
|
-
await this.handleExportCommand(text);
|
|
1132
|
-
this.editor.setText("");
|
|
1133
|
-
return;
|
|
1134
|
-
}
|
|
1135
|
-
if (text === "/share") {
|
|
1136
|
-
await this.handleShareCommand();
|
|
1137
|
-
this.editor.setText("");
|
|
1138
|
-
return;
|
|
1139
|
-
}
|
|
1140
|
-
if (text === "/copy") {
|
|
1141
|
-
await this.handleCopyCommand();
|
|
1142
|
-
this.editor.setText("");
|
|
1143
|
-
return;
|
|
1144
|
-
}
|
|
1145
|
-
if (text === "/session") {
|
|
1146
|
-
this.handleSessionCommand();
|
|
1147
|
-
this.editor.setText("");
|
|
1148
|
-
return;
|
|
1149
|
-
}
|
|
1150
|
-
if (text === "/changelog") {
|
|
1151
|
-
this.handleChangelogCommand();
|
|
1152
|
-
this.editor.setText("");
|
|
1153
|
-
return;
|
|
1154
|
-
}
|
|
1155
|
-
if (text === "/hotkeys") {
|
|
1156
|
-
this.handleHotkeysCommand();
|
|
1157
|
-
this.editor.setText("");
|
|
1158
|
-
return;
|
|
1159
|
-
}
|
|
1160
|
-
if (text === "/extensions" || text === "/status") {
|
|
1161
|
-
this.showExtensionsDashboard();
|
|
1162
|
-
this.editor.setText("");
|
|
1163
|
-
return;
|
|
1164
|
-
}
|
|
1165
|
-
if (text === "/branch") {
|
|
1166
|
-
if (this.settingsManager.getDoubleEscapeAction() === "tree") {
|
|
1167
|
-
this.showTreeSelector();
|
|
1168
|
-
} else {
|
|
1169
|
-
this.showUserMessageSelector();
|
|
1170
|
-
}
|
|
1171
|
-
this.editor.setText("");
|
|
1172
|
-
return;
|
|
1173
|
-
}
|
|
1174
|
-
if (text === "/tree") {
|
|
1175
|
-
this.showTreeSelector();
|
|
1176
|
-
this.editor.setText("");
|
|
1177
|
-
return;
|
|
1178
|
-
}
|
|
1179
|
-
if (text === "/login") {
|
|
1180
|
-
this.showOAuthSelector("login");
|
|
1181
|
-
this.editor.setText("");
|
|
1182
|
-
return;
|
|
1183
|
-
}
|
|
1184
|
-
if (text === "/logout") {
|
|
1185
|
-
this.showOAuthSelector("logout");
|
|
1186
|
-
this.editor.setText("");
|
|
1187
|
-
return;
|
|
1188
|
-
}
|
|
1189
|
-
if (text === "/new") {
|
|
1190
|
-
this.editor.setText("");
|
|
1191
|
-
await this.handleClearCommand();
|
|
1192
|
-
return;
|
|
1193
|
-
}
|
|
1194
|
-
if (text === "/compact" || text.startsWith("/compact ")) {
|
|
1195
|
-
const customInstructions = text.startsWith("/compact ") ? text.slice(9).trim() : undefined;
|
|
1196
|
-
this.editor.setText("");
|
|
1197
|
-
await this.handleCompactCommand(customInstructions);
|
|
1198
|
-
return;
|
|
1199
|
-
}
|
|
1200
|
-
if (text === "/background" || text === "/bg") {
|
|
1201
|
-
this.editor.setText("");
|
|
1202
|
-
this.handleBackgroundCommand();
|
|
1203
|
-
return;
|
|
1204
|
-
}
|
|
1205
|
-
if (text === "/debug") {
|
|
1206
|
-
this.handleDebugCommand();
|
|
1207
|
-
this.editor.setText("");
|
|
1208
|
-
return;
|
|
1209
|
-
}
|
|
1210
|
-
if (text === "/arminsayshi") {
|
|
1211
|
-
this.handleArminSaysHi();
|
|
1212
|
-
this.editor.setText("");
|
|
1213
|
-
return;
|
|
1214
|
-
}
|
|
1215
|
-
if (text === "/resume") {
|
|
1216
|
-
this.showSessionSelector();
|
|
1217
|
-
this.editor.setText("");
|
|
1218
|
-
return;
|
|
1219
|
-
}
|
|
1220
|
-
if (text === "/exit") {
|
|
1221
|
-
this.editor.setText("");
|
|
1222
|
-
void this.shutdown();
|
|
1223
|
-
return;
|
|
1224
|
-
}
|
|
501
|
+
handleDumpCommand(): Promise<void> {
|
|
502
|
+
return this.commandController.handleDumpCommand();
|
|
503
|
+
}
|
|
1225
504
|
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
const command = isExcluded ? text.slice(2).trim() : text.slice(1).trim();
|
|
1230
|
-
if (command) {
|
|
1231
|
-
if (this.session.isBashRunning) {
|
|
1232
|
-
this.showWarning("A bash command is already running. Press Esc to cancel it first.");
|
|
1233
|
-
this.editor.setText(text);
|
|
1234
|
-
return;
|
|
1235
|
-
}
|
|
1236
|
-
this.editor.addToHistory(text);
|
|
1237
|
-
await this.handleBashCommand(command, isExcluded);
|
|
1238
|
-
this.isBashMode = false;
|
|
1239
|
-
this.updateEditorBorderColor();
|
|
1240
|
-
return;
|
|
1241
|
-
}
|
|
1242
|
-
}
|
|
505
|
+
handleShareCommand(): Promise<void> {
|
|
506
|
+
return this.commandController.handleShareCommand();
|
|
507
|
+
}
|
|
1243
508
|
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
this.showStatus("Compaction in progress. Retry after it completes to send images.");
|
|
1248
|
-
return;
|
|
1249
|
-
}
|
|
1250
|
-
this.queueCompactionMessage(text, "steer");
|
|
1251
|
-
return;
|
|
1252
|
-
}
|
|
509
|
+
handleCopyCommand(): Promise<void> {
|
|
510
|
+
return this.commandController.handleCopyCommand();
|
|
511
|
+
}
|
|
1253
512
|
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
this.editor.addToHistory(text);
|
|
1258
|
-
this.editor.setText("");
|
|
1259
|
-
const images = this.pendingImages.length > 0 ? [...this.pendingImages] : undefined;
|
|
1260
|
-
this.pendingImages = [];
|
|
1261
|
-
await this.session.prompt(text, { streamingBehavior: "steer", images });
|
|
1262
|
-
this.updatePendingMessagesDisplay();
|
|
1263
|
-
this.ui.requestRender();
|
|
1264
|
-
return;
|
|
1265
|
-
}
|
|
513
|
+
handleSessionCommand(): void {
|
|
514
|
+
this.commandController.handleSessionCommand();
|
|
515
|
+
}
|
|
1266
516
|
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
// Generate session title on first message
|
|
1272
|
-
const hasUserMessages = this.agent.state.messages.some((m) => m.role === "user");
|
|
1273
|
-
if (!hasUserMessages && !this.sessionManager.getSessionTitle()) {
|
|
1274
|
-
const registry = this.session.modelRegistry;
|
|
1275
|
-
const smolModel = this.settingsManager.getModelRole("smol");
|
|
1276
|
-
generateSessionTitle(text, registry, smolModel, this.session.sessionId)
|
|
1277
|
-
.then(async (title) => {
|
|
1278
|
-
if (title) {
|
|
1279
|
-
await this.sessionManager.setSessionTitle(title);
|
|
1280
|
-
setTerminalTitle(`omp: ${title}`);
|
|
1281
|
-
}
|
|
1282
|
-
})
|
|
1283
|
-
.catch(() => {});
|
|
1284
|
-
}
|
|
517
|
+
handleChangelogCommand(): void {
|
|
518
|
+
this.commandController.handleChangelogCommand();
|
|
519
|
+
}
|
|
1285
520
|
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
const images = this.pendingImages.length > 0 ? [...this.pendingImages] : undefined;
|
|
1289
|
-
this.pendingImages = [];
|
|
1290
|
-
this.onInputCallback({ text, images });
|
|
1291
|
-
}
|
|
1292
|
-
this.editor.addToHistory(text);
|
|
1293
|
-
};
|
|
521
|
+
handleHotkeysCommand(): void {
|
|
522
|
+
this.commandController.handleHotkeysCommand();
|
|
1294
523
|
}
|
|
1295
524
|
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
await this.handleEvent(event);
|
|
1299
|
-
});
|
|
525
|
+
handleClearCommand(): Promise<void> {
|
|
526
|
+
return this.commandController.handleClearCommand();
|
|
1300
527
|
}
|
|
1301
528
|
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
}
|
|
529
|
+
handleDebugCommand(): void {
|
|
530
|
+
this.commandController.handleDebugCommand();
|
|
531
|
+
}
|
|
1306
532
|
|
|
1307
|
-
|
|
1308
|
-
this.
|
|
533
|
+
handleArminSaysHi(): void {
|
|
534
|
+
this.commandController.handleArminSaysHi();
|
|
535
|
+
}
|
|
1309
536
|
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
if (this.retryEscapeHandler) {
|
|
1314
|
-
this.editor.onEscape = this.retryEscapeHandler;
|
|
1315
|
-
this.retryEscapeHandler = undefined;
|
|
1316
|
-
}
|
|
1317
|
-
if (this.retryLoader) {
|
|
1318
|
-
this.retryLoader.stop();
|
|
1319
|
-
this.retryLoader = undefined;
|
|
1320
|
-
this.statusContainer.clear();
|
|
1321
|
-
}
|
|
1322
|
-
if (this.loadingAnimation) {
|
|
1323
|
-
this.loadingAnimation.stop();
|
|
1324
|
-
}
|
|
1325
|
-
this.statusContainer.clear();
|
|
1326
|
-
this.loadingAnimation = new Loader(
|
|
1327
|
-
this.ui,
|
|
1328
|
-
(spinner) => theme.fg("accent", spinner),
|
|
1329
|
-
(text) => theme.fg("muted", text),
|
|
1330
|
-
`Working${theme.format.ellipsis} (esc to interrupt)`,
|
|
1331
|
-
getSymbolTheme().spinnerFrames,
|
|
1332
|
-
);
|
|
1333
|
-
this.statusContainer.addChild(this.loadingAnimation);
|
|
1334
|
-
this.startVoiceProgressTimer();
|
|
1335
|
-
this.ui.requestRender();
|
|
1336
|
-
break;
|
|
1337
|
-
|
|
1338
|
-
case "message_start":
|
|
1339
|
-
if (event.message.role === "hookMessage" || event.message.role === "custom") {
|
|
1340
|
-
this.addMessageToChat(event.message);
|
|
1341
|
-
this.ui.requestRender();
|
|
1342
|
-
} else if (event.message.role === "user") {
|
|
1343
|
-
this.addMessageToChat(event.message);
|
|
1344
|
-
this.editor.setText("");
|
|
1345
|
-
this.updatePendingMessagesDisplay();
|
|
1346
|
-
this.ui.requestRender();
|
|
1347
|
-
} else if (event.message.role === "fileMention") {
|
|
1348
|
-
this.addMessageToChat(event.message);
|
|
1349
|
-
this.ui.requestRender();
|
|
1350
|
-
} else if (event.message.role === "assistant") {
|
|
1351
|
-
this.streamingComponent = new AssistantMessageComponent(undefined, this.hideThinkingBlock);
|
|
1352
|
-
this.streamingMessage = event.message;
|
|
1353
|
-
this.chatContainer.addChild(this.streamingComponent);
|
|
1354
|
-
this.streamingComponent.updateContent(this.streamingMessage);
|
|
1355
|
-
this.ui.requestRender();
|
|
1356
|
-
}
|
|
1357
|
-
break;
|
|
1358
|
-
|
|
1359
|
-
case "message_update":
|
|
1360
|
-
if (this.streamingComponent && event.message.role === "assistant") {
|
|
1361
|
-
this.streamingMessage = event.message;
|
|
1362
|
-
this.streamingComponent.updateContent(this.streamingMessage);
|
|
1363
|
-
|
|
1364
|
-
for (const content of this.streamingMessage.content) {
|
|
1365
|
-
if (content.type === "toolCall") {
|
|
1366
|
-
if (!this.pendingTools.has(content.id)) {
|
|
1367
|
-
this.chatContainer.addChild(new Text("", 0, 0));
|
|
1368
|
-
const tool = this.session.getToolByName(content.name);
|
|
1369
|
-
const component = new ToolExecutionComponent(
|
|
1370
|
-
content.name,
|
|
1371
|
-
content.arguments,
|
|
1372
|
-
{
|
|
1373
|
-
showImages: this.settingsManager.getShowImages(),
|
|
1374
|
-
},
|
|
1375
|
-
tool,
|
|
1376
|
-
this.ui,
|
|
1377
|
-
this.sessionManager.getCwd(),
|
|
1378
|
-
);
|
|
1379
|
-
component.setExpanded(this.toolOutputExpanded);
|
|
1380
|
-
this.chatContainer.addChild(component);
|
|
1381
|
-
this.pendingTools.set(content.id, component);
|
|
1382
|
-
} else {
|
|
1383
|
-
const component = this.pendingTools.get(content.id);
|
|
1384
|
-
if (component) {
|
|
1385
|
-
component.updateArgs(content.arguments);
|
|
1386
|
-
}
|
|
1387
|
-
}
|
|
1388
|
-
}
|
|
1389
|
-
}
|
|
1390
|
-
this.ui.requestRender();
|
|
1391
|
-
}
|
|
1392
|
-
break;
|
|
1393
|
-
|
|
1394
|
-
case "message_end":
|
|
1395
|
-
if (event.message.role === "user") break;
|
|
1396
|
-
if (this.streamingComponent && event.message.role === "assistant") {
|
|
1397
|
-
this.streamingMessage = event.message;
|
|
1398
|
-
// Don't show "Aborted" text for TTSR aborts - we'll show a nicer message
|
|
1399
|
-
if (this.session.isTtsrAbortPending && this.streamingMessage.stopReason === "aborted") {
|
|
1400
|
-
// TTSR abort - suppress the "Aborted" rendering in the component
|
|
1401
|
-
const msgWithoutAbort = { ...this.streamingMessage, stopReason: "stop" as const };
|
|
1402
|
-
this.streamingComponent.updateContent(msgWithoutAbort);
|
|
1403
|
-
} else {
|
|
1404
|
-
this.streamingComponent.updateContent(this.streamingMessage);
|
|
1405
|
-
}
|
|
1406
|
-
|
|
1407
|
-
if (this.streamingMessage.stopReason === "aborted" || this.streamingMessage.stopReason === "error") {
|
|
1408
|
-
// Skip error handling for TTSR aborts
|
|
1409
|
-
if (!this.session.isTtsrAbortPending) {
|
|
1410
|
-
let errorMessage: string;
|
|
1411
|
-
if (this.streamingMessage.stopReason === "aborted") {
|
|
1412
|
-
const retryAttempt = this.session.retryAttempt;
|
|
1413
|
-
errorMessage =
|
|
1414
|
-
retryAttempt > 0
|
|
1415
|
-
? `Aborted after ${retryAttempt} retry attempt${retryAttempt > 1 ? "s" : ""}`
|
|
1416
|
-
: "Operation aborted";
|
|
1417
|
-
} else {
|
|
1418
|
-
errorMessage = this.streamingMessage.errorMessage || "Error";
|
|
1419
|
-
}
|
|
1420
|
-
for (const [, component] of this.pendingTools.entries()) {
|
|
1421
|
-
component.updateResult({
|
|
1422
|
-
content: [{ type: "text", text: errorMessage }],
|
|
1423
|
-
isError: true,
|
|
1424
|
-
});
|
|
1425
|
-
}
|
|
1426
|
-
}
|
|
1427
|
-
this.pendingTools.clear();
|
|
1428
|
-
} else {
|
|
1429
|
-
// Args are now complete - trigger diff computation for edit tools
|
|
1430
|
-
for (const [, component] of this.pendingTools.entries()) {
|
|
1431
|
-
component.setArgsComplete();
|
|
1432
|
-
}
|
|
1433
|
-
}
|
|
1434
|
-
this.streamingComponent = undefined;
|
|
1435
|
-
this.streamingMessage = undefined;
|
|
1436
|
-
this.statusLine.invalidate();
|
|
1437
|
-
this.updateEditorTopBorder();
|
|
1438
|
-
}
|
|
1439
|
-
this.ui.requestRender();
|
|
1440
|
-
break;
|
|
1441
|
-
|
|
1442
|
-
case "tool_execution_start": {
|
|
1443
|
-
if (!this.pendingTools.has(event.toolCallId)) {
|
|
1444
|
-
const tool = this.session.getToolByName(event.toolName);
|
|
1445
|
-
const component = new ToolExecutionComponent(
|
|
1446
|
-
event.toolName,
|
|
1447
|
-
event.args,
|
|
1448
|
-
{
|
|
1449
|
-
showImages: this.settingsManager.getShowImages(),
|
|
1450
|
-
},
|
|
1451
|
-
tool,
|
|
1452
|
-
this.ui,
|
|
1453
|
-
this.sessionManager.getCwd(),
|
|
1454
|
-
);
|
|
1455
|
-
component.setExpanded(this.toolOutputExpanded);
|
|
1456
|
-
this.chatContainer.addChild(component);
|
|
1457
|
-
this.pendingTools.set(event.toolCallId, component);
|
|
1458
|
-
this.ui.requestRender();
|
|
1459
|
-
}
|
|
1460
|
-
break;
|
|
1461
|
-
}
|
|
537
|
+
handleBashCommand(command: string, excludeFromContext?: boolean): Promise<void> {
|
|
538
|
+
return this.commandController.handleBashCommand(command, excludeFromContext);
|
|
539
|
+
}
|
|
1462
540
|
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
component.updateResult({ ...event.partialResult, isError: false }, true);
|
|
1467
|
-
this.ui.requestRender();
|
|
1468
|
-
}
|
|
1469
|
-
break;
|
|
1470
|
-
}
|
|
541
|
+
handleCompactCommand(customInstructions?: string): Promise<void> {
|
|
542
|
+
return this.commandController.handleCompactCommand(customInstructions);
|
|
543
|
+
}
|
|
1471
544
|
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
component.updateResult({ ...event.result, isError: event.isError });
|
|
1476
|
-
this.pendingTools.delete(event.toolCallId);
|
|
1477
|
-
this.ui.requestRender();
|
|
1478
|
-
}
|
|
1479
|
-
break;
|
|
1480
|
-
}
|
|
545
|
+
executeCompaction(customInstructions?: string, isAuto?: boolean): Promise<void> {
|
|
546
|
+
return this.commandController.executeCompaction(customInstructions, isAuto);
|
|
547
|
+
}
|
|
1481
548
|
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
this.loadingAnimation.stop();
|
|
1486
|
-
this.loadingAnimation = undefined;
|
|
1487
|
-
this.statusContainer.clear();
|
|
1488
|
-
}
|
|
1489
|
-
if (this.streamingComponent) {
|
|
1490
|
-
this.chatContainer.removeChild(this.streamingComponent);
|
|
1491
|
-
this.streamingComponent = undefined;
|
|
1492
|
-
this.streamingMessage = undefined;
|
|
1493
|
-
}
|
|
1494
|
-
this.pendingTools.clear();
|
|
1495
|
-
if (this.settingsManager.getVoiceEnabled() && this.voiceAutoModeEnabled) {
|
|
1496
|
-
const lastAssistant = this.findLastAssistantMessage();
|
|
1497
|
-
if (lastAssistant && lastAssistant.stopReason !== "aborted" && lastAssistant.stopReason !== "error") {
|
|
1498
|
-
const text = this.extractAssistantText(lastAssistant);
|
|
1499
|
-
if (text) {
|
|
1500
|
-
this.voiceSupervisor.notifyResult(text);
|
|
1501
|
-
}
|
|
1502
|
-
}
|
|
1503
|
-
}
|
|
1504
|
-
this.ui.requestRender();
|
|
1505
|
-
this.sendCompletionNotification();
|
|
1506
|
-
break;
|
|
1507
|
-
|
|
1508
|
-
case "auto_compaction_start": {
|
|
1509
|
-
// Allow input during compaction; submissions are queued
|
|
1510
|
-
// Set up escape to abort auto-compaction
|
|
1511
|
-
this.autoCompactionEscapeHandler = this.editor.onEscape;
|
|
1512
|
-
this.editor.onEscape = () => {
|
|
1513
|
-
this.session.abortCompaction();
|
|
1514
|
-
};
|
|
1515
|
-
// Show compacting indicator with reason
|
|
1516
|
-
this.statusContainer.clear();
|
|
1517
|
-
const reasonText = event.reason === "overflow" ? "Context overflow detected, " : "";
|
|
1518
|
-
this.autoCompactionLoader = new Loader(
|
|
1519
|
-
this.ui,
|
|
1520
|
-
(spinner) => theme.fg("accent", spinner),
|
|
1521
|
-
(text) => theme.fg("muted", text),
|
|
1522
|
-
`${reasonText}Auto-compacting${theme.format.ellipsis} (esc to cancel)`,
|
|
1523
|
-
getSymbolTheme().spinnerFrames,
|
|
1524
|
-
);
|
|
1525
|
-
this.statusContainer.addChild(this.autoCompactionLoader);
|
|
1526
|
-
this.ui.requestRender();
|
|
1527
|
-
break;
|
|
1528
|
-
}
|
|
549
|
+
openInBrowser(urlOrPath: string): void {
|
|
550
|
+
this.commandController.openInBrowser(urlOrPath);
|
|
551
|
+
}
|
|
1529
552
|
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
this.autoCompactionEscapeHandler = undefined;
|
|
1535
|
-
}
|
|
1536
|
-
// Stop loader
|
|
1537
|
-
if (this.autoCompactionLoader) {
|
|
1538
|
-
this.autoCompactionLoader.stop();
|
|
1539
|
-
this.autoCompactionLoader = undefined;
|
|
1540
|
-
this.statusContainer.clear();
|
|
1541
|
-
}
|
|
1542
|
-
// Handle result
|
|
1543
|
-
if (event.aborted) {
|
|
1544
|
-
this.showStatus("Auto-compaction cancelled");
|
|
1545
|
-
} else if (event.result) {
|
|
1546
|
-
// Rebuild chat to show compacted state
|
|
1547
|
-
this.chatContainer.clear();
|
|
1548
|
-
this.rebuildChatFromMessages();
|
|
1549
|
-
// Add compaction component at bottom so user sees it without scrolling
|
|
1550
|
-
this.addMessageToChat({
|
|
1551
|
-
role: "compactionSummary",
|
|
1552
|
-
tokensBefore: event.result.tokensBefore,
|
|
1553
|
-
summary: event.result.summary,
|
|
1554
|
-
timestamp: Date.now(),
|
|
1555
|
-
});
|
|
1556
|
-
this.statusLine.invalidate();
|
|
1557
|
-
this.updateEditorTopBorder();
|
|
1558
|
-
}
|
|
1559
|
-
await this.flushCompactionQueue({ willRetry: event.willRetry });
|
|
1560
|
-
this.ui.requestRender();
|
|
1561
|
-
break;
|
|
1562
|
-
}
|
|
553
|
+
// Selector handling
|
|
554
|
+
showSettingsSelector(): void {
|
|
555
|
+
this.selectorController.showSettingsSelector();
|
|
556
|
+
}
|
|
1563
557
|
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
this.editor.onEscape = () => {
|
|
1568
|
-
this.session.abortRetry();
|
|
1569
|
-
};
|
|
1570
|
-
// Show retry indicator
|
|
1571
|
-
this.statusContainer.clear();
|
|
1572
|
-
const delaySeconds = Math.round(event.delayMs / 1000);
|
|
1573
|
-
this.retryLoader = new Loader(
|
|
1574
|
-
this.ui,
|
|
1575
|
-
(spinner) => theme.fg("warning", spinner),
|
|
1576
|
-
(text) => theme.fg("muted", text),
|
|
1577
|
-
`Retrying (${event.attempt}/${event.maxAttempts}) in ${delaySeconds}s${theme.format.ellipsis} (esc to cancel)`,
|
|
1578
|
-
getSymbolTheme().spinnerFrames,
|
|
1579
|
-
);
|
|
1580
|
-
this.statusContainer.addChild(this.retryLoader);
|
|
1581
|
-
this.ui.requestRender();
|
|
1582
|
-
break;
|
|
1583
|
-
}
|
|
558
|
+
showHistorySearch(): void {
|
|
559
|
+
this.selectorController.showHistorySearch();
|
|
560
|
+
}
|
|
1584
561
|
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
this.editor.onEscape = this.retryEscapeHandler;
|
|
1589
|
-
this.retryEscapeHandler = undefined;
|
|
1590
|
-
}
|
|
1591
|
-
// Stop loader
|
|
1592
|
-
if (this.retryLoader) {
|
|
1593
|
-
this.retryLoader.stop();
|
|
1594
|
-
this.retryLoader = undefined;
|
|
1595
|
-
this.statusContainer.clear();
|
|
1596
|
-
}
|
|
1597
|
-
// Show error only on final failure (success shows normal response)
|
|
1598
|
-
if (!event.success) {
|
|
1599
|
-
this.showError(`Retry failed after ${event.attempt} attempts: ${event.finalError || "Unknown error"}`);
|
|
1600
|
-
}
|
|
1601
|
-
this.ui.requestRender();
|
|
1602
|
-
break;
|
|
1603
|
-
}
|
|
562
|
+
showExtensionsDashboard(): void {
|
|
563
|
+
this.selectorController.showExtensionsDashboard();
|
|
564
|
+
}
|
|
1604
565
|
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
const component = new TtsrNotificationComponent(event.rules);
|
|
1608
|
-
component.setExpanded(this.toolOutputExpanded);
|
|
1609
|
-
this.chatContainer.addChild(component);
|
|
1610
|
-
this.ui.requestRender();
|
|
1611
|
-
break;
|
|
1612
|
-
}
|
|
1613
|
-
}
|
|
566
|
+
showModelSelector(options?: { temporaryOnly?: boolean }): void {
|
|
567
|
+
this.selectorController.showModelSelector(options);
|
|
1614
568
|
}
|
|
1615
569
|
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
const method = this.settingsManager.getNotificationOnComplete();
|
|
1620
|
-
if (method === "off") return;
|
|
1621
|
-
const protocol = method === "auto" ? detectNotificationProtocol() : method;
|
|
1622
|
-
const title = this.sessionManager.getSessionTitle();
|
|
1623
|
-
const message = title ? `${title}: Complete` : "Complete";
|
|
1624
|
-
sendNotification(protocol, message);
|
|
1625
|
-
}
|
|
1626
|
-
|
|
1627
|
-
/** Extract text content from a user message */
|
|
1628
|
-
private getUserMessageText(message: Message): string {
|
|
1629
|
-
if (message.role !== "user") return "";
|
|
1630
|
-
const textBlocks =
|
|
1631
|
-
typeof message.content === "string"
|
|
1632
|
-
? [{ type: "text", text: message.content }]
|
|
1633
|
-
: message.content.filter((c: { type: string }) => c.type === "text");
|
|
1634
|
-
return textBlocks.map((c) => (c as { text: string }).text).join("");
|
|
1635
|
-
}
|
|
1636
|
-
|
|
1637
|
-
/**
|
|
1638
|
-
* Show a status message in the chat.
|
|
1639
|
-
*
|
|
1640
|
-
* If multiple status messages are emitted back-to-back (without anything else being added to the chat),
|
|
1641
|
-
* we update the previous status line instead of appending new ones to avoid log spam.
|
|
1642
|
-
*/
|
|
1643
|
-
private showStatus(message: string, options?: { dim?: boolean }): void {
|
|
1644
|
-
if (this.isBackgrounded) {
|
|
1645
|
-
return;
|
|
1646
|
-
}
|
|
1647
|
-
const children = this.chatContainer.children;
|
|
1648
|
-
const last = children.length > 0 ? children[children.length - 1] : undefined;
|
|
1649
|
-
const secondLast = children.length > 1 ? children[children.length - 2] : undefined;
|
|
1650
|
-
const useDim = options?.dim ?? true;
|
|
1651
|
-
const rendered = useDim ? theme.fg("dim", message) : message;
|
|
1652
|
-
|
|
1653
|
-
if (last && secondLast && last === this.lastStatusText && secondLast === this.lastStatusSpacer) {
|
|
1654
|
-
this.lastStatusText.setText(rendered);
|
|
1655
|
-
this.ui.requestRender();
|
|
1656
|
-
return;
|
|
1657
|
-
}
|
|
570
|
+
showUserMessageSelector(): void {
|
|
571
|
+
this.selectorController.showUserMessageSelector();
|
|
572
|
+
}
|
|
1658
573
|
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
this.chatContainer.addChild(spacer);
|
|
1662
|
-
this.chatContainer.addChild(text);
|
|
1663
|
-
this.lastStatusSpacer = spacer;
|
|
1664
|
-
this.lastStatusText = text;
|
|
1665
|
-
this.ui.requestRender();
|
|
574
|
+
showTreeSelector(): void {
|
|
575
|
+
this.selectorController.showTreeSelector();
|
|
1666
576
|
}
|
|
1667
577
|
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
case "bashExecution": {
|
|
1671
|
-
const component = new BashExecutionComponent(message.command, this.ui, message.excludeFromContext);
|
|
1672
|
-
if (message.output) {
|
|
1673
|
-
component.appendOutput(message.output);
|
|
1674
|
-
}
|
|
1675
|
-
component.setComplete(
|
|
1676
|
-
message.exitCode,
|
|
1677
|
-
message.cancelled,
|
|
1678
|
-
message.truncated ? ({ truncated: true } as TruncationResult) : undefined,
|
|
1679
|
-
message.fullOutputPath,
|
|
1680
|
-
);
|
|
1681
|
-
this.chatContainer.addChild(component);
|
|
1682
|
-
break;
|
|
1683
|
-
}
|
|
1684
|
-
case "hookMessage":
|
|
1685
|
-
case "custom": {
|
|
1686
|
-
if (message.display) {
|
|
1687
|
-
const renderer = this.session.extensionRunner?.getMessageRenderer(message.customType);
|
|
1688
|
-
// Both HookMessage and CustomMessage have the same structure, cast for compatibility
|
|
1689
|
-
this.chatContainer.addChild(new CustomMessageComponent(message as CustomMessage<unknown>, renderer));
|
|
1690
|
-
}
|
|
1691
|
-
break;
|
|
1692
|
-
}
|
|
1693
|
-
case "compactionSummary": {
|
|
1694
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
1695
|
-
const component = new CompactionSummaryMessageComponent(message);
|
|
1696
|
-
component.setExpanded(this.toolOutputExpanded);
|
|
1697
|
-
this.chatContainer.addChild(component);
|
|
1698
|
-
break;
|
|
1699
|
-
}
|
|
1700
|
-
case "branchSummary": {
|
|
1701
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
1702
|
-
const component = new BranchSummaryMessageComponent(message);
|
|
1703
|
-
component.setExpanded(this.toolOutputExpanded);
|
|
1704
|
-
this.chatContainer.addChild(component);
|
|
1705
|
-
break;
|
|
1706
|
-
}
|
|
1707
|
-
case "fileMention": {
|
|
1708
|
-
// Render compact file mention display
|
|
1709
|
-
for (const file of message.files) {
|
|
1710
|
-
const text = `${theme.fg("dim", `${theme.tree.last} `)}${theme.fg("muted", "Read")} ${theme.fg(
|
|
1711
|
-
"accent",
|
|
1712
|
-
file.path,
|
|
1713
|
-
)} ${theme.fg("dim", `(${file.lineCount} lines)`)}`;
|
|
1714
|
-
this.chatContainer.addChild(new Text(text, 0, 0));
|
|
1715
|
-
}
|
|
1716
|
-
break;
|
|
1717
|
-
}
|
|
1718
|
-
case "user": {
|
|
1719
|
-
const textContent = this.getUserMessageText(message);
|
|
1720
|
-
if (textContent) {
|
|
1721
|
-
const userComponent = new UserMessageComponent(textContent);
|
|
1722
|
-
this.chatContainer.addChild(userComponent);
|
|
1723
|
-
if (options?.populateHistory) {
|
|
1724
|
-
this.editor.addToHistory(textContent);
|
|
1725
|
-
}
|
|
1726
|
-
}
|
|
1727
|
-
break;
|
|
1728
|
-
}
|
|
1729
|
-
case "assistant": {
|
|
1730
|
-
const assistantComponent = new AssistantMessageComponent(message, this.hideThinkingBlock);
|
|
1731
|
-
this.chatContainer.addChild(assistantComponent);
|
|
1732
|
-
break;
|
|
1733
|
-
}
|
|
1734
|
-
case "toolResult": {
|
|
1735
|
-
// Tool results are rendered inline with tool calls, handled separately
|
|
1736
|
-
break;
|
|
1737
|
-
}
|
|
1738
|
-
default: {
|
|
1739
|
-
const _exhaustive: never = message;
|
|
1740
|
-
}
|
|
1741
|
-
}
|
|
578
|
+
showSessionSelector(): void {
|
|
579
|
+
this.selectorController.showSessionSelector();
|
|
1742
580
|
}
|
|
1743
581
|
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
* @param options.updateFooter Update footer state
|
|
1748
|
-
* @param options.populateHistory Add user messages to editor history
|
|
1749
|
-
*/
|
|
1750
|
-
private renderSessionContext(
|
|
1751
|
-
sessionContext: SessionContext,
|
|
1752
|
-
options: { updateFooter?: boolean; populateHistory?: boolean } = {},
|
|
1753
|
-
): void {
|
|
1754
|
-
this.pendingTools.clear();
|
|
582
|
+
handleResumeSession(sessionPath: string): Promise<void> {
|
|
583
|
+
return this.selectorController.handleResumeSession(sessionPath);
|
|
584
|
+
}
|
|
1755
585
|
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
}
|
|
586
|
+
showOAuthSelector(mode: "login" | "logout"): Promise<void> {
|
|
587
|
+
return this.selectorController.showOAuthSelector(mode);
|
|
588
|
+
}
|
|
1760
589
|
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
this.addMessageToChat(message);
|
|
1765
|
-
// Render tool call components
|
|
1766
|
-
for (const content of message.content) {
|
|
1767
|
-
if (content.type === "toolCall") {
|
|
1768
|
-
const tool = this.session.getToolByName(content.name);
|
|
1769
|
-
const component = new ToolExecutionComponent(
|
|
1770
|
-
content.name,
|
|
1771
|
-
content.arguments,
|
|
1772
|
-
{ showImages: this.settingsManager.getShowImages() },
|
|
1773
|
-
tool,
|
|
1774
|
-
this.ui,
|
|
1775
|
-
this.sessionManager.getCwd(),
|
|
1776
|
-
);
|
|
1777
|
-
component.setExpanded(this.toolOutputExpanded);
|
|
1778
|
-
this.chatContainer.addChild(component);
|
|
1779
|
-
|
|
1780
|
-
if (message.stopReason === "aborted" || message.stopReason === "error") {
|
|
1781
|
-
let errorMessage: string;
|
|
1782
|
-
if (message.stopReason === "aborted") {
|
|
1783
|
-
const retryAttempt = this.session.retryAttempt;
|
|
1784
|
-
errorMessage =
|
|
1785
|
-
retryAttempt > 0
|
|
1786
|
-
? `Aborted after ${retryAttempt} retry attempt${retryAttempt > 1 ? "s" : ""}`
|
|
1787
|
-
: "Operation aborted";
|
|
1788
|
-
} else {
|
|
1789
|
-
errorMessage = message.errorMessage || "Error";
|
|
1790
|
-
}
|
|
1791
|
-
component.updateResult({ content: [{ type: "text", text: errorMessage }], isError: true });
|
|
1792
|
-
} else {
|
|
1793
|
-
this.pendingTools.set(content.id, component);
|
|
1794
|
-
}
|
|
1795
|
-
}
|
|
1796
|
-
}
|
|
1797
|
-
} else if (message.role === "toolResult") {
|
|
1798
|
-
// Match tool results to pending tool components
|
|
1799
|
-
const component = this.pendingTools.get(message.toolCallId);
|
|
1800
|
-
if (component) {
|
|
1801
|
-
component.updateResult(message);
|
|
1802
|
-
this.pendingTools.delete(message.toolCallId);
|
|
1803
|
-
}
|
|
1804
|
-
} else {
|
|
1805
|
-
// All other messages use standard rendering
|
|
1806
|
-
this.addMessageToChat(message, options);
|
|
1807
|
-
}
|
|
1808
|
-
}
|
|
590
|
+
showHookConfirm(title: string, message: string): Promise<boolean> {
|
|
591
|
+
return this.extensionUiController.showHookConfirm(title, message);
|
|
592
|
+
}
|
|
1809
593
|
|
|
1810
|
-
|
|
1811
|
-
|
|
594
|
+
// Input handling
|
|
595
|
+
handleCtrlC(): void {
|
|
596
|
+
this.inputController.handleCtrlC();
|
|
1812
597
|
}
|
|
1813
598
|
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
this.renderSessionContext(context, {
|
|
1818
|
-
updateFooter: true,
|
|
1819
|
-
populateHistory: true,
|
|
1820
|
-
});
|
|
599
|
+
handleCtrlD(): void {
|
|
600
|
+
this.inputController.handleCtrlD();
|
|
601
|
+
}
|
|
1821
602
|
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
const compactionCount = allEntries.filter((e) => e.type === "compaction").length;
|
|
1825
|
-
if (compactionCount > 0) {
|
|
1826
|
-
const times = compactionCount === 1 ? "1 time" : `${compactionCount} times`;
|
|
1827
|
-
this.showStatus(`Session compacted ${times}`);
|
|
1828
|
-
}
|
|
603
|
+
handleCtrlZ(): void {
|
|
604
|
+
this.inputController.handleCtrlZ();
|
|
1829
605
|
}
|
|
1830
606
|
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
this.onInputCallback = (input) => {
|
|
1834
|
-
this.onInputCallback = undefined;
|
|
1835
|
-
resolve(input);
|
|
1836
|
-
};
|
|
1837
|
-
});
|
|
607
|
+
handleDequeue(): void {
|
|
608
|
+
this.inputController.handleDequeue();
|
|
1838
609
|
}
|
|
1839
610
|
|
|
1840
|
-
|
|
1841
|
-
this.
|
|
1842
|
-
const context = this.sessionManager.buildSessionContext();
|
|
1843
|
-
this.renderSessionContext(context);
|
|
611
|
+
handleBackgroundCommand(): void {
|
|
612
|
+
this.inputController.handleBackgroundCommand();
|
|
1844
613
|
}
|
|
1845
614
|
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
615
|
+
handleImagePaste(): Promise<boolean> {
|
|
616
|
+
return this.inputController.handleImagePaste();
|
|
617
|
+
}
|
|
1849
618
|
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
if (now - this.lastSigintTime < 500) {
|
|
1853
|
-
void this.shutdown();
|
|
1854
|
-
} else {
|
|
1855
|
-
this.clearEditor();
|
|
1856
|
-
this.lastSigintTime = now;
|
|
1857
|
-
}
|
|
619
|
+
cycleThinkingLevel(): void {
|
|
620
|
+
this.inputController.cycleThinkingLevel();
|
|
1858
621
|
}
|
|
1859
622
|
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
void this.shutdown();
|
|
623
|
+
cycleRoleModel(options?: { temporary?: boolean }): Promise<void> {
|
|
624
|
+
return this.inputController.cycleRoleModel(options);
|
|
1863
625
|
}
|
|
1864
626
|
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
*/
|
|
1869
|
-
private async shutdown(): Promise<void> {
|
|
1870
|
-
this.voiceAutoModeEnabled = false;
|
|
1871
|
-
await this.voiceSupervisor.stop();
|
|
627
|
+
toggleToolOutputExpansion(): void {
|
|
628
|
+
this.inputController.toggleToolOutputExpansion();
|
|
629
|
+
}
|
|
1872
630
|
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
// Emit shutdown event to hooks
|
|
1877
|
-
await this.session.emitCustomToolSessionEvent("shutdown");
|
|
1878
|
-
|
|
1879
|
-
this.stop();
|
|
1880
|
-
process.exit(0);
|
|
1881
|
-
}
|
|
1882
|
-
|
|
1883
|
-
private handleCtrlZ(): void {
|
|
1884
|
-
// Set up handler to restore TUI when resumed
|
|
1885
|
-
process.once("SIGCONT", () => {
|
|
1886
|
-
this.ui.start();
|
|
1887
|
-
this.ui.requestRender(true);
|
|
1888
|
-
});
|
|
1889
|
-
|
|
1890
|
-
// Stop the TUI (restore terminal to normal mode)
|
|
1891
|
-
this.ui.stop();
|
|
1892
|
-
|
|
1893
|
-
// Send SIGTSTP to process group (pid=0 means all processes in group)
|
|
1894
|
-
process.kill(0, "SIGTSTP");
|
|
1895
|
-
}
|
|
1896
|
-
|
|
1897
|
-
/**
|
|
1898
|
-
* Handle Alt+Up: pop the last queued message and restore it to the editor.
|
|
1899
|
-
*/
|
|
1900
|
-
private handleDequeue(): void {
|
|
1901
|
-
const message = this.session.popLastQueuedMessage();
|
|
1902
|
-
if (!message) return;
|
|
1903
|
-
|
|
1904
|
-
// Prepend to existing editor text (if any)
|
|
1905
|
-
const currentText = this.editor.getText();
|
|
1906
|
-
const newText = currentText ? `${message}\n\n${currentText}` : message;
|
|
1907
|
-
this.editor.setText(newText);
|
|
1908
|
-
this.updatePendingMessagesDisplay();
|
|
1909
|
-
this.ui.requestRender();
|
|
1910
|
-
}
|
|
1911
|
-
|
|
1912
|
-
private handleBackgroundCommand(): void {
|
|
1913
|
-
if (this.isBackgrounded) {
|
|
1914
|
-
this.showStatus("Background mode already enabled");
|
|
1915
|
-
return;
|
|
1916
|
-
}
|
|
1917
|
-
if (!this.session.isStreaming && this.session.queuedMessageCount === 0) {
|
|
1918
|
-
this.showWarning("Agent is idle; nothing to background");
|
|
1919
|
-
return;
|
|
1920
|
-
}
|
|
1921
|
-
|
|
1922
|
-
this.isBackgrounded = true;
|
|
1923
|
-
const backgroundUiContext = this.createBackgroundUiContext();
|
|
1924
|
-
|
|
1925
|
-
// Background mode disables interactive UI so tools like ask fail fast.
|
|
1926
|
-
this.setToolUIContext(backgroundUiContext, false);
|
|
1927
|
-
this.initializeHookRunner(backgroundUiContext, false);
|
|
1928
|
-
|
|
1929
|
-
if (this.loadingAnimation) {
|
|
1930
|
-
this.loadingAnimation.stop();
|
|
1931
|
-
this.loadingAnimation = undefined;
|
|
1932
|
-
}
|
|
1933
|
-
if (this.autoCompactionLoader) {
|
|
1934
|
-
this.autoCompactionLoader.stop();
|
|
1935
|
-
this.autoCompactionLoader = undefined;
|
|
1936
|
-
}
|
|
1937
|
-
if (this.retryLoader) {
|
|
1938
|
-
this.retryLoader.stop();
|
|
1939
|
-
this.retryLoader = undefined;
|
|
1940
|
-
}
|
|
1941
|
-
this.statusContainer.clear();
|
|
1942
|
-
this.statusLine.dispose();
|
|
1943
|
-
|
|
1944
|
-
if (this.unsubscribe) {
|
|
1945
|
-
this.unsubscribe();
|
|
1946
|
-
}
|
|
1947
|
-
this.unsubscribe = this.session.subscribe(async (event) => {
|
|
1948
|
-
await this.handleBackgroundEvent(event);
|
|
1949
|
-
});
|
|
1950
|
-
|
|
1951
|
-
// Backgrounding keeps the current process to preserve in-flight agent state.
|
|
1952
|
-
if (this.isInitialized) {
|
|
1953
|
-
this.ui.stop();
|
|
1954
|
-
this.isInitialized = false;
|
|
1955
|
-
}
|
|
1956
|
-
|
|
1957
|
-
process.stdout.write("Background mode enabled. Run `bg` to continue in background.\n");
|
|
1958
|
-
|
|
1959
|
-
if (process.platform === "win32" || !process.stdout.isTTY) {
|
|
1960
|
-
process.stdout.write("Backgrounding requires POSIX job control; continuing in foreground.\n");
|
|
1961
|
-
return;
|
|
1962
|
-
}
|
|
1963
|
-
|
|
1964
|
-
process.kill(0, "SIGTSTP");
|
|
1965
|
-
}
|
|
1966
|
-
|
|
1967
|
-
private async handleBackgroundEvent(event: AgentSessionEvent): Promise<void> {
|
|
1968
|
-
if (event.type !== "agent_end") {
|
|
1969
|
-
return;
|
|
1970
|
-
}
|
|
1971
|
-
if (this.session.queuedMessageCount > 0 || this.session.isStreaming) {
|
|
1972
|
-
return;
|
|
1973
|
-
}
|
|
1974
|
-
this.sendCompletionNotification();
|
|
1975
|
-
await this.shutdown();
|
|
1976
|
-
}
|
|
1977
|
-
|
|
1978
|
-
/**
|
|
1979
|
-
* Handle Ctrl+V for image paste from clipboard.
|
|
1980
|
-
* Returns true if an image was found and added, false otherwise.
|
|
1981
|
-
*/
|
|
1982
|
-
private async handleImagePaste(): Promise<boolean> {
|
|
1983
|
-
try {
|
|
1984
|
-
const image = await readImageFromClipboard();
|
|
1985
|
-
if (image) {
|
|
1986
|
-
let imageData = image;
|
|
1987
|
-
if (this.settingsManager.getImageAutoResize()) {
|
|
1988
|
-
try {
|
|
1989
|
-
const resized = await resizeImage({
|
|
1990
|
-
type: "image",
|
|
1991
|
-
data: image.data,
|
|
1992
|
-
mimeType: image.mimeType,
|
|
1993
|
-
});
|
|
1994
|
-
imageData = { data: resized.data, mimeType: resized.mimeType };
|
|
1995
|
-
} catch {
|
|
1996
|
-
imageData = image;
|
|
1997
|
-
}
|
|
1998
|
-
}
|
|
1999
|
-
|
|
2000
|
-
this.pendingImages.push({
|
|
2001
|
-
type: "image",
|
|
2002
|
-
data: imageData.data,
|
|
2003
|
-
mimeType: imageData.mimeType,
|
|
2004
|
-
});
|
|
2005
|
-
// Insert styled placeholder at cursor like Claude does
|
|
2006
|
-
const imageNum = this.pendingImages.length;
|
|
2007
|
-
const placeholder = theme.bold(theme.underline(`[Image #${imageNum}]`));
|
|
2008
|
-
this.editor.insertText(`${placeholder} `);
|
|
2009
|
-
this.ui.requestRender();
|
|
2010
|
-
return true;
|
|
2011
|
-
}
|
|
2012
|
-
// No image in clipboard - show hint
|
|
2013
|
-
this.showStatus("No image in clipboard (use terminal paste for text)");
|
|
2014
|
-
return false;
|
|
2015
|
-
} catch {
|
|
2016
|
-
this.showStatus("Failed to read clipboard");
|
|
2017
|
-
return false;
|
|
2018
|
-
}
|
|
2019
|
-
}
|
|
2020
|
-
|
|
2021
|
-
private setVoiceStatus(text: string | undefined): void {
|
|
2022
|
-
this.statusLine.setHookStatus("voice", text);
|
|
2023
|
-
this.ui.requestRender();
|
|
2024
|
-
}
|
|
2025
|
-
|
|
2026
|
-
private async handleVoiceInterrupt(reason?: string): Promise<void> {
|
|
2027
|
-
const now = Date.now();
|
|
2028
|
-
if (now - this.lastVoiceInterruptAt < 200) return;
|
|
2029
|
-
this.lastVoiceInterruptAt = now;
|
|
2030
|
-
if (this.session.isBashRunning) {
|
|
2031
|
-
this.session.abortBash();
|
|
2032
|
-
}
|
|
2033
|
-
if (this.session.isStreaming) {
|
|
2034
|
-
await this.session.abort();
|
|
2035
|
-
}
|
|
2036
|
-
if (reason) {
|
|
2037
|
-
this.showStatus(reason);
|
|
2038
|
-
}
|
|
2039
|
-
}
|
|
2040
|
-
|
|
2041
|
-
private stopVoiceProgressTimer(): void {
|
|
2042
|
-
if (this.voiceProgressTimer) {
|
|
2043
|
-
clearTimeout(this.voiceProgressTimer);
|
|
2044
|
-
this.voiceProgressTimer = undefined;
|
|
2045
|
-
}
|
|
2046
|
-
}
|
|
2047
|
-
|
|
2048
|
-
private startVoiceProgressTimer(): void {
|
|
2049
|
-
this.stopVoiceProgressTimer();
|
|
2050
|
-
if (!this.settingsManager.getVoiceEnabled() || !this.voiceAutoModeEnabled) return;
|
|
2051
|
-
this.voiceProgressSpoken = false;
|
|
2052
|
-
this.voiceProgressLastLength = 0;
|
|
2053
|
-
this.voiceProgressTimer = setTimeout(() => {
|
|
2054
|
-
void this.maybeSpeakProgress();
|
|
2055
|
-
}, VOICE_PROGRESS_DELAY_MS);
|
|
2056
|
-
}
|
|
2057
|
-
|
|
2058
|
-
private async maybeSpeakProgress(): Promise<void> {
|
|
2059
|
-
if (!this.session.isStreaming || this.voiceProgressSpoken || !this.voiceAutoModeEnabled) return;
|
|
2060
|
-
const streaming = this.streamingMessage;
|
|
2061
|
-
if (!streaming) return;
|
|
2062
|
-
const text = this.extractAssistantText(streaming);
|
|
2063
|
-
if (!text || text.length < VOICE_PROGRESS_MIN_CHARS) {
|
|
2064
|
-
if (this.session.isStreaming) {
|
|
2065
|
-
this.voiceProgressTimer = setTimeout(() => {
|
|
2066
|
-
void this.maybeSpeakProgress();
|
|
2067
|
-
}, VOICE_PROGRESS_DELAY_MS);
|
|
2068
|
-
}
|
|
2069
|
-
return;
|
|
2070
|
-
}
|
|
2071
|
-
|
|
2072
|
-
const delta = text.length - this.voiceProgressLastLength;
|
|
2073
|
-
if (delta < VOICE_PROGRESS_DELTA_CHARS) {
|
|
2074
|
-
if (this.session.isStreaming) {
|
|
2075
|
-
this.voiceProgressTimer = setTimeout(() => {
|
|
2076
|
-
void this.maybeSpeakProgress();
|
|
2077
|
-
}, VOICE_PROGRESS_DELAY_MS);
|
|
2078
|
-
}
|
|
2079
|
-
return;
|
|
2080
|
-
}
|
|
2081
|
-
|
|
2082
|
-
this.voiceProgressLastLength = text.length;
|
|
2083
|
-
this.voiceProgressSpoken = true;
|
|
2084
|
-
this.voiceSupervisor.notifyProgress(text);
|
|
2085
|
-
}
|
|
2086
|
-
|
|
2087
|
-
private async submitVoiceText(text: string): Promise<void> {
|
|
2088
|
-
const cleaned = text.trim();
|
|
2089
|
-
if (!cleaned) {
|
|
2090
|
-
this.showWarning("No speech detected. Try again.");
|
|
2091
|
-
return;
|
|
2092
|
-
}
|
|
2093
|
-
const toSend = cleaned;
|
|
2094
|
-
this.editor.addToHistory(toSend);
|
|
2095
|
-
|
|
2096
|
-
if (this.session.isStreaming) {
|
|
2097
|
-
await this.session.abort();
|
|
2098
|
-
await this.session.steer(toSend);
|
|
2099
|
-
this.updatePendingMessagesDisplay();
|
|
2100
|
-
return;
|
|
2101
|
-
}
|
|
2102
|
-
|
|
2103
|
-
if (this.onInputCallback) {
|
|
2104
|
-
this.onInputCallback({ text: toSend });
|
|
2105
|
-
}
|
|
2106
|
-
}
|
|
2107
|
-
|
|
2108
|
-
private findLastAssistantMessage(): AssistantMessage | undefined {
|
|
2109
|
-
for (let i = this.session.messages.length - 1; i >= 0; i--) {
|
|
2110
|
-
const message = this.session.messages[i];
|
|
2111
|
-
if (message?.role === "assistant") {
|
|
2112
|
-
return message as AssistantMessage;
|
|
2113
|
-
}
|
|
2114
|
-
}
|
|
2115
|
-
return undefined;
|
|
2116
|
-
}
|
|
2117
|
-
|
|
2118
|
-
private extractAssistantText(message: AssistantMessage): string {
|
|
2119
|
-
let text = "";
|
|
2120
|
-
for (const content of message.content) {
|
|
2121
|
-
if (content.type === "text") {
|
|
2122
|
-
text += content.text;
|
|
2123
|
-
}
|
|
2124
|
-
}
|
|
2125
|
-
return text.trim();
|
|
2126
|
-
}
|
|
2127
|
-
|
|
2128
|
-
private updateEditorBorderColor(): void {
|
|
2129
|
-
if (this.isBashMode) {
|
|
2130
|
-
this.editor.borderColor = theme.getBashModeBorderColor();
|
|
2131
|
-
} else {
|
|
2132
|
-
const level = this.session.thinkingLevel || "off";
|
|
2133
|
-
this.editor.borderColor = theme.getThinkingBorderColor(level);
|
|
2134
|
-
}
|
|
2135
|
-
// Update footer content in editor's top border
|
|
2136
|
-
this.updateEditorTopBorder();
|
|
2137
|
-
this.ui.requestRender();
|
|
2138
|
-
}
|
|
2139
|
-
|
|
2140
|
-
private updateEditorTopBorder(): void {
|
|
2141
|
-
const width = this.ui.getWidth();
|
|
2142
|
-
const topBorder = this.statusLine.getTopBorder(width);
|
|
2143
|
-
this.editor.setTopBorder(topBorder);
|
|
2144
|
-
}
|
|
2145
|
-
|
|
2146
|
-
private cycleThinkingLevel(): void {
|
|
2147
|
-
const newLevel = this.session.cycleThinkingLevel();
|
|
2148
|
-
if (newLevel === undefined) {
|
|
2149
|
-
this.showStatus("Current model does not support thinking");
|
|
2150
|
-
} else {
|
|
2151
|
-
this.statusLine.invalidate();
|
|
2152
|
-
this.updateEditorBorderColor();
|
|
2153
|
-
}
|
|
2154
|
-
}
|
|
2155
|
-
|
|
2156
|
-
private async cycleRoleModel(options?: { temporary?: boolean }): Promise<void> {
|
|
2157
|
-
try {
|
|
2158
|
-
const roleOrder = ["slow", "default", "smol"];
|
|
2159
|
-
const result = await this.session.cycleRoleModels(roleOrder, options);
|
|
2160
|
-
if (!result) {
|
|
2161
|
-
this.showStatus("Only one role model available");
|
|
2162
|
-
return;
|
|
2163
|
-
}
|
|
2164
|
-
|
|
2165
|
-
this.statusLine.invalidate();
|
|
2166
|
-
this.updateEditorBorderColor();
|
|
2167
|
-
const roleLabel = result.role === "default" ? "default" : result.role;
|
|
2168
|
-
const roleLabelStyled = theme.bold(theme.fg("accent", roleLabel));
|
|
2169
|
-
const thinkingStr =
|
|
2170
|
-
result.model.reasoning && result.thinkingLevel !== "off" ? ` (thinking: ${result.thinkingLevel})` : "";
|
|
2171
|
-
const tempLabel = options?.temporary ? " (temporary)" : "";
|
|
2172
|
-
const cycleSeparator = theme.fg("dim", " > ");
|
|
2173
|
-
const cycleLabel = roleOrder
|
|
2174
|
-
.map((role) => {
|
|
2175
|
-
if (role === result.role) {
|
|
2176
|
-
return theme.bold(theme.fg("accent", role));
|
|
2177
|
-
}
|
|
2178
|
-
return theme.fg("muted", role);
|
|
2179
|
-
})
|
|
2180
|
-
.join(cycleSeparator);
|
|
2181
|
-
const orderLabel = ` (cycle: ${cycleLabel})`;
|
|
2182
|
-
this.showStatus(
|
|
2183
|
-
`Switched to ${roleLabelStyled}: ${result.model.name || result.model.id}${thinkingStr}${tempLabel}${orderLabel}`,
|
|
2184
|
-
{ dim: false },
|
|
2185
|
-
);
|
|
2186
|
-
} catch (error) {
|
|
2187
|
-
this.showError(error instanceof Error ? error.message : String(error));
|
|
2188
|
-
}
|
|
2189
|
-
}
|
|
2190
|
-
|
|
2191
|
-
private toggleToolOutputExpansion(): void {
|
|
2192
|
-
this.toolOutputExpanded = !this.toolOutputExpanded;
|
|
2193
|
-
for (const child of this.chatContainer.children) {
|
|
2194
|
-
if (isExpandable(child)) {
|
|
2195
|
-
child.setExpanded(this.toolOutputExpanded);
|
|
2196
|
-
}
|
|
2197
|
-
}
|
|
2198
|
-
this.ui.requestRender();
|
|
2199
|
-
}
|
|
2200
|
-
|
|
2201
|
-
private toggleThinkingBlockVisibility(): void {
|
|
2202
|
-
this.hideThinkingBlock = !this.hideThinkingBlock;
|
|
2203
|
-
this.settingsManager.setHideThinkingBlock(this.hideThinkingBlock);
|
|
2204
|
-
|
|
2205
|
-
// Rebuild chat from session messages
|
|
2206
|
-
this.chatContainer.clear();
|
|
2207
|
-
this.rebuildChatFromMessages();
|
|
2208
|
-
|
|
2209
|
-
// If streaming, re-add the streaming component with updated visibility and re-render
|
|
2210
|
-
if (this.streamingComponent && this.streamingMessage) {
|
|
2211
|
-
this.streamingComponent.setHideThinkingBlock(this.hideThinkingBlock);
|
|
2212
|
-
this.streamingComponent.updateContent(this.streamingMessage);
|
|
2213
|
-
this.chatContainer.addChild(this.streamingComponent);
|
|
2214
|
-
}
|
|
2215
|
-
|
|
2216
|
-
this.showStatus(`Thinking blocks: ${this.hideThinkingBlock ? "hidden" : "visible"}`);
|
|
2217
|
-
}
|
|
2218
|
-
|
|
2219
|
-
private openExternalEditor(): void {
|
|
2220
|
-
// Determine editor (respect $VISUAL, then $EDITOR)
|
|
2221
|
-
const editorCmd = process.env.VISUAL || process.env.EDITOR;
|
|
2222
|
-
if (!editorCmd) {
|
|
2223
|
-
this.showWarning("No editor configured. Set $VISUAL or $EDITOR environment variable.");
|
|
2224
|
-
return;
|
|
2225
|
-
}
|
|
2226
|
-
|
|
2227
|
-
const currentText = this.editor.getText();
|
|
2228
|
-
const tmpFile = path.join(os.tmpdir(), `omp-editor-${nanoid()}.omp.md`);
|
|
2229
|
-
|
|
2230
|
-
try {
|
|
2231
|
-
// Write current content to temp file
|
|
2232
|
-
fs.writeFileSync(tmpFile, currentText, "utf-8");
|
|
2233
|
-
|
|
2234
|
-
// Stop TUI to release terminal
|
|
2235
|
-
this.ui.stop();
|
|
2236
|
-
|
|
2237
|
-
// Split by space to support editor arguments (e.g., "code --wait")
|
|
2238
|
-
const [editor, ...editorArgs] = editorCmd.split(" ");
|
|
2239
|
-
|
|
2240
|
-
// Spawn editor synchronously with inherited stdio for interactive editing
|
|
2241
|
-
const result = Bun.spawnSync([editor, ...editorArgs, tmpFile], {
|
|
2242
|
-
stdin: "inherit",
|
|
2243
|
-
stdout: "inherit",
|
|
2244
|
-
stderr: "inherit",
|
|
2245
|
-
});
|
|
2246
|
-
|
|
2247
|
-
// On successful exit (exitCode 0), replace editor content
|
|
2248
|
-
if (result.exitCode === 0) {
|
|
2249
|
-
const newContent = fs.readFileSync(tmpFile, "utf-8").replace(/\n$/, "");
|
|
2250
|
-
this.editor.setText(newContent);
|
|
2251
|
-
}
|
|
2252
|
-
// On non-zero exit, keep original text (no action needed)
|
|
2253
|
-
} finally {
|
|
2254
|
-
// Clean up temp file
|
|
2255
|
-
try {
|
|
2256
|
-
fs.unlinkSync(tmpFile);
|
|
2257
|
-
} catch {
|
|
2258
|
-
// Ignore cleanup errors
|
|
2259
|
-
}
|
|
2260
|
-
|
|
2261
|
-
// Restart TUI
|
|
2262
|
-
this.ui.start();
|
|
2263
|
-
this.ui.requestRender();
|
|
2264
|
-
}
|
|
2265
|
-
}
|
|
2266
|
-
|
|
2267
|
-
// =========================================================================
|
|
2268
|
-
// UI helpers
|
|
2269
|
-
// =========================================================================
|
|
2270
|
-
|
|
2271
|
-
clearEditor(): void {
|
|
2272
|
-
if (this.isBackgrounded) {
|
|
2273
|
-
return;
|
|
2274
|
-
}
|
|
2275
|
-
this.editor.setText("");
|
|
2276
|
-
this.pendingImages = [];
|
|
2277
|
-
this.ui.requestRender();
|
|
2278
|
-
}
|
|
2279
|
-
|
|
2280
|
-
showError(errorMessage: string): void {
|
|
2281
|
-
if (this.isBackgrounded) {
|
|
2282
|
-
console.error(`Error: ${errorMessage}`);
|
|
2283
|
-
return;
|
|
2284
|
-
}
|
|
2285
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
2286
|
-
this.chatContainer.addChild(new Text(theme.fg("error", `Error: ${errorMessage}`), 1, 0));
|
|
2287
|
-
this.ui.requestRender();
|
|
2288
|
-
}
|
|
2289
|
-
|
|
2290
|
-
showWarning(warningMessage: string): void {
|
|
2291
|
-
if (this.isBackgrounded) {
|
|
2292
|
-
console.error(`Warning: ${warningMessage}`);
|
|
2293
|
-
return;
|
|
2294
|
-
}
|
|
2295
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
2296
|
-
this.chatContainer.addChild(new Text(theme.fg("warning", `Warning: ${warningMessage}`), 1, 0));
|
|
2297
|
-
this.ui.requestRender();
|
|
2298
|
-
}
|
|
2299
|
-
|
|
2300
|
-
showNewVersionNotification(newVersion: string): void {
|
|
2301
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
2302
|
-
this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
|
|
2303
|
-
this.chatContainer.addChild(
|
|
2304
|
-
new Text(
|
|
2305
|
-
theme.bold(theme.fg("warning", "Update Available")) +
|
|
2306
|
-
"\n" +
|
|
2307
|
-
theme.fg("muted", `New version ${newVersion} is available. Run: `) +
|
|
2308
|
-
theme.fg("accent", "omp update"),
|
|
2309
|
-
1,
|
|
2310
|
-
0,
|
|
2311
|
-
),
|
|
2312
|
-
);
|
|
2313
|
-
this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
|
|
2314
|
-
this.ui.requestRender();
|
|
2315
|
-
}
|
|
2316
|
-
|
|
2317
|
-
private updatePendingMessagesDisplay(): void {
|
|
2318
|
-
this.pendingMessagesContainer.clear();
|
|
2319
|
-
const queuedMessages = this.session.getQueuedMessages();
|
|
2320
|
-
const steeringMessages = [
|
|
2321
|
-
...queuedMessages.steering.map((message) => ({ message, label: "Steer" })),
|
|
2322
|
-
...this.compactionQueuedMessages
|
|
2323
|
-
.filter((entry) => entry.mode === "steer")
|
|
2324
|
-
.map((entry) => ({ message: entry.text, label: "Steer" })),
|
|
2325
|
-
];
|
|
2326
|
-
const followUpMessages = [
|
|
2327
|
-
...queuedMessages.followUp.map((message) => ({ message, label: "Follow-up" })),
|
|
2328
|
-
...this.compactionQueuedMessages
|
|
2329
|
-
.filter((entry) => entry.mode === "followUp")
|
|
2330
|
-
.map((entry) => ({ message: entry.text, label: "Follow-up" })),
|
|
2331
|
-
];
|
|
2332
|
-
const allMessages = [...steeringMessages, ...followUpMessages];
|
|
2333
|
-
if (allMessages.length > 0) {
|
|
2334
|
-
this.pendingMessagesContainer.addChild(new Spacer(1));
|
|
2335
|
-
for (const entry of allMessages) {
|
|
2336
|
-
const queuedText = theme.fg("dim", `${entry.label}: ${entry.message}`);
|
|
2337
|
-
this.pendingMessagesContainer.addChild(new TruncatedText(queuedText, 1, 0));
|
|
2338
|
-
}
|
|
2339
|
-
}
|
|
2340
|
-
}
|
|
2341
|
-
|
|
2342
|
-
private queueCompactionMessage(text: string, mode: "steer" | "followUp"): void {
|
|
2343
|
-
this.compactionQueuedMessages.push({ text, mode });
|
|
2344
|
-
this.editor.addToHistory(text);
|
|
2345
|
-
this.editor.setText("");
|
|
2346
|
-
this.updatePendingMessagesDisplay();
|
|
2347
|
-
this.showStatus("Queued message for after compaction");
|
|
2348
|
-
}
|
|
2349
|
-
|
|
2350
|
-
private isKnownSlashCommand(text: string): boolean {
|
|
2351
|
-
if (!text.startsWith("/")) return false;
|
|
2352
|
-
const spaceIndex = text.indexOf(" ");
|
|
2353
|
-
const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
|
|
2354
|
-
if (!commandName) return false;
|
|
2355
|
-
|
|
2356
|
-
if (this.session.extensionRunner?.getCommand(commandName)) {
|
|
2357
|
-
return true;
|
|
2358
|
-
}
|
|
2359
|
-
|
|
2360
|
-
if (this.session.customCommands.some((cmd) => cmd.command.name === commandName)) {
|
|
2361
|
-
return true;
|
|
2362
|
-
}
|
|
2363
|
-
|
|
2364
|
-
return this.fileSlashCommands.has(commandName);
|
|
2365
|
-
}
|
|
2366
|
-
|
|
2367
|
-
private async flushCompactionQueue(options?: { willRetry?: boolean }): Promise<void> {
|
|
2368
|
-
if (this.compactionQueuedMessages.length === 0) {
|
|
2369
|
-
return;
|
|
2370
|
-
}
|
|
2371
|
-
|
|
2372
|
-
const queuedMessages = [...this.compactionQueuedMessages];
|
|
2373
|
-
this.compactionQueuedMessages = [];
|
|
2374
|
-
this.updatePendingMessagesDisplay();
|
|
2375
|
-
|
|
2376
|
-
const restoreQueue = (error: unknown) => {
|
|
2377
|
-
this.session.clearQueue();
|
|
2378
|
-
this.compactionQueuedMessages = queuedMessages;
|
|
2379
|
-
this.updatePendingMessagesDisplay();
|
|
2380
|
-
this.showError(
|
|
2381
|
-
`Failed to send queued message${queuedMessages.length > 1 ? "s" : ""}: ${
|
|
2382
|
-
error instanceof Error ? error.message : String(error)
|
|
2383
|
-
}`,
|
|
2384
|
-
);
|
|
2385
|
-
};
|
|
2386
|
-
|
|
2387
|
-
try {
|
|
2388
|
-
if (options?.willRetry) {
|
|
2389
|
-
for (const message of queuedMessages) {
|
|
2390
|
-
if (this.isKnownSlashCommand(message.text)) {
|
|
2391
|
-
await this.session.prompt(message.text);
|
|
2392
|
-
} else if (message.mode === "followUp") {
|
|
2393
|
-
await this.session.followUp(message.text);
|
|
2394
|
-
} else {
|
|
2395
|
-
await this.session.steer(message.text);
|
|
2396
|
-
}
|
|
2397
|
-
}
|
|
2398
|
-
this.updatePendingMessagesDisplay();
|
|
2399
|
-
return;
|
|
2400
|
-
}
|
|
2401
|
-
|
|
2402
|
-
const firstPromptIndex = queuedMessages.findIndex((message) => !this.isKnownSlashCommand(message.text));
|
|
2403
|
-
if (firstPromptIndex === -1) {
|
|
2404
|
-
for (const message of queuedMessages) {
|
|
2405
|
-
await this.session.prompt(message.text);
|
|
2406
|
-
}
|
|
2407
|
-
return;
|
|
2408
|
-
}
|
|
2409
|
-
|
|
2410
|
-
const preCommands = queuedMessages.slice(0, firstPromptIndex);
|
|
2411
|
-
const firstPrompt = queuedMessages[firstPromptIndex];
|
|
2412
|
-
const rest = queuedMessages.slice(firstPromptIndex + 1);
|
|
2413
|
-
|
|
2414
|
-
for (const message of preCommands) {
|
|
2415
|
-
await this.session.prompt(message.text);
|
|
2416
|
-
}
|
|
2417
|
-
|
|
2418
|
-
const promptPromise = this.session.prompt(firstPrompt.text).catch((error) => {
|
|
2419
|
-
restoreQueue(error);
|
|
2420
|
-
});
|
|
2421
|
-
|
|
2422
|
-
for (const message of rest) {
|
|
2423
|
-
if (this.isKnownSlashCommand(message.text)) {
|
|
2424
|
-
await this.session.prompt(message.text);
|
|
2425
|
-
} else if (message.mode === "followUp") {
|
|
2426
|
-
await this.session.followUp(message.text);
|
|
2427
|
-
} else {
|
|
2428
|
-
await this.session.steer(message.text);
|
|
2429
|
-
}
|
|
2430
|
-
}
|
|
2431
|
-
this.updatePendingMessagesDisplay();
|
|
2432
|
-
void promptPromise;
|
|
2433
|
-
} catch (error) {
|
|
2434
|
-
restoreQueue(error);
|
|
2435
|
-
}
|
|
631
|
+
toggleThinkingBlockVisibility(): void {
|
|
632
|
+
this.inputController.toggleThinkingBlockVisibility();
|
|
2436
633
|
}
|
|
2437
634
|
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
for (const component of this.pendingBashComponents) {
|
|
2441
|
-
this.pendingMessagesContainer.removeChild(component);
|
|
2442
|
-
this.chatContainer.addChild(component);
|
|
2443
|
-
}
|
|
2444
|
-
this.pendingBashComponents = [];
|
|
2445
|
-
}
|
|
2446
|
-
|
|
2447
|
-
// =========================================================================
|
|
2448
|
-
// Selectors
|
|
2449
|
-
// =========================================================================
|
|
2450
|
-
|
|
2451
|
-
/**
|
|
2452
|
-
* Shows a selector component in place of the editor.
|
|
2453
|
-
* @param create Factory that receives a `done` callback and returns the component and focus target
|
|
2454
|
-
*/
|
|
2455
|
-
private showSelector(create: (done: () => void) => { component: Component; focus: Component }): void {
|
|
2456
|
-
const done = () => {
|
|
2457
|
-
this.editorContainer.clear();
|
|
2458
|
-
this.editorContainer.addChild(this.editor);
|
|
2459
|
-
this.ui.setFocus(this.editor);
|
|
2460
|
-
};
|
|
2461
|
-
const { component, focus } = create(done);
|
|
2462
|
-
this.editorContainer.clear();
|
|
2463
|
-
this.editorContainer.addChild(component);
|
|
2464
|
-
this.ui.setFocus(focus);
|
|
2465
|
-
this.ui.requestRender();
|
|
635
|
+
openExternalEditor(): void {
|
|
636
|
+
this.inputController.openExternalEditor();
|
|
2466
637
|
}
|
|
2467
638
|
|
|
2468
|
-
|
|
2469
|
-
this.
|
|
2470
|
-
const selector = new SettingsSelectorComponent(
|
|
2471
|
-
this.settingsManager,
|
|
2472
|
-
{
|
|
2473
|
-
availableThinkingLevels: this.session.getAvailableThinkingLevels(),
|
|
2474
|
-
thinkingLevel: this.session.thinkingLevel,
|
|
2475
|
-
availableThemes: getAvailableThemes(),
|
|
2476
|
-
cwd: process.cwd(),
|
|
2477
|
-
},
|
|
2478
|
-
{
|
|
2479
|
-
onChange: (id, value) => this.handleSettingChange(id, value),
|
|
2480
|
-
onThemePreview: (themeName) => {
|
|
2481
|
-
const result = setTheme(themeName, true);
|
|
2482
|
-
if (result.success) {
|
|
2483
|
-
this.ui.invalidate();
|
|
2484
|
-
this.ui.requestRender();
|
|
2485
|
-
}
|
|
2486
|
-
},
|
|
2487
|
-
onStatusLinePreview: (settings) => {
|
|
2488
|
-
// Update status line with preview settings
|
|
2489
|
-
const currentSettings = this.settingsManager.getStatusLineSettings();
|
|
2490
|
-
this.statusLine.updateSettings({ ...currentSettings, ...settings });
|
|
2491
|
-
this.updateEditorTopBorder();
|
|
2492
|
-
this.ui.requestRender();
|
|
2493
|
-
},
|
|
2494
|
-
getStatusLinePreview: () => {
|
|
2495
|
-
// Return the rendered status line for inline preview
|
|
2496
|
-
const width = this.ui.getWidth();
|
|
2497
|
-
return this.statusLine.getTopBorder(width).content;
|
|
2498
|
-
},
|
|
2499
|
-
onPluginsChanged: () => {
|
|
2500
|
-
this.ui.requestRender();
|
|
2501
|
-
},
|
|
2502
|
-
onCancel: () => {
|
|
2503
|
-
done();
|
|
2504
|
-
// Restore status line to saved settings
|
|
2505
|
-
this.statusLine.updateSettings(this.settingsManager.getStatusLineSettings());
|
|
2506
|
-
this.updateEditorTopBorder();
|
|
2507
|
-
this.ui.requestRender();
|
|
2508
|
-
},
|
|
2509
|
-
},
|
|
2510
|
-
);
|
|
2511
|
-
return { component: selector, focus: selector };
|
|
2512
|
-
});
|
|
639
|
+
registerExtensionShortcuts(): void {
|
|
640
|
+
this.inputController.registerExtensionShortcuts();
|
|
2513
641
|
}
|
|
2514
642
|
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
this.showSelector((done) => {
|
|
2520
|
-
const component = new HistorySearchComponent(
|
|
2521
|
-
historyStorage,
|
|
2522
|
-
(prompt) => {
|
|
2523
|
-
done();
|
|
2524
|
-
this.editor.setText(prompt);
|
|
2525
|
-
this.ui.requestRender();
|
|
2526
|
-
},
|
|
2527
|
-
() => {
|
|
2528
|
-
done();
|
|
2529
|
-
this.ui.requestRender();
|
|
2530
|
-
},
|
|
2531
|
-
);
|
|
2532
|
-
return { component, focus: component };
|
|
2533
|
-
});
|
|
643
|
+
// Voice handling
|
|
644
|
+
setVoiceStatus(text: string | undefined): void {
|
|
645
|
+
this.voiceManager.setVoiceStatus(text);
|
|
2534
646
|
}
|
|
2535
647
|
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
* Replaces /status with a unified view of all providers and extensions.
|
|
2539
|
-
*/
|
|
2540
|
-
private showExtensionsDashboard(): void {
|
|
2541
|
-
this.showSelector((done) => {
|
|
2542
|
-
const dashboard = new ExtensionDashboard(process.cwd(), this.settingsManager, this.ui.terminal.rows);
|
|
2543
|
-
dashboard.onClose = () => {
|
|
2544
|
-
done();
|
|
2545
|
-
this.ui.requestRender();
|
|
2546
|
-
};
|
|
2547
|
-
return { component: dashboard, focus: dashboard };
|
|
2548
|
-
});
|
|
648
|
+
handleVoiceInterrupt(reason?: string): Promise<void> {
|
|
649
|
+
return this.voiceManager.handleVoiceInterrupt(reason);
|
|
2549
650
|
}
|
|
2550
651
|
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
* Most settings are saved directly via SettingsManager in the definitions.
|
|
2554
|
-
* This handles side effects and session-specific settings.
|
|
2555
|
-
*/
|
|
2556
|
-
private handleSettingChange(id: string, value: string | boolean): void {
|
|
2557
|
-
// Discovery provider toggles
|
|
2558
|
-
if (id.startsWith("discovery.")) {
|
|
2559
|
-
const providerId = id.replace("discovery.", "");
|
|
2560
|
-
if (value) {
|
|
2561
|
-
enableProvider(providerId);
|
|
2562
|
-
} else {
|
|
2563
|
-
disableProvider(providerId);
|
|
2564
|
-
}
|
|
2565
|
-
return;
|
|
2566
|
-
}
|
|
2567
|
-
|
|
2568
|
-
switch (id) {
|
|
2569
|
-
// Session-managed settings (not in SettingsManager)
|
|
2570
|
-
case "autoCompact":
|
|
2571
|
-
this.session.setAutoCompactionEnabled(value as boolean);
|
|
2572
|
-
this.statusLine.setAutoCompactEnabled(value as boolean);
|
|
2573
|
-
break;
|
|
2574
|
-
case "steeringMode":
|
|
2575
|
-
this.session.setSteeringMode(value as "all" | "one-at-a-time");
|
|
2576
|
-
break;
|
|
2577
|
-
case "followUpMode":
|
|
2578
|
-
this.session.setFollowUpMode(value as "all" | "one-at-a-time");
|
|
2579
|
-
break;
|
|
2580
|
-
case "interruptMode":
|
|
2581
|
-
this.session.setInterruptMode(value as "immediate" | "wait");
|
|
2582
|
-
break;
|
|
2583
|
-
case "thinkingLevel":
|
|
2584
|
-
this.session.setThinkingLevel(value as ThinkingLevel);
|
|
2585
|
-
this.statusLine.invalidate();
|
|
2586
|
-
this.updateEditorBorderColor();
|
|
2587
|
-
break;
|
|
2588
|
-
|
|
2589
|
-
// Settings with UI side effects
|
|
2590
|
-
case "showImages":
|
|
2591
|
-
for (const child of this.chatContainer.children) {
|
|
2592
|
-
if (child instanceof ToolExecutionComponent) {
|
|
2593
|
-
child.setShowImages(value as boolean);
|
|
2594
|
-
}
|
|
2595
|
-
}
|
|
2596
|
-
break;
|
|
2597
|
-
case "hideThinking":
|
|
2598
|
-
this.hideThinkingBlock = value as boolean;
|
|
2599
|
-
for (const child of this.chatContainer.children) {
|
|
2600
|
-
if (child instanceof AssistantMessageComponent) {
|
|
2601
|
-
child.setHideThinkingBlock(value as boolean);
|
|
2602
|
-
}
|
|
2603
|
-
}
|
|
2604
|
-
this.chatContainer.clear();
|
|
2605
|
-
this.rebuildChatFromMessages();
|
|
2606
|
-
break;
|
|
2607
|
-
case "theme": {
|
|
2608
|
-
const result = setTheme(value as string, true);
|
|
2609
|
-
this.statusLine.invalidate();
|
|
2610
|
-
this.updateEditorTopBorder();
|
|
2611
|
-
this.ui.invalidate();
|
|
2612
|
-
if (!result.success) {
|
|
2613
|
-
this.showError(`Failed to load theme "${value}": ${result.error}\nFell back to dark theme.`);
|
|
2614
|
-
}
|
|
2615
|
-
break;
|
|
2616
|
-
}
|
|
2617
|
-
case "symbolPreset": {
|
|
2618
|
-
setSymbolPreset(value as "unicode" | "nerd" | "ascii");
|
|
2619
|
-
this.statusLine.invalidate();
|
|
2620
|
-
this.updateEditorTopBorder();
|
|
2621
|
-
this.ui.invalidate();
|
|
2622
|
-
break;
|
|
2623
|
-
}
|
|
2624
|
-
case "voiceEnabled": {
|
|
2625
|
-
if (!value) {
|
|
2626
|
-
this.voiceAutoModeEnabled = false;
|
|
2627
|
-
this.stopVoiceProgressTimer();
|
|
2628
|
-
void this.voiceSupervisor.stop();
|
|
2629
|
-
this.setVoiceStatus(undefined);
|
|
2630
|
-
}
|
|
2631
|
-
break;
|
|
2632
|
-
}
|
|
2633
|
-
case "statusLinePreset":
|
|
2634
|
-
case "statusLineSeparator":
|
|
2635
|
-
case "statusLineShowHooks":
|
|
2636
|
-
case "statusLineSegments":
|
|
2637
|
-
case "statusLineModelThinking":
|
|
2638
|
-
case "statusLinePathAbbreviate":
|
|
2639
|
-
case "statusLinePathMaxLength":
|
|
2640
|
-
case "statusLinePathStripWorkPrefix":
|
|
2641
|
-
case "statusLineGitShowBranch":
|
|
2642
|
-
case "statusLineGitShowStaged":
|
|
2643
|
-
case "statusLineGitShowUnstaged":
|
|
2644
|
-
case "statusLineGitShowUntracked":
|
|
2645
|
-
case "statusLineTimeFormat":
|
|
2646
|
-
case "statusLineTimeShowSeconds": {
|
|
2647
|
-
this.statusLine.updateSettings(this.settingsManager.getStatusLineSettings());
|
|
2648
|
-
this.updateEditorTopBorder();
|
|
2649
|
-
this.ui.requestRender();
|
|
2650
|
-
break;
|
|
2651
|
-
}
|
|
2652
|
-
|
|
2653
|
-
// Provider settings - update runtime preferences
|
|
2654
|
-
case "webSearchProvider":
|
|
2655
|
-
setPreferredWebSearchProvider(value as "auto" | "exa" | "perplexity" | "anthropic");
|
|
2656
|
-
break;
|
|
2657
|
-
case "imageProvider":
|
|
2658
|
-
setPreferredImageProvider(value as "auto" | "gemini" | "openrouter");
|
|
2659
|
-
break;
|
|
2660
|
-
|
|
2661
|
-
// All other settings are handled by the definitions (get/set on SettingsManager)
|
|
2662
|
-
// No additional side effects needed
|
|
2663
|
-
}
|
|
652
|
+
startVoiceProgressTimer(): void {
|
|
653
|
+
this.voiceManager.startVoiceProgressTimer();
|
|
2664
654
|
}
|
|
2665
655
|
|
|
2666
|
-
|
|
2667
|
-
this.
|
|
2668
|
-
const selector = new ModelSelectorComponent(
|
|
2669
|
-
this.ui,
|
|
2670
|
-
this.session.model,
|
|
2671
|
-
this.settingsManager,
|
|
2672
|
-
this.session.modelRegistry,
|
|
2673
|
-
this.session.scopedModels,
|
|
2674
|
-
async (model, role) => {
|
|
2675
|
-
try {
|
|
2676
|
-
if (role === "temporary") {
|
|
2677
|
-
// Temporary: update agent state but don't persist to settings
|
|
2678
|
-
await this.session.setModelTemporary(model);
|
|
2679
|
-
this.statusLine.invalidate();
|
|
2680
|
-
this.updateEditorBorderColor();
|
|
2681
|
-
this.showStatus(`Temporary model: ${model.id}`);
|
|
2682
|
-
done();
|
|
2683
|
-
this.ui.requestRender();
|
|
2684
|
-
} else if (role === "default") {
|
|
2685
|
-
// Default: update agent state and persist
|
|
2686
|
-
await this.session.setModel(model, role);
|
|
2687
|
-
this.statusLine.invalidate();
|
|
2688
|
-
this.updateEditorBorderColor();
|
|
2689
|
-
this.showStatus(`Default model: ${model.id}`);
|
|
2690
|
-
// Don't call done() - selector stays open for role assignment
|
|
2691
|
-
} else {
|
|
2692
|
-
// Other roles (smol, slow): just update settings, not current model
|
|
2693
|
-
const roleLabel = role === "smol" ? "Smol" : role;
|
|
2694
|
-
this.showStatus(`${roleLabel} model: ${model.id}`);
|
|
2695
|
-
// Don't call done() - selector stays open
|
|
2696
|
-
}
|
|
2697
|
-
} catch (error) {
|
|
2698
|
-
this.showError(error instanceof Error ? error.message : String(error));
|
|
2699
|
-
}
|
|
2700
|
-
},
|
|
2701
|
-
() => {
|
|
2702
|
-
done();
|
|
2703
|
-
this.ui.requestRender();
|
|
2704
|
-
},
|
|
2705
|
-
options,
|
|
2706
|
-
);
|
|
2707
|
-
return { component: selector, focus: selector };
|
|
2708
|
-
});
|
|
656
|
+
stopVoiceProgressTimer(): void {
|
|
657
|
+
this.voiceManager.stopVoiceProgressTimer();
|
|
2709
658
|
}
|
|
2710
659
|
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
if (userMessages.length === 0) {
|
|
2715
|
-
this.showStatus("No messages to branch from");
|
|
2716
|
-
return;
|
|
2717
|
-
}
|
|
2718
|
-
|
|
2719
|
-
this.showSelector((done) => {
|
|
2720
|
-
const selector = new UserMessageSelectorComponent(
|
|
2721
|
-
userMessages.map((m) => ({ id: m.entryId, text: m.text })),
|
|
2722
|
-
async (entryId) => {
|
|
2723
|
-
const result = await this.session.branch(entryId);
|
|
2724
|
-
if (result.cancelled) {
|
|
2725
|
-
// Hook cancelled the branch
|
|
2726
|
-
done();
|
|
2727
|
-
this.ui.requestRender();
|
|
2728
|
-
return;
|
|
2729
|
-
}
|
|
2730
|
-
|
|
2731
|
-
this.chatContainer.clear();
|
|
2732
|
-
this.renderInitialMessages();
|
|
2733
|
-
this.editor.setText(result.selectedText);
|
|
2734
|
-
done();
|
|
2735
|
-
this.showStatus("Branched to new session");
|
|
2736
|
-
},
|
|
2737
|
-
() => {
|
|
2738
|
-
done();
|
|
2739
|
-
this.ui.requestRender();
|
|
2740
|
-
},
|
|
2741
|
-
);
|
|
2742
|
-
return { component: selector, focus: selector.getMessageList() };
|
|
2743
|
-
});
|
|
660
|
+
maybeSpeakProgress(): Promise<void> {
|
|
661
|
+
return this.voiceManager.maybeSpeakProgress();
|
|
2744
662
|
}
|
|
2745
663
|
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
const realLeafId = this.sessionManager.getLeafId();
|
|
2749
|
-
|
|
2750
|
-
// Find the visible leaf for display (skip metadata entries like labels)
|
|
2751
|
-
let visibleLeafId = realLeafId;
|
|
2752
|
-
while (visibleLeafId) {
|
|
2753
|
-
const entry = this.sessionManager.getEntry(visibleLeafId);
|
|
2754
|
-
if (!entry) break;
|
|
2755
|
-
if (entry.type !== "label" && entry.type !== "custom") break;
|
|
2756
|
-
visibleLeafId = entry.parentId ?? null;
|
|
2757
|
-
}
|
|
2758
|
-
|
|
2759
|
-
if (tree.length === 0) {
|
|
2760
|
-
this.showStatus("No entries in session");
|
|
2761
|
-
return;
|
|
2762
|
-
}
|
|
2763
|
-
|
|
2764
|
-
this.showSelector((done) => {
|
|
2765
|
-
const selector = new TreeSelectorComponent(
|
|
2766
|
-
tree,
|
|
2767
|
-
visibleLeafId,
|
|
2768
|
-
this.ui.terminal.rows,
|
|
2769
|
-
async (entryId) => {
|
|
2770
|
-
// Selecting the visible leaf is a no-op (already there)
|
|
2771
|
-
if (entryId === visibleLeafId) {
|
|
2772
|
-
done();
|
|
2773
|
-
this.showStatus("Already at this point");
|
|
2774
|
-
return;
|
|
2775
|
-
}
|
|
2776
|
-
|
|
2777
|
-
// Ask about summarization (or skip if disabled in settings)
|
|
2778
|
-
done(); // Close selector first
|
|
2779
|
-
|
|
2780
|
-
const branchSummariesEnabled = this.settingsManager.getBranchSummaryEnabled();
|
|
2781
|
-
const wantsSummary = branchSummariesEnabled
|
|
2782
|
-
? await this.showHookConfirm("Summarize branch?", "Create a summary of the branch you're leaving?")
|
|
2783
|
-
: false;
|
|
2784
|
-
|
|
2785
|
-
// Set up escape handler and loader if summarizing
|
|
2786
|
-
let summaryLoader: Loader | undefined;
|
|
2787
|
-
const originalOnEscape = this.editor.onEscape;
|
|
2788
|
-
|
|
2789
|
-
if (wantsSummary) {
|
|
2790
|
-
this.editor.onEscape = () => {
|
|
2791
|
-
this.session.abortBranchSummary();
|
|
2792
|
-
};
|
|
2793
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
2794
|
-
summaryLoader = new Loader(
|
|
2795
|
-
this.ui,
|
|
2796
|
-
(spinner) => theme.fg("accent", spinner),
|
|
2797
|
-
(text) => theme.fg("muted", text),
|
|
2798
|
-
"Summarizing branch... (esc to cancel)",
|
|
2799
|
-
getSymbolTheme().spinnerFrames,
|
|
2800
|
-
);
|
|
2801
|
-
this.statusContainer.addChild(summaryLoader);
|
|
2802
|
-
this.ui.requestRender();
|
|
2803
|
-
}
|
|
2804
|
-
|
|
2805
|
-
try {
|
|
2806
|
-
const result = await this.session.navigateTree(entryId, { summarize: wantsSummary });
|
|
2807
|
-
|
|
2808
|
-
if (result.aborted) {
|
|
2809
|
-
// Summarization aborted - re-show tree selector
|
|
2810
|
-
this.showStatus("Branch summarization cancelled");
|
|
2811
|
-
this.showTreeSelector();
|
|
2812
|
-
return;
|
|
2813
|
-
}
|
|
2814
|
-
if (result.cancelled) {
|
|
2815
|
-
this.showStatus("Navigation cancelled");
|
|
2816
|
-
return;
|
|
2817
|
-
}
|
|
2818
|
-
|
|
2819
|
-
// Update UI
|
|
2820
|
-
this.chatContainer.clear();
|
|
2821
|
-
this.renderInitialMessages();
|
|
2822
|
-
if (result.editorText) {
|
|
2823
|
-
this.editor.setText(result.editorText);
|
|
2824
|
-
}
|
|
2825
|
-
this.showStatus("Navigated to selected point");
|
|
2826
|
-
} catch (error) {
|
|
2827
|
-
this.showError(error instanceof Error ? error.message : String(error));
|
|
2828
|
-
} finally {
|
|
2829
|
-
if (summaryLoader) {
|
|
2830
|
-
summaryLoader.stop();
|
|
2831
|
-
this.statusContainer.clear();
|
|
2832
|
-
}
|
|
2833
|
-
this.editor.onEscape = originalOnEscape;
|
|
2834
|
-
}
|
|
2835
|
-
},
|
|
2836
|
-
() => {
|
|
2837
|
-
done();
|
|
2838
|
-
this.ui.requestRender();
|
|
2839
|
-
},
|
|
2840
|
-
(entryId, label) => {
|
|
2841
|
-
this.sessionManager.appendLabelChange(entryId, label);
|
|
2842
|
-
this.ui.requestRender();
|
|
2843
|
-
},
|
|
2844
|
-
);
|
|
2845
|
-
return { component: selector, focus: selector };
|
|
2846
|
-
});
|
|
664
|
+
submitVoiceText(text: string): Promise<void> {
|
|
665
|
+
return this.voiceManager.submitVoiceText(text);
|
|
2847
666
|
}
|
|
2848
667
|
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
const selector = new SessionSelectorComponent(
|
|
2853
|
-
sessions,
|
|
2854
|
-
async (sessionPath) => {
|
|
2855
|
-
done();
|
|
2856
|
-
await this.handleResumeSession(sessionPath);
|
|
2857
|
-
},
|
|
2858
|
-
() => {
|
|
2859
|
-
done();
|
|
2860
|
-
this.ui.requestRender();
|
|
2861
|
-
},
|
|
2862
|
-
() => {
|
|
2863
|
-
void this.shutdown();
|
|
2864
|
-
},
|
|
2865
|
-
);
|
|
2866
|
-
return { component: selector, focus: selector.getSessionList() };
|
|
2867
|
-
});
|
|
668
|
+
// Hook UI methods
|
|
669
|
+
initHooksAndCustomTools(): Promise<void> {
|
|
670
|
+
return this.extensionUiController.initHooksAndCustomTools();
|
|
2868
671
|
}
|
|
2869
672
|
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
}
|
|
2876
|
-
this.statusContainer.clear();
|
|
2877
|
-
|
|
2878
|
-
// Clear UI state
|
|
2879
|
-
this.pendingMessagesContainer.clear();
|
|
2880
|
-
this.compactionQueuedMessages = [];
|
|
2881
|
-
this.streamingComponent = undefined;
|
|
2882
|
-
this.streamingMessage = undefined;
|
|
2883
|
-
this.pendingTools.clear();
|
|
2884
|
-
|
|
2885
|
-
// Switch session via AgentSession (emits hook and tool session events)
|
|
2886
|
-
await this.session.switchSession(sessionPath);
|
|
2887
|
-
|
|
2888
|
-
// Clear and re-render the chat
|
|
2889
|
-
this.chatContainer.clear();
|
|
2890
|
-
this.renderInitialMessages();
|
|
2891
|
-
this.showStatus("Resumed session");
|
|
673
|
+
emitCustomToolSessionEvent(
|
|
674
|
+
reason: "start" | "switch" | "branch" | "tree" | "shutdown",
|
|
675
|
+
previousSessionFile?: string,
|
|
676
|
+
): Promise<void> {
|
|
677
|
+
return this.extensionUiController.emitCustomToolSessionEvent(reason, previousSessionFile);
|
|
2892
678
|
}
|
|
2893
679
|
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
const providers = this.session.modelRegistry.authStorage.list();
|
|
2897
|
-
const loggedInProviders = providers.filter((p) => this.session.modelRegistry.authStorage.hasOAuth(p));
|
|
2898
|
-
if (loggedInProviders.length === 0) {
|
|
2899
|
-
this.showStatus("No OAuth providers logged in. Use /login first.");
|
|
2900
|
-
return;
|
|
2901
|
-
}
|
|
2902
|
-
}
|
|
2903
|
-
|
|
2904
|
-
this.showSelector((done) => {
|
|
2905
|
-
const selector = new OAuthSelectorComponent(
|
|
2906
|
-
mode,
|
|
2907
|
-
this.session.modelRegistry.authStorage,
|
|
2908
|
-
async (providerId: string) => {
|
|
2909
|
-
done();
|
|
2910
|
-
|
|
2911
|
-
if (mode === "login") {
|
|
2912
|
-
this.showStatus(`Logging in to ${providerId}...`);
|
|
2913
|
-
|
|
2914
|
-
try {
|
|
2915
|
-
await this.session.modelRegistry.authStorage.login(providerId as OAuthProvider, {
|
|
2916
|
-
onAuth: (info: { url: string; instructions?: string }) => {
|
|
2917
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
2918
|
-
this.chatContainer.addChild(new Text(theme.fg("dim", info.url), 1, 0));
|
|
2919
|
-
// Use OSC 8 hyperlink escape sequence for clickable link
|
|
2920
|
-
const hyperlink = `\x1b]8;;${info.url}\x07Click here to login\x1b]8;;\x07`;
|
|
2921
|
-
this.chatContainer.addChild(new Text(theme.fg("accent", hyperlink), 1, 0));
|
|
2922
|
-
if (info.instructions) {
|
|
2923
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
2924
|
-
this.chatContainer.addChild(new Text(theme.fg("warning", info.instructions), 1, 0));
|
|
2925
|
-
}
|
|
2926
|
-
this.ui.requestRender();
|
|
2927
|
-
|
|
2928
|
-
this.openInBrowser(info.url);
|
|
2929
|
-
},
|
|
2930
|
-
onPrompt: async (prompt: { message: string; placeholder?: string }) => {
|
|
2931
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
2932
|
-
this.chatContainer.addChild(new Text(theme.fg("warning", prompt.message), 1, 0));
|
|
2933
|
-
if (prompt.placeholder) {
|
|
2934
|
-
this.chatContainer.addChild(new Text(theme.fg("dim", prompt.placeholder), 1, 0));
|
|
2935
|
-
}
|
|
2936
|
-
this.ui.requestRender();
|
|
2937
|
-
|
|
2938
|
-
return new Promise<string>((resolve) => {
|
|
2939
|
-
const codeInput = new Input();
|
|
2940
|
-
codeInput.onSubmit = () => {
|
|
2941
|
-
const code = codeInput.getValue();
|
|
2942
|
-
this.editorContainer.clear();
|
|
2943
|
-
this.editorContainer.addChild(this.editor);
|
|
2944
|
-
this.ui.setFocus(this.editor);
|
|
2945
|
-
resolve(code);
|
|
2946
|
-
};
|
|
2947
|
-
this.editorContainer.clear();
|
|
2948
|
-
this.editorContainer.addChild(codeInput);
|
|
2949
|
-
this.ui.setFocus(codeInput);
|
|
2950
|
-
this.ui.requestRender();
|
|
2951
|
-
});
|
|
2952
|
-
},
|
|
2953
|
-
onProgress: (message: string) => {
|
|
2954
|
-
this.chatContainer.addChild(new Text(theme.fg("dim", message), 1, 0));
|
|
2955
|
-
this.ui.requestRender();
|
|
2956
|
-
},
|
|
2957
|
-
});
|
|
2958
|
-
// Refresh models to pick up new baseUrl (e.g., github-copilot)
|
|
2959
|
-
await this.session.modelRegistry.refresh();
|
|
2960
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
2961
|
-
this.chatContainer.addChild(
|
|
2962
|
-
new Text(
|
|
2963
|
-
theme.fg("success", `${theme.status.success} Successfully logged in to ${providerId}`),
|
|
2964
|
-
1,
|
|
2965
|
-
0,
|
|
2966
|
-
),
|
|
2967
|
-
);
|
|
2968
|
-
this.chatContainer.addChild(
|
|
2969
|
-
new Text(theme.fg("dim", `Credentials saved to ${getAuthPath()}`), 1, 0),
|
|
2970
|
-
);
|
|
2971
|
-
this.ui.requestRender();
|
|
2972
|
-
} catch (error: unknown) {
|
|
2973
|
-
this.showError(`Login failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2974
|
-
}
|
|
2975
|
-
} else {
|
|
2976
|
-
try {
|
|
2977
|
-
await this.session.modelRegistry.authStorage.logout(providerId);
|
|
2978
|
-
// Refresh models to reset baseUrl
|
|
2979
|
-
await this.session.modelRegistry.refresh();
|
|
2980
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
2981
|
-
this.chatContainer.addChild(
|
|
2982
|
-
new Text(
|
|
2983
|
-
theme.fg("success", `${theme.status.success} Successfully logged out of ${providerId}`),
|
|
2984
|
-
1,
|
|
2985
|
-
0,
|
|
2986
|
-
),
|
|
2987
|
-
);
|
|
2988
|
-
this.chatContainer.addChild(
|
|
2989
|
-
new Text(theme.fg("dim", `Credentials removed from ${getAuthPath()}`), 1, 0),
|
|
2990
|
-
);
|
|
2991
|
-
this.ui.requestRender();
|
|
2992
|
-
} catch (error: unknown) {
|
|
2993
|
-
this.showError(`Logout failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2994
|
-
}
|
|
2995
|
-
}
|
|
2996
|
-
},
|
|
2997
|
-
() => {
|
|
2998
|
-
done();
|
|
2999
|
-
this.ui.requestRender();
|
|
3000
|
-
},
|
|
3001
|
-
);
|
|
3002
|
-
return { component: selector, focus: selector };
|
|
3003
|
-
});
|
|
680
|
+
setHookWidget(key: string, content: unknown): void {
|
|
681
|
+
this.extensionUiController.setHookWidget(key, content);
|
|
3004
682
|
}
|
|
3005
683
|
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
// =========================================================================
|
|
3009
|
-
|
|
3010
|
-
private openInBrowser(urlOrPath: string): void {
|
|
3011
|
-
try {
|
|
3012
|
-
const args =
|
|
3013
|
-
process.platform === "darwin"
|
|
3014
|
-
? ["open", urlOrPath]
|
|
3015
|
-
: process.platform === "win32"
|
|
3016
|
-
? ["cmd", "/c", "start", "", urlOrPath]
|
|
3017
|
-
: ["xdg-open", urlOrPath];
|
|
3018
|
-
Bun.spawn(args, { stdin: "ignore", stdout: "ignore", stderr: "ignore" });
|
|
3019
|
-
} catch {
|
|
3020
|
-
// Best-effort: browser opening is non-critical
|
|
3021
|
-
}
|
|
684
|
+
setHookStatus(key: string, text: string | undefined): void {
|
|
685
|
+
this.extensionUiController.setHookStatus(key, text);
|
|
3022
686
|
}
|
|
3023
687
|
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
const arg = parts.length > 1 ? parts[1] : undefined;
|
|
3027
|
-
|
|
3028
|
-
// Check for clipboard export
|
|
3029
|
-
if (arg === "--copy" || arg === "clipboard" || arg === "copy") {
|
|
3030
|
-
try {
|
|
3031
|
-
const formatted = this.session.formatSessionAsText();
|
|
3032
|
-
if (!formatted) {
|
|
3033
|
-
this.showError("No messages to export yet.");
|
|
3034
|
-
return;
|
|
3035
|
-
}
|
|
3036
|
-
await copyToClipboard(formatted);
|
|
3037
|
-
this.showStatus("Session copied to clipboard");
|
|
3038
|
-
} catch (error: unknown) {
|
|
3039
|
-
this.showError(`Failed to copy session: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
3040
|
-
}
|
|
3041
|
-
return;
|
|
3042
|
-
}
|
|
3043
|
-
|
|
3044
|
-
// HTML file export
|
|
3045
|
-
try {
|
|
3046
|
-
const filePath = await this.session.exportToHtml(arg);
|
|
3047
|
-
this.showStatus(`Session exported to: ${filePath}`);
|
|
3048
|
-
this.openInBrowser(filePath);
|
|
3049
|
-
} catch (error: unknown) {
|
|
3050
|
-
this.showError(`Failed to export session: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
3051
|
-
}
|
|
688
|
+
showHookSelector(title: string, options: string[]): Promise<string | undefined> {
|
|
689
|
+
return this.extensionUiController.showHookSelector(title, options);
|
|
3052
690
|
}
|
|
3053
691
|
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
try {
|
|
3057
|
-
const authResult = Bun.spawnSync(["gh", "auth", "status"]);
|
|
3058
|
-
if (authResult.exitCode !== 0) {
|
|
3059
|
-
this.showError("GitHub CLI is not logged in. Run 'gh auth login' first.");
|
|
3060
|
-
return;
|
|
3061
|
-
}
|
|
3062
|
-
} catch {
|
|
3063
|
-
this.showError("GitHub CLI (gh) is not installed. Install it from https://cli.github.com/");
|
|
3064
|
-
return;
|
|
3065
|
-
}
|
|
3066
|
-
|
|
3067
|
-
// Export to a temp file
|
|
3068
|
-
const tmpFile = path.join(os.tmpdir(), "session.html");
|
|
3069
|
-
try {
|
|
3070
|
-
await this.session.exportToHtml(tmpFile);
|
|
3071
|
-
} catch (error: unknown) {
|
|
3072
|
-
this.showError(`Failed to export session: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
3073
|
-
return;
|
|
3074
|
-
}
|
|
3075
|
-
|
|
3076
|
-
// Show cancellable loader, replacing the editor
|
|
3077
|
-
const loader = new BorderedLoader(this.ui, theme, "Creating gist...");
|
|
3078
|
-
this.editorContainer.clear();
|
|
3079
|
-
this.editorContainer.addChild(loader);
|
|
3080
|
-
this.ui.setFocus(loader);
|
|
3081
|
-
this.ui.requestRender();
|
|
3082
|
-
|
|
3083
|
-
const restoreEditor = () => {
|
|
3084
|
-
loader.dispose();
|
|
3085
|
-
this.editorContainer.clear();
|
|
3086
|
-
this.editorContainer.addChild(this.editor);
|
|
3087
|
-
this.ui.setFocus(this.editor);
|
|
3088
|
-
try {
|
|
3089
|
-
fs.unlinkSync(tmpFile);
|
|
3090
|
-
} catch {
|
|
3091
|
-
// Ignore cleanup errors
|
|
3092
|
-
}
|
|
3093
|
-
};
|
|
3094
|
-
|
|
3095
|
-
// Create a secret gist asynchronously
|
|
3096
|
-
let proc: ReturnType<typeof Bun.spawn> | null = null;
|
|
3097
|
-
|
|
3098
|
-
loader.onAbort = () => {
|
|
3099
|
-
proc?.kill();
|
|
3100
|
-
restoreEditor();
|
|
3101
|
-
this.showStatus("Share cancelled");
|
|
3102
|
-
};
|
|
3103
|
-
|
|
3104
|
-
try {
|
|
3105
|
-
const result = await new Promise<{ stdout: string; stderr: string; code: number | null }>((resolve) => {
|
|
3106
|
-
proc = Bun.spawn(["gh", "gist", "create", "--public=false", tmpFile], {
|
|
3107
|
-
stdout: "pipe",
|
|
3108
|
-
stderr: "pipe",
|
|
3109
|
-
});
|
|
3110
|
-
let stdout = "";
|
|
3111
|
-
let stderr = "";
|
|
3112
|
-
|
|
3113
|
-
const stdoutReader = (proc.stdout as ReadableStream<Uint8Array>).getReader();
|
|
3114
|
-
const stderrReader = (proc.stderr as ReadableStream<Uint8Array>).getReader();
|
|
3115
|
-
const decoder = new TextDecoder();
|
|
3116
|
-
|
|
3117
|
-
(async () => {
|
|
3118
|
-
try {
|
|
3119
|
-
while (true) {
|
|
3120
|
-
const { done, value } = await stdoutReader.read();
|
|
3121
|
-
if (done) break;
|
|
3122
|
-
stdout += decoder.decode(value);
|
|
3123
|
-
}
|
|
3124
|
-
} catch {}
|
|
3125
|
-
})();
|
|
3126
|
-
|
|
3127
|
-
(async () => {
|
|
3128
|
-
try {
|
|
3129
|
-
while (true) {
|
|
3130
|
-
const { done, value } = await stderrReader.read();
|
|
3131
|
-
if (done) break;
|
|
3132
|
-
stderr += decoder.decode(value);
|
|
3133
|
-
}
|
|
3134
|
-
} catch {}
|
|
3135
|
-
})();
|
|
3136
|
-
|
|
3137
|
-
proc.exited.then((code) => resolve({ stdout, stderr, code }));
|
|
3138
|
-
});
|
|
3139
|
-
|
|
3140
|
-
if (loader.signal.aborted) return;
|
|
3141
|
-
|
|
3142
|
-
restoreEditor();
|
|
3143
|
-
|
|
3144
|
-
if (result.code !== 0) {
|
|
3145
|
-
const errorMsg = result.stderr?.trim() || "Unknown error";
|
|
3146
|
-
this.showError(`Failed to create gist: ${errorMsg}`);
|
|
3147
|
-
return;
|
|
3148
|
-
}
|
|
3149
|
-
|
|
3150
|
-
// Extract gist ID from the URL returned by gh
|
|
3151
|
-
// gh returns something like: https://gist.github.com/username/GIST_ID
|
|
3152
|
-
const gistUrl = result.stdout?.trim();
|
|
3153
|
-
const gistId = gistUrl?.split("/").pop();
|
|
3154
|
-
if (!gistId) {
|
|
3155
|
-
this.showError("Failed to parse gist ID from gh output");
|
|
3156
|
-
return;
|
|
3157
|
-
}
|
|
3158
|
-
|
|
3159
|
-
// Create the preview URL
|
|
3160
|
-
const previewUrl = `https://gistpreview.github.io/?${gistId}`;
|
|
3161
|
-
this.showStatus(`Share URL: ${previewUrl}\nGist: ${gistUrl}`);
|
|
3162
|
-
this.openInBrowser(previewUrl);
|
|
3163
|
-
} catch (error: unknown) {
|
|
3164
|
-
if (!loader.signal.aborted) {
|
|
3165
|
-
restoreEditor();
|
|
3166
|
-
this.showError(`Failed to create gist: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
3167
|
-
}
|
|
3168
|
-
}
|
|
692
|
+
hideHookSelector(): void {
|
|
693
|
+
this.extensionUiController.hideHookSelector();
|
|
3169
694
|
}
|
|
3170
695
|
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
if (!text) {
|
|
3174
|
-
this.showError("No agent messages to copy yet.");
|
|
3175
|
-
return;
|
|
3176
|
-
}
|
|
3177
|
-
|
|
3178
|
-
try {
|
|
3179
|
-
await copyToClipboard(text);
|
|
3180
|
-
this.showStatus("Copied last agent message to clipboard");
|
|
3181
|
-
} catch (error) {
|
|
3182
|
-
this.showError(error instanceof Error ? error.message : String(error));
|
|
3183
|
-
}
|
|
696
|
+
showHookInput(title: string, placeholder?: string): Promise<string | undefined> {
|
|
697
|
+
return this.extensionUiController.showHookInput(title, placeholder);
|
|
3184
698
|
}
|
|
3185
699
|
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
let info = `${theme.bold("Session Info")}\n\n`;
|
|
3190
|
-
info += `${theme.fg("dim", "File:")} ${stats.sessionFile ?? "In-memory"}\n`;
|
|
3191
|
-
info += `${theme.fg("dim", "ID:")} ${stats.sessionId}\n\n`;
|
|
3192
|
-
info += `${theme.bold("Messages")}\n`;
|
|
3193
|
-
info += `${theme.fg("dim", "User:")} ${stats.userMessages}\n`;
|
|
3194
|
-
info += `${theme.fg("dim", "Assistant:")} ${stats.assistantMessages}\n`;
|
|
3195
|
-
info += `${theme.fg("dim", "Tool Calls:")} ${stats.toolCalls}\n`;
|
|
3196
|
-
info += `${theme.fg("dim", "Tool Results:")} ${stats.toolResults}\n`;
|
|
3197
|
-
info += `${theme.fg("dim", "Total:")} ${stats.totalMessages}\n\n`;
|
|
3198
|
-
info += `${theme.bold("Tokens")}\n`;
|
|
3199
|
-
info += `${theme.fg("dim", "Input:")} ${stats.tokens.input.toLocaleString()}\n`;
|
|
3200
|
-
info += `${theme.fg("dim", "Output:")} ${stats.tokens.output.toLocaleString()}\n`;
|
|
3201
|
-
if (stats.tokens.cacheRead > 0) {
|
|
3202
|
-
info += `${theme.fg("dim", "Cache Read:")} ${stats.tokens.cacheRead.toLocaleString()}\n`;
|
|
3203
|
-
}
|
|
3204
|
-
if (stats.tokens.cacheWrite > 0) {
|
|
3205
|
-
info += `${theme.fg("dim", "Cache Write:")} ${stats.tokens.cacheWrite.toLocaleString()}\n`;
|
|
3206
|
-
}
|
|
3207
|
-
info += `${theme.fg("dim", "Total:")} ${stats.tokens.total.toLocaleString()}\n`;
|
|
3208
|
-
|
|
3209
|
-
if (stats.cost > 0) {
|
|
3210
|
-
info += `\n${theme.bold("Cost")}\n`;
|
|
3211
|
-
info += `${theme.fg("dim", "Total:")} ${stats.cost.toFixed(4)}`;
|
|
3212
|
-
}
|
|
3213
|
-
|
|
3214
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
3215
|
-
this.chatContainer.addChild(new Text(info, 1, 0));
|
|
3216
|
-
this.ui.requestRender();
|
|
700
|
+
hideHookInput(): void {
|
|
701
|
+
this.extensionUiController.hideHookInput();
|
|
3217
702
|
}
|
|
3218
703
|
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
const allEntries = parseChangelog(changelogPath);
|
|
3222
|
-
|
|
3223
|
-
const changelogMarkdown =
|
|
3224
|
-
allEntries.length > 0
|
|
3225
|
-
? allEntries
|
|
3226
|
-
.reverse()
|
|
3227
|
-
.map((e) => e.content)
|
|
3228
|
-
.join("\n\n")
|
|
3229
|
-
: "No changelog entries found.";
|
|
3230
|
-
|
|
3231
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
3232
|
-
this.chatContainer.addChild(new DynamicBorder());
|
|
3233
|
-
this.chatContainer.addChild(new Text(theme.bold(theme.fg("accent", "What's New")), 1, 0));
|
|
3234
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
3235
|
-
this.chatContainer.addChild(new Markdown(changelogMarkdown, 1, 1, getMarkdownTheme()));
|
|
3236
|
-
this.chatContainer.addChild(new DynamicBorder());
|
|
3237
|
-
this.ui.requestRender();
|
|
704
|
+
showHookEditor(title: string, prefill?: string): Promise<string | undefined> {
|
|
705
|
+
return this.extensionUiController.showHookEditor(title, prefill);
|
|
3238
706
|
}
|
|
3239
707
|
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
*/
|
|
3243
|
-
private registerExtensionShortcuts(): void {
|
|
3244
|
-
const runner = this.session.extensionRunner;
|
|
3245
|
-
if (!runner) return;
|
|
3246
|
-
|
|
3247
|
-
const shortcuts = runner.getShortcuts();
|
|
3248
|
-
for (const [keyId, shortcut] of shortcuts) {
|
|
3249
|
-
this.editor.setCustomKeyHandler(keyId, () => {
|
|
3250
|
-
const ctx = runner.createCommandContext();
|
|
3251
|
-
try {
|
|
3252
|
-
shortcut.handler(ctx);
|
|
3253
|
-
} catch (err) {
|
|
3254
|
-
runner.emitError({
|
|
3255
|
-
extensionPath: shortcut.extensionPath,
|
|
3256
|
-
event: "shortcut",
|
|
3257
|
-
error: err instanceof Error ? err.message : String(err),
|
|
3258
|
-
stack: err instanceof Error ? err.stack : undefined,
|
|
3259
|
-
});
|
|
3260
|
-
}
|
|
3261
|
-
});
|
|
3262
|
-
}
|
|
708
|
+
hideHookEditor(): void {
|
|
709
|
+
this.extensionUiController.hideHookEditor();
|
|
3263
710
|
}
|
|
3264
711
|
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
**Navigation**
|
|
3268
|
-
| Key | Action |
|
|
3269
|
-
|-----|--------|
|
|
3270
|
-
| \`Arrow keys\` | Move cursor / browse history (Up when empty) |
|
|
3271
|
-
| \`Option+Left/Right\` | Move by word |
|
|
3272
|
-
| \`Ctrl+A\` / \`Home\` / \`Cmd+Left\` | Start of line |
|
|
3273
|
-
| \`Ctrl+E\` / \`End\` / \`Cmd+Right\` | End of line |
|
|
3274
|
-
|
|
3275
|
-
**Editing**
|
|
3276
|
-
| Key | Action |
|
|
3277
|
-
|-----|--------|
|
|
3278
|
-
| \`Enter\` | Send message |
|
|
3279
|
-
| \`Shift+Enter\` / \`Alt+Enter\` | New line |
|
|
3280
|
-
| \`Ctrl+W\` / \`Option+Backspace\` | Delete word backwards |
|
|
3281
|
-
| \`Ctrl+U\` | Delete to start of line |
|
|
3282
|
-
| \`Ctrl+K\` | Delete to end of line |
|
|
3283
|
-
|
|
3284
|
-
**Other**
|
|
3285
|
-
| Key | Action |
|
|
3286
|
-
|-----|--------|
|
|
3287
|
-
| \`Tab\` | Path completion / accept autocomplete |
|
|
3288
|
-
| \`Escape\` | Cancel autocomplete / abort streaming |
|
|
3289
|
-
| \`Ctrl+C\` | Clear editor (first) / exit (second) |
|
|
3290
|
-
| \`Ctrl+D\` | Exit (when editor is empty) |
|
|
3291
|
-
| \`Ctrl+Z\` | Suspend to background |
|
|
3292
|
-
| \`Shift+Tab\` | Cycle thinking level |
|
|
3293
|
-
| \`Ctrl+P\` | Cycle role models (slow/default/smol) |
|
|
3294
|
-
| \`Shift+Ctrl+P\` | Cycle role models (temporary) |
|
|
3295
|
-
| \`Ctrl+Y\` | Select model (temporary) |
|
|
3296
|
-
| \`Ctrl+L\` | Select model (set roles) |
|
|
3297
|
-
| \`Ctrl+R\` | Search prompt history |
|
|
3298
|
-
| \`Ctrl+O\` | Toggle tool output expansion |
|
|
3299
|
-
| \`Ctrl+T\` | Toggle thinking block visibility |
|
|
3300
|
-
| \`Ctrl+G\` | Edit message in external editor |
|
|
3301
|
-
| \`/\` | Slash commands |
|
|
3302
|
-
| \`!\` | Run bash command |
|
|
3303
|
-
| \`!!\` | Run bash command (excluded from context) |
|
|
3304
|
-
`;
|
|
3305
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
3306
|
-
this.chatContainer.addChild(new DynamicBorder());
|
|
3307
|
-
this.chatContainer.addChild(new Text(theme.bold(theme.fg("accent", "Keyboard Shortcuts")), 1, 0));
|
|
3308
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
3309
|
-
this.chatContainer.addChild(new Markdown(hotkeys.trim(), 1, 1, getMarkdownTheme()));
|
|
3310
|
-
this.chatContainer.addChild(new DynamicBorder());
|
|
3311
|
-
this.ui.requestRender();
|
|
712
|
+
showHookNotify(message: string, type?: "info" | "warning" | "error"): void {
|
|
713
|
+
this.extensionUiController.showHookNotify(message, type);
|
|
3312
714
|
}
|
|
3313
715
|
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
await this.session.newSession();
|
|
3324
|
-
|
|
3325
|
-
// Update status line (token counts, cost reset)
|
|
3326
|
-
this.statusLine.invalidate();
|
|
3327
|
-
this.updateEditorTopBorder();
|
|
3328
|
-
|
|
3329
|
-
// Clear UI state
|
|
3330
|
-
this.chatContainer.clear();
|
|
3331
|
-
this.pendingMessagesContainer.clear();
|
|
3332
|
-
this.compactionQueuedMessages = [];
|
|
3333
|
-
this.streamingComponent = undefined;
|
|
3334
|
-
this.streamingMessage = undefined;
|
|
3335
|
-
this.pendingTools.clear();
|
|
3336
|
-
|
|
3337
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
3338
|
-
this.chatContainer.addChild(
|
|
3339
|
-
new Text(`${theme.fg("accent", `${theme.status.success} New session started`)}`, 1, 1),
|
|
3340
|
-
);
|
|
3341
|
-
this.ui.requestRender();
|
|
3342
|
-
}
|
|
3343
|
-
|
|
3344
|
-
private handleDebugCommand(): void {
|
|
3345
|
-
const width = this.ui.terminal.columns;
|
|
3346
|
-
const allLines = this.ui.render(width);
|
|
3347
|
-
|
|
3348
|
-
const debugLogPath = getDebugLogPath();
|
|
3349
|
-
const debugData = [
|
|
3350
|
-
`Debug output at ${new Date().toISOString()}`,
|
|
3351
|
-
`Terminal width: ${width}`,
|
|
3352
|
-
`Total lines: ${allLines.length}`,
|
|
3353
|
-
"",
|
|
3354
|
-
"=== All rendered lines with visible widths ===",
|
|
3355
|
-
...allLines.map((line, idx) => {
|
|
3356
|
-
const vw = visibleWidth(line);
|
|
3357
|
-
const escaped = JSON.stringify(line);
|
|
3358
|
-
return `[${idx}] (w=${vw}) ${escaped}`;
|
|
3359
|
-
}),
|
|
3360
|
-
"",
|
|
3361
|
-
"=== Agent messages (JSONL) ===",
|
|
3362
|
-
...this.session.messages.map((msg) => JSON.stringify(msg)),
|
|
3363
|
-
"",
|
|
3364
|
-
].join("\n");
|
|
3365
|
-
|
|
3366
|
-
fs.mkdirSync(path.dirname(debugLogPath), { recursive: true });
|
|
3367
|
-
fs.writeFileSync(debugLogPath, debugData);
|
|
3368
|
-
|
|
3369
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
3370
|
-
this.chatContainer.addChild(
|
|
3371
|
-
new Text(
|
|
3372
|
-
`${theme.fg("accent", `${theme.status.success} Debug log written`)}\n${theme.fg("muted", debugLogPath)}`,
|
|
3373
|
-
1,
|
|
3374
|
-
1,
|
|
3375
|
-
),
|
|
3376
|
-
);
|
|
3377
|
-
this.ui.requestRender();
|
|
3378
|
-
}
|
|
3379
|
-
|
|
3380
|
-
private handleArminSaysHi(): void {
|
|
3381
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
3382
|
-
this.chatContainer.addChild(new ArminComponent(this.ui));
|
|
3383
|
-
this.ui.requestRender();
|
|
3384
|
-
}
|
|
3385
|
-
|
|
3386
|
-
private async handleBashCommand(command: string, excludeFromContext = false): Promise<void> {
|
|
3387
|
-
const isDeferred = this.session.isStreaming;
|
|
3388
|
-
this.bashComponent = new BashExecutionComponent(command, this.ui, excludeFromContext);
|
|
3389
|
-
|
|
3390
|
-
if (isDeferred) {
|
|
3391
|
-
// Show in pending area when agent is streaming
|
|
3392
|
-
this.pendingMessagesContainer.addChild(this.bashComponent);
|
|
3393
|
-
this.pendingBashComponents.push(this.bashComponent);
|
|
3394
|
-
} else {
|
|
3395
|
-
// Show in chat immediately when agent is idle
|
|
3396
|
-
this.chatContainer.addChild(this.bashComponent);
|
|
3397
|
-
}
|
|
3398
|
-
this.ui.requestRender();
|
|
3399
|
-
|
|
3400
|
-
try {
|
|
3401
|
-
const result = await this.session.executeBash(
|
|
3402
|
-
command,
|
|
3403
|
-
(chunk) => {
|
|
3404
|
-
if (this.bashComponent) {
|
|
3405
|
-
this.bashComponent.appendOutput(chunk);
|
|
3406
|
-
this.ui.requestRender();
|
|
3407
|
-
}
|
|
3408
|
-
},
|
|
3409
|
-
{ excludeFromContext },
|
|
3410
|
-
);
|
|
3411
|
-
|
|
3412
|
-
if (this.bashComponent) {
|
|
3413
|
-
this.bashComponent.setComplete(
|
|
3414
|
-
result.exitCode,
|
|
3415
|
-
result.cancelled,
|
|
3416
|
-
result.truncated ? ({ truncated: true, content: result.output } as TruncationResult) : undefined,
|
|
3417
|
-
result.fullOutputPath,
|
|
3418
|
-
);
|
|
3419
|
-
}
|
|
3420
|
-
} catch (error) {
|
|
3421
|
-
if (this.bashComponent) {
|
|
3422
|
-
this.bashComponent.setComplete(undefined, false);
|
|
3423
|
-
}
|
|
3424
|
-
this.showError(`Bash command failed: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
3425
|
-
}
|
|
3426
|
-
|
|
3427
|
-
this.bashComponent = undefined;
|
|
3428
|
-
this.ui.requestRender();
|
|
716
|
+
showHookCustom<T>(
|
|
717
|
+
factory: (
|
|
718
|
+
tui: TUI,
|
|
719
|
+
theme: Theme,
|
|
720
|
+
keybindings: KeybindingsManager,
|
|
721
|
+
done: (result: T) => void,
|
|
722
|
+
) => (Component & { dispose?(): void }) | Promise<Component & { dispose?(): void }>,
|
|
723
|
+
): Promise<T> {
|
|
724
|
+
return this.extensionUiController.showHookCustom(factory);
|
|
3429
725
|
}
|
|
3430
726
|
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
const messageCount = entries.filter((e) => e.type === "message").length;
|
|
3434
|
-
|
|
3435
|
-
if (messageCount < 2) {
|
|
3436
|
-
this.showWarning("Nothing to compact (no messages yet)");
|
|
3437
|
-
return;
|
|
3438
|
-
}
|
|
3439
|
-
|
|
3440
|
-
await this.executeCompaction(customInstructions, false);
|
|
727
|
+
showExtensionError(extensionPath: string, error: string): void {
|
|
728
|
+
this.extensionUiController.showExtensionError(extensionPath, error);
|
|
3441
729
|
}
|
|
3442
730
|
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
if (this.loadingAnimation) {
|
|
3446
|
-
this.loadingAnimation.stop();
|
|
3447
|
-
this.loadingAnimation = undefined;
|
|
3448
|
-
}
|
|
3449
|
-
this.statusContainer.clear();
|
|
3450
|
-
|
|
3451
|
-
// Set up escape handler during compaction
|
|
3452
|
-
const originalOnEscape = this.editor.onEscape;
|
|
3453
|
-
this.editor.onEscape = () => {
|
|
3454
|
-
this.session.abortCompaction();
|
|
3455
|
-
};
|
|
3456
|
-
|
|
3457
|
-
// Show compacting status
|
|
3458
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
3459
|
-
const label = isAuto ? "Auto-compacting context... (esc to cancel)" : "Compacting context... (esc to cancel)";
|
|
3460
|
-
const compactingLoader = new Loader(
|
|
3461
|
-
this.ui,
|
|
3462
|
-
(spinner) => theme.fg("accent", spinner),
|
|
3463
|
-
(text) => theme.fg("muted", text),
|
|
3464
|
-
label,
|
|
3465
|
-
getSymbolTheme().spinnerFrames,
|
|
3466
|
-
);
|
|
3467
|
-
this.statusContainer.addChild(compactingLoader);
|
|
3468
|
-
this.ui.requestRender();
|
|
3469
|
-
|
|
3470
|
-
try {
|
|
3471
|
-
const result = await this.session.compact(customInstructions);
|
|
3472
|
-
|
|
3473
|
-
// Rebuild UI
|
|
3474
|
-
this.rebuildChatFromMessages();
|
|
3475
|
-
|
|
3476
|
-
// Add compaction component at bottom so user sees it without scrolling
|
|
3477
|
-
const msg = createCompactionSummaryMessage(result.summary, result.tokensBefore, new Date().toISOString());
|
|
3478
|
-
this.addMessageToChat(msg);
|
|
3479
|
-
|
|
3480
|
-
this.statusLine.invalidate();
|
|
3481
|
-
this.updateEditorTopBorder();
|
|
3482
|
-
} catch (error) {
|
|
3483
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
3484
|
-
if (message === "Compaction cancelled" || (error instanceof Error && error.name === "AbortError")) {
|
|
3485
|
-
this.showError("Compaction cancelled");
|
|
3486
|
-
} else {
|
|
3487
|
-
this.showError(`Compaction failed: ${message}`);
|
|
3488
|
-
}
|
|
3489
|
-
} finally {
|
|
3490
|
-
compactingLoader.stop();
|
|
3491
|
-
this.statusContainer.clear();
|
|
3492
|
-
this.editor.onEscape = originalOnEscape;
|
|
3493
|
-
}
|
|
3494
|
-
await this.flushCompactionQueue({ willRetry: false });
|
|
731
|
+
showToolError(toolName: string, error: string): void {
|
|
732
|
+
this.extensionUiController.showToolError(toolName, error);
|
|
3495
733
|
}
|
|
3496
734
|
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
this.loadingAnimation.stop();
|
|
3500
|
-
this.loadingAnimation = undefined;
|
|
3501
|
-
}
|
|
3502
|
-
this.statusLine.dispose();
|
|
3503
|
-
if (this.unsubscribe) {
|
|
3504
|
-
this.unsubscribe();
|
|
3505
|
-
}
|
|
3506
|
-
if (this.cleanupUnsubscribe) {
|
|
3507
|
-
this.cleanupUnsubscribe();
|
|
3508
|
-
}
|
|
3509
|
-
if (this.isInitialized) {
|
|
3510
|
-
this.ui.stop();
|
|
3511
|
-
this.isInitialized = false;
|
|
3512
|
-
}
|
|
735
|
+
private subscribeToAgent(): void {
|
|
736
|
+
this.eventController.subscribeToAgent();
|
|
3513
737
|
}
|
|
3514
738
|
}
|