@oh-my-pi/pi-coding-agent 3.15.0 → 3.20.0
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 +61 -1
- package/docs/extensions.md +1055 -0
- package/docs/rpc.md +69 -13
- package/docs/session-tree-plan.md +1 -1
- package/examples/extensions/README.md +141 -0
- package/examples/extensions/api-demo.ts +87 -0
- package/examples/extensions/chalk-logger.ts +26 -0
- package/examples/extensions/hello.ts +33 -0
- package/examples/extensions/pirate.ts +44 -0
- package/examples/extensions/plan-mode.ts +551 -0
- package/examples/extensions/subagent/agents/reviewer.md +35 -0
- package/examples/extensions/todo.ts +299 -0
- package/examples/extensions/tools.ts +145 -0
- package/examples/extensions/with-deps/index.ts +36 -0
- package/examples/extensions/with-deps/package-lock.json +31 -0
- package/examples/extensions/with-deps/package.json +16 -0
- package/examples/sdk/02-custom-model.ts +3 -3
- package/examples/sdk/05-tools.ts +7 -3
- package/examples/sdk/06-extensions.ts +81 -0
- package/examples/sdk/06-hooks.ts +14 -13
- package/examples/sdk/08-prompt-templates.ts +42 -0
- package/examples/sdk/08-slash-commands.ts +17 -12
- package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
- package/examples/sdk/12-full-control.ts +6 -6
- package/package.json +11 -7
- package/src/capability/extension-module.ts +34 -0
- package/src/cli/args.ts +22 -7
- package/src/cli/file-processor.ts +38 -67
- package/src/cli/list-models.ts +1 -1
- package/src/config.ts +25 -14
- package/src/core/agent-session.ts +505 -242
- package/src/core/auth-storage.ts +33 -21
- package/src/core/compaction/branch-summarization.ts +4 -4
- package/src/core/compaction/compaction.ts +3 -3
- package/src/core/custom-commands/bundled/wt/index.ts +430 -0
- package/src/core/custom-commands/loader.ts +9 -0
- package/src/core/custom-tools/wrapper.ts +5 -0
- package/src/core/event-bus.ts +59 -0
- package/src/core/export-html/vendor/highlight.min.js +1213 -0
- package/src/core/export-html/vendor/marked.min.js +6 -0
- package/src/core/extensions/index.ts +100 -0
- package/src/core/extensions/loader.ts +501 -0
- package/src/core/extensions/runner.ts +477 -0
- package/src/core/extensions/types.ts +712 -0
- package/src/core/extensions/wrapper.ts +147 -0
- package/src/core/hooks/types.ts +2 -2
- package/src/core/index.ts +10 -21
- package/src/core/keybindings.ts +199 -0
- package/src/core/messages.ts +26 -7
- package/src/core/model-registry.ts +123 -46
- package/src/core/model-resolver.ts +7 -5
- package/src/core/prompt-templates.ts +242 -0
- package/src/core/sdk.ts +378 -295
- package/src/core/session-manager.ts +72 -58
- package/src/core/settings-manager.ts +118 -22
- package/src/core/system-prompt.ts +24 -1
- package/src/core/terminal-notify.ts +37 -0
- package/src/core/tools/context.ts +4 -4
- package/src/core/tools/exa/mcp-client.ts +5 -4
- package/src/core/tools/exa/render.ts +176 -131
- package/src/core/tools/gemini-image.ts +361 -0
- package/src/core/tools/git.ts +216 -0
- package/src/core/tools/index.ts +28 -15
- package/src/core/tools/lsp/config.ts +5 -4
- package/src/core/tools/lsp/index.ts +17 -12
- package/src/core/tools/lsp/render.ts +39 -47
- package/src/core/tools/read.ts +66 -29
- package/src/core/tools/render-utils.ts +268 -0
- package/src/core/tools/renderers.ts +243 -225
- package/src/core/tools/task/discovery.ts +2 -2
- package/src/core/tools/task/executor.ts +66 -58
- package/src/core/tools/task/index.ts +29 -10
- package/src/core/tools/task/model-resolver.ts +8 -13
- package/src/core/tools/task/omp-command.ts +24 -0
- package/src/core/tools/task/render.ts +35 -60
- package/src/core/tools/task/types.ts +3 -0
- package/src/core/tools/web-fetch.ts +29 -28
- package/src/core/tools/web-search/index.ts +6 -5
- package/src/core/tools/web-search/providers/exa.ts +6 -5
- package/src/core/tools/web-search/render.ts +66 -111
- package/src/core/voice-controller.ts +135 -0
- package/src/core/voice-supervisor.ts +1003 -0
- package/src/core/voice.ts +308 -0
- package/src/discovery/builtin.ts +75 -1
- package/src/discovery/claude.ts +47 -1
- package/src/discovery/codex.ts +54 -2
- package/src/discovery/gemini.ts +55 -2
- package/src/discovery/helpers.ts +100 -1
- package/src/discovery/index.ts +2 -0
- package/src/index.ts +14 -9
- package/src/lib/worktree/collapse.ts +179 -0
- package/src/lib/worktree/constants.ts +14 -0
- package/src/lib/worktree/errors.ts +23 -0
- package/src/lib/worktree/git.ts +110 -0
- package/src/lib/worktree/index.ts +23 -0
- package/src/lib/worktree/operations.ts +216 -0
- package/src/lib/worktree/session.ts +114 -0
- package/src/lib/worktree/stats.ts +67 -0
- package/src/main.ts +61 -37
- package/src/migrations.ts +37 -7
- package/src/modes/interactive/components/bash-execution.ts +6 -4
- package/src/modes/interactive/components/custom-editor.ts +55 -0
- package/src/modes/interactive/components/custom-message.ts +95 -0
- package/src/modes/interactive/components/extensions/extension-list.ts +5 -0
- package/src/modes/interactive/components/extensions/inspector-panel.ts +18 -12
- package/src/modes/interactive/components/extensions/state-manager.ts +12 -0
- package/src/modes/interactive/components/extensions/types.ts +1 -0
- package/src/modes/interactive/components/footer.ts +324 -0
- package/src/modes/interactive/components/hook-editor.ts +1 -0
- package/src/modes/interactive/components/hook-selector.ts +3 -3
- package/src/modes/interactive/components/model-selector.ts +7 -6
- package/src/modes/interactive/components/oauth-selector.ts +3 -3
- package/src/modes/interactive/components/settings-defs.ts +55 -6
- package/src/modes/interactive/components/status-line/separators.ts +4 -4
- package/src/modes/interactive/components/status-line.ts +45 -35
- package/src/modes/interactive/components/tool-execution.ts +95 -23
- package/src/modes/interactive/interactive-mode.ts +644 -113
- package/src/modes/interactive/theme/defaults/alabaster.json +99 -0
- package/src/modes/interactive/theme/defaults/amethyst.json +103 -0
- package/src/modes/interactive/theme/defaults/anthracite.json +100 -0
- package/src/modes/interactive/theme/defaults/basalt.json +90 -0
- package/src/modes/interactive/theme/defaults/birch.json +101 -0
- package/src/modes/interactive/theme/defaults/dark-abyss.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-aurora.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-cavern.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-copper.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-cosmos.json +96 -0
- package/src/modes/interactive/theme/defaults/dark-eclipse.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-ember.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-equinox.json +96 -0
- package/src/modes/interactive/theme/defaults/dark-lavender.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-lunar.json +95 -0
- package/src/modes/interactive/theme/defaults/dark-midnight.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-nebula.json +96 -0
- package/src/modes/interactive/theme/defaults/dark-rainforest.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-reef.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-sakura.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-slate.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-solstice.json +96 -0
- package/src/modes/interactive/theme/defaults/dark-starfall.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-swamp.json +96 -0
- package/src/modes/interactive/theme/defaults/dark-taiga.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-terminal.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-tundra.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-twilight.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-volcanic.json +97 -0
- package/src/modes/interactive/theme/defaults/graphite.json +99 -0
- package/src/modes/interactive/theme/defaults/index.ts +128 -0
- package/src/modes/interactive/theme/defaults/light-aurora-day.json +97 -0
- package/src/modes/interactive/theme/defaults/light-canyon.json +97 -0
- package/src/modes/interactive/theme/defaults/light-cirrus.json +96 -0
- package/src/modes/interactive/theme/defaults/light-coral.json +94 -0
- package/src/modes/interactive/theme/defaults/light-dawn.json +96 -0
- package/src/modes/interactive/theme/defaults/light-dunes.json +97 -0
- package/src/modes/interactive/theme/defaults/light-eucalyptus.json +94 -0
- package/src/modes/interactive/theme/defaults/light-frost.json +94 -0
- package/src/modes/interactive/theme/defaults/light-glacier.json +97 -0
- package/src/modes/interactive/theme/defaults/light-haze.json +96 -0
- package/src/modes/interactive/theme/defaults/light-honeycomb.json +94 -0
- package/src/modes/interactive/theme/defaults/light-lagoon.json +97 -0
- package/src/modes/interactive/theme/defaults/light-lavender.json +94 -0
- package/src/modes/interactive/theme/defaults/light-meadow.json +97 -0
- package/src/modes/interactive/theme/defaults/light-mint.json +94 -0
- package/src/modes/interactive/theme/defaults/light-opal.json +97 -0
- package/src/modes/interactive/theme/defaults/light-orchard.json +97 -0
- package/src/modes/interactive/theme/defaults/light-paper.json +94 -0
- package/src/modes/interactive/theme/defaults/light-prism.json +96 -0
- package/src/modes/interactive/theme/defaults/light-sand.json +94 -0
- package/src/modes/interactive/theme/defaults/light-savanna.json +97 -0
- package/src/modes/interactive/theme/defaults/light-soleil.json +96 -0
- package/src/modes/interactive/theme/defaults/light-wetland.json +97 -0
- package/src/modes/interactive/theme/defaults/light-zenith.json +95 -0
- package/src/modes/interactive/theme/defaults/limestone.json +100 -0
- package/src/modes/interactive/theme/defaults/mahogany.json +104 -0
- package/src/modes/interactive/theme/defaults/marble.json +99 -0
- package/src/modes/interactive/theme/defaults/obsidian.json +90 -0
- package/src/modes/interactive/theme/defaults/onyx.json +90 -0
- package/src/modes/interactive/theme/defaults/pearl.json +99 -0
- package/src/modes/interactive/theme/defaults/porcelain.json +90 -0
- package/src/modes/interactive/theme/defaults/quartz.json +102 -0
- package/src/modes/interactive/theme/defaults/sandstone.json +101 -0
- package/src/modes/interactive/theme/defaults/titanium.json +89 -0
- package/src/modes/print-mode.ts +14 -72
- package/src/modes/rpc/rpc-client.ts +23 -9
- package/src/modes/rpc/rpc-mode.ts +137 -125
- package/src/modes/rpc/rpc-types.ts +46 -24
- package/src/prompts/task.md +1 -0
- package/src/prompts/tools/gemini-image.md +4 -0
- package/src/prompts/tools/git.md +9 -0
- package/src/prompts/voice-summary.md +12 -0
- package/src/utils/image-convert.ts +26 -0
- package/src/utils/image-resize.ts +215 -0
- package/src/utils/shell-snapshot.ts +22 -20
|
@@ -25,12 +25,14 @@ import {
|
|
|
25
25
|
} from "@oh-my-pi/pi-tui";
|
|
26
26
|
import { getAuthPath, getDebugLogPath } from "../../config";
|
|
27
27
|
import type { AgentSession, AgentSessionEvent } from "../../core/agent-session";
|
|
28
|
-
import type {
|
|
29
|
-
import type
|
|
30
|
-
import { createCompactionSummaryMessage } from "../../core/messages";
|
|
28
|
+
import type { ExtensionUIContext } from "../../core/extensions/index";
|
|
29
|
+
import { type CustomMessage, createCompactionSummaryMessage } from "../../core/messages";
|
|
31
30
|
import { getRecentSessions, type SessionContext, SessionManager } from "../../core/session-manager";
|
|
31
|
+
import { loadSlashCommands } from "../../core/slash-commands";
|
|
32
|
+
import { detectNotificationProtocol, isNotificationSuppressed, sendNotification } from "../../core/terminal-notify";
|
|
32
33
|
import { generateSessionTitle, setTerminalTitle } from "../../core/title-generator";
|
|
33
34
|
import type { TruncationResult } from "../../core/tools/truncate";
|
|
35
|
+
import { VoiceSupervisor } from "../../core/voice-supervisor";
|
|
34
36
|
import { disableProvider, enableProvider } from "../../discovery";
|
|
35
37
|
import { getChangelogPath, parseChangelog } from "../../utils/changelog";
|
|
36
38
|
import { copyToClipboard, readImageFromClipboard } from "../../utils/clipboard";
|
|
@@ -42,11 +44,11 @@ import { BorderedLoader } from "./components/bordered-loader";
|
|
|
42
44
|
import { BranchSummaryMessageComponent } from "./components/branch-summary-message";
|
|
43
45
|
import { CompactionSummaryMessageComponent } from "./components/compaction-summary-message";
|
|
44
46
|
import { CustomEditor } from "./components/custom-editor";
|
|
47
|
+
import { CustomMessageComponent } from "./components/custom-message";
|
|
45
48
|
import { DynamicBorder } from "./components/dynamic-border";
|
|
46
49
|
import { ExtensionDashboard } from "./components/extensions";
|
|
47
50
|
import { HookEditorComponent } from "./components/hook-editor";
|
|
48
51
|
import { HookInputComponent } from "./components/hook-input";
|
|
49
|
-
import { HookMessageComponent } from "./components/hook-message";
|
|
50
52
|
import { HookSelectorComponent } from "./components/hook-selector";
|
|
51
53
|
import { ModelSelectorComponent } from "./components/model-selector";
|
|
52
54
|
import { OAuthSelectorComponent } from "./components/oauth-selector";
|
|
@@ -80,6 +82,10 @@ function isExpandable(obj: unknown): obj is Expandable {
|
|
|
80
82
|
return typeof obj === "object" && obj !== null && "setExpanded" in obj && typeof obj.setExpanded === "function";
|
|
81
83
|
}
|
|
82
84
|
|
|
85
|
+
const VOICE_PROGRESS_DELAY_MS = 15000;
|
|
86
|
+
const VOICE_PROGRESS_MIN_CHARS = 160;
|
|
87
|
+
const VOICE_PROGRESS_DELTA_CHARS = 120;
|
|
88
|
+
|
|
83
89
|
export class InteractiveMode {
|
|
84
90
|
private session: AgentSession;
|
|
85
91
|
private ui: TUI;
|
|
@@ -115,6 +121,9 @@ export class InteractiveMode {
|
|
|
115
121
|
// Thinking block visibility state
|
|
116
122
|
private hideThinkingBlock = false;
|
|
117
123
|
|
|
124
|
+
// Background mode flag (no UI, no interactive prompts)
|
|
125
|
+
private isBackgrounded = false;
|
|
126
|
+
|
|
118
127
|
// Agent subscription unsubscribe function
|
|
119
128
|
private unsubscribe?: () => void;
|
|
120
129
|
|
|
@@ -133,6 +142,14 @@ export class InteractiveMode {
|
|
|
133
142
|
// Track pending images from clipboard paste (attached to next message)
|
|
134
143
|
private pendingImages: ImageContent[] = [];
|
|
135
144
|
|
|
145
|
+
// Voice mode state
|
|
146
|
+
private voiceSupervisor: VoiceSupervisor;
|
|
147
|
+
private voiceAutoModeEnabled = false;
|
|
148
|
+
private voiceProgressTimer: ReturnType<typeof setTimeout> | undefined = undefined;
|
|
149
|
+
private voiceProgressSpoken = false;
|
|
150
|
+
private voiceProgressLastLength = 0;
|
|
151
|
+
private lastVoiceInterruptAt = 0;
|
|
152
|
+
|
|
136
153
|
// Auto-compaction state
|
|
137
154
|
private autoCompactionLoader: Loader | undefined = undefined;
|
|
138
155
|
private autoCompactionEscapeHandler?: () => void;
|
|
@@ -146,9 +163,6 @@ export class InteractiveMode {
|
|
|
146
163
|
private hookInput: HookInputComponent | undefined = undefined;
|
|
147
164
|
private hookEditor: HookEditorComponent | undefined = undefined;
|
|
148
165
|
|
|
149
|
-
// Custom tools for custom rendering
|
|
150
|
-
private customTools: Map<string, LoadedCustomTool>;
|
|
151
|
-
|
|
152
166
|
// Convenience accessors
|
|
153
167
|
private get agent() {
|
|
154
168
|
return this.session.agent;
|
|
@@ -164,8 +178,7 @@ export class InteractiveMode {
|
|
|
164
178
|
session: AgentSession,
|
|
165
179
|
version: string,
|
|
166
180
|
changelogMarkdown: string | undefined = undefined,
|
|
167
|
-
|
|
168
|
-
private setToolUIContext: (uiContext: HookUIContext, hasUI: boolean) => void = () => {},
|
|
181
|
+
private setToolUIContext: (uiContext: ExtensionUIContext, hasUI: boolean) => void = () => {},
|
|
169
182
|
private lspServers:
|
|
170
183
|
| Array<{ name: string; status: "ready" | "error"; fileTypes: string[] }>
|
|
171
184
|
| undefined = undefined,
|
|
@@ -174,16 +187,36 @@ export class InteractiveMode {
|
|
|
174
187
|
this.session = session;
|
|
175
188
|
this.version = version;
|
|
176
189
|
this.changelogMarkdown = changelogMarkdown;
|
|
177
|
-
this.customTools = new Map(customTools.map((ct) => [ct.tool.name, ct]));
|
|
178
190
|
this.ui = new TUI(new ProcessTerminal());
|
|
179
191
|
this.chatContainer = new Container();
|
|
180
192
|
this.pendingMessagesContainer = new Container();
|
|
181
193
|
this.statusContainer = new Container();
|
|
182
194
|
this.editor = new CustomEditor(getEditorTheme());
|
|
195
|
+
this.editor.setUseTerminalCursor(true);
|
|
183
196
|
this.editorContainer = new Container();
|
|
184
197
|
this.editorContainer.addChild(this.editor);
|
|
185
198
|
this.statusLine = new StatusLineComponent(session);
|
|
186
199
|
this.statusLine.setAutoCompactEnabled(session.autoCompactionEnabled);
|
|
200
|
+
this.voiceSupervisor = new VoiceSupervisor(this.session.modelRegistry, {
|
|
201
|
+
onSendToAgent: async (text) => {
|
|
202
|
+
await this.submitVoiceText(text);
|
|
203
|
+
},
|
|
204
|
+
onInterruptAgent: async (reason) => {
|
|
205
|
+
await this.handleVoiceInterrupt(reason);
|
|
206
|
+
},
|
|
207
|
+
onStatus: (status) => {
|
|
208
|
+
this.setVoiceStatus(status);
|
|
209
|
+
},
|
|
210
|
+
onError: (error) => {
|
|
211
|
+
this.showError(error.message);
|
|
212
|
+
this.voiceAutoModeEnabled = false;
|
|
213
|
+
void this.voiceSupervisor.stop();
|
|
214
|
+
this.setVoiceStatus(undefined);
|
|
215
|
+
},
|
|
216
|
+
onWarning: (message) => {
|
|
217
|
+
this.showWarning(message);
|
|
218
|
+
},
|
|
219
|
+
});
|
|
187
220
|
|
|
188
221
|
// Define slash commands for autocomplete
|
|
189
222
|
const slashCommands: SlashCommand[] = [
|
|
@@ -203,6 +236,8 @@ export class InteractiveMode {
|
|
|
203
236
|
{ name: "logout", description: "Logout from OAuth provider" },
|
|
204
237
|
{ name: "new", description: "Start a new session" },
|
|
205
238
|
{ name: "compact", description: "Manually compact the session context" },
|
|
239
|
+
{ name: "background", description: "Detach UI and continue running in background" },
|
|
240
|
+
{ name: "bg", description: "Alias for /background" },
|
|
206
241
|
{ name: "resume", description: "Resume a different session" },
|
|
207
242
|
{ name: "exit", description: "Exit the application" },
|
|
208
243
|
];
|
|
@@ -210,14 +245,15 @@ export class InteractiveMode {
|
|
|
210
245
|
// Load hide thinking block setting
|
|
211
246
|
this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
|
|
212
247
|
|
|
213
|
-
//
|
|
214
|
-
const
|
|
248
|
+
// Load and convert file commands to SlashCommand format
|
|
249
|
+
const fileCommands = loadSlashCommands({ cwd: process.cwd() });
|
|
250
|
+
const fileSlashCommands: SlashCommand[] = fileCommands.map((cmd) => ({
|
|
215
251
|
name: cmd.name,
|
|
216
252
|
description: cmd.description,
|
|
217
253
|
}));
|
|
218
254
|
|
|
219
255
|
// Convert hook commands to SlashCommand format
|
|
220
|
-
const hookCommands: SlashCommand[] = (this.session.
|
|
256
|
+
const hookCommands: SlashCommand[] = (this.session.extensionRunner?.getRegisteredCommands() ?? []).map((cmd) => ({
|
|
221
257
|
name: cmd.name,
|
|
222
258
|
description: cmd.description ?? "(hook command)",
|
|
223
259
|
}));
|
|
@@ -307,6 +343,10 @@ export class InteractiveMode {
|
|
|
307
343
|
this.ui.start();
|
|
308
344
|
this.isInitialized = true;
|
|
309
345
|
|
|
346
|
+
// Set terminal title
|
|
347
|
+
const cwdBasename = path.basename(process.cwd());
|
|
348
|
+
this.ui.terminal.setTitle(`pi - ${cwdBasename}`);
|
|
349
|
+
|
|
310
350
|
// Initialize hooks with TUI-based UI context
|
|
311
351
|
await this.initHooksAndCustomTools();
|
|
312
352
|
|
|
@@ -339,12 +379,14 @@ export class InteractiveMode {
|
|
|
339
379
|
*/
|
|
340
380
|
private async initHooksAndCustomTools(): Promise<void> {
|
|
341
381
|
// Create and set hook & tool UI context
|
|
342
|
-
const uiContext:
|
|
382
|
+
const uiContext: ExtensionUIContext = {
|
|
343
383
|
select: (title, options) => this.showHookSelector(title, options),
|
|
344
384
|
confirm: (title, message) => this.showHookConfirm(title, message),
|
|
345
385
|
input: (title, placeholder) => this.showHookInput(title, placeholder),
|
|
346
386
|
notify: (message, type) => this.showHookNotify(message, type),
|
|
347
387
|
setStatus: (key, text) => this.setHookStatus(key, text),
|
|
388
|
+
setWidget: (key, content) => this.setHookWidget(key, content),
|
|
389
|
+
setTitle: (title) => setTerminalTitle(title),
|
|
348
390
|
custom: (factory) => this.showHookCustom(factory),
|
|
349
391
|
setEditorText: (text) => this.editor.setText(text),
|
|
350
392
|
getEditorText: () => this.editor.getText(),
|
|
@@ -355,37 +397,34 @@ export class InteractiveMode {
|
|
|
355
397
|
};
|
|
356
398
|
this.setToolUIContext(uiContext, true);
|
|
357
399
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
reason: "start",
|
|
361
|
-
previousSessionFile: undefined,
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
const hookRunner = this.session.hookRunner;
|
|
365
|
-
if (!hookRunner) {
|
|
400
|
+
const extensionRunner = this.session.extensionRunner;
|
|
401
|
+
if (!extensionRunner) {
|
|
366
402
|
return; // No hooks loaded
|
|
367
403
|
}
|
|
368
404
|
|
|
369
|
-
|
|
405
|
+
extensionRunner.initialize({
|
|
370
406
|
getModel: () => this.session.model,
|
|
371
|
-
sendMessageHandler: (message,
|
|
407
|
+
sendMessageHandler: (message, options) => {
|
|
372
408
|
const wasStreaming = this.session.isStreaming;
|
|
373
409
|
this.session
|
|
374
|
-
.
|
|
410
|
+
.sendCustomMessage(message, options)
|
|
375
411
|
.then(() => {
|
|
376
412
|
// For non-streaming cases with display=true, update UI
|
|
377
413
|
// (streaming cases update via message_end event)
|
|
378
|
-
if (!wasStreaming && message.display) {
|
|
414
|
+
if (!this.isBackgrounded && !wasStreaming && message.display) {
|
|
379
415
|
this.rebuildChatFromMessages();
|
|
380
416
|
}
|
|
381
417
|
})
|
|
382
418
|
.catch((err) => {
|
|
383
|
-
this.showError(`
|
|
419
|
+
this.showError(`Extension sendMessage failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
384
420
|
});
|
|
385
421
|
},
|
|
386
422
|
appendEntryHandler: (customType, data) => {
|
|
387
423
|
this.sessionManager.appendCustomEntry(customType, data);
|
|
388
424
|
},
|
|
425
|
+
getActiveToolsHandler: () => this.session.getActiveToolNames(),
|
|
426
|
+
getAllToolsHandler: () => this.session.getAllToolNames(),
|
|
427
|
+
setActiveToolsHandler: (toolNames: string[]) => this.session.setActiveToolsByName(toolNames),
|
|
389
428
|
newSessionHandler: async (options) => {
|
|
390
429
|
// Stop any loading animation
|
|
391
430
|
if (this.loadingAnimation) {
|
|
@@ -455,41 +494,204 @@ export class InteractiveMode {
|
|
|
455
494
|
abort: () => {
|
|
456
495
|
this.session.abort();
|
|
457
496
|
},
|
|
458
|
-
|
|
497
|
+
hasPendingMessages: () => this.session.queuedMessageCount > 0,
|
|
459
498
|
uiContext,
|
|
460
499
|
hasUI: true,
|
|
461
500
|
});
|
|
462
501
|
|
|
463
|
-
// Subscribe to
|
|
464
|
-
|
|
465
|
-
this.
|
|
502
|
+
// Subscribe to extension errors
|
|
503
|
+
extensionRunner.onError((error) => {
|
|
504
|
+
this.showExtensionError(error.extensionPath, error.error);
|
|
466
505
|
});
|
|
467
506
|
|
|
468
507
|
// Emit session_start event
|
|
469
|
-
await
|
|
508
|
+
await extensionRunner.emit({
|
|
470
509
|
type: "session_start",
|
|
471
510
|
});
|
|
472
511
|
}
|
|
473
512
|
|
|
474
513
|
/**
|
|
475
|
-
*
|
|
514
|
+
* Set extension widget content.
|
|
515
|
+
*/
|
|
516
|
+
private setHookWidget(key: string, content: unknown): void {
|
|
517
|
+
this.statusLine.setHookStatus(key, String(content));
|
|
518
|
+
this.ui.requestRender();
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
private initializeHookRunner(uiContext: ExtensionUIContext, hasUI: boolean): void {
|
|
522
|
+
const extensionRunner = this.session.extensionRunner;
|
|
523
|
+
if (!extensionRunner) {
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
extensionRunner.initialize({
|
|
528
|
+
getModel: () => this.session.model,
|
|
529
|
+
sendMessageHandler: (message, options) => {
|
|
530
|
+
const wasStreaming = this.session.isStreaming;
|
|
531
|
+
this.session
|
|
532
|
+
.sendCustomMessage(message, options)
|
|
533
|
+
.then(() => {
|
|
534
|
+
// For non-streaming cases with display=true, update UI
|
|
535
|
+
// (streaming cases update via message_end event)
|
|
536
|
+
if (!this.isBackgrounded && !wasStreaming && message.display) {
|
|
537
|
+
this.rebuildChatFromMessages();
|
|
538
|
+
}
|
|
539
|
+
})
|
|
540
|
+
.catch((err: Error) => {
|
|
541
|
+
const errorText = `Extension sendMessage failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
542
|
+
if (this.isBackgrounded) {
|
|
543
|
+
console.error(errorText);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
this.showError(errorText);
|
|
547
|
+
});
|
|
548
|
+
},
|
|
549
|
+
appendEntryHandler: (customType, data) => {
|
|
550
|
+
this.sessionManager.appendCustomEntry(customType, data);
|
|
551
|
+
},
|
|
552
|
+
getActiveToolsHandler: () => this.session.getActiveToolNames(),
|
|
553
|
+
getAllToolsHandler: () => this.session.getAllToolNames(),
|
|
554
|
+
setActiveToolsHandler: (toolNames) => this.session.setActiveToolsByName(toolNames),
|
|
555
|
+
newSessionHandler: async (options) => {
|
|
556
|
+
if (this.isBackgrounded) {
|
|
557
|
+
return { cancelled: true };
|
|
558
|
+
}
|
|
559
|
+
// Stop any loading animation
|
|
560
|
+
if (this.loadingAnimation) {
|
|
561
|
+
this.loadingAnimation.stop();
|
|
562
|
+
this.loadingAnimation = undefined;
|
|
563
|
+
}
|
|
564
|
+
this.statusContainer.clear();
|
|
565
|
+
|
|
566
|
+
// Create new session
|
|
567
|
+
const success = await this.session.newSession({ parentSession: options?.parentSession });
|
|
568
|
+
if (!success) {
|
|
569
|
+
return { cancelled: true };
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Call setup callback if provided
|
|
573
|
+
if (options?.setup) {
|
|
574
|
+
await options.setup(this.sessionManager);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Clear UI state
|
|
578
|
+
this.chatContainer.clear();
|
|
579
|
+
this.pendingMessagesContainer.clear();
|
|
580
|
+
this.streamingComponent = undefined;
|
|
581
|
+
this.streamingMessage = undefined;
|
|
582
|
+
this.pendingTools.clear();
|
|
583
|
+
|
|
584
|
+
this.chatContainer.addChild(new Spacer(1));
|
|
585
|
+
this.chatContainer.addChild(
|
|
586
|
+
new Text(`${theme.fg("accent", `${theme.status.success} New session started`)}`, 1, 1),
|
|
587
|
+
);
|
|
588
|
+
this.ui.requestRender();
|
|
589
|
+
|
|
590
|
+
return { cancelled: false };
|
|
591
|
+
},
|
|
592
|
+
branchHandler: async (entryId) => {
|
|
593
|
+
if (this.isBackgrounded) {
|
|
594
|
+
return { cancelled: true };
|
|
595
|
+
}
|
|
596
|
+
const result = await this.session.branch(entryId);
|
|
597
|
+
if (result.cancelled) {
|
|
598
|
+
return { cancelled: true };
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Update UI
|
|
602
|
+
this.chatContainer.clear();
|
|
603
|
+
this.renderInitialMessages();
|
|
604
|
+
this.editor.setText(result.selectedText);
|
|
605
|
+
this.showStatus("Branched to new session");
|
|
606
|
+
|
|
607
|
+
return { cancelled: false };
|
|
608
|
+
},
|
|
609
|
+
navigateTreeHandler: async (targetId, options) => {
|
|
610
|
+
if (this.isBackgrounded) {
|
|
611
|
+
return { cancelled: true };
|
|
612
|
+
}
|
|
613
|
+
const result = await this.session.navigateTree(targetId, { summarize: options?.summarize });
|
|
614
|
+
if (result.cancelled) {
|
|
615
|
+
return { cancelled: true };
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Update UI
|
|
619
|
+
this.chatContainer.clear();
|
|
620
|
+
this.renderInitialMessages();
|
|
621
|
+
if (result.editorText) {
|
|
622
|
+
this.editor.setText(result.editorText);
|
|
623
|
+
}
|
|
624
|
+
this.showStatus("Navigated to selected point");
|
|
625
|
+
|
|
626
|
+
return { cancelled: false };
|
|
627
|
+
},
|
|
628
|
+
isIdle: () => !this.session.isStreaming,
|
|
629
|
+
waitForIdle: () => this.session.agent.waitForIdle(),
|
|
630
|
+
abort: () => {
|
|
631
|
+
this.session.abort();
|
|
632
|
+
},
|
|
633
|
+
hasPendingMessages: () => this.session.queuedMessageCount > 0,
|
|
634
|
+
uiContext,
|
|
635
|
+
hasUI,
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
private createBackgroundUiContext(): ExtensionUIContext {
|
|
640
|
+
return {
|
|
641
|
+
select: async (_title: string, _options: string[]) => undefined,
|
|
642
|
+
confirm: async (_title: string, _message: string) => false,
|
|
643
|
+
input: async (_title: string, _placeholder?: string) => undefined,
|
|
644
|
+
notify: () => {},
|
|
645
|
+
setStatus: () => {},
|
|
646
|
+
setWidget: () => {},
|
|
647
|
+
setTitle: () => {},
|
|
648
|
+
custom: async <T>(
|
|
649
|
+
_factory: (
|
|
650
|
+
tui: TUI,
|
|
651
|
+
theme: Theme,
|
|
652
|
+
done: (result: T) => void,
|
|
653
|
+
) => (Component & { dispose?(): void }) | Promise<Component & { dispose?(): void }>,
|
|
654
|
+
) => undefined as T,
|
|
655
|
+
setEditorText: () => {},
|
|
656
|
+
getEditorText: () => "",
|
|
657
|
+
editor: async () => undefined,
|
|
658
|
+
get theme() {
|
|
659
|
+
return theme;
|
|
660
|
+
},
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Emit session event to all extension tools.
|
|
476
666
|
*/
|
|
477
|
-
private async emitCustomToolSessionEvent(
|
|
478
|
-
|
|
479
|
-
|
|
667
|
+
private async emitCustomToolSessionEvent(
|
|
668
|
+
reason: "start" | "switch" | "branch" | "tree" | "shutdown",
|
|
669
|
+
previousSessionFile?: string,
|
|
670
|
+
): Promise<void> {
|
|
671
|
+
const event = { reason, previousSessionFile };
|
|
672
|
+
const uiContext = this.session.extensionRunner?.getUIContext();
|
|
673
|
+
if (!uiContext) {
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
for (const registeredTool of this.session.extensionRunner?.getAllRegisteredTools() ?? []) {
|
|
677
|
+
if (registeredTool.definition.onSession) {
|
|
480
678
|
try {
|
|
481
|
-
await
|
|
679
|
+
await registeredTool.definition.onSession(event, {
|
|
680
|
+
ui: uiContext,
|
|
681
|
+
hasUI: !this.isBackgrounded,
|
|
682
|
+
cwd: this.sessionManager.getCwd(),
|
|
482
683
|
sessionManager: this.session.sessionManager,
|
|
483
684
|
modelRegistry: this.session.modelRegistry,
|
|
484
685
|
model: this.session.model,
|
|
485
686
|
isIdle: () => !this.session.isStreaming,
|
|
687
|
+
hasPendingMessages: () => this.session.queuedMessageCount > 0,
|
|
486
688
|
hasQueuedMessages: () => this.session.queuedMessageCount > 0,
|
|
487
689
|
abort: () => {
|
|
488
690
|
this.session.abort();
|
|
489
691
|
},
|
|
490
692
|
});
|
|
491
693
|
} catch (err) {
|
|
492
|
-
this.showToolError(
|
|
694
|
+
this.showToolError(registeredTool.definition.name, err instanceof Error ? err.message : String(err));
|
|
493
695
|
}
|
|
494
696
|
}
|
|
495
697
|
}
|
|
@@ -499,6 +701,10 @@ export class InteractiveMode {
|
|
|
499
701
|
* Show a tool error in the chat.
|
|
500
702
|
*/
|
|
501
703
|
private showToolError(toolName: string, error: string): void {
|
|
704
|
+
if (this.isBackgrounded) {
|
|
705
|
+
console.error(`Tool "${toolName}" error: ${error}`);
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
502
708
|
const errorText = new Text(theme.fg("error", `Tool "${toolName}" error: ${error}`), 1, 0);
|
|
503
709
|
this.chatContainer.addChild(errorText);
|
|
504
710
|
this.ui.requestRender();
|
|
@@ -508,6 +714,9 @@ export class InteractiveMode {
|
|
|
508
714
|
* Set hook status text in the footer.
|
|
509
715
|
*/
|
|
510
716
|
private setHookStatus(key: string, text: string | undefined): void {
|
|
717
|
+
if (this.isBackgrounded) {
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
511
720
|
this.statusLine.setHookStatus(key, text);
|
|
512
721
|
this.ui.requestRender();
|
|
513
722
|
}
|
|
@@ -678,10 +887,10 @@ export class InteractiveMode {
|
|
|
678
887
|
}
|
|
679
888
|
|
|
680
889
|
/**
|
|
681
|
-
* Show
|
|
890
|
+
* Show an extension error in the UI.
|
|
682
891
|
*/
|
|
683
|
-
private
|
|
684
|
-
const errorText = new Text(theme.fg("error", `
|
|
892
|
+
private showExtensionError(extensionPath: string, error: string): void {
|
|
893
|
+
const errorText = new Text(theme.fg("error", `Extension "${extensionPath}" error: ${error}`), 1, 0);
|
|
685
894
|
this.chatContainer.addChild(errorText);
|
|
686
895
|
this.ui.requestRender();
|
|
687
896
|
}
|
|
@@ -699,7 +908,7 @@ export class InteractiveMode {
|
|
|
699
908
|
if (this.loadingAnimation) {
|
|
700
909
|
// Abort and restore queued messages to editor
|
|
701
910
|
const queuedMessages = this.session.clearQueue();
|
|
702
|
-
const queuedText = queuedMessages.join("\n\n");
|
|
911
|
+
const queuedText = [...queuedMessages.steering, ...queuedMessages.followUp].join("\n\n");
|
|
703
912
|
const currentText = this.editor.getText();
|
|
704
913
|
const combinedText = [queuedText, currentText].filter((t) => t.trim()).join("\n\n");
|
|
705
914
|
this.editor.setText(combinedText);
|
|
@@ -712,10 +921,14 @@ export class InteractiveMode {
|
|
|
712
921
|
this.isBashMode = false;
|
|
713
922
|
this.updateEditorBorderColor();
|
|
714
923
|
} else if (!this.editor.getText().trim()) {
|
|
715
|
-
// Double-escape with empty editor triggers /branch
|
|
924
|
+
// Double-escape with empty editor triggers /tree or /branch based on setting
|
|
716
925
|
const now = Date.now();
|
|
717
926
|
if (now - this.lastEscapeTime < 500) {
|
|
718
|
-
this.
|
|
927
|
+
if (this.settingsManager.getDoubleEscapeAction() === "tree") {
|
|
928
|
+
this.showTreeSelector();
|
|
929
|
+
} else {
|
|
930
|
+
this.showUserMessageSelector();
|
|
931
|
+
}
|
|
719
932
|
this.lastEscapeTime = 0;
|
|
720
933
|
} else {
|
|
721
934
|
this.lastEscapeTime = now;
|
|
@@ -729,6 +942,7 @@ export class InteractiveMode {
|
|
|
729
942
|
this.editor.onShiftTab = () => this.cycleThinkingLevel();
|
|
730
943
|
this.editor.onCtrlP = () => this.cycleModel("forward");
|
|
731
944
|
this.editor.onShiftCtrlP = () => this.cycleModel("backward");
|
|
945
|
+
this.editor.onCtrlY = () => this.cycleRoleModel();
|
|
732
946
|
|
|
733
947
|
// Global debug handler on TUI (works regardless of focus)
|
|
734
948
|
this.ui.onDebug = () => this.handleDebugCommand();
|
|
@@ -736,9 +950,15 @@ export class InteractiveMode {
|
|
|
736
950
|
this.editor.onCtrlO = () => this.toggleToolOutputExpansion();
|
|
737
951
|
this.editor.onCtrlT = () => this.toggleThinkingBlockVisibility();
|
|
738
952
|
this.editor.onCtrlG = () => this.openExternalEditor();
|
|
953
|
+
this.editor.onCtrlY = () => {
|
|
954
|
+
void this.toggleVoiceListening();
|
|
955
|
+
};
|
|
739
956
|
this.editor.onQuestionMark = () => this.handleHotkeysCommand();
|
|
740
957
|
this.editor.onCtrlV = () => this.handleImagePaste();
|
|
741
958
|
|
|
959
|
+
// Wire up extension shortcuts
|
|
960
|
+
this.registerExtensionShortcuts();
|
|
961
|
+
|
|
742
962
|
this.editor.onChange = (text: string) => {
|
|
743
963
|
const wasBashMode = this.isBashMode;
|
|
744
964
|
this.isBashMode = text.trimStart().startsWith("!");
|
|
@@ -746,6 +966,25 @@ export class InteractiveMode {
|
|
|
746
966
|
this.updateEditorBorderColor();
|
|
747
967
|
}
|
|
748
968
|
};
|
|
969
|
+
|
|
970
|
+
this.editor.onAltEnter = async (text: string) => {
|
|
971
|
+
text = text.trim();
|
|
972
|
+
if (!text) return;
|
|
973
|
+
|
|
974
|
+
// Alt+Enter queues a follow-up message (waits until agent finishes)
|
|
975
|
+
// This handles extension commands (execute immediately), prompt template expansion, and queueing
|
|
976
|
+
if (this.session.isStreaming) {
|
|
977
|
+
this.editor.addToHistory(text);
|
|
978
|
+
this.editor.setText("");
|
|
979
|
+
await this.session.prompt(text, { streamingBehavior: "followUp" });
|
|
980
|
+
this.updatePendingMessagesDisplay();
|
|
981
|
+
this.ui.requestRender();
|
|
982
|
+
}
|
|
983
|
+
// If not streaming, Alt+Enter acts like regular Enter (trigger onSubmit)
|
|
984
|
+
else if (this.editor.onSubmit) {
|
|
985
|
+
this.editor.onSubmit(text);
|
|
986
|
+
}
|
|
987
|
+
};
|
|
749
988
|
}
|
|
750
989
|
|
|
751
990
|
private setupEditorSubmitHandler(): void {
|
|
@@ -800,7 +1039,11 @@ export class InteractiveMode {
|
|
|
800
1039
|
return;
|
|
801
1040
|
}
|
|
802
1041
|
if (text === "/branch") {
|
|
803
|
-
this.
|
|
1042
|
+
if (this.settingsManager.getDoubleEscapeAction() === "tree") {
|
|
1043
|
+
this.showTreeSelector();
|
|
1044
|
+
} else {
|
|
1045
|
+
this.showUserMessageSelector();
|
|
1046
|
+
}
|
|
804
1047
|
this.editor.setText("");
|
|
805
1048
|
return;
|
|
806
1049
|
}
|
|
@@ -835,6 +1078,11 @@ export class InteractiveMode {
|
|
|
835
1078
|
}
|
|
836
1079
|
return;
|
|
837
1080
|
}
|
|
1081
|
+
if (text === "/background" || text === "/bg") {
|
|
1082
|
+
this.editor.setText("");
|
|
1083
|
+
this.handleBackgroundCommand();
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
838
1086
|
if (text === "/debug") {
|
|
839
1087
|
this.handleDebugCommand();
|
|
840
1088
|
this.editor.setText("");
|
|
@@ -856,9 +1104,10 @@ export class InteractiveMode {
|
|
|
856
1104
|
return;
|
|
857
1105
|
}
|
|
858
1106
|
|
|
859
|
-
// Handle bash command
|
|
1107
|
+
// Handle bash command (! for normal, !! for excluded from context)
|
|
860
1108
|
if (text.startsWith("!")) {
|
|
861
|
-
const
|
|
1109
|
+
const isExcluded = text.startsWith("!!");
|
|
1110
|
+
const command = isExcluded ? text.slice(2).trim() : text.slice(1).trim();
|
|
862
1111
|
if (command) {
|
|
863
1112
|
if (this.session.isBashRunning) {
|
|
864
1113
|
this.showWarning("A bash command is already running. Press Esc to cancel it first.");
|
|
@@ -866,7 +1115,7 @@ export class InteractiveMode {
|
|
|
866
1115
|
return;
|
|
867
1116
|
}
|
|
868
1117
|
this.editor.addToHistory(text);
|
|
869
|
-
await this.handleBashCommand(command);
|
|
1118
|
+
await this.handleBashCommand(command, isExcluded);
|
|
870
1119
|
this.isBashMode = false;
|
|
871
1120
|
this.updateEditorBorderColor();
|
|
872
1121
|
return;
|
|
@@ -878,39 +1127,13 @@ export class InteractiveMode {
|
|
|
878
1127
|
return;
|
|
879
1128
|
}
|
|
880
1129
|
|
|
881
|
-
//
|
|
882
|
-
//
|
|
883
|
-
if (text.startsWith("/") && this.session.hookRunner) {
|
|
884
|
-
const spaceIndex = text.indexOf(" ");
|
|
885
|
-
const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
|
|
886
|
-
const command = this.session.hookRunner.getCommand(commandName);
|
|
887
|
-
if (command) {
|
|
888
|
-
this.editor.addToHistory(text);
|
|
889
|
-
this.editor.setText("");
|
|
890
|
-
await this.session.prompt(text);
|
|
891
|
-
return;
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
// Custom commands (TypeScript slash commands) - route through session.prompt()
|
|
896
|
-
if (text.startsWith("/") && this.session.customCommands.length > 0) {
|
|
897
|
-
const spaceIndex = text.indexOf(" ");
|
|
898
|
-
const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
|
|
899
|
-
const hasCustomCommand = this.session.customCommands.some((c) => c.command.name === commandName);
|
|
900
|
-
if (hasCustomCommand) {
|
|
901
|
-
this.editor.addToHistory(text);
|
|
902
|
-
this.editor.setText("");
|
|
903
|
-
await this.session.prompt(text);
|
|
904
|
-
return;
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
// Queue regular messages if agent is streaming
|
|
1130
|
+
// If streaming, use prompt() with steer behavior
|
|
1131
|
+
// This handles extension commands (execute immediately), prompt template expansion, and queueing
|
|
909
1132
|
if (this.session.isStreaming) {
|
|
910
|
-
await this.session.queueMessage(text);
|
|
911
|
-
this.updatePendingMessagesDisplay();
|
|
912
1133
|
this.editor.addToHistory(text);
|
|
913
1134
|
this.editor.setText("");
|
|
1135
|
+
await this.session.prompt(text, { streamingBehavior: "steer" });
|
|
1136
|
+
this.updatePendingMessagesDisplay();
|
|
914
1137
|
this.ui.requestRender();
|
|
915
1138
|
return;
|
|
916
1139
|
}
|
|
@@ -972,11 +1195,12 @@ export class InteractiveMode {
|
|
|
972
1195
|
getSymbolTheme().spinnerFrames,
|
|
973
1196
|
);
|
|
974
1197
|
this.statusContainer.addChild(this.loadingAnimation);
|
|
1198
|
+
this.startVoiceProgressTimer();
|
|
975
1199
|
this.ui.requestRender();
|
|
976
1200
|
break;
|
|
977
1201
|
|
|
978
1202
|
case "message_start":
|
|
979
|
-
if (event.message.role === "hookMessage") {
|
|
1203
|
+
if (event.message.role === "hookMessage" || event.message.role === "custom") {
|
|
980
1204
|
this.addMessageToChat(event.message);
|
|
981
1205
|
this.ui.requestRender();
|
|
982
1206
|
} else if (event.message.role === "user") {
|
|
@@ -1002,14 +1226,16 @@ export class InteractiveMode {
|
|
|
1002
1226
|
if (content.type === "toolCall") {
|
|
1003
1227
|
if (!this.pendingTools.has(content.id)) {
|
|
1004
1228
|
this.chatContainer.addChild(new Text("", 0, 0));
|
|
1229
|
+
const tool = this.session.getToolByName(content.name);
|
|
1005
1230
|
const component = new ToolExecutionComponent(
|
|
1006
1231
|
content.name,
|
|
1007
1232
|
content.arguments,
|
|
1008
1233
|
{
|
|
1009
1234
|
showImages: this.settingsManager.getShowImages(),
|
|
1010
1235
|
},
|
|
1011
|
-
|
|
1236
|
+
tool,
|
|
1012
1237
|
this.ui,
|
|
1238
|
+
this.sessionManager.getCwd(),
|
|
1013
1239
|
);
|
|
1014
1240
|
component.setExpanded(this.toolOutputExpanded);
|
|
1015
1241
|
this.chatContainer.addChild(component);
|
|
@@ -1070,14 +1296,16 @@ export class InteractiveMode {
|
|
|
1070
1296
|
|
|
1071
1297
|
case "tool_execution_start": {
|
|
1072
1298
|
if (!this.pendingTools.has(event.toolCallId)) {
|
|
1299
|
+
const tool = this.session.getToolByName(event.toolName);
|
|
1073
1300
|
const component = new ToolExecutionComponent(
|
|
1074
1301
|
event.toolName,
|
|
1075
1302
|
event.args,
|
|
1076
1303
|
{
|
|
1077
1304
|
showImages: this.settingsManager.getShowImages(),
|
|
1078
1305
|
},
|
|
1079
|
-
|
|
1306
|
+
tool,
|
|
1080
1307
|
this.ui,
|
|
1308
|
+
this.sessionManager.getCwd(),
|
|
1081
1309
|
);
|
|
1082
1310
|
component.setExpanded(this.toolOutputExpanded);
|
|
1083
1311
|
this.chatContainer.addChild(component);
|
|
@@ -1107,6 +1335,7 @@ export class InteractiveMode {
|
|
|
1107
1335
|
}
|
|
1108
1336
|
|
|
1109
1337
|
case "agent_end":
|
|
1338
|
+
this.stopVoiceProgressTimer();
|
|
1110
1339
|
if (this.loadingAnimation) {
|
|
1111
1340
|
this.loadingAnimation.stop();
|
|
1112
1341
|
this.loadingAnimation = undefined;
|
|
@@ -1118,7 +1347,17 @@ export class InteractiveMode {
|
|
|
1118
1347
|
this.streamingMessage = undefined;
|
|
1119
1348
|
}
|
|
1120
1349
|
this.pendingTools.clear();
|
|
1350
|
+
if (this.settingsManager.getVoiceEnabled() && this.voiceAutoModeEnabled) {
|
|
1351
|
+
const lastAssistant = this.findLastAssistantMessage();
|
|
1352
|
+
if (lastAssistant && lastAssistant.stopReason !== "aborted" && lastAssistant.stopReason !== "error") {
|
|
1353
|
+
const text = this.extractAssistantText(lastAssistant);
|
|
1354
|
+
if (text) {
|
|
1355
|
+
this.voiceSupervisor.notifyResult(text);
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1121
1359
|
this.ui.requestRender();
|
|
1360
|
+
this.sendCompletionNotification();
|
|
1122
1361
|
break;
|
|
1123
1362
|
|
|
1124
1363
|
case "auto_compaction_start": {
|
|
@@ -1231,6 +1470,14 @@ export class InteractiveMode {
|
|
|
1231
1470
|
}
|
|
1232
1471
|
}
|
|
1233
1472
|
|
|
1473
|
+
private sendCompletionNotification(): void {
|
|
1474
|
+
if (isNotificationSuppressed()) return;
|
|
1475
|
+
const method = this.settingsManager.getNotificationOnComplete();
|
|
1476
|
+
if (method === "off") return;
|
|
1477
|
+
const protocol = method === "auto" ? detectNotificationProtocol() : method;
|
|
1478
|
+
sendNotification(protocol, "Agent complete");
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1234
1481
|
/** Extract text content from a user message */
|
|
1235
1482
|
private getUserMessageText(message: Message): string {
|
|
1236
1483
|
if (message.role !== "user") return "";
|
|
@@ -1248,6 +1495,9 @@ export class InteractiveMode {
|
|
|
1248
1495
|
* we update the previous status line instead of appending new ones to avoid log spam.
|
|
1249
1496
|
*/
|
|
1250
1497
|
private showStatus(message: string): void {
|
|
1498
|
+
if (this.isBackgrounded) {
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1251
1501
|
const children = this.chatContainer.children;
|
|
1252
1502
|
const last = children.length > 0 ? children[children.length - 1] : undefined;
|
|
1253
1503
|
const secondLast = children.length > 1 ? children[children.length - 2] : undefined;
|
|
@@ -1270,7 +1520,7 @@ export class InteractiveMode {
|
|
|
1270
1520
|
private addMessageToChat(message: AgentMessage, options?: { populateHistory?: boolean }): void {
|
|
1271
1521
|
switch (message.role) {
|
|
1272
1522
|
case "bashExecution": {
|
|
1273
|
-
const component = new BashExecutionComponent(message.command, this.ui);
|
|
1523
|
+
const component = new BashExecutionComponent(message.command, this.ui, message.excludeFromContext);
|
|
1274
1524
|
if (message.output) {
|
|
1275
1525
|
component.appendOutput(message.output);
|
|
1276
1526
|
}
|
|
@@ -1283,10 +1533,12 @@ export class InteractiveMode {
|
|
|
1283
1533
|
this.chatContainer.addChild(component);
|
|
1284
1534
|
break;
|
|
1285
1535
|
}
|
|
1286
|
-
case "hookMessage":
|
|
1536
|
+
case "hookMessage":
|
|
1537
|
+
case "custom": {
|
|
1287
1538
|
if (message.display) {
|
|
1288
|
-
const renderer = this.session.
|
|
1289
|
-
|
|
1539
|
+
const renderer = this.session.extensionRunner?.getMessageRenderer(message.customType);
|
|
1540
|
+
// Both HookMessage and CustomMessage have the same structure, cast for compatibility
|
|
1541
|
+
this.chatContainer.addChild(new CustomMessageComponent(message as CustomMessage<unknown>, renderer));
|
|
1290
1542
|
}
|
|
1291
1543
|
break;
|
|
1292
1544
|
}
|
|
@@ -1362,12 +1614,14 @@ export class InteractiveMode {
|
|
|
1362
1614
|
// Render tool call components
|
|
1363
1615
|
for (const content of message.content) {
|
|
1364
1616
|
if (content.type === "toolCall") {
|
|
1617
|
+
const tool = this.session.getToolByName(content.name);
|
|
1365
1618
|
const component = new ToolExecutionComponent(
|
|
1366
1619
|
content.name,
|
|
1367
1620
|
content.arguments,
|
|
1368
1621
|
{ showImages: this.settingsManager.getShowImages() },
|
|
1369
|
-
|
|
1622
|
+
tool,
|
|
1370
1623
|
this.ui,
|
|
1624
|
+
this.sessionManager.getCwd(),
|
|
1371
1625
|
);
|
|
1372
1626
|
component.setExpanded(this.toolOutputExpanded);
|
|
1373
1627
|
this.chatContainer.addChild(component);
|
|
@@ -1454,18 +1708,13 @@ export class InteractiveMode {
|
|
|
1454
1708
|
* Emits shutdown event to hooks and tools, then exits.
|
|
1455
1709
|
*/
|
|
1456
1710
|
private async shutdown(): Promise<void> {
|
|
1711
|
+
this.voiceAutoModeEnabled = false;
|
|
1712
|
+
await this.voiceSupervisor.stop();
|
|
1713
|
+
|
|
1457
1714
|
// Flush pending session writes before shutdown
|
|
1458
1715
|
await this.sessionManager.flush();
|
|
1459
1716
|
|
|
1460
1717
|
// Emit shutdown event to hooks
|
|
1461
|
-
const hookRunner = this.session.hookRunner;
|
|
1462
|
-
if (hookRunner?.hasHandlers("session_shutdown")) {
|
|
1463
|
-
await hookRunner.emit({
|
|
1464
|
-
type: "session_shutdown",
|
|
1465
|
-
});
|
|
1466
|
-
}
|
|
1467
|
-
|
|
1468
|
-
// Emit shutdown event to custom tools
|
|
1469
1718
|
await this.session.emitCustomToolSessionEvent("shutdown");
|
|
1470
1719
|
|
|
1471
1720
|
this.stop();
|
|
@@ -1486,6 +1735,72 @@ export class InteractiveMode {
|
|
|
1486
1735
|
process.kill(0, "SIGTSTP");
|
|
1487
1736
|
}
|
|
1488
1737
|
|
|
1738
|
+
private handleBackgroundCommand(): void {
|
|
1739
|
+
if (this.isBackgrounded) {
|
|
1740
|
+
this.showStatus("Background mode already enabled");
|
|
1741
|
+
return;
|
|
1742
|
+
}
|
|
1743
|
+
if (!this.session.isStreaming && this.session.queuedMessageCount === 0) {
|
|
1744
|
+
this.showWarning("Agent is idle; nothing to background");
|
|
1745
|
+
return;
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
this.isBackgrounded = true;
|
|
1749
|
+
const backgroundUiContext = this.createBackgroundUiContext();
|
|
1750
|
+
|
|
1751
|
+
// Background mode disables interactive UI so tools like ask fail fast.
|
|
1752
|
+
this.setToolUIContext(backgroundUiContext, false);
|
|
1753
|
+
this.initializeHookRunner(backgroundUiContext, false);
|
|
1754
|
+
|
|
1755
|
+
if (this.loadingAnimation) {
|
|
1756
|
+
this.loadingAnimation.stop();
|
|
1757
|
+
this.loadingAnimation = undefined;
|
|
1758
|
+
}
|
|
1759
|
+
if (this.autoCompactionLoader) {
|
|
1760
|
+
this.autoCompactionLoader.stop();
|
|
1761
|
+
this.autoCompactionLoader = undefined;
|
|
1762
|
+
}
|
|
1763
|
+
if (this.retryLoader) {
|
|
1764
|
+
this.retryLoader.stop();
|
|
1765
|
+
this.retryLoader = undefined;
|
|
1766
|
+
}
|
|
1767
|
+
this.statusContainer.clear();
|
|
1768
|
+
this.statusLine.dispose();
|
|
1769
|
+
|
|
1770
|
+
if (this.unsubscribe) {
|
|
1771
|
+
this.unsubscribe();
|
|
1772
|
+
}
|
|
1773
|
+
this.unsubscribe = this.session.subscribe(async (event) => {
|
|
1774
|
+
await this.handleBackgroundEvent(event);
|
|
1775
|
+
});
|
|
1776
|
+
|
|
1777
|
+
// Backgrounding keeps the current process to preserve in-flight agent state.
|
|
1778
|
+
if (this.isInitialized) {
|
|
1779
|
+
this.ui.stop();
|
|
1780
|
+
this.isInitialized = false;
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
process.stdout.write("Background mode enabled. Run `bg` to continue in background.\n");
|
|
1784
|
+
|
|
1785
|
+
if (process.platform === "win32" || !process.stdout.isTTY) {
|
|
1786
|
+
process.stdout.write("Backgrounding requires POSIX job control; continuing in foreground.\n");
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
process.kill(0, "SIGTSTP");
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
private async handleBackgroundEvent(event: AgentSessionEvent): Promise<void> {
|
|
1794
|
+
if (event.type !== "agent_end") {
|
|
1795
|
+
return;
|
|
1796
|
+
}
|
|
1797
|
+
if (this.session.queuedMessageCount > 0 || this.session.isStreaming) {
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
this.sendCompletionNotification();
|
|
1801
|
+
await this.shutdown();
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1489
1804
|
/**
|
|
1490
1805
|
* Handle Ctrl+V for image paste from clipboard.
|
|
1491
1806
|
* Returns true if an image was found and added, false otherwise.
|
|
@@ -1515,6 +1830,138 @@ export class InteractiveMode {
|
|
|
1515
1830
|
}
|
|
1516
1831
|
}
|
|
1517
1832
|
|
|
1833
|
+
private setVoiceStatus(text: string | undefined): void {
|
|
1834
|
+
this.statusLine.setHookStatus("voice", text);
|
|
1835
|
+
this.ui.requestRender();
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
private async handleVoiceInterrupt(reason?: string): Promise<void> {
|
|
1839
|
+
const now = Date.now();
|
|
1840
|
+
if (now - this.lastVoiceInterruptAt < 200) return;
|
|
1841
|
+
this.lastVoiceInterruptAt = now;
|
|
1842
|
+
if (this.session.isBashRunning) {
|
|
1843
|
+
this.session.abortBash();
|
|
1844
|
+
}
|
|
1845
|
+
if (this.session.isStreaming) {
|
|
1846
|
+
await this.session.abort();
|
|
1847
|
+
}
|
|
1848
|
+
if (reason) {
|
|
1849
|
+
this.showStatus(reason);
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
private stopVoiceProgressTimer(): void {
|
|
1854
|
+
if (this.voiceProgressTimer) {
|
|
1855
|
+
clearTimeout(this.voiceProgressTimer);
|
|
1856
|
+
this.voiceProgressTimer = undefined;
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
private startVoiceProgressTimer(): void {
|
|
1861
|
+
this.stopVoiceProgressTimer();
|
|
1862
|
+
if (!this.settingsManager.getVoiceEnabled() || !this.voiceAutoModeEnabled) return;
|
|
1863
|
+
this.voiceProgressSpoken = false;
|
|
1864
|
+
this.voiceProgressLastLength = 0;
|
|
1865
|
+
this.voiceProgressTimer = setTimeout(() => {
|
|
1866
|
+
void this.maybeSpeakProgress();
|
|
1867
|
+
}, VOICE_PROGRESS_DELAY_MS);
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
private async maybeSpeakProgress(): Promise<void> {
|
|
1871
|
+
if (!this.session.isStreaming || this.voiceProgressSpoken || !this.voiceAutoModeEnabled) return;
|
|
1872
|
+
const streaming = this.streamingMessage;
|
|
1873
|
+
if (!streaming) return;
|
|
1874
|
+
const text = this.extractAssistantText(streaming);
|
|
1875
|
+
if (!text || text.length < VOICE_PROGRESS_MIN_CHARS) {
|
|
1876
|
+
if (this.session.isStreaming) {
|
|
1877
|
+
this.voiceProgressTimer = setTimeout(() => {
|
|
1878
|
+
void this.maybeSpeakProgress();
|
|
1879
|
+
}, VOICE_PROGRESS_DELAY_MS);
|
|
1880
|
+
}
|
|
1881
|
+
return;
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
const delta = text.length - this.voiceProgressLastLength;
|
|
1885
|
+
if (delta < VOICE_PROGRESS_DELTA_CHARS) {
|
|
1886
|
+
if (this.session.isStreaming) {
|
|
1887
|
+
this.voiceProgressTimer = setTimeout(() => {
|
|
1888
|
+
void this.maybeSpeakProgress();
|
|
1889
|
+
}, VOICE_PROGRESS_DELAY_MS);
|
|
1890
|
+
}
|
|
1891
|
+
return;
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
this.voiceProgressLastLength = text.length;
|
|
1895
|
+
this.voiceProgressSpoken = true;
|
|
1896
|
+
this.voiceSupervisor.notifyProgress(text);
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
private async toggleVoiceListening(): Promise<void> {
|
|
1900
|
+
if (!this.settingsManager.getVoiceEnabled()) {
|
|
1901
|
+
this.settingsManager.setVoiceEnabled(true);
|
|
1902
|
+
this.showStatus("Voice mode enabled.");
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
if (this.voiceAutoModeEnabled) {
|
|
1906
|
+
this.voiceAutoModeEnabled = false;
|
|
1907
|
+
this.stopVoiceProgressTimer();
|
|
1908
|
+
await this.voiceSupervisor.stop();
|
|
1909
|
+
this.setVoiceStatus(undefined);
|
|
1910
|
+
this.showStatus("Voice mode disabled.");
|
|
1911
|
+
return;
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
this.voiceAutoModeEnabled = true;
|
|
1915
|
+
try {
|
|
1916
|
+
await this.voiceSupervisor.start();
|
|
1917
|
+
} catch (error) {
|
|
1918
|
+
this.voiceAutoModeEnabled = false;
|
|
1919
|
+
this.setVoiceStatus(undefined);
|
|
1920
|
+
this.showError(error instanceof Error ? error.message : String(error));
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
private async submitVoiceText(text: string): Promise<void> {
|
|
1925
|
+
const cleaned = text.trim();
|
|
1926
|
+
if (!cleaned) {
|
|
1927
|
+
this.showWarning("No speech detected. Try again.");
|
|
1928
|
+
return;
|
|
1929
|
+
}
|
|
1930
|
+
const toSend = cleaned;
|
|
1931
|
+
this.editor.addToHistory(toSend);
|
|
1932
|
+
|
|
1933
|
+
if (this.session.isStreaming) {
|
|
1934
|
+
await this.session.abort();
|
|
1935
|
+
await this.session.steer(toSend);
|
|
1936
|
+
this.updatePendingMessagesDisplay();
|
|
1937
|
+
return;
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
if (this.onInputCallback) {
|
|
1941
|
+
this.onInputCallback({ text: toSend });
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
private findLastAssistantMessage(): AssistantMessage | undefined {
|
|
1946
|
+
for (let i = this.session.messages.length - 1; i >= 0; i--) {
|
|
1947
|
+
const message = this.session.messages[i];
|
|
1948
|
+
if (message?.role === "assistant") {
|
|
1949
|
+
return message as AssistantMessage;
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
return undefined;
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
private extractAssistantText(message: AssistantMessage): string {
|
|
1956
|
+
let text = "";
|
|
1957
|
+
for (const content of message.content) {
|
|
1958
|
+
if (content.type === "text") {
|
|
1959
|
+
text += content.text;
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
return text.trim();
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1518
1965
|
private updateEditorBorderColor(): void {
|
|
1519
1966
|
if (this.isBashMode) {
|
|
1520
1967
|
this.editor.borderColor = theme.getBashModeBorderColor();
|
|
@@ -1561,6 +2008,25 @@ export class InteractiveMode {
|
|
|
1561
2008
|
}
|
|
1562
2009
|
}
|
|
1563
2010
|
|
|
2011
|
+
private async cycleRoleModel(): Promise<void> {
|
|
2012
|
+
try {
|
|
2013
|
+
const result = await this.session.cycleRoleModels(["slow", "default", "smol"]);
|
|
2014
|
+
if (!result) {
|
|
2015
|
+
this.showStatus("Only one role model available");
|
|
2016
|
+
return;
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
this.statusLine.invalidate();
|
|
2020
|
+
this.updateEditorBorderColor();
|
|
2021
|
+
const roleLabel = result.role === "default" ? "default" : result.role;
|
|
2022
|
+
const thinkingStr =
|
|
2023
|
+
result.model.reasoning && result.thinkingLevel !== "off" ? ` (thinking: ${result.thinkingLevel})` : "";
|
|
2024
|
+
this.showStatus(`Switched to ${roleLabel}: ${result.model.name || result.model.id}${thinkingStr}`);
|
|
2025
|
+
} catch (error) {
|
|
2026
|
+
this.showError(error instanceof Error ? error.message : String(error));
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
|
|
1564
2030
|
private toggleToolOutputExpansion(): void {
|
|
1565
2031
|
this.toolOutputExpanded = !this.toolOutputExpanded;
|
|
1566
2032
|
for (const child of this.chatContainer.children) {
|
|
@@ -1642,18 +2108,29 @@ export class InteractiveMode {
|
|
|
1642
2108
|
// =========================================================================
|
|
1643
2109
|
|
|
1644
2110
|
clearEditor(): void {
|
|
2111
|
+
if (this.isBackgrounded) {
|
|
2112
|
+
return;
|
|
2113
|
+
}
|
|
1645
2114
|
this.editor.setText("");
|
|
1646
2115
|
this.pendingImages = [];
|
|
1647
2116
|
this.ui.requestRender();
|
|
1648
2117
|
}
|
|
1649
2118
|
|
|
1650
2119
|
showError(errorMessage: string): void {
|
|
2120
|
+
if (this.isBackgrounded) {
|
|
2121
|
+
console.error(`Error: ${errorMessage}`);
|
|
2122
|
+
return;
|
|
2123
|
+
}
|
|
1651
2124
|
this.chatContainer.addChild(new Spacer(1));
|
|
1652
2125
|
this.chatContainer.addChild(new Text(theme.fg("error", `Error: ${errorMessage}`), 1, 0));
|
|
1653
2126
|
this.ui.requestRender();
|
|
1654
2127
|
}
|
|
1655
2128
|
|
|
1656
2129
|
showWarning(warningMessage: string): void {
|
|
2130
|
+
if (this.isBackgrounded) {
|
|
2131
|
+
console.error(`Warning: ${warningMessage}`);
|
|
2132
|
+
return;
|
|
2133
|
+
}
|
|
1657
2134
|
this.chatContainer.addChild(new Spacer(1));
|
|
1658
2135
|
this.chatContainer.addChild(new Text(theme.fg("warning", `Warning: ${warningMessage}`), 1, 0));
|
|
1659
2136
|
this.ui.requestRender();
|
|
@@ -1679,10 +2156,13 @@ export class InteractiveMode {
|
|
|
1679
2156
|
private updatePendingMessagesDisplay(): void {
|
|
1680
2157
|
this.pendingMessagesContainer.clear();
|
|
1681
2158
|
const queuedMessages = this.session.getQueuedMessages();
|
|
1682
|
-
|
|
2159
|
+
const steeringMessages = queuedMessages.steering.map((message) => ({ message, label: "Steer" }));
|
|
2160
|
+
const followUpMessages = queuedMessages.followUp.map((message) => ({ message, label: "Follow-up" }));
|
|
2161
|
+
const allMessages = [...steeringMessages, ...followUpMessages];
|
|
2162
|
+
if (allMessages.length > 0) {
|
|
1683
2163
|
this.pendingMessagesContainer.addChild(new Spacer(1));
|
|
1684
|
-
for (const
|
|
1685
|
-
const queuedText = theme.fg("dim",
|
|
2164
|
+
for (const entry of allMessages) {
|
|
2165
|
+
const queuedText = theme.fg("dim", `${entry.label}: ${entry.message}`);
|
|
1686
2166
|
this.pendingMessagesContainer.addChild(new TruncatedText(queuedText, 1, 0));
|
|
1687
2167
|
}
|
|
1688
2168
|
}
|
|
@@ -1803,8 +2283,11 @@ export class InteractiveMode {
|
|
|
1803
2283
|
this.session.setAutoCompactionEnabled(value as boolean);
|
|
1804
2284
|
this.statusLine.setAutoCompactEnabled(value as boolean);
|
|
1805
2285
|
break;
|
|
1806
|
-
case "
|
|
1807
|
-
this.session.
|
|
2286
|
+
case "steeringMode":
|
|
2287
|
+
this.session.setSteeringMode(value as "all" | "one-at-a-time");
|
|
2288
|
+
break;
|
|
2289
|
+
case "followUpMode":
|
|
2290
|
+
this.session.setFollowUpMode(value as "all" | "one-at-a-time");
|
|
1808
2291
|
break;
|
|
1809
2292
|
case "interruptMode":
|
|
1810
2293
|
this.session.setInterruptMode(value as "immediate" | "wait");
|
|
@@ -1850,6 +2333,15 @@ export class InteractiveMode {
|
|
|
1850
2333
|
this.ui.invalidate();
|
|
1851
2334
|
break;
|
|
1852
2335
|
}
|
|
2336
|
+
case "voiceEnabled": {
|
|
2337
|
+
if (!value) {
|
|
2338
|
+
this.voiceAutoModeEnabled = false;
|
|
2339
|
+
this.stopVoiceProgressTimer();
|
|
2340
|
+
void this.voiceSupervisor.stop();
|
|
2341
|
+
this.setVoiceStatus(undefined);
|
|
2342
|
+
}
|
|
2343
|
+
break;
|
|
2344
|
+
}
|
|
1853
2345
|
case "statusLinePreset":
|
|
1854
2346
|
case "statusLineSeparator":
|
|
1855
2347
|
case "statusLineShowHooks":
|
|
@@ -2156,7 +2648,7 @@ export class InteractiveMode {
|
|
|
2156
2648
|
},
|
|
2157
2649
|
});
|
|
2158
2650
|
// Refresh models to pick up new baseUrl (e.g., github-copilot)
|
|
2159
|
-
this.session.modelRegistry.refresh();
|
|
2651
|
+
await this.session.modelRegistry.refresh();
|
|
2160
2652
|
this.chatContainer.addChild(new Spacer(1));
|
|
2161
2653
|
this.chatContainer.addChild(
|
|
2162
2654
|
new Text(
|
|
@@ -2174,9 +2666,9 @@ export class InteractiveMode {
|
|
|
2174
2666
|
}
|
|
2175
2667
|
} else {
|
|
2176
2668
|
try {
|
|
2177
|
-
this.session.modelRegistry.authStorage.logout(providerId);
|
|
2669
|
+
await this.session.modelRegistry.authStorage.logout(providerId);
|
|
2178
2670
|
// Refresh models to reset baseUrl
|
|
2179
|
-
this.session.modelRegistry.refresh();
|
|
2671
|
+
await this.session.modelRegistry.refresh();
|
|
2180
2672
|
this.chatContainer.addChild(new Spacer(1));
|
|
2181
2673
|
this.chatContainer.addChild(
|
|
2182
2674
|
new Text(
|
|
@@ -2208,8 +2700,17 @@ export class InteractiveMode {
|
|
|
2208
2700
|
// =========================================================================
|
|
2209
2701
|
|
|
2210
2702
|
private openInBrowser(urlOrPath: string): void {
|
|
2211
|
-
|
|
2212
|
-
|
|
2703
|
+
try {
|
|
2704
|
+
const args =
|
|
2705
|
+
process.platform === "darwin"
|
|
2706
|
+
? ["open", urlOrPath]
|
|
2707
|
+
: process.platform === "win32"
|
|
2708
|
+
? ["cmd", "/c", "start", "", urlOrPath]
|
|
2709
|
+
: ["xdg-open", urlOrPath];
|
|
2710
|
+
Bun.spawn(args, { stdin: "ignore", stdout: "ignore", stderr: "ignore" });
|
|
2711
|
+
} catch {
|
|
2712
|
+
// Best-effort: browser opening is non-critical
|
|
2713
|
+
}
|
|
2213
2714
|
}
|
|
2214
2715
|
|
|
2215
2716
|
private async handleExportCommand(text: string): Promise<void> {
|
|
@@ -2378,7 +2879,7 @@ export class InteractiveMode {
|
|
|
2378
2879
|
const stats = this.session.getSessionStats();
|
|
2379
2880
|
|
|
2380
2881
|
let info = `${theme.bold("Session Info")}\n\n`;
|
|
2381
|
-
info += `${theme.fg("dim", "File:")} ${stats.sessionFile}\n`;
|
|
2882
|
+
info += `${theme.fg("dim", "File:")} ${stats.sessionFile ?? "In-memory"}\n`;
|
|
2382
2883
|
info += `${theme.fg("dim", "ID:")} ${stats.sessionId}\n\n`;
|
|
2383
2884
|
info += `${theme.bold("Messages")}\n`;
|
|
2384
2885
|
info += `${theme.fg("dim", "User:")} ${stats.userMessages}\n`;
|
|
@@ -2428,6 +2929,31 @@ export class InteractiveMode {
|
|
|
2428
2929
|
this.ui.requestRender();
|
|
2429
2930
|
}
|
|
2430
2931
|
|
|
2932
|
+
/**
|
|
2933
|
+
* Register extension-defined keyboard shortcuts with the editor.
|
|
2934
|
+
*/
|
|
2935
|
+
private registerExtensionShortcuts(): void {
|
|
2936
|
+
const runner = this.session.extensionRunner;
|
|
2937
|
+
if (!runner) return;
|
|
2938
|
+
|
|
2939
|
+
const shortcuts = runner.getShortcuts();
|
|
2940
|
+
for (const [keyId, shortcut] of shortcuts) {
|
|
2941
|
+
this.editor.setCustomKeyHandler(keyId, () => {
|
|
2942
|
+
const ctx = runner.createCommandContext();
|
|
2943
|
+
try {
|
|
2944
|
+
shortcut.handler(ctx);
|
|
2945
|
+
} catch (err) {
|
|
2946
|
+
runner.emitError({
|
|
2947
|
+
extensionPath: shortcut.extensionPath,
|
|
2948
|
+
event: "shortcut",
|
|
2949
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2950
|
+
stack: err instanceof Error ? err.stack : undefined,
|
|
2951
|
+
});
|
|
2952
|
+
}
|
|
2953
|
+
});
|
|
2954
|
+
}
|
|
2955
|
+
}
|
|
2956
|
+
|
|
2431
2957
|
private handleHotkeysCommand(): void {
|
|
2432
2958
|
const hotkeys = `
|
|
2433
2959
|
**Navigation**
|
|
@@ -2457,6 +2983,7 @@ export class InteractiveMode {
|
|
|
2457
2983
|
| \`Ctrl+Z\` | Suspend to background |
|
|
2458
2984
|
| \`Shift+Tab\` | Cycle thinking level |
|
|
2459
2985
|
| \`Ctrl+P\` | Cycle models |
|
|
2986
|
+
| \`Ctrl+Y\` | Cycle role models (slow/default/smol) |
|
|
2460
2987
|
| \`Ctrl+O\` | Toggle tool output expansion |
|
|
2461
2988
|
| \`Ctrl+T\` | Toggle thinking block visibility |
|
|
2462
2989
|
| \`Ctrl+G\` | Edit message in external editor |
|
|
@@ -2543,9 +3070,9 @@ export class InteractiveMode {
|
|
|
2543
3070
|
this.ui.requestRender();
|
|
2544
3071
|
}
|
|
2545
3072
|
|
|
2546
|
-
private async handleBashCommand(command: string): Promise<void> {
|
|
3073
|
+
private async handleBashCommand(command: string, excludeFromContext = false): Promise<void> {
|
|
2547
3074
|
const isDeferred = this.session.isStreaming;
|
|
2548
|
-
this.bashComponent = new BashExecutionComponent(command, this.ui);
|
|
3075
|
+
this.bashComponent = new BashExecutionComponent(command, this.ui, excludeFromContext);
|
|
2549
3076
|
|
|
2550
3077
|
if (isDeferred) {
|
|
2551
3078
|
// Show in pending area when agent is streaming
|
|
@@ -2558,12 +3085,16 @@ export class InteractiveMode {
|
|
|
2558
3085
|
this.ui.requestRender();
|
|
2559
3086
|
|
|
2560
3087
|
try {
|
|
2561
|
-
const result = await this.session.executeBash(
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
this.
|
|
2565
|
-
|
|
2566
|
-
|
|
3088
|
+
const result = await this.session.executeBash(
|
|
3089
|
+
command,
|
|
3090
|
+
(chunk) => {
|
|
3091
|
+
if (this.bashComponent) {
|
|
3092
|
+
this.bashComponent.appendOutput(chunk);
|
|
3093
|
+
this.ui.requestRender();
|
|
3094
|
+
}
|
|
3095
|
+
},
|
|
3096
|
+
{ excludeFromContext },
|
|
3097
|
+
);
|
|
2567
3098
|
|
|
2568
3099
|
if (this.bashComponent) {
|
|
2569
3100
|
this.bashComponent.setComplete(
|