@oh-my-pi/pi-coding-agent 3.15.1 → 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 +51 -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-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.ts +45 -37
- package/src/modes/interactive/components/tool-execution.ts +95 -23
- package/src/modes/interactive/interactive-mode.ts +643 -113
- package/src/modes/interactive/theme/defaults/index.ts +16 -16
- 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,7 +187,6 @@ 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();
|
|
@@ -185,6 +197,26 @@ export class InteractiveMode {
|
|
|
185
197
|
this.editorContainer.addChild(this.editor);
|
|
186
198
|
this.statusLine = new StatusLineComponent(session);
|
|
187
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
|
+
});
|
|
188
220
|
|
|
189
221
|
// Define slash commands for autocomplete
|
|
190
222
|
const slashCommands: SlashCommand[] = [
|
|
@@ -204,6 +236,8 @@ export class InteractiveMode {
|
|
|
204
236
|
{ name: "logout", description: "Logout from OAuth provider" },
|
|
205
237
|
{ name: "new", description: "Start a new session" },
|
|
206
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" },
|
|
207
241
|
{ name: "resume", description: "Resume a different session" },
|
|
208
242
|
{ name: "exit", description: "Exit the application" },
|
|
209
243
|
];
|
|
@@ -211,14 +245,15 @@ export class InteractiveMode {
|
|
|
211
245
|
// Load hide thinking block setting
|
|
212
246
|
this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
|
|
213
247
|
|
|
214
|
-
//
|
|
215
|
-
const
|
|
248
|
+
// Load and convert file commands to SlashCommand format
|
|
249
|
+
const fileCommands = loadSlashCommands({ cwd: process.cwd() });
|
|
250
|
+
const fileSlashCommands: SlashCommand[] = fileCommands.map((cmd) => ({
|
|
216
251
|
name: cmd.name,
|
|
217
252
|
description: cmd.description,
|
|
218
253
|
}));
|
|
219
254
|
|
|
220
255
|
// Convert hook commands to SlashCommand format
|
|
221
|
-
const hookCommands: SlashCommand[] = (this.session.
|
|
256
|
+
const hookCommands: SlashCommand[] = (this.session.extensionRunner?.getRegisteredCommands() ?? []).map((cmd) => ({
|
|
222
257
|
name: cmd.name,
|
|
223
258
|
description: cmd.description ?? "(hook command)",
|
|
224
259
|
}));
|
|
@@ -308,6 +343,10 @@ export class InteractiveMode {
|
|
|
308
343
|
this.ui.start();
|
|
309
344
|
this.isInitialized = true;
|
|
310
345
|
|
|
346
|
+
// Set terminal title
|
|
347
|
+
const cwdBasename = path.basename(process.cwd());
|
|
348
|
+
this.ui.terminal.setTitle(`pi - ${cwdBasename}`);
|
|
349
|
+
|
|
311
350
|
// Initialize hooks with TUI-based UI context
|
|
312
351
|
await this.initHooksAndCustomTools();
|
|
313
352
|
|
|
@@ -340,12 +379,14 @@ export class InteractiveMode {
|
|
|
340
379
|
*/
|
|
341
380
|
private async initHooksAndCustomTools(): Promise<void> {
|
|
342
381
|
// Create and set hook & tool UI context
|
|
343
|
-
const uiContext:
|
|
382
|
+
const uiContext: ExtensionUIContext = {
|
|
344
383
|
select: (title, options) => this.showHookSelector(title, options),
|
|
345
384
|
confirm: (title, message) => this.showHookConfirm(title, message),
|
|
346
385
|
input: (title, placeholder) => this.showHookInput(title, placeholder),
|
|
347
386
|
notify: (message, type) => this.showHookNotify(message, type),
|
|
348
387
|
setStatus: (key, text) => this.setHookStatus(key, text),
|
|
388
|
+
setWidget: (key, content) => this.setHookWidget(key, content),
|
|
389
|
+
setTitle: (title) => setTerminalTitle(title),
|
|
349
390
|
custom: (factory) => this.showHookCustom(factory),
|
|
350
391
|
setEditorText: (text) => this.editor.setText(text),
|
|
351
392
|
getEditorText: () => this.editor.getText(),
|
|
@@ -356,37 +397,34 @@ export class InteractiveMode {
|
|
|
356
397
|
};
|
|
357
398
|
this.setToolUIContext(uiContext, true);
|
|
358
399
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
reason: "start",
|
|
362
|
-
previousSessionFile: undefined,
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
const hookRunner = this.session.hookRunner;
|
|
366
|
-
if (!hookRunner) {
|
|
400
|
+
const extensionRunner = this.session.extensionRunner;
|
|
401
|
+
if (!extensionRunner) {
|
|
367
402
|
return; // No hooks loaded
|
|
368
403
|
}
|
|
369
404
|
|
|
370
|
-
|
|
405
|
+
extensionRunner.initialize({
|
|
371
406
|
getModel: () => this.session.model,
|
|
372
|
-
sendMessageHandler: (message,
|
|
407
|
+
sendMessageHandler: (message, options) => {
|
|
373
408
|
const wasStreaming = this.session.isStreaming;
|
|
374
409
|
this.session
|
|
375
|
-
.
|
|
410
|
+
.sendCustomMessage(message, options)
|
|
376
411
|
.then(() => {
|
|
377
412
|
// For non-streaming cases with display=true, update UI
|
|
378
413
|
// (streaming cases update via message_end event)
|
|
379
|
-
if (!wasStreaming && message.display) {
|
|
414
|
+
if (!this.isBackgrounded && !wasStreaming && message.display) {
|
|
380
415
|
this.rebuildChatFromMessages();
|
|
381
416
|
}
|
|
382
417
|
})
|
|
383
418
|
.catch((err) => {
|
|
384
|
-
this.showError(`
|
|
419
|
+
this.showError(`Extension sendMessage failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
385
420
|
});
|
|
386
421
|
},
|
|
387
422
|
appendEntryHandler: (customType, data) => {
|
|
388
423
|
this.sessionManager.appendCustomEntry(customType, data);
|
|
389
424
|
},
|
|
425
|
+
getActiveToolsHandler: () => this.session.getActiveToolNames(),
|
|
426
|
+
getAllToolsHandler: () => this.session.getAllToolNames(),
|
|
427
|
+
setActiveToolsHandler: (toolNames: string[]) => this.session.setActiveToolsByName(toolNames),
|
|
390
428
|
newSessionHandler: async (options) => {
|
|
391
429
|
// Stop any loading animation
|
|
392
430
|
if (this.loadingAnimation) {
|
|
@@ -456,41 +494,204 @@ export class InteractiveMode {
|
|
|
456
494
|
abort: () => {
|
|
457
495
|
this.session.abort();
|
|
458
496
|
},
|
|
459
|
-
|
|
497
|
+
hasPendingMessages: () => this.session.queuedMessageCount > 0,
|
|
460
498
|
uiContext,
|
|
461
499
|
hasUI: true,
|
|
462
500
|
});
|
|
463
501
|
|
|
464
|
-
// Subscribe to
|
|
465
|
-
|
|
466
|
-
this.
|
|
502
|
+
// Subscribe to extension errors
|
|
503
|
+
extensionRunner.onError((error) => {
|
|
504
|
+
this.showExtensionError(error.extensionPath, error.error);
|
|
467
505
|
});
|
|
468
506
|
|
|
469
507
|
// Emit session_start event
|
|
470
|
-
await
|
|
508
|
+
await extensionRunner.emit({
|
|
471
509
|
type: "session_start",
|
|
472
510
|
});
|
|
473
511
|
}
|
|
474
512
|
|
|
475
513
|
/**
|
|
476
|
-
*
|
|
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.
|
|
477
666
|
*/
|
|
478
|
-
private async emitCustomToolSessionEvent(
|
|
479
|
-
|
|
480
|
-
|
|
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) {
|
|
481
678
|
try {
|
|
482
|
-
await
|
|
679
|
+
await registeredTool.definition.onSession(event, {
|
|
680
|
+
ui: uiContext,
|
|
681
|
+
hasUI: !this.isBackgrounded,
|
|
682
|
+
cwd: this.sessionManager.getCwd(),
|
|
483
683
|
sessionManager: this.session.sessionManager,
|
|
484
684
|
modelRegistry: this.session.modelRegistry,
|
|
485
685
|
model: this.session.model,
|
|
486
686
|
isIdle: () => !this.session.isStreaming,
|
|
687
|
+
hasPendingMessages: () => this.session.queuedMessageCount > 0,
|
|
487
688
|
hasQueuedMessages: () => this.session.queuedMessageCount > 0,
|
|
488
689
|
abort: () => {
|
|
489
690
|
this.session.abort();
|
|
490
691
|
},
|
|
491
692
|
});
|
|
492
693
|
} catch (err) {
|
|
493
|
-
this.showToolError(
|
|
694
|
+
this.showToolError(registeredTool.definition.name, err instanceof Error ? err.message : String(err));
|
|
494
695
|
}
|
|
495
696
|
}
|
|
496
697
|
}
|
|
@@ -500,6 +701,10 @@ export class InteractiveMode {
|
|
|
500
701
|
* Show a tool error in the chat.
|
|
501
702
|
*/
|
|
502
703
|
private showToolError(toolName: string, error: string): void {
|
|
704
|
+
if (this.isBackgrounded) {
|
|
705
|
+
console.error(`Tool "${toolName}" error: ${error}`);
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
503
708
|
const errorText = new Text(theme.fg("error", `Tool "${toolName}" error: ${error}`), 1, 0);
|
|
504
709
|
this.chatContainer.addChild(errorText);
|
|
505
710
|
this.ui.requestRender();
|
|
@@ -509,6 +714,9 @@ export class InteractiveMode {
|
|
|
509
714
|
* Set hook status text in the footer.
|
|
510
715
|
*/
|
|
511
716
|
private setHookStatus(key: string, text: string | undefined): void {
|
|
717
|
+
if (this.isBackgrounded) {
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
512
720
|
this.statusLine.setHookStatus(key, text);
|
|
513
721
|
this.ui.requestRender();
|
|
514
722
|
}
|
|
@@ -679,10 +887,10 @@ export class InteractiveMode {
|
|
|
679
887
|
}
|
|
680
888
|
|
|
681
889
|
/**
|
|
682
|
-
* Show
|
|
890
|
+
* Show an extension error in the UI.
|
|
683
891
|
*/
|
|
684
|
-
private
|
|
685
|
-
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);
|
|
686
894
|
this.chatContainer.addChild(errorText);
|
|
687
895
|
this.ui.requestRender();
|
|
688
896
|
}
|
|
@@ -700,7 +908,7 @@ export class InteractiveMode {
|
|
|
700
908
|
if (this.loadingAnimation) {
|
|
701
909
|
// Abort and restore queued messages to editor
|
|
702
910
|
const queuedMessages = this.session.clearQueue();
|
|
703
|
-
const queuedText = queuedMessages.join("\n\n");
|
|
911
|
+
const queuedText = [...queuedMessages.steering, ...queuedMessages.followUp].join("\n\n");
|
|
704
912
|
const currentText = this.editor.getText();
|
|
705
913
|
const combinedText = [queuedText, currentText].filter((t) => t.trim()).join("\n\n");
|
|
706
914
|
this.editor.setText(combinedText);
|
|
@@ -713,10 +921,14 @@ export class InteractiveMode {
|
|
|
713
921
|
this.isBashMode = false;
|
|
714
922
|
this.updateEditorBorderColor();
|
|
715
923
|
} else if (!this.editor.getText().trim()) {
|
|
716
|
-
// Double-escape with empty editor triggers /branch
|
|
924
|
+
// Double-escape with empty editor triggers /tree or /branch based on setting
|
|
717
925
|
const now = Date.now();
|
|
718
926
|
if (now - this.lastEscapeTime < 500) {
|
|
719
|
-
this.
|
|
927
|
+
if (this.settingsManager.getDoubleEscapeAction() === "tree") {
|
|
928
|
+
this.showTreeSelector();
|
|
929
|
+
} else {
|
|
930
|
+
this.showUserMessageSelector();
|
|
931
|
+
}
|
|
720
932
|
this.lastEscapeTime = 0;
|
|
721
933
|
} else {
|
|
722
934
|
this.lastEscapeTime = now;
|
|
@@ -730,6 +942,7 @@ export class InteractiveMode {
|
|
|
730
942
|
this.editor.onShiftTab = () => this.cycleThinkingLevel();
|
|
731
943
|
this.editor.onCtrlP = () => this.cycleModel("forward");
|
|
732
944
|
this.editor.onShiftCtrlP = () => this.cycleModel("backward");
|
|
945
|
+
this.editor.onCtrlY = () => this.cycleRoleModel();
|
|
733
946
|
|
|
734
947
|
// Global debug handler on TUI (works regardless of focus)
|
|
735
948
|
this.ui.onDebug = () => this.handleDebugCommand();
|
|
@@ -737,9 +950,15 @@ export class InteractiveMode {
|
|
|
737
950
|
this.editor.onCtrlO = () => this.toggleToolOutputExpansion();
|
|
738
951
|
this.editor.onCtrlT = () => this.toggleThinkingBlockVisibility();
|
|
739
952
|
this.editor.onCtrlG = () => this.openExternalEditor();
|
|
953
|
+
this.editor.onCtrlY = () => {
|
|
954
|
+
void this.toggleVoiceListening();
|
|
955
|
+
};
|
|
740
956
|
this.editor.onQuestionMark = () => this.handleHotkeysCommand();
|
|
741
957
|
this.editor.onCtrlV = () => this.handleImagePaste();
|
|
742
958
|
|
|
959
|
+
// Wire up extension shortcuts
|
|
960
|
+
this.registerExtensionShortcuts();
|
|
961
|
+
|
|
743
962
|
this.editor.onChange = (text: string) => {
|
|
744
963
|
const wasBashMode = this.isBashMode;
|
|
745
964
|
this.isBashMode = text.trimStart().startsWith("!");
|
|
@@ -747,6 +966,25 @@ export class InteractiveMode {
|
|
|
747
966
|
this.updateEditorBorderColor();
|
|
748
967
|
}
|
|
749
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
|
+
};
|
|
750
988
|
}
|
|
751
989
|
|
|
752
990
|
private setupEditorSubmitHandler(): void {
|
|
@@ -801,7 +1039,11 @@ export class InteractiveMode {
|
|
|
801
1039
|
return;
|
|
802
1040
|
}
|
|
803
1041
|
if (text === "/branch") {
|
|
804
|
-
this.
|
|
1042
|
+
if (this.settingsManager.getDoubleEscapeAction() === "tree") {
|
|
1043
|
+
this.showTreeSelector();
|
|
1044
|
+
} else {
|
|
1045
|
+
this.showUserMessageSelector();
|
|
1046
|
+
}
|
|
805
1047
|
this.editor.setText("");
|
|
806
1048
|
return;
|
|
807
1049
|
}
|
|
@@ -836,6 +1078,11 @@ export class InteractiveMode {
|
|
|
836
1078
|
}
|
|
837
1079
|
return;
|
|
838
1080
|
}
|
|
1081
|
+
if (text === "/background" || text === "/bg") {
|
|
1082
|
+
this.editor.setText("");
|
|
1083
|
+
this.handleBackgroundCommand();
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
839
1086
|
if (text === "/debug") {
|
|
840
1087
|
this.handleDebugCommand();
|
|
841
1088
|
this.editor.setText("");
|
|
@@ -857,9 +1104,10 @@ export class InteractiveMode {
|
|
|
857
1104
|
return;
|
|
858
1105
|
}
|
|
859
1106
|
|
|
860
|
-
// Handle bash command
|
|
1107
|
+
// Handle bash command (! for normal, !! for excluded from context)
|
|
861
1108
|
if (text.startsWith("!")) {
|
|
862
|
-
const
|
|
1109
|
+
const isExcluded = text.startsWith("!!");
|
|
1110
|
+
const command = isExcluded ? text.slice(2).trim() : text.slice(1).trim();
|
|
863
1111
|
if (command) {
|
|
864
1112
|
if (this.session.isBashRunning) {
|
|
865
1113
|
this.showWarning("A bash command is already running. Press Esc to cancel it first.");
|
|
@@ -867,7 +1115,7 @@ export class InteractiveMode {
|
|
|
867
1115
|
return;
|
|
868
1116
|
}
|
|
869
1117
|
this.editor.addToHistory(text);
|
|
870
|
-
await this.handleBashCommand(command);
|
|
1118
|
+
await this.handleBashCommand(command, isExcluded);
|
|
871
1119
|
this.isBashMode = false;
|
|
872
1120
|
this.updateEditorBorderColor();
|
|
873
1121
|
return;
|
|
@@ -879,39 +1127,13 @@ export class InteractiveMode {
|
|
|
879
1127
|
return;
|
|
880
1128
|
}
|
|
881
1129
|
|
|
882
|
-
//
|
|
883
|
-
//
|
|
884
|
-
if (text.startsWith("/") && this.session.hookRunner) {
|
|
885
|
-
const spaceIndex = text.indexOf(" ");
|
|
886
|
-
const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
|
|
887
|
-
const command = this.session.hookRunner.getCommand(commandName);
|
|
888
|
-
if (command) {
|
|
889
|
-
this.editor.addToHistory(text);
|
|
890
|
-
this.editor.setText("");
|
|
891
|
-
await this.session.prompt(text);
|
|
892
|
-
return;
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
// Custom commands (TypeScript slash commands) - route through session.prompt()
|
|
897
|
-
if (text.startsWith("/") && this.session.customCommands.length > 0) {
|
|
898
|
-
const spaceIndex = text.indexOf(" ");
|
|
899
|
-
const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
|
|
900
|
-
const hasCustomCommand = this.session.customCommands.some((c) => c.command.name === commandName);
|
|
901
|
-
if (hasCustomCommand) {
|
|
902
|
-
this.editor.addToHistory(text);
|
|
903
|
-
this.editor.setText("");
|
|
904
|
-
await this.session.prompt(text);
|
|
905
|
-
return;
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
// 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
|
|
910
1132
|
if (this.session.isStreaming) {
|
|
911
|
-
await this.session.queueMessage(text);
|
|
912
|
-
this.updatePendingMessagesDisplay();
|
|
913
1133
|
this.editor.addToHistory(text);
|
|
914
1134
|
this.editor.setText("");
|
|
1135
|
+
await this.session.prompt(text, { streamingBehavior: "steer" });
|
|
1136
|
+
this.updatePendingMessagesDisplay();
|
|
915
1137
|
this.ui.requestRender();
|
|
916
1138
|
return;
|
|
917
1139
|
}
|
|
@@ -973,11 +1195,12 @@ export class InteractiveMode {
|
|
|
973
1195
|
getSymbolTheme().spinnerFrames,
|
|
974
1196
|
);
|
|
975
1197
|
this.statusContainer.addChild(this.loadingAnimation);
|
|
1198
|
+
this.startVoiceProgressTimer();
|
|
976
1199
|
this.ui.requestRender();
|
|
977
1200
|
break;
|
|
978
1201
|
|
|
979
1202
|
case "message_start":
|
|
980
|
-
if (event.message.role === "hookMessage") {
|
|
1203
|
+
if (event.message.role === "hookMessage" || event.message.role === "custom") {
|
|
981
1204
|
this.addMessageToChat(event.message);
|
|
982
1205
|
this.ui.requestRender();
|
|
983
1206
|
} else if (event.message.role === "user") {
|
|
@@ -1003,14 +1226,16 @@ export class InteractiveMode {
|
|
|
1003
1226
|
if (content.type === "toolCall") {
|
|
1004
1227
|
if (!this.pendingTools.has(content.id)) {
|
|
1005
1228
|
this.chatContainer.addChild(new Text("", 0, 0));
|
|
1229
|
+
const tool = this.session.getToolByName(content.name);
|
|
1006
1230
|
const component = new ToolExecutionComponent(
|
|
1007
1231
|
content.name,
|
|
1008
1232
|
content.arguments,
|
|
1009
1233
|
{
|
|
1010
1234
|
showImages: this.settingsManager.getShowImages(),
|
|
1011
1235
|
},
|
|
1012
|
-
|
|
1236
|
+
tool,
|
|
1013
1237
|
this.ui,
|
|
1238
|
+
this.sessionManager.getCwd(),
|
|
1014
1239
|
);
|
|
1015
1240
|
component.setExpanded(this.toolOutputExpanded);
|
|
1016
1241
|
this.chatContainer.addChild(component);
|
|
@@ -1071,14 +1296,16 @@ export class InteractiveMode {
|
|
|
1071
1296
|
|
|
1072
1297
|
case "tool_execution_start": {
|
|
1073
1298
|
if (!this.pendingTools.has(event.toolCallId)) {
|
|
1299
|
+
const tool = this.session.getToolByName(event.toolName);
|
|
1074
1300
|
const component = new ToolExecutionComponent(
|
|
1075
1301
|
event.toolName,
|
|
1076
1302
|
event.args,
|
|
1077
1303
|
{
|
|
1078
1304
|
showImages: this.settingsManager.getShowImages(),
|
|
1079
1305
|
},
|
|
1080
|
-
|
|
1306
|
+
tool,
|
|
1081
1307
|
this.ui,
|
|
1308
|
+
this.sessionManager.getCwd(),
|
|
1082
1309
|
);
|
|
1083
1310
|
component.setExpanded(this.toolOutputExpanded);
|
|
1084
1311
|
this.chatContainer.addChild(component);
|
|
@@ -1108,6 +1335,7 @@ export class InteractiveMode {
|
|
|
1108
1335
|
}
|
|
1109
1336
|
|
|
1110
1337
|
case "agent_end":
|
|
1338
|
+
this.stopVoiceProgressTimer();
|
|
1111
1339
|
if (this.loadingAnimation) {
|
|
1112
1340
|
this.loadingAnimation.stop();
|
|
1113
1341
|
this.loadingAnimation = undefined;
|
|
@@ -1119,7 +1347,17 @@ export class InteractiveMode {
|
|
|
1119
1347
|
this.streamingMessage = undefined;
|
|
1120
1348
|
}
|
|
1121
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
|
+
}
|
|
1122
1359
|
this.ui.requestRender();
|
|
1360
|
+
this.sendCompletionNotification();
|
|
1123
1361
|
break;
|
|
1124
1362
|
|
|
1125
1363
|
case "auto_compaction_start": {
|
|
@@ -1232,6 +1470,14 @@ export class InteractiveMode {
|
|
|
1232
1470
|
}
|
|
1233
1471
|
}
|
|
1234
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
|
+
|
|
1235
1481
|
/** Extract text content from a user message */
|
|
1236
1482
|
private getUserMessageText(message: Message): string {
|
|
1237
1483
|
if (message.role !== "user") return "";
|
|
@@ -1249,6 +1495,9 @@ export class InteractiveMode {
|
|
|
1249
1495
|
* we update the previous status line instead of appending new ones to avoid log spam.
|
|
1250
1496
|
*/
|
|
1251
1497
|
private showStatus(message: string): void {
|
|
1498
|
+
if (this.isBackgrounded) {
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1252
1501
|
const children = this.chatContainer.children;
|
|
1253
1502
|
const last = children.length > 0 ? children[children.length - 1] : undefined;
|
|
1254
1503
|
const secondLast = children.length > 1 ? children[children.length - 2] : undefined;
|
|
@@ -1271,7 +1520,7 @@ export class InteractiveMode {
|
|
|
1271
1520
|
private addMessageToChat(message: AgentMessage, options?: { populateHistory?: boolean }): void {
|
|
1272
1521
|
switch (message.role) {
|
|
1273
1522
|
case "bashExecution": {
|
|
1274
|
-
const component = new BashExecutionComponent(message.command, this.ui);
|
|
1523
|
+
const component = new BashExecutionComponent(message.command, this.ui, message.excludeFromContext);
|
|
1275
1524
|
if (message.output) {
|
|
1276
1525
|
component.appendOutput(message.output);
|
|
1277
1526
|
}
|
|
@@ -1284,10 +1533,12 @@ export class InteractiveMode {
|
|
|
1284
1533
|
this.chatContainer.addChild(component);
|
|
1285
1534
|
break;
|
|
1286
1535
|
}
|
|
1287
|
-
case "hookMessage":
|
|
1536
|
+
case "hookMessage":
|
|
1537
|
+
case "custom": {
|
|
1288
1538
|
if (message.display) {
|
|
1289
|
-
const renderer = this.session.
|
|
1290
|
-
|
|
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));
|
|
1291
1542
|
}
|
|
1292
1543
|
break;
|
|
1293
1544
|
}
|
|
@@ -1363,12 +1614,14 @@ export class InteractiveMode {
|
|
|
1363
1614
|
// Render tool call components
|
|
1364
1615
|
for (const content of message.content) {
|
|
1365
1616
|
if (content.type === "toolCall") {
|
|
1617
|
+
const tool = this.session.getToolByName(content.name);
|
|
1366
1618
|
const component = new ToolExecutionComponent(
|
|
1367
1619
|
content.name,
|
|
1368
1620
|
content.arguments,
|
|
1369
1621
|
{ showImages: this.settingsManager.getShowImages() },
|
|
1370
|
-
|
|
1622
|
+
tool,
|
|
1371
1623
|
this.ui,
|
|
1624
|
+
this.sessionManager.getCwd(),
|
|
1372
1625
|
);
|
|
1373
1626
|
component.setExpanded(this.toolOutputExpanded);
|
|
1374
1627
|
this.chatContainer.addChild(component);
|
|
@@ -1455,18 +1708,13 @@ export class InteractiveMode {
|
|
|
1455
1708
|
* Emits shutdown event to hooks and tools, then exits.
|
|
1456
1709
|
*/
|
|
1457
1710
|
private async shutdown(): Promise<void> {
|
|
1711
|
+
this.voiceAutoModeEnabled = false;
|
|
1712
|
+
await this.voiceSupervisor.stop();
|
|
1713
|
+
|
|
1458
1714
|
// Flush pending session writes before shutdown
|
|
1459
1715
|
await this.sessionManager.flush();
|
|
1460
1716
|
|
|
1461
1717
|
// Emit shutdown event to hooks
|
|
1462
|
-
const hookRunner = this.session.hookRunner;
|
|
1463
|
-
if (hookRunner?.hasHandlers("session_shutdown")) {
|
|
1464
|
-
await hookRunner.emit({
|
|
1465
|
-
type: "session_shutdown",
|
|
1466
|
-
});
|
|
1467
|
-
}
|
|
1468
|
-
|
|
1469
|
-
// Emit shutdown event to custom tools
|
|
1470
1718
|
await this.session.emitCustomToolSessionEvent("shutdown");
|
|
1471
1719
|
|
|
1472
1720
|
this.stop();
|
|
@@ -1487,6 +1735,72 @@ export class InteractiveMode {
|
|
|
1487
1735
|
process.kill(0, "SIGTSTP");
|
|
1488
1736
|
}
|
|
1489
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
|
+
|
|
1490
1804
|
/**
|
|
1491
1805
|
* Handle Ctrl+V for image paste from clipboard.
|
|
1492
1806
|
* Returns true if an image was found and added, false otherwise.
|
|
@@ -1516,6 +1830,138 @@ export class InteractiveMode {
|
|
|
1516
1830
|
}
|
|
1517
1831
|
}
|
|
1518
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
|
+
|
|
1519
1965
|
private updateEditorBorderColor(): void {
|
|
1520
1966
|
if (this.isBashMode) {
|
|
1521
1967
|
this.editor.borderColor = theme.getBashModeBorderColor();
|
|
@@ -1562,6 +2008,25 @@ export class InteractiveMode {
|
|
|
1562
2008
|
}
|
|
1563
2009
|
}
|
|
1564
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
|
+
|
|
1565
2030
|
private toggleToolOutputExpansion(): void {
|
|
1566
2031
|
this.toolOutputExpanded = !this.toolOutputExpanded;
|
|
1567
2032
|
for (const child of this.chatContainer.children) {
|
|
@@ -1643,18 +2108,29 @@ export class InteractiveMode {
|
|
|
1643
2108
|
// =========================================================================
|
|
1644
2109
|
|
|
1645
2110
|
clearEditor(): void {
|
|
2111
|
+
if (this.isBackgrounded) {
|
|
2112
|
+
return;
|
|
2113
|
+
}
|
|
1646
2114
|
this.editor.setText("");
|
|
1647
2115
|
this.pendingImages = [];
|
|
1648
2116
|
this.ui.requestRender();
|
|
1649
2117
|
}
|
|
1650
2118
|
|
|
1651
2119
|
showError(errorMessage: string): void {
|
|
2120
|
+
if (this.isBackgrounded) {
|
|
2121
|
+
console.error(`Error: ${errorMessage}`);
|
|
2122
|
+
return;
|
|
2123
|
+
}
|
|
1652
2124
|
this.chatContainer.addChild(new Spacer(1));
|
|
1653
2125
|
this.chatContainer.addChild(new Text(theme.fg("error", `Error: ${errorMessage}`), 1, 0));
|
|
1654
2126
|
this.ui.requestRender();
|
|
1655
2127
|
}
|
|
1656
2128
|
|
|
1657
2129
|
showWarning(warningMessage: string): void {
|
|
2130
|
+
if (this.isBackgrounded) {
|
|
2131
|
+
console.error(`Warning: ${warningMessage}`);
|
|
2132
|
+
return;
|
|
2133
|
+
}
|
|
1658
2134
|
this.chatContainer.addChild(new Spacer(1));
|
|
1659
2135
|
this.chatContainer.addChild(new Text(theme.fg("warning", `Warning: ${warningMessage}`), 1, 0));
|
|
1660
2136
|
this.ui.requestRender();
|
|
@@ -1680,10 +2156,13 @@ export class InteractiveMode {
|
|
|
1680
2156
|
private updatePendingMessagesDisplay(): void {
|
|
1681
2157
|
this.pendingMessagesContainer.clear();
|
|
1682
2158
|
const queuedMessages = this.session.getQueuedMessages();
|
|
1683
|
-
|
|
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) {
|
|
1684
2163
|
this.pendingMessagesContainer.addChild(new Spacer(1));
|
|
1685
|
-
for (const
|
|
1686
|
-
const queuedText = theme.fg("dim",
|
|
2164
|
+
for (const entry of allMessages) {
|
|
2165
|
+
const queuedText = theme.fg("dim", `${entry.label}: ${entry.message}`);
|
|
1687
2166
|
this.pendingMessagesContainer.addChild(new TruncatedText(queuedText, 1, 0));
|
|
1688
2167
|
}
|
|
1689
2168
|
}
|
|
@@ -1804,8 +2283,11 @@ export class InteractiveMode {
|
|
|
1804
2283
|
this.session.setAutoCompactionEnabled(value as boolean);
|
|
1805
2284
|
this.statusLine.setAutoCompactEnabled(value as boolean);
|
|
1806
2285
|
break;
|
|
1807
|
-
case "
|
|
1808
|
-
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");
|
|
1809
2291
|
break;
|
|
1810
2292
|
case "interruptMode":
|
|
1811
2293
|
this.session.setInterruptMode(value as "immediate" | "wait");
|
|
@@ -1851,6 +2333,15 @@ export class InteractiveMode {
|
|
|
1851
2333
|
this.ui.invalidate();
|
|
1852
2334
|
break;
|
|
1853
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
|
+
}
|
|
1854
2345
|
case "statusLinePreset":
|
|
1855
2346
|
case "statusLineSeparator":
|
|
1856
2347
|
case "statusLineShowHooks":
|
|
@@ -2157,7 +2648,7 @@ export class InteractiveMode {
|
|
|
2157
2648
|
},
|
|
2158
2649
|
});
|
|
2159
2650
|
// Refresh models to pick up new baseUrl (e.g., github-copilot)
|
|
2160
|
-
this.session.modelRegistry.refresh();
|
|
2651
|
+
await this.session.modelRegistry.refresh();
|
|
2161
2652
|
this.chatContainer.addChild(new Spacer(1));
|
|
2162
2653
|
this.chatContainer.addChild(
|
|
2163
2654
|
new Text(
|
|
@@ -2175,9 +2666,9 @@ export class InteractiveMode {
|
|
|
2175
2666
|
}
|
|
2176
2667
|
} else {
|
|
2177
2668
|
try {
|
|
2178
|
-
this.session.modelRegistry.authStorage.logout(providerId);
|
|
2669
|
+
await this.session.modelRegistry.authStorage.logout(providerId);
|
|
2179
2670
|
// Refresh models to reset baseUrl
|
|
2180
|
-
this.session.modelRegistry.refresh();
|
|
2671
|
+
await this.session.modelRegistry.refresh();
|
|
2181
2672
|
this.chatContainer.addChild(new Spacer(1));
|
|
2182
2673
|
this.chatContainer.addChild(
|
|
2183
2674
|
new Text(
|
|
@@ -2209,8 +2700,17 @@ export class InteractiveMode {
|
|
|
2209
2700
|
// =========================================================================
|
|
2210
2701
|
|
|
2211
2702
|
private openInBrowser(urlOrPath: string): void {
|
|
2212
|
-
|
|
2213
|
-
|
|
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
|
+
}
|
|
2214
2714
|
}
|
|
2215
2715
|
|
|
2216
2716
|
private async handleExportCommand(text: string): Promise<void> {
|
|
@@ -2379,7 +2879,7 @@ export class InteractiveMode {
|
|
|
2379
2879
|
const stats = this.session.getSessionStats();
|
|
2380
2880
|
|
|
2381
2881
|
let info = `${theme.bold("Session Info")}\n\n`;
|
|
2382
|
-
info += `${theme.fg("dim", "File:")} ${stats.sessionFile}\n`;
|
|
2882
|
+
info += `${theme.fg("dim", "File:")} ${stats.sessionFile ?? "In-memory"}\n`;
|
|
2383
2883
|
info += `${theme.fg("dim", "ID:")} ${stats.sessionId}\n\n`;
|
|
2384
2884
|
info += `${theme.bold("Messages")}\n`;
|
|
2385
2885
|
info += `${theme.fg("dim", "User:")} ${stats.userMessages}\n`;
|
|
@@ -2429,6 +2929,31 @@ export class InteractiveMode {
|
|
|
2429
2929
|
this.ui.requestRender();
|
|
2430
2930
|
}
|
|
2431
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
|
+
|
|
2432
2957
|
private handleHotkeysCommand(): void {
|
|
2433
2958
|
const hotkeys = `
|
|
2434
2959
|
**Navigation**
|
|
@@ -2458,6 +2983,7 @@ export class InteractiveMode {
|
|
|
2458
2983
|
| \`Ctrl+Z\` | Suspend to background |
|
|
2459
2984
|
| \`Shift+Tab\` | Cycle thinking level |
|
|
2460
2985
|
| \`Ctrl+P\` | Cycle models |
|
|
2986
|
+
| \`Ctrl+Y\` | Cycle role models (slow/default/smol) |
|
|
2461
2987
|
| \`Ctrl+O\` | Toggle tool output expansion |
|
|
2462
2988
|
| \`Ctrl+T\` | Toggle thinking block visibility |
|
|
2463
2989
|
| \`Ctrl+G\` | Edit message in external editor |
|
|
@@ -2544,9 +3070,9 @@ export class InteractiveMode {
|
|
|
2544
3070
|
this.ui.requestRender();
|
|
2545
3071
|
}
|
|
2546
3072
|
|
|
2547
|
-
private async handleBashCommand(command: string): Promise<void> {
|
|
3073
|
+
private async handleBashCommand(command: string, excludeFromContext = false): Promise<void> {
|
|
2548
3074
|
const isDeferred = this.session.isStreaming;
|
|
2549
|
-
this.bashComponent = new BashExecutionComponent(command, this.ui);
|
|
3075
|
+
this.bashComponent = new BashExecutionComponent(command, this.ui, excludeFromContext);
|
|
2550
3076
|
|
|
2551
3077
|
if (isDeferred) {
|
|
2552
3078
|
// Show in pending area when agent is streaming
|
|
@@ -2559,12 +3085,16 @@ export class InteractiveMode {
|
|
|
2559
3085
|
this.ui.requestRender();
|
|
2560
3086
|
|
|
2561
3087
|
try {
|
|
2562
|
-
const result = await this.session.executeBash(
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
this.
|
|
2566
|
-
|
|
2567
|
-
|
|
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
|
+
);
|
|
2568
3098
|
|
|
2569
3099
|
if (this.bashComponent) {
|
|
2570
3100
|
this.bashComponent.setComplete(
|