@kolisachint/hoocode-agent 0.4.106 → 0.4.108
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 +4 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +4 -4
- package/dist/cli/args.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +0 -1
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -2
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts +1 -3
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +2 -2
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/bash-executor.d.ts.map +1 -1
- package/dist/core/bash-executor.js +1 -1
- package/dist/core/bash-executor.js.map +1 -1
- package/dist/core/extension-mcp-servers.d.ts +1 -1
- package/dist/core/extension-mcp-servers.d.ts.map +1 -1
- package/dist/core/extension-mcp-servers.js +1 -1
- package/dist/core/extension-mcp-servers.js.map +1 -1
- package/dist/core/extensions/plugins/index.d.ts.map +1 -1
- package/dist/core/extensions/plugins/index.js +1 -1
- package/dist/core/extensions/plugins/index.js.map +1 -1
- package/dist/core/extensions/types.d.ts +1 -3
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/mode-prompts.d.ts +1 -1
- package/dist/core/mode-prompts.d.ts.map +1 -1
- package/dist/core/mode-prompts.js +1 -1
- package/dist/core/mode-prompts.js.map +1 -1
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +1 -1
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +1 -2
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager.d.ts +2 -97
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +4 -65
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +2 -3
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/telemetry.d.ts.map +1 -1
- package/dist/core/telemetry.js +1 -1
- package/dist/core/telemetry.js.map +1 -1
- package/dist/core/timings.d.ts +1 -1
- package/dist/core/timings.d.ts.map +1 -1
- package/dist/core/timings.js +2 -2
- package/dist/core/timings.js.map +1 -1
- package/dist/core/tools/{browser-continue.d.ts → browser/browser-continue.d.ts} +1 -1
- package/dist/core/tools/browser/browser-continue.d.ts.map +1 -0
- package/dist/core/tools/{browser-continue.js → browser/browser-continue.js} +2 -2
- package/dist/core/tools/browser/browser-continue.js.map +1 -0
- package/dist/core/tools/{browser-run.d.ts → browser/browser-run.d.ts} +2 -2
- package/dist/core/tools/browser/browser-run.d.ts.map +1 -0
- package/dist/core/tools/{browser-run.js → browser/browser-run.js} +2 -2
- package/dist/core/tools/browser/browser-run.js.map +1 -0
- package/dist/core/tools/browser/browsertools-shared.d.ts.map +1 -0
- package/dist/core/tools/{browsertools-shared.js → browser/browsertools-shared.js} +1 -1
- package/dist/core/tools/browser/browsertools-shared.js.map +1 -0
- package/dist/core/tools/browser/index.d.ts +8 -0
- package/dist/core/tools/browser/index.d.ts.map +1 -0
- package/dist/core/tools/browser/index.js +8 -0
- package/dist/core/tools/browser/index.js.map +1 -0
- package/dist/core/tools/{docedit.d.ts → doc/docedit.d.ts} +2 -2
- package/dist/core/tools/doc/docedit.d.ts.map +1 -0
- package/dist/core/tools/{docedit.js → doc/docedit.js} +7 -7
- package/dist/core/tools/doc/docedit.js.map +1 -0
- package/dist/core/tools/{docgrep.d.ts → doc/docgrep.d.ts} +2 -2
- package/dist/core/tools/doc/docgrep.d.ts.map +1 -0
- package/dist/core/tools/{docgrep.js → doc/docgrep.js} +6 -6
- package/dist/core/tools/doc/docgrep.js.map +1 -0
- package/dist/core/tools/{docpeek.d.ts → doc/docpeek.d.ts} +2 -2
- package/dist/core/tools/{docpeek.js.map → doc/docpeek.d.ts.map} +1 -1
- package/dist/core/tools/{docpeek.js → doc/docpeek.js} +6 -6
- package/dist/core/tools/doc/docpeek.js.map +1 -0
- package/dist/core/tools/{docread.d.ts → doc/docread.d.ts} +2 -2
- package/dist/core/tools/doc/docread.d.ts.map +1 -0
- package/dist/core/tools/{docread.js → doc/docread.js} +6 -6
- package/dist/core/tools/doc/docread.js.map +1 -0
- package/dist/core/tools/{docscan.d.ts → doc/docscan.d.ts} +2 -2
- package/dist/core/tools/doc/docscan.d.ts.map +1 -0
- package/dist/core/tools/{docscan.js → doc/docscan.js} +6 -6
- package/dist/core/tools/doc/docscan.js.map +1 -0
- package/dist/core/tools/{docwrite.d.ts → doc/docwrite.d.ts} +1 -1
- package/dist/core/tools/doc/docwrite.d.ts.map +1 -0
- package/dist/core/tools/{docwrite.js → doc/docwrite.js} +6 -6
- package/dist/core/tools/doc/docwrite.js.map +1 -0
- package/dist/core/tools/doc/index.d.ts +11 -0
- package/dist/core/tools/doc/index.d.ts.map +1 -0
- package/dist/core/tools/doc/index.js +11 -0
- package/dist/core/tools/doc/index.js.map +1 -0
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +1 -1
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/index.d.ts +42 -28
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +61 -199
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/output-accumulator.d.ts.map +1 -1
- package/dist/core/tools/output-accumulator.js +1 -1
- package/dist/core/tools/output-accumulator.js.map +1 -1
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +1 -1
- package/dist/core/tools/read.js.map +1 -1
- package/dist/extensions/core/ask-options.d.ts +10 -0
- package/dist/extensions/core/ask-options.d.ts.map +1 -0
- package/dist/extensions/core/ask-options.js +83 -0
- package/dist/extensions/core/ask-options.js.map +1 -0
- package/dist/extensions/core/config.d.ts +80 -0
- package/dist/extensions/core/config.d.ts.map +1 -0
- package/dist/extensions/core/config.js +110 -0
- package/dist/extensions/core/config.js.map +1 -0
- package/dist/extensions/core/cost.d.ts +11 -0
- package/dist/extensions/core/cost.d.ts.map +1 -0
- package/dist/extensions/core/cost.js +64 -0
- package/dist/extensions/core/cost.js.map +1 -0
- package/dist/extensions/core/hoo-core.d.ts +16 -179
- package/dist/extensions/core/hoo-core.d.ts.map +1 -1
- package/dist/extensions/core/hoo-core.js +27 -1663
- package/dist/extensions/core/hoo-core.js.map +1 -1
- package/dist/extensions/core/loop.d.ts +15 -0
- package/dist/extensions/core/loop.d.ts.map +1 -0
- package/dist/extensions/core/loop.js +235 -0
- package/dist/extensions/core/loop.js.map +1 -0
- package/dist/extensions/core/marketplace.d.ts +17 -0
- package/dist/extensions/core/marketplace.d.ts.map +1 -0
- package/dist/extensions/core/marketplace.js +167 -0
- package/dist/extensions/core/marketplace.js.map +1 -0
- package/dist/extensions/core/mcp-loader.d.ts +26 -0
- package/dist/extensions/core/mcp-loader.d.ts.map +1 -0
- package/dist/extensions/core/mcp-loader.js +391 -0
- package/dist/extensions/core/mcp-loader.js.map +1 -0
- package/dist/extensions/core/modes.d.ts +56 -0
- package/dist/extensions/core/modes.d.ts.map +1 -0
- package/dist/extensions/core/modes.js +268 -0
- package/dist/extensions/core/modes.js.map +1 -0
- package/dist/extensions/core/permission-gate.d.ts +10 -0
- package/dist/extensions/core/permission-gate.d.ts.map +1 -0
- package/dist/extensions/core/permission-gate.js +167 -0
- package/dist/extensions/core/permission-gate.js.map +1 -0
- package/dist/extensions/core/scaffold.d.ts +9 -0
- package/dist/extensions/core/scaffold.d.ts.map +1 -0
- package/dist/extensions/core/scaffold.js +146 -0
- package/dist/extensions/core/scaffold.js.map +1 -0
- package/dist/extensions/core/thinking-escalation.d.ts +18 -0
- package/dist/extensions/core/thinking-escalation.d.ts.map +1 -0
- package/dist/extensions/core/thinking-escalation.js +70 -0
- package/dist/extensions/core/thinking-escalation.js.map +1 -0
- package/dist/index.d.ts +2 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -4
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +2 -2
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/bash-execution-controller.d.ts +34 -0
- package/dist/modes/interactive/bash-execution-controller.d.ts.map +1 -0
- package/dist/modes/interactive/bash-execution-controller.js +98 -0
- package/dist/modes/interactive/bash-execution-controller.js.map +1 -0
- package/dist/modes/interactive/command-executor.d.ts +0 -5
- package/dist/modes/interactive/command-executor.d.ts.map +1 -1
- package/dist/modes/interactive/command-executor.js +0 -7
- package/dist/modes/interactive/command-executor.js.map +1 -1
- package/dist/modes/interactive/components/branch-summary-message.d.ts +1 -1
- package/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/branch-summary-message.js.map +1 -1
- package/dist/modes/interactive/components/compaction-summary-message.d.ts +1 -1
- package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
- package/dist/modes/interactive/components/custom-message.d.ts +1 -1
- package/dist/modes/interactive/components/custom-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-message.js.map +1 -1
- package/dist/modes/interactive/components/index.d.ts +0 -2
- package/dist/modes/interactive/components/index.d.ts.map +1 -1
- package/dist/modes/interactive/components/index.js +0 -2
- package/dist/modes/interactive/components/index.js.map +1 -1
- package/dist/modes/interactive/extension-chrome.d.ts +56 -0
- package/dist/modes/interactive/extension-chrome.d.ts.map +1 -0
- package/dist/modes/interactive/extension-chrome.js +172 -0
- package/dist/modes/interactive/extension-chrome.js.map +1 -0
- package/dist/modes/interactive/extension-dialogs.d.ts +82 -0
- package/dist/modes/interactive/extension-dialogs.d.ts.map +1 -0
- package/dist/modes/interactive/extension-dialogs.js +259 -0
- package/dist/modes/interactive/extension-dialogs.js.map +1 -0
- package/dist/modes/interactive/interactive-mode.d.ts +16 -206
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +359 -2370
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/login-controller.d.ts +50 -0
- package/dist/modes/interactive/login-controller.d.ts.map +1 -0
- package/dist/modes/interactive/login-controller.js +352 -0
- package/dist/modes/interactive/login-controller.js.map +1 -0
- package/dist/modes/interactive/message-queue-controller.d.ts +49 -0
- package/dist/modes/interactive/message-queue-controller.d.ts.map +1 -0
- package/dist/modes/interactive/message-queue-controller.js +184 -0
- package/dist/modes/interactive/message-queue-controller.js.map +1 -0
- package/dist/modes/interactive/model-controller.d.ts +43 -0
- package/dist/modes/interactive/model-controller.d.ts.map +1 -0
- package/dist/modes/interactive/model-controller.js +184 -0
- package/dist/modes/interactive/model-controller.js.map +1 -0
- package/dist/modes/interactive/resource-display.d.ts +59 -0
- package/dist/modes/interactive/resource-display.d.ts.map +1 -0
- package/dist/modes/interactive/resource-display.js +499 -0
- package/dist/modes/interactive/resource-display.js.map +1 -0
- package/dist/modes/interactive/startup-checks.d.ts +17 -0
- package/dist/modes/interactive/startup-checks.d.ts.map +1 -0
- package/dist/modes/interactive/startup-checks.js +102 -0
- package/dist/modes/interactive/startup-checks.js.map +1 -0
- package/dist/modes/interactive/team-focus.d.ts +56 -0
- package/dist/modes/interactive/team-focus.d.ts.map +1 -0
- package/dist/modes/interactive/team-focus.js +167 -0
- package/dist/modes/interactive/team-focus.js.map +1 -0
- package/dist/modes/interactive/voice/voice-controller.d.ts +66 -0
- package/dist/modes/interactive/voice/voice-controller.d.ts.map +1 -0
- package/dist/modes/interactive/voice/voice-controller.js +312 -0
- package/dist/modes/interactive/voice/voice-controller.js.map +1 -0
- package/dist/modes/interactive/voice/voice-panel.d.ts.map +1 -0
- package/dist/modes/interactive/voice/voice-panel.js.map +1 -0
- package/dist/modes/interactive/voice/voice-transcribe.d.ts.map +1 -0
- package/dist/modes/interactive/voice/voice-transcribe.js.map +1 -0
- package/dist/modes/rpc/rpc-client.d.ts +1 -2
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +1 -2
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/resources-cli.d.ts.map +1 -1
- package/dist/resources-cli.js +1 -1
- package/dist/resources-cli.js.map +1 -1
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +1 -1
- package/dist/utils/tools-manager.js.map +1 -1
- package/dist/utils/version-check.d.ts.map +1 -1
- package/dist/utils/version-check.js +2 -2
- package/dist/utils/version-check.js.map +1 -1
- package/docs/project-local-resources.md +1 -1
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +4 -4
- package/dist/core/compaction/branch-summarization.d.ts +0 -88
- package/dist/core/compaction/branch-summarization.d.ts.map +0 -1
- package/dist/core/compaction/branch-summarization.js +0 -243
- package/dist/core/compaction/branch-summarization.js.map +0 -1
- package/dist/core/compaction/compaction.d.ts +0 -123
- package/dist/core/compaction/compaction.d.ts.map +0 -1
- package/dist/core/compaction/compaction.js +0 -635
- package/dist/core/compaction/compaction.js.map +0 -1
- package/dist/core/compaction/index.d.ts +0 -7
- package/dist/core/compaction/index.d.ts.map +0 -1
- package/dist/core/compaction/index.js +0 -7
- package/dist/core/compaction/index.js.map +0 -1
- package/dist/core/compaction/utils.d.ts +0 -38
- package/dist/core/compaction/utils.d.ts.map +0 -1
- package/dist/core/compaction/utils.js +0 -156
- package/dist/core/compaction/utils.js.map +0 -1
- package/dist/core/messages.d.ts +0 -124
- package/dist/core/messages.d.ts.map +0 -1
- package/dist/core/messages.js +0 -221
- package/dist/core/messages.js.map +0 -1
- package/dist/core/tools/browser-continue.d.ts.map +0 -1
- package/dist/core/tools/browser-continue.js.map +0 -1
- package/dist/core/tools/browser-run.d.ts.map +0 -1
- package/dist/core/tools/browser-run.js.map +0 -1
- package/dist/core/tools/browsertools-shared.d.ts.map +0 -1
- package/dist/core/tools/browsertools-shared.js.map +0 -1
- package/dist/core/tools/docedit.d.ts.map +0 -1
- package/dist/core/tools/docedit.js.map +0 -1
- package/dist/core/tools/docgrep.d.ts.map +0 -1
- package/dist/core/tools/docgrep.js.map +0 -1
- package/dist/core/tools/docpeek.d.ts.map +0 -1
- package/dist/core/tools/docread.d.ts.map +0 -1
- package/dist/core/tools/docread.js.map +0 -1
- package/dist/core/tools/docscan.d.ts.map +0 -1
- package/dist/core/tools/docscan.js.map +0 -1
- package/dist/core/tools/docwrite.d.ts.map +0 -1
- package/dist/core/tools/docwrite.js.map +0 -1
- package/dist/core/tools/output-compression.d.ts +0 -53
- package/dist/core/tools/output-compression.d.ts.map +0 -1
- package/dist/core/tools/output-compression.js +0 -392
- package/dist/core/tools/output-compression.js.map +0 -1
- package/dist/modes/interactive/components/armin.d.ts +0 -34
- package/dist/modes/interactive/components/armin.d.ts.map +0 -1
- package/dist/modes/interactive/components/armin.js +0 -333
- package/dist/modes/interactive/components/armin.js.map +0 -1
- package/dist/modes/interactive/components/daxnuts.d.ts +0 -23
- package/dist/modes/interactive/components/daxnuts.d.ts.map +0 -1
- package/dist/modes/interactive/components/daxnuts.js +0 -140
- package/dist/modes/interactive/components/daxnuts.js.map +0 -1
- package/dist/modes/interactive/components/voice-panel.d.ts.map +0 -1
- package/dist/modes/interactive/components/voice-panel.js.map +0 -1
- package/dist/modes/interactive/voice-transcribe.d.ts.map +0 -1
- package/dist/modes/interactive/voice-transcribe.js.map +0 -1
- /package/dist/core/tools/{browsertools-shared.d.ts → browser/browsertools-shared.d.ts} +0 -0
- /package/dist/modes/interactive/{components → voice}/voice-panel.d.ts +0 -0
- /package/dist/modes/interactive/{components → voice}/voice-panel.js +0 -0
- /package/dist/modes/interactive/{voice-transcribe.d.ts → voice/voice-transcribe.d.ts} +0 -0
- /package/dist/modes/interactive/{voice-transcribe.js → voice/voice-transcribe.js} +0 -0
|
@@ -6,32 +6,25 @@ import * as crypto from "node:crypto";
|
|
|
6
6
|
import * as fs from "node:fs";
|
|
7
7
|
import * as os from "node:os";
|
|
8
8
|
import * as path from "node:path";
|
|
9
|
-
import {
|
|
10
|
-
import { CombinedAutocompleteProvider, Container, fuzzyFilter, getCapabilities, hyperlink, Loader, Markdown, matchesKey, ProcessTerminal, Spacer, setKeybindings, Text,
|
|
11
|
-
import {
|
|
12
|
-
import { APP_NAME, APP_TITLE,
|
|
9
|
+
import { createCompactionSummaryMessage } from "@kolisachint/hoocode-agent-core";
|
|
10
|
+
import { CombinedAutocompleteProvider, Container, fuzzyFilter, getCapabilities, hyperlink, Loader, Markdown, matchesKey, ProcessTerminal, Spacer, setKeybindings, Text, TUI, } from "@kolisachint/hoocode-tui";
|
|
11
|
+
import { spawnSync } from "child_process";
|
|
12
|
+
import { APP_NAME, APP_TITLE, VERSION } from "../../config.js";
|
|
13
13
|
import { parseSkillBlock } from "../../core/agent-session.js";
|
|
14
14
|
import { FooterDataProvider } from "../../core/footer-data-provider.js";
|
|
15
15
|
import { KeybindingsManager } from "../../core/keybindings.js";
|
|
16
|
-
import { createCompactionSummaryMessage } from "../../core/messages.js";
|
|
17
|
-
import { defaultModelPerProvider, findExactModelReferenceMatch, resolveModelScope } from "../../core/model-resolver.js";
|
|
18
|
-
import { DefaultPackageManager } from "../../core/package-manager.js";
|
|
19
|
-
import { BUILT_IN_PROVIDER_DISPLAY_NAMES } from "../../core/provider-display-names.js";
|
|
20
16
|
import { formatMissingSessionCwdPrompt, MissingSessionCwdError } from "../../core/session-cwd.js";
|
|
21
17
|
import { SessionManager } from "../../core/session-manager.js";
|
|
22
18
|
import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.js";
|
|
23
19
|
import { taskStore } from "../../core/task-store.js";
|
|
24
|
-
import { TeamApprovalCoordinator } from "../../core/team-approvals.js";
|
|
25
20
|
import { buildCompactWordmark } from "../../core/wordmark.js";
|
|
26
|
-
import { getChangelogPath, getNewEntries, parseChangelog } from "../../utils/changelog.js";
|
|
27
21
|
import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.js";
|
|
28
22
|
import { parseGitUrl } from "../../utils/git.js";
|
|
29
|
-
import { getCwdRelativePath } from "../../utils/paths.js";
|
|
30
23
|
import { killTrackedDetachedChildren } from "../../utils/shell.js";
|
|
31
24
|
import { ensureTool } from "../../utils/tools-manager.js";
|
|
32
25
|
import { checkForNewHooCodeVersion } from "../../utils/version-check.js";
|
|
26
|
+
import { BashExecutionController } from "./bash-execution-controller.js";
|
|
33
27
|
import { CommandExecutor } from "./command-executor.js";
|
|
34
|
-
import { AskOptionsComponent } from "./components/ask-options.js";
|
|
35
28
|
import { AssistantMessageComponent } from "./components/assistant-message.js";
|
|
36
29
|
import { BashExecutionComponent } from "./components/bash-execution.js";
|
|
37
30
|
import { BranchSummaryMessageComponent } from "./components/branch-summary-message.js";
|
|
@@ -39,44 +32,28 @@ import { CompactionSummaryMessageComponent } from "./components/compaction-summa
|
|
|
39
32
|
import { CountdownTimer } from "./components/countdown-timer.js";
|
|
40
33
|
import { CustomEditor } from "./components/custom-editor.js";
|
|
41
34
|
import { CustomMessageComponent } from "./components/custom-message.js";
|
|
42
|
-
import { DaxnutsComponent } from "./components/daxnuts.js";
|
|
43
35
|
import { DynamicBorder } from "./components/dynamic-border.js";
|
|
44
|
-
import { ExtensionEditorComponent } from "./components/extension-editor.js";
|
|
45
|
-
import { ExtensionInputComponent } from "./components/extension-input.js";
|
|
46
|
-
import { ExtensionSelectorComponent } from "./components/extension-selector.js";
|
|
47
36
|
import { FooterComponent } from "./components/footer.js";
|
|
48
|
-
import {
|
|
49
|
-
import { LoginDialogComponent } from "./components/login-dialog.js";
|
|
50
|
-
import { ModelSelectorComponent } from "./components/model-selector.js";
|
|
51
|
-
import { OAuthSelectorComponent } from "./components/oauth-selector.js";
|
|
52
|
-
import { ScopedModelsSelectorComponent } from "./components/scoped-models-selector.js";
|
|
37
|
+
import { keyHint, keyText, rawKeyHint } from "./components/keybinding-hints.js";
|
|
53
38
|
import { SessionSelectorComponent } from "./components/session-selector.js";
|
|
54
39
|
import { SettingsSelectorComponent } from "./components/settings-selector.js";
|
|
55
40
|
import { SkillInvocationMessageComponent } from "./components/skill-invocation-message.js";
|
|
56
41
|
import { TaskPanelComponent } from "./components/task-panel.js";
|
|
57
|
-
import { TeamAttachPanelComponent } from "./components/team-attach-panel.js";
|
|
58
42
|
import { ToolExecutionComponent } from "./components/tool-execution.js";
|
|
59
43
|
import { TreeSelectorComponent } from "./components/tree-selector.js";
|
|
60
44
|
import { UserMessageComponent } from "./components/user-message.js";
|
|
61
45
|
import { UserMessageSelectorComponent } from "./components/user-message-selector.js";
|
|
62
|
-
import {
|
|
46
|
+
import { ExtensionChrome } from "./extension-chrome.js";
|
|
47
|
+
import { ExtensionDialogs } from "./extension-dialogs.js";
|
|
48
|
+
import { LoginController } from "./login-controller.js";
|
|
49
|
+
import { MessageQueueController } from "./message-queue-controller.js";
|
|
50
|
+
import { ModelController } from "./model-controller.js";
|
|
51
|
+
import { ExpandableText, formatDisplayPath, isExpandable, showLoadedResources as renderLoadedResources, } from "./resource-display.js";
|
|
52
|
+
import { checkForPackageUpdates, checkTmuxKeyboardSetup, getChangelogForDisplay } from "./startup-checks.js";
|
|
53
|
+
import { TeamFocusController } from "./team-focus.js";
|
|
63
54
|
import { getAvailableThemes, getAvailableThemesWithPaths, getEditorTheme, getMarkdownTheme, getThemeByName, initTheme, onThemeChange, setRegisteredThemes, setTheme, setThemeInstance, stopThemeWatcher, Theme, theme, } from "./theme/theme.js";
|
|
64
|
-
import {
|
|
65
|
-
|
|
66
|
-
return typeof obj === "object" && obj !== null && "setExpanded" in obj && typeof obj.setExpanded === "function";
|
|
67
|
-
}
|
|
68
|
-
class ExpandableText extends Text {
|
|
69
|
-
getCollapsedText;
|
|
70
|
-
getExpandedText;
|
|
71
|
-
constructor(getCollapsedText, getExpandedText, expanded = false, paddingX = 0, paddingY = 0) {
|
|
72
|
-
super(expanded ? getExpandedText() : getCollapsedText(), paddingX, paddingY);
|
|
73
|
-
this.getCollapsedText = getCollapsedText;
|
|
74
|
-
this.getExpandedText = getExpandedText;
|
|
75
|
-
}
|
|
76
|
-
setExpanded(expanded) {
|
|
77
|
-
this.setText(expanded ? this.getExpandedText() : this.getCollapsedText());
|
|
78
|
-
}
|
|
79
|
-
}
|
|
55
|
+
import { VoiceController } from "./voice/voice-controller.js";
|
|
56
|
+
/** Interface for components that can be expanded/collapsed */
|
|
80
57
|
const DEAD_TERMINAL_ERROR_CODES = new Set(["EIO", "EPIPE", "ENOTCONN"]);
|
|
81
58
|
function isDeadTerminalError(error) {
|
|
82
59
|
if (!error || typeof error !== "object" || !("code" in error)) {
|
|
@@ -85,39 +62,6 @@ function isDeadTerminalError(error) {
|
|
|
85
62
|
const code = error.code;
|
|
86
63
|
return code !== undefined && DEAD_TERMINAL_ERROR_CODES.has(code);
|
|
87
64
|
}
|
|
88
|
-
// Trailing-silence window: how long a pause while speaking lasts before the
|
|
89
|
-
// capture auto-stops. Passed to `voicetools serve` via `--silence-ms` (see
|
|
90
|
-
// VoiceDaemon.spawn) so the binary's real cutoff matches the on-screen
|
|
91
|
-
// countdown this same value drives. Kept generous so a thinking pause mid-
|
|
92
|
-
// sentence doesn't cut the user off (the binary's own default is 600ms).
|
|
93
|
-
const VOICE_SILENCE_MS = 3000;
|
|
94
|
-
/** How long to keep the warm voice model in memory after the last capture
|
|
95
|
-
* completes. The daemon auto-shuts down after this window, releasing the
|
|
96
|
-
* ~900 MB resident model; the next ctrl+r pays a cold-start respawn cost. */
|
|
97
|
-
const VOICE_IDLE_TIMEOUT_MS = 60_000;
|
|
98
|
-
const VOICE_UNAVAILABLE_MESSAGE = "Voice input failed: voicetools binary unavailable and could not be downloaded. " +
|
|
99
|
-
"Install it, set VOICETOOLS_BIN, or ensure a published release exists for this platform.";
|
|
100
|
-
const ANTHROPIC_SUBSCRIPTION_AUTH_WARNING = "Anthropic subscription auth: billed per token as extra usage, not plan limits.";
|
|
101
|
-
function isAnthropicSubscriptionAuthKey(apiKey) {
|
|
102
|
-
return typeof apiKey === "string" && apiKey.startsWith("sk-ant-oat");
|
|
103
|
-
}
|
|
104
|
-
function isUnknownModel(model) {
|
|
105
|
-
return !!model && model.provider === "unknown" && model.id === "unknown" && model.api === "unknown";
|
|
106
|
-
}
|
|
107
|
-
function hasDefaultModelProvider(providerId) {
|
|
108
|
-
return providerId in defaultModelPerProvider;
|
|
109
|
-
}
|
|
110
|
-
const BEDROCK_PROVIDER_ID = "amazon-bedrock";
|
|
111
|
-
const BUILT_IN_MODEL_PROVIDERS = new Set(getProviders());
|
|
112
|
-
export function isApiKeyLoginProvider(providerId, oauthProviderIds, builtInProviderIds = BUILT_IN_MODEL_PROVIDERS) {
|
|
113
|
-
if (BUILT_IN_PROVIDER_DISPLAY_NAMES[providerId]) {
|
|
114
|
-
return true;
|
|
115
|
-
}
|
|
116
|
-
if (builtInProviderIds.has(providerId)) {
|
|
117
|
-
return false;
|
|
118
|
-
}
|
|
119
|
-
return !oauthProviderIds.has(providerId);
|
|
120
|
-
}
|
|
121
65
|
export class InteractiveMode {
|
|
122
66
|
options;
|
|
123
67
|
runtimeHost;
|
|
@@ -128,19 +72,7 @@ export class InteractiveMode {
|
|
|
128
72
|
defaultEditor;
|
|
129
73
|
editor;
|
|
130
74
|
editorComponentFactory;
|
|
131
|
-
|
|
132
|
-
// Persistent `voicetools serve` process, once probed successfully. Stays alive
|
|
133
|
-
// (and warm) across captures for the life of the interactive session.
|
|
134
|
-
voiceDaemon;
|
|
135
|
-
// Sticky for the session: set once `serve` is confirmed unsupported (old binary)
|
|
136
|
-
// so later presses skip straight to the per-press `transcribe` fallback.
|
|
137
|
-
voiceDaemonUnsupported = false;
|
|
138
|
-
/** True while the voicetools binary is being resolved/downloaded before a session starts. */
|
|
139
|
-
voiceStarting = false;
|
|
140
|
-
/** True while a capture (daemon or legacy) is in flight. */
|
|
141
|
-
voiceActive = false;
|
|
142
|
-
/** The live multi-line status panel for the current capture (undefined when idle). */
|
|
143
|
-
voicePanel;
|
|
75
|
+
voice;
|
|
144
76
|
autocompleteProvider;
|
|
145
77
|
autocompleteProviderWrappers = [];
|
|
146
78
|
fdPath;
|
|
@@ -163,7 +95,6 @@ export class InteractiveMode {
|
|
|
163
95
|
lastEscapeTime = 0;
|
|
164
96
|
changelogMarkdown = undefined;
|
|
165
97
|
startupNoticesShown = false;
|
|
166
|
-
anthropicSubscriptionWarningShown = false;
|
|
167
98
|
// Status line tracking (for mutating immediately-sequential status updates)
|
|
168
99
|
lastStatusSpacer = undefined;
|
|
169
100
|
lastStatusText = undefined;
|
|
@@ -185,10 +116,6 @@ export class InteractiveMode {
|
|
|
185
116
|
signalCleanupHandlers = [];
|
|
186
117
|
// Track if editor is in bash mode (text starts with !)
|
|
187
118
|
isBashMode = false;
|
|
188
|
-
// Track current bash execution component
|
|
189
|
-
bashComponent = undefined;
|
|
190
|
-
// Track pending bash components (shown in pending area, moved to chat on submit)
|
|
191
|
-
pendingBashComponents = [];
|
|
192
119
|
// Auto-compaction state
|
|
193
120
|
autoCompactionLoader = undefined;
|
|
194
121
|
autoCompactionEscapeHandler;
|
|
@@ -196,35 +123,31 @@ export class InteractiveMode {
|
|
|
196
123
|
retryLoader = undefined;
|
|
197
124
|
retryCountdown = undefined;
|
|
198
125
|
retryEscapeHandler;
|
|
199
|
-
// Messages queued while compaction is running
|
|
200
|
-
compactionQueuedMessages = [];
|
|
201
126
|
// Shutdown state
|
|
202
127
|
shutdownRequested = false;
|
|
203
128
|
// Extension UI state
|
|
204
|
-
|
|
205
|
-
extensionInput = undefined;
|
|
206
|
-
askOptions = undefined;
|
|
207
|
-
extensionEditor = undefined;
|
|
129
|
+
dialogs;
|
|
208
130
|
extensionTerminalInputUnsubscribers = new Set();
|
|
209
131
|
// Extension widgets (components rendered above/below the editor)
|
|
210
|
-
|
|
211
|
-
extensionWidgetsBelow = new Map();
|
|
132
|
+
chrome;
|
|
212
133
|
widgetContainerAbove;
|
|
213
134
|
widgetContainerBelow;
|
|
214
135
|
// Task panel shown just above the editor (active subagent tasks)
|
|
215
136
|
taskPanel;
|
|
216
137
|
// hooteams team client (--team): steering + attach share its single SSE stream
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
//
|
|
221
|
-
|
|
138
|
+
teamFocus;
|
|
139
|
+
// Bash command execution (the `!cmd` prompt mode)
|
|
140
|
+
bashExecution;
|
|
141
|
+
// Message queueing (compaction queue + pending-messages display)
|
|
142
|
+
messageQueue;
|
|
143
|
+
// Model selection (/model, /models, cycle keys)
|
|
144
|
+
modelController;
|
|
145
|
+
// Auth / login flows (/login, /logout)
|
|
146
|
+
loginController;
|
|
222
147
|
// Header container that holds the built-in or custom header
|
|
223
148
|
headerContainer;
|
|
224
149
|
// Built-in header (logo + keybinding hints + changelog)
|
|
225
150
|
builtInHeader = undefined;
|
|
226
|
-
// Custom header from extension (undefined = use built-in header)
|
|
227
|
-
customHeader = undefined;
|
|
228
151
|
// Convenience accessors
|
|
229
152
|
get session() {
|
|
230
153
|
return this.runtimeHost.session;
|
|
@@ -277,11 +200,10 @@ export class InteractiveMode {
|
|
|
277
200
|
rebuildChatFromMessages: () => self.rebuildChatFromMessages(),
|
|
278
201
|
getMarkdownThemeWithSettings: () => self.getMarkdownThemeWithSettings(),
|
|
279
202
|
stopLoadingAnimation: () => self.stopLoadingAnimation(),
|
|
280
|
-
findExactModelMatch: (searchTerm) => self.findExactModelMatch(searchTerm),
|
|
281
|
-
maybeWarnAboutAnthropicSubscriptionAuth: (model) => self.maybeWarnAboutAnthropicSubscriptionAuth(model),
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
showExtensionConfirm: (title, message) => self.showExtensionConfirm(title, message),
|
|
203
|
+
findExactModelMatch: (searchTerm) => self.modelController.findExactModelMatch(searchTerm),
|
|
204
|
+
maybeWarnAboutAnthropicSubscriptionAuth: (model) => self.modelController.maybeWarnAboutAnthropicSubscriptionAuth(model),
|
|
205
|
+
showModelSelector: (searchTerm) => self.modelController.showModelSelector(searchTerm),
|
|
206
|
+
showExtensionConfirm: (title, message) => self.dialogs.confirm(title, message),
|
|
285
207
|
promptForMissingSessionCwd: (error) => self.promptForMissingSessionCwd(error),
|
|
286
208
|
handleFatalRuntimeError: (prefix, error) => self.handleFatalRuntimeError(prefix, error),
|
|
287
209
|
};
|
|
@@ -338,10 +260,89 @@ export class InteractiveMode {
|
|
|
338
260
|
this.footer = new FooterComponent(this.session, this.footerDataProvider);
|
|
339
261
|
this.footer.setAutoCompactEnabled(this.session.autoCompactionEnabled);
|
|
340
262
|
this.footerDataProvider.setSubagentEnabled(this.session.getActiveToolNames().includes("Task"));
|
|
263
|
+
this.chrome = new ExtensionChrome({
|
|
264
|
+
ui: this.ui,
|
|
265
|
+
widgetContainerAbove: this.widgetContainerAbove,
|
|
266
|
+
widgetContainerBelow: this.widgetContainerBelow,
|
|
267
|
+
headerContainer: this.headerContainer,
|
|
268
|
+
footer: this.footer,
|
|
269
|
+
footerDataProvider: this.footerDataProvider,
|
|
270
|
+
getBuiltInHeader: () => this.builtInHeader,
|
|
271
|
+
isToolOutputExpanded: () => this.toolOutputExpanded,
|
|
272
|
+
});
|
|
341
273
|
this.taskPanel = new TaskPanelComponent(this.ui);
|
|
342
|
-
this.
|
|
343
|
-
|
|
344
|
-
|
|
274
|
+
this.dialogs = new ExtensionDialogs({
|
|
275
|
+
ui: this.ui,
|
|
276
|
+
editorContainer: this.editorContainer,
|
|
277
|
+
keybindings: this.keybindings,
|
|
278
|
+
getEditor: () => this.editor,
|
|
279
|
+
});
|
|
280
|
+
this.teamFocus = new TeamFocusController({
|
|
281
|
+
ui: this.ui,
|
|
282
|
+
taskPanel: this.taskPanel,
|
|
283
|
+
editorContainer: this.editorContainer,
|
|
284
|
+
getEditor: () => this.editor,
|
|
285
|
+
showStatus: (message) => this.showStatus(message),
|
|
286
|
+
showWarning: (message) => this.showWarning(message),
|
|
287
|
+
showAskOptions: (questions, opts) => this.dialogs.showAskOptions(questions, opts),
|
|
288
|
+
isAskOptionsOpen: () => this.dialogs.isAskOptionsOpen(),
|
|
289
|
+
});
|
|
290
|
+
this.voice = new VoiceController({
|
|
291
|
+
ui: this.ui,
|
|
292
|
+
statusContainer: this.statusContainer,
|
|
293
|
+
sendEditorInput: (data) => this.editor.handleInput(data),
|
|
294
|
+
showError: (message) => this.showError(message),
|
|
295
|
+
});
|
|
296
|
+
const self = this;
|
|
297
|
+
this.bashExecution = new BashExecutionController({
|
|
298
|
+
get session() {
|
|
299
|
+
return self.session;
|
|
300
|
+
},
|
|
301
|
+
ui: this.ui,
|
|
302
|
+
pendingMessagesContainer: this.pendingMessagesContainer,
|
|
303
|
+
chatContainer: this.chatContainer,
|
|
304
|
+
showError: (message) => this.showError(message),
|
|
305
|
+
});
|
|
306
|
+
this.messageQueue = new MessageQueueController({
|
|
307
|
+
get session() {
|
|
308
|
+
return self.session;
|
|
309
|
+
},
|
|
310
|
+
getEditor: () => this.editor,
|
|
311
|
+
pendingMessagesContainer: this.pendingMessagesContainer,
|
|
312
|
+
showStatus: (message) => this.showStatus(message),
|
|
313
|
+
showError: (message) => this.showError(message),
|
|
314
|
+
});
|
|
315
|
+
this.modelController = new ModelController({
|
|
316
|
+
ui: this.ui,
|
|
317
|
+
get session() {
|
|
318
|
+
return self.session;
|
|
319
|
+
},
|
|
320
|
+
showSelector: (create) => this.showSelector(create),
|
|
321
|
+
showStatus: (message) => this.showStatus(message),
|
|
322
|
+
showError: (message) => this.showError(message),
|
|
323
|
+
showWarning: (message) => this.showWarning(message),
|
|
324
|
+
updateEditorBorderColor: () => this.updateEditorBorderColor(),
|
|
325
|
+
invalidateFooter: () => this.footer.invalidate(),
|
|
326
|
+
setAvailableProviderCount: (count) => this.footerDataProvider.setAvailableProviderCount(count),
|
|
327
|
+
});
|
|
328
|
+
this.loginController = new LoginController({
|
|
329
|
+
ui: this.ui,
|
|
330
|
+
get session() {
|
|
331
|
+
return self.session;
|
|
332
|
+
},
|
|
333
|
+
getEditor: () => this.editor,
|
|
334
|
+
editorContainer: this.editorContainer,
|
|
335
|
+
showSelector: (create) => this.showSelector(create),
|
|
336
|
+
showStatus: (message) => this.showStatus(message),
|
|
337
|
+
showError: (message) => this.showError(message),
|
|
338
|
+
updateAvailableProviderCount: () => this.modelController.updateAvailableProviderCount(),
|
|
339
|
+
updateEditorBorderColor: () => this.updateEditorBorderColor(),
|
|
340
|
+
invalidateFooter: () => this.footer.invalidate(),
|
|
341
|
+
maybeWarnAboutAnthropicSubscriptionAuth: (model) => this.modelController.maybeWarnAboutAnthropicSubscriptionAuth(model),
|
|
342
|
+
});
|
|
343
|
+
this.taskPanel.onNudge = (role) => this.teamFocus.showNudgeInput(role);
|
|
344
|
+
this.taskPanel.onAttach = (role) => this.teamFocus.showAttach(role);
|
|
345
|
+
this.taskPanel.onExitFocus = () => this.teamFocus.exitFocus();
|
|
345
346
|
// Load hide thinking block setting
|
|
346
347
|
this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
|
|
347
348
|
// Register themes from resource loader and initialize
|
|
@@ -492,7 +493,10 @@ export class InteractiveMode {
|
|
|
492
493
|
return;
|
|
493
494
|
this.registerSignalHandlers();
|
|
494
495
|
// Load changelog (only show new entries, skip for resumed sessions)
|
|
495
|
-
this.changelogMarkdown =
|
|
496
|
+
this.changelogMarkdown = getChangelogForDisplay({
|
|
497
|
+
hasMessages: this.session.state.messages.length > 0,
|
|
498
|
+
settingsManager: this.settingsManager,
|
|
499
|
+
});
|
|
496
500
|
// Ensure fd and rg are available (downloads if missing, adds to PATH via getBinDir)
|
|
497
501
|
// Both are needed: fd for autocomplete, rg for grep tool and bash commands
|
|
498
502
|
const [fdPath] = await Promise.all([ensureTool("fd"), ensureTool("rg")]);
|
|
@@ -506,7 +510,7 @@ export class InteractiveMode {
|
|
|
506
510
|
? buildCompactWordmark({
|
|
507
511
|
appName: APP_NAME,
|
|
508
512
|
version: this.version,
|
|
509
|
-
cwd:
|
|
513
|
+
cwd: formatDisplayPath(this.sessionManager.getCwd()),
|
|
510
514
|
accent: (text) => theme.fg("accent", text),
|
|
511
515
|
dim: (text) => theme.fg("dim", text),
|
|
512
516
|
muted: (text) => theme.fg("muted", text),
|
|
@@ -529,7 +533,7 @@ export class InteractiveMode {
|
|
|
529
533
|
hint("app.tools.expand", "to expand tools"),
|
|
530
534
|
hint("app.thinking.toggle", "to expand thinking"),
|
|
531
535
|
hint("app.tasks.cycleView", "to cycle task panel view"),
|
|
532
|
-
...(this.
|
|
536
|
+
...(this.teamFocus.connected ? [hint("app.team.focus", "to focus team roster")] : []),
|
|
533
537
|
hint("app.editor.external", "for external editor"),
|
|
534
538
|
rawKeyHint("/", "for commands"),
|
|
535
539
|
rawKeyHint("!", "to run bash"),
|
|
@@ -552,7 +556,7 @@ export class InteractiveMode {
|
|
|
552
556
|
this.ui.addChild(this.chatContainer);
|
|
553
557
|
this.ui.addChild(this.pendingMessagesContainer);
|
|
554
558
|
this.ui.addChild(this.statusContainer);
|
|
555
|
-
this.renderWidgets(); // Initialize with default spacer
|
|
559
|
+
this.chrome.renderWidgets(); // Initialize with default spacer
|
|
556
560
|
this.ui.addChild(this.widgetContainerAbove);
|
|
557
561
|
this.ui.addChild(this.taskPanel);
|
|
558
562
|
this.ui.addChild(this.editorContainer);
|
|
@@ -583,7 +587,7 @@ export class InteractiveMode {
|
|
|
583
587
|
this.ui.requestRender();
|
|
584
588
|
});
|
|
585
589
|
// Initialize available provider count for footer display
|
|
586
|
-
await this.updateAvailableProviderCount();
|
|
590
|
+
await this.modelController.updateAvailableProviderCount();
|
|
587
591
|
}
|
|
588
592
|
/**
|
|
589
593
|
* Update terminal title with session name and cwd.
|
|
@@ -611,13 +615,13 @@ export class InteractiveMode {
|
|
|
611
615
|
}
|
|
612
616
|
});
|
|
613
617
|
// Start package update check asynchronously
|
|
614
|
-
this.
|
|
618
|
+
checkForPackageUpdates(this.sessionManager.getCwd(), this.settingsManager).then((updates) => {
|
|
615
619
|
if (updates.length > 0) {
|
|
616
620
|
this.showPackageUpdateNotification(updates);
|
|
617
621
|
}
|
|
618
622
|
});
|
|
619
623
|
// Check tmux keyboard setup asynchronously
|
|
620
|
-
|
|
624
|
+
checkTmuxKeyboardSetup().then((warning) => {
|
|
621
625
|
if (warning) {
|
|
622
626
|
this.showWarning(warning);
|
|
623
627
|
}
|
|
@@ -634,7 +638,7 @@ export class InteractiveMode {
|
|
|
634
638
|
if (modelFallbackMessage) {
|
|
635
639
|
this.showWarning(modelFallbackMessage);
|
|
636
640
|
}
|
|
637
|
-
void this.maybeWarnAboutAnthropicSubscriptionAuth();
|
|
641
|
+
void this.modelController.maybeWarnAboutAnthropicSubscriptionAuth();
|
|
638
642
|
// Process initial messages
|
|
639
643
|
if (initialMessage) {
|
|
640
644
|
try {
|
|
@@ -668,98 +672,6 @@ export class InteractiveMode {
|
|
|
668
672
|
}
|
|
669
673
|
}
|
|
670
674
|
}
|
|
671
|
-
async checkForPackageUpdates() {
|
|
672
|
-
if (process.env.HOOCODE_OFFLINE ?? process.env.PI_OFFLINE) {
|
|
673
|
-
return [];
|
|
674
|
-
}
|
|
675
|
-
try {
|
|
676
|
-
const packageManager = new DefaultPackageManager({
|
|
677
|
-
cwd: this.sessionManager.getCwd(),
|
|
678
|
-
agentDir: getAgentDir(),
|
|
679
|
-
settingsManager: this.settingsManager,
|
|
680
|
-
});
|
|
681
|
-
const updates = await packageManager.checkForAvailableUpdates();
|
|
682
|
-
return updates.map((update) => update.displayName);
|
|
683
|
-
}
|
|
684
|
-
catch {
|
|
685
|
-
return [];
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
async checkTmuxKeyboardSetup() {
|
|
689
|
-
if (!process.env.TMUX)
|
|
690
|
-
return undefined;
|
|
691
|
-
const runTmuxShow = (option) => {
|
|
692
|
-
return new Promise((resolve) => {
|
|
693
|
-
const proc = spawn("tmux", ["show", "-gv", option], {
|
|
694
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
695
|
-
});
|
|
696
|
-
let stdout = "";
|
|
697
|
-
const timer = setTimeout(() => {
|
|
698
|
-
proc.kill();
|
|
699
|
-
resolve(undefined);
|
|
700
|
-
}, 2000);
|
|
701
|
-
proc.stdout?.on("data", (data) => {
|
|
702
|
-
stdout += data.toString();
|
|
703
|
-
});
|
|
704
|
-
proc.on("error", () => {
|
|
705
|
-
clearTimeout(timer);
|
|
706
|
-
resolve(undefined);
|
|
707
|
-
});
|
|
708
|
-
proc.on("close", (code) => {
|
|
709
|
-
clearTimeout(timer);
|
|
710
|
-
resolve(code === 0 ? stdout.trim() : undefined);
|
|
711
|
-
});
|
|
712
|
-
});
|
|
713
|
-
};
|
|
714
|
-
const [extendedKeys, extendedKeysFormat] = await Promise.all([
|
|
715
|
-
runTmuxShow("extended-keys"),
|
|
716
|
-
runTmuxShow("extended-keys-format"),
|
|
717
|
-
]);
|
|
718
|
-
// If we couldn't query tmux (timeout, sandbox, etc.), don't warn
|
|
719
|
-
if (extendedKeys === undefined)
|
|
720
|
-
return undefined;
|
|
721
|
-
if (extendedKeys !== "on" && extendedKeys !== "always") {
|
|
722
|
-
return "tmux extended-keys is off. Modified Enter keys may not work. Add `set -g extended-keys on` to ~/.tmux.conf and restart tmux.";
|
|
723
|
-
}
|
|
724
|
-
if (extendedKeysFormat === "xterm") {
|
|
725
|
-
return `tmux extended-keys-format is xterm. ${APP_NAME} works best with csi-u. Add \`set -g extended-keys-format csi-u\` to ~/.tmux.conf and restart tmux.`;
|
|
726
|
-
}
|
|
727
|
-
return undefined;
|
|
728
|
-
}
|
|
729
|
-
/**
|
|
730
|
-
* Get changelog entries to display on startup.
|
|
731
|
-
* Only shows new entries since last seen version, skips for resumed sessions.
|
|
732
|
-
*/
|
|
733
|
-
getChangelogForDisplay() {
|
|
734
|
-
// Skip changelog for resumed/continued sessions (already have messages)
|
|
735
|
-
if (this.session.state.messages.length > 0) {
|
|
736
|
-
return undefined;
|
|
737
|
-
}
|
|
738
|
-
const lastVersion = this.settingsManager.getLastChangelogVersion();
|
|
739
|
-
const changelogPath = getChangelogPath();
|
|
740
|
-
const entries = parseChangelog(changelogPath);
|
|
741
|
-
if (!lastVersion) {
|
|
742
|
-
// Fresh install - record the latest entry's version (not VERSION, which may
|
|
743
|
-
// overshoot the latest entry and silently swallow it once it appears).
|
|
744
|
-
// Fall back to VERSION only if no entries exist yet.
|
|
745
|
-
const seedVersion = entries.length > 0 ? `${entries[0].major}.${entries[0].minor}.${entries[0].patch}` : VERSION;
|
|
746
|
-
this.settingsManager.setLastChangelogVersion(seedVersion);
|
|
747
|
-
this.reportInstallTelemetry(seedVersion);
|
|
748
|
-
return undefined;
|
|
749
|
-
}
|
|
750
|
-
const newEntries = getNewEntries(entries, lastVersion);
|
|
751
|
-
if (newEntries.length > 0) {
|
|
752
|
-
const latest = newEntries[0];
|
|
753
|
-
const latestVersion = `${latest.major}.${latest.minor}.${latest.patch}`;
|
|
754
|
-
this.settingsManager.setLastChangelogVersion(latestVersion);
|
|
755
|
-
this.reportInstallTelemetry(latestVersion);
|
|
756
|
-
return newEntries.map((e) => e.content).join("\n\n");
|
|
757
|
-
}
|
|
758
|
-
return undefined;
|
|
759
|
-
}
|
|
760
|
-
reportInstallTelemetry(_version) {
|
|
761
|
-
// Disabled in HooCode fork: this is forked from pi (upstream pi.dev install-telemetry endpoint is not run by this fork).
|
|
762
|
-
}
|
|
763
675
|
getMarkdownThemeWithSettings() {
|
|
764
676
|
return {
|
|
765
677
|
...getMarkdownTheme(),
|
|
@@ -769,475 +681,24 @@ export class InteractiveMode {
|
|
|
769
681
|
// =========================================================================
|
|
770
682
|
// Extension System
|
|
771
683
|
// =========================================================================
|
|
772
|
-
formatDisplayPath(p) {
|
|
773
|
-
const home = os.homedir();
|
|
774
|
-
let result = p;
|
|
775
|
-
// Replace home directory with ~
|
|
776
|
-
if (result.startsWith(home)) {
|
|
777
|
-
result = `~${result.slice(home.length)}`;
|
|
778
|
-
}
|
|
779
|
-
return result;
|
|
780
|
-
}
|
|
781
|
-
formatExtensionDisplayPath(path) {
|
|
782
|
-
let result = this.formatDisplayPath(path);
|
|
783
|
-
result = result.replace(/\/index\.ts$/, "").replace(/\/index\.js$/, "");
|
|
784
|
-
return result;
|
|
785
|
-
}
|
|
786
|
-
formatContextPath(p) {
|
|
787
|
-
const cwd = path.resolve(this.sessionManager.getCwd());
|
|
788
|
-
const absolutePath = path.isAbsolute(p) ? path.resolve(p) : path.resolve(cwd, p);
|
|
789
|
-
const relativePath = getCwdRelativePath(absolutePath, cwd);
|
|
790
|
-
if (relativePath !== undefined) {
|
|
791
|
-
return relativePath;
|
|
792
|
-
}
|
|
793
|
-
return this.formatDisplayPath(absolutePath);
|
|
794
|
-
}
|
|
795
684
|
getStartupExpansionState() {
|
|
796
685
|
return this.options.verbose || this.toolOutputExpanded;
|
|
797
686
|
}
|
|
798
|
-
/**
|
|
799
|
-
* Get a short path relative to the package root for display.
|
|
800
|
-
*/
|
|
801
|
-
getShortPath(fullPath, sourceInfo) {
|
|
802
|
-
const baseDir = sourceInfo?.baseDir;
|
|
803
|
-
if (baseDir && this.isPackageSource(sourceInfo)) {
|
|
804
|
-
const relativePath = path.relative(path.resolve(baseDir), path.resolve(fullPath));
|
|
805
|
-
if (relativePath &&
|
|
806
|
-
relativePath !== "." &&
|
|
807
|
-
!relativePath.startsWith("..") &&
|
|
808
|
-
!relativePath.startsWith(`..${path.sep}`) &&
|
|
809
|
-
!path.isAbsolute(relativePath)) {
|
|
810
|
-
return relativePath.replace(/\\/g, "/");
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
const source = sourceInfo?.source ?? "";
|
|
814
|
-
const npmMatch = fullPath.match(/node_modules\/(@?[^/]+(?:\/[^/]+)?)\/(.*)/);
|
|
815
|
-
if (npmMatch && source.startsWith("npm:")) {
|
|
816
|
-
return npmMatch[2];
|
|
817
|
-
}
|
|
818
|
-
const gitMatch = fullPath.match(/git\/[^/]+\/[^/]+\/(.*)/);
|
|
819
|
-
if (gitMatch && source.startsWith("git:")) {
|
|
820
|
-
return gitMatch[1];
|
|
821
|
-
}
|
|
822
|
-
return this.formatDisplayPath(fullPath);
|
|
823
|
-
}
|
|
824
|
-
getCompactPathLabel(resourcePath, sourceInfo) {
|
|
825
|
-
const shortPath = this.getShortPath(resourcePath, sourceInfo);
|
|
826
|
-
const normalizedPath = shortPath.replace(/\\/g, "/");
|
|
827
|
-
const segments = normalizedPath.split("/").filter((segment) => segment.length > 0 && segment !== "~");
|
|
828
|
-
if (segments.length > 0) {
|
|
829
|
-
return segments[segments.length - 1];
|
|
830
|
-
}
|
|
831
|
-
return shortPath;
|
|
832
|
-
}
|
|
833
|
-
getCompactPackageSourceLabel(sourceInfo) {
|
|
834
|
-
const source = sourceInfo?.source ?? "";
|
|
835
|
-
if (source.startsWith("npm:")) {
|
|
836
|
-
return source.slice("npm:".length) || source;
|
|
837
|
-
}
|
|
838
|
-
const gitSource = parseGitUrl(source);
|
|
839
|
-
if (gitSource) {
|
|
840
|
-
return gitSource.path || source;
|
|
841
|
-
}
|
|
842
|
-
return source;
|
|
843
|
-
}
|
|
844
|
-
getCompactExtensionLabel(resourcePath, sourceInfo) {
|
|
845
|
-
if (!this.isPackageSource(sourceInfo)) {
|
|
846
|
-
return this.getCompactPathLabel(resourcePath, sourceInfo);
|
|
847
|
-
}
|
|
848
|
-
const sourceLabel = this.getCompactPackageSourceLabel(sourceInfo);
|
|
849
|
-
if (!sourceLabel) {
|
|
850
|
-
return this.getCompactPathLabel(resourcePath, sourceInfo);
|
|
851
|
-
}
|
|
852
|
-
const shortPath = this.getShortPath(resourcePath, sourceInfo).replace(/\\/g, "/");
|
|
853
|
-
const packagePath = shortPath.startsWith("extensions/") ? shortPath.slice("extensions/".length) : shortPath;
|
|
854
|
-
const parsedPath = path.posix.parse(packagePath);
|
|
855
|
-
if (parsedPath.name === "index") {
|
|
856
|
-
return !parsedPath.dir || parsedPath.dir === "." ? sourceLabel : `${sourceLabel}:${parsedPath.dir}`;
|
|
857
|
-
}
|
|
858
|
-
return `${sourceLabel}:${packagePath}`;
|
|
859
|
-
}
|
|
860
|
-
getCompactDisplayPathSegments(resourcePath) {
|
|
861
|
-
return this.formatDisplayPath(resourcePath)
|
|
862
|
-
.replace(/\\/g, "/")
|
|
863
|
-
.split("/")
|
|
864
|
-
.filter((segment) => segment.length > 0 && segment !== "~");
|
|
865
|
-
}
|
|
866
|
-
getCompactNonPackageExtensionLabel(resourcePath, index, allPaths) {
|
|
867
|
-
const segments = allPaths[index]?.segments;
|
|
868
|
-
if (!segments || segments.length === 0) {
|
|
869
|
-
return this.getCompactPathLabel(resourcePath);
|
|
870
|
-
}
|
|
871
|
-
for (let segmentCount = 1; segmentCount <= segments.length; segmentCount += 1) {
|
|
872
|
-
const candidate = segments.slice(-segmentCount).join("/");
|
|
873
|
-
const isUnique = allPaths.every((item, itemIndex) => {
|
|
874
|
-
if (itemIndex === index) {
|
|
875
|
-
return true;
|
|
876
|
-
}
|
|
877
|
-
return item.segments.slice(-segmentCount).join("/") !== candidate;
|
|
878
|
-
});
|
|
879
|
-
if (isUnique) {
|
|
880
|
-
return candidate;
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
return segments.join("/");
|
|
884
|
-
}
|
|
885
|
-
getCompactExtensionLabels(extensions) {
|
|
886
|
-
const nonPackageExtensions = extensions
|
|
887
|
-
.map((extension) => {
|
|
888
|
-
const segments = this.getCompactDisplayPathSegments(extension.path);
|
|
889
|
-
const lastSegment = segments[segments.length - 1];
|
|
890
|
-
if (segments.length > 1 && (lastSegment === "index.ts" || lastSegment === "index.js")) {
|
|
891
|
-
segments.pop();
|
|
892
|
-
}
|
|
893
|
-
return {
|
|
894
|
-
path: extension.path,
|
|
895
|
-
sourceInfo: extension.sourceInfo,
|
|
896
|
-
segments,
|
|
897
|
-
};
|
|
898
|
-
})
|
|
899
|
-
.filter((extension) => !this.isPackageSource(extension.sourceInfo));
|
|
900
|
-
return extensions.map((extension) => {
|
|
901
|
-
if (extension.displayName) {
|
|
902
|
-
return extension.displayName;
|
|
903
|
-
}
|
|
904
|
-
if (this.isPackageSource(extension.sourceInfo)) {
|
|
905
|
-
return this.getCompactExtensionLabel(extension.path, extension.sourceInfo);
|
|
906
|
-
}
|
|
907
|
-
const nonPackageIndex = nonPackageExtensions.findIndex((item) => item.path === extension.path);
|
|
908
|
-
if (nonPackageIndex === -1) {
|
|
909
|
-
return this.getCompactPathLabel(extension.path, extension.sourceInfo);
|
|
910
|
-
}
|
|
911
|
-
return this.getCompactNonPackageExtensionLabel(extension.path, nonPackageIndex, nonPackageExtensions);
|
|
912
|
-
});
|
|
913
|
-
}
|
|
914
|
-
getDisplaySourceInfo(sourceInfo) {
|
|
915
|
-
const source = sourceInfo?.source ?? "local";
|
|
916
|
-
const scope = sourceInfo?.scope ?? "project";
|
|
917
|
-
if (source === "local") {
|
|
918
|
-
if (scope === "user") {
|
|
919
|
-
return { label: "user", color: "muted" };
|
|
920
|
-
}
|
|
921
|
-
if (scope === "project") {
|
|
922
|
-
return { label: "project", color: "muted" };
|
|
923
|
-
}
|
|
924
|
-
if (scope === "temporary") {
|
|
925
|
-
return { label: "path", scopeLabel: "temp", color: "muted" };
|
|
926
|
-
}
|
|
927
|
-
return { label: "path", color: "muted" };
|
|
928
|
-
}
|
|
929
|
-
if (source === "cli") {
|
|
930
|
-
return { label: "path", scopeLabel: scope === "temporary" ? "temp" : undefined, color: "muted" };
|
|
931
|
-
}
|
|
932
|
-
const scopeLabel = scope === "user" ? "user" : scope === "project" ? "project" : scope === "temporary" ? "temp" : undefined;
|
|
933
|
-
return { label: source, scopeLabel, color: "accent" };
|
|
934
|
-
}
|
|
935
|
-
getScopeGroup(sourceInfo) {
|
|
936
|
-
const source = sourceInfo?.source ?? "local";
|
|
937
|
-
const scope = sourceInfo?.scope ?? "project";
|
|
938
|
-
if (source === "cli" || scope === "temporary")
|
|
939
|
-
return "path";
|
|
940
|
-
if (scope === "user")
|
|
941
|
-
return "user";
|
|
942
|
-
if (scope === "project")
|
|
943
|
-
return "project";
|
|
944
|
-
return "path";
|
|
945
|
-
}
|
|
946
|
-
isPackageSource(sourceInfo) {
|
|
947
|
-
const source = sourceInfo?.source ?? "";
|
|
948
|
-
return source.startsWith("npm:") || source.startsWith("git:");
|
|
949
|
-
}
|
|
950
|
-
buildScopeGroups(items) {
|
|
951
|
-
const groups = {
|
|
952
|
-
user: { scope: "user", paths: [], packages: new Map() },
|
|
953
|
-
project: { scope: "project", paths: [], packages: new Map() },
|
|
954
|
-
path: { scope: "path", paths: [], packages: new Map() },
|
|
955
|
-
};
|
|
956
|
-
for (const item of items) {
|
|
957
|
-
const groupKey = this.getScopeGroup(item.sourceInfo);
|
|
958
|
-
const group = groups[groupKey];
|
|
959
|
-
const source = item.sourceInfo?.source ?? "local";
|
|
960
|
-
if (this.isPackageSource(item.sourceInfo)) {
|
|
961
|
-
const list = group.packages.get(source) ?? [];
|
|
962
|
-
list.push(item);
|
|
963
|
-
group.packages.set(source, list);
|
|
964
|
-
}
|
|
965
|
-
else {
|
|
966
|
-
group.paths.push(item);
|
|
967
|
-
}
|
|
968
|
-
}
|
|
969
|
-
return [groups.project, groups.user, groups.path].filter((group) => group.paths.length > 0 || group.packages.size > 0);
|
|
970
|
-
}
|
|
971
|
-
formatScopeGroups(groups, options) {
|
|
972
|
-
const lines = [];
|
|
973
|
-
for (const group of groups) {
|
|
974
|
-
lines.push(` ${theme.fg("accent", group.scope)}`);
|
|
975
|
-
const sortedPaths = [...group.paths].sort((a, b) => a.path.localeCompare(b.path));
|
|
976
|
-
for (const item of sortedPaths) {
|
|
977
|
-
lines.push(theme.fg("dim", ` ${options.formatPath(item)}`));
|
|
978
|
-
}
|
|
979
|
-
const sortedPackages = Array.from(group.packages.entries()).sort(([a], [b]) => a.localeCompare(b));
|
|
980
|
-
for (const [source, items] of sortedPackages) {
|
|
981
|
-
lines.push(` ${theme.fg("mdLink", source)}`);
|
|
982
|
-
const sortedPackagePaths = [...items].sort((a, b) => a.path.localeCompare(b.path));
|
|
983
|
-
for (const item of sortedPackagePaths) {
|
|
984
|
-
lines.push(theme.fg("dim", ` ${options.formatPackagePath(item, source)}`));
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
return lines.join("\n");
|
|
989
|
-
}
|
|
990
|
-
findSourceInfoForPath(p, sourceInfos) {
|
|
991
|
-
const exact = sourceInfos.get(p);
|
|
992
|
-
if (exact)
|
|
993
|
-
return exact;
|
|
994
|
-
let current = p;
|
|
995
|
-
while (current.includes("/")) {
|
|
996
|
-
current = current.substring(0, current.lastIndexOf("/"));
|
|
997
|
-
const parent = sourceInfos.get(current);
|
|
998
|
-
if (parent)
|
|
999
|
-
return parent;
|
|
1000
|
-
}
|
|
1001
|
-
return undefined;
|
|
1002
|
-
}
|
|
1003
|
-
formatPathWithSource(p, sourceInfo) {
|
|
1004
|
-
if (sourceInfo) {
|
|
1005
|
-
const shortPath = this.getShortPath(p, sourceInfo);
|
|
1006
|
-
const { label, scopeLabel } = this.getDisplaySourceInfo(sourceInfo);
|
|
1007
|
-
const labelText = scopeLabel ? `${label} (${scopeLabel})` : label;
|
|
1008
|
-
return `${labelText} ${shortPath}`;
|
|
1009
|
-
}
|
|
1010
|
-
return this.formatDisplayPath(p);
|
|
1011
|
-
}
|
|
1012
|
-
formatDiagnostics(diagnostics, sourceInfos) {
|
|
1013
|
-
const lines = [];
|
|
1014
|
-
// Group collision diagnostics by name
|
|
1015
|
-
const collisions = new Map();
|
|
1016
|
-
const otherDiagnostics = [];
|
|
1017
|
-
for (const d of diagnostics) {
|
|
1018
|
-
if (d.type === "collision" && d.collision) {
|
|
1019
|
-
const list = collisions.get(d.collision.name) ?? [];
|
|
1020
|
-
list.push(d);
|
|
1021
|
-
collisions.set(d.collision.name, list);
|
|
1022
|
-
}
|
|
1023
|
-
else {
|
|
1024
|
-
otherDiagnostics.push(d);
|
|
1025
|
-
}
|
|
1026
|
-
}
|
|
1027
|
-
// Format collision diagnostics grouped by name
|
|
1028
|
-
for (const [name, collisionList] of collisions) {
|
|
1029
|
-
const first = collisionList[0]?.collision;
|
|
1030
|
-
if (!first)
|
|
1031
|
-
continue;
|
|
1032
|
-
lines.push(theme.fg("warning", ` "${name}" collision:`));
|
|
1033
|
-
lines.push(theme.fg("dim", ` ${theme.fg("success", "✓")} ${this.formatPathWithSource(first.winnerPath, this.findSourceInfoForPath(first.winnerPath, sourceInfos))}`));
|
|
1034
|
-
for (const d of collisionList) {
|
|
1035
|
-
if (d.collision) {
|
|
1036
|
-
lines.push(theme.fg("dim", ` ${theme.fg("warning", "✗")} ${this.formatPathWithSource(d.collision.loserPath, this.findSourceInfoForPath(d.collision.loserPath, sourceInfos))} (skipped)`));
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
for (const d of otherDiagnostics) {
|
|
1041
|
-
if (d.path) {
|
|
1042
|
-
const formattedPath = this.formatPathWithSource(d.path, this.findSourceInfoForPath(d.path, sourceInfos));
|
|
1043
|
-
lines.push(theme.fg(d.type === "error" ? "error" : "warning", ` ${formattedPath}`));
|
|
1044
|
-
lines.push(theme.fg(d.type === "error" ? "error" : "warning", ` ${d.message}`));
|
|
1045
|
-
}
|
|
1046
|
-
else {
|
|
1047
|
-
lines.push(theme.fg(d.type === "error" ? "error" : "warning", ` ${d.message}`));
|
|
1048
|
-
}
|
|
1049
|
-
}
|
|
1050
|
-
return lines.join("\n");
|
|
1051
|
-
}
|
|
687
|
+
/** Render the startup/reload resource listing (see resource-display.ts). */
|
|
1052
688
|
showLoadedResources(options) {
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
};
|
|
1066
|
-
const addLoadedSection = (name, collapsedBody, expandedBody = collapsedBody, color = "mdHeading") => {
|
|
1067
|
-
const section = new ExpandableText(() => `${sectionHeader(name, color)}\n${collapsedBody}`, () => `${sectionHeader(name, color)}\n${expandedBody}`, this.getStartupExpansionState(), 0, 0);
|
|
1068
|
-
this.chatContainer.addChild(section);
|
|
1069
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
1070
|
-
};
|
|
1071
|
-
const skillsResult = this.session.resourceLoader.getSkills();
|
|
1072
|
-
const promptsResult = this.session.resourceLoader.getPrompts();
|
|
1073
|
-
const themesResult = this.session.resourceLoader.getThemes();
|
|
1074
|
-
const extensions = options?.extensions ??
|
|
1075
|
-
this.session.resourceLoader
|
|
1076
|
-
.getExtensions()
|
|
1077
|
-
.extensions.filter((extension) => !extension.internal)
|
|
1078
|
-
.map((extension) => ({
|
|
1079
|
-
path: extension.path,
|
|
1080
|
-
sourceInfo: extension.sourceInfo,
|
|
1081
|
-
displayName: extension.displayName,
|
|
1082
|
-
}));
|
|
1083
|
-
const sourceInfos = new Map();
|
|
1084
|
-
for (const extension of extensions) {
|
|
1085
|
-
if (extension.sourceInfo) {
|
|
1086
|
-
sourceInfos.set(extension.path, extension.sourceInfo);
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1089
|
-
for (const skill of skillsResult.skills) {
|
|
1090
|
-
if (skill.sourceInfo) {
|
|
1091
|
-
sourceInfos.set(skill.filePath, skill.sourceInfo);
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
for (const prompt of promptsResult.prompts) {
|
|
1095
|
-
if (prompt.sourceInfo) {
|
|
1096
|
-
sourceInfos.set(prompt.filePath, prompt.sourceInfo);
|
|
1097
|
-
}
|
|
1098
|
-
}
|
|
1099
|
-
for (const loadedTheme of themesResult.themes) {
|
|
1100
|
-
if (loadedTheme.sourcePath && loadedTheme.sourceInfo) {
|
|
1101
|
-
sourceInfos.set(loadedTheme.sourcePath, loadedTheme.sourceInfo);
|
|
1102
|
-
}
|
|
1103
|
-
}
|
|
1104
|
-
if (showListing) {
|
|
1105
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
1106
|
-
const { agentsFiles: contextFiles, warnings: contextWarnings } = this.session.resourceLoader.getAgentsFiles();
|
|
1107
|
-
const skills = skillsResult.skills;
|
|
1108
|
-
const templates = this.session.promptTemplates;
|
|
1109
|
-
const loadedThemes = themesResult.themes;
|
|
1110
|
-
const customThemes = loadedThemes.filter((t) => t.sourcePath);
|
|
1111
|
-
const totalItems = contextFiles.length + skills.length + templates.length + extensions.length + customThemes.length;
|
|
1112
|
-
// Meta items: active mode and subagent system prompt (always shown)
|
|
1113
|
-
const metaItems = [];
|
|
1114
|
-
const rawMode = this.footerDataProvider.getActiveMode().replace(" + subagent", "");
|
|
1115
|
-
metaItems.push(`mode/${rawMode}`);
|
|
1116
|
-
if (this.footerDataProvider.getSubagentEnabled()) {
|
|
1117
|
-
metaItems.push("subagent_system_prompt");
|
|
1118
|
-
}
|
|
1119
|
-
if (totalItems > 0 && totalItems <= 5) {
|
|
1120
|
-
const allCompactItems = [...metaItems];
|
|
1121
|
-
if (contextFiles.length > 0) {
|
|
1122
|
-
allCompactItems.push(...contextFiles.map((contextFile) => this.formatContextPath(contextFile.path)));
|
|
1123
|
-
}
|
|
1124
|
-
if (skills.length > 0) {
|
|
1125
|
-
allCompactItems.push(...skills.map((skill) => skill.name));
|
|
1126
|
-
}
|
|
1127
|
-
if (templates.length > 0) {
|
|
1128
|
-
allCompactItems.push(...templates.map((template) => `/${template.name}`));
|
|
1129
|
-
}
|
|
1130
|
-
if (extensions.length > 0) {
|
|
1131
|
-
allCompactItems.push(...this.getCompactExtensionLabels(extensions));
|
|
1132
|
-
}
|
|
1133
|
-
if (customThemes.length > 0) {
|
|
1134
|
-
allCompactItems.push(...customThemes.map((loadedTheme) => loadedTheme.name ?? this.getCompactPathLabel(loadedTheme.sourcePath, loadedTheme.sourceInfo)));
|
|
1135
|
-
}
|
|
1136
|
-
this.chatContainer.addChild(new Text(`${theme.fg("mdHeading", "[Resources]")} ${theme.fg("dim", allCompactItems.join(", "))}`, 0, 0));
|
|
1137
|
-
}
|
|
1138
|
-
else if (totalItems === 0) {
|
|
1139
|
-
this.chatContainer.addChild(new Text(`${theme.fg("mdHeading", "[Resources]")} ${theme.fg("dim", metaItems.join(", "))}`, 0, 0));
|
|
1140
|
-
}
|
|
1141
|
-
else {
|
|
1142
|
-
addLoadedSection("Resources", formatCompactList(metaItems), formatCompactList(metaItems));
|
|
1143
|
-
if (contextFiles.length > 0) {
|
|
1144
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
1145
|
-
const contextList = contextFiles
|
|
1146
|
-
.map((f) => theme.fg("dim", ` ${this.formatDisplayPath(f.path)}`))
|
|
1147
|
-
.join("\n");
|
|
1148
|
-
const contextCompactList = formatCompactList(contextFiles.map((contextFile) => this.formatContextPath(contextFile.path)), { sort: false });
|
|
1149
|
-
addLoadedSection("Context", contextCompactList, contextList);
|
|
1150
|
-
}
|
|
1151
|
-
if (skills.length > 0) {
|
|
1152
|
-
const groups = this.buildScopeGroups(skills.map((skill) => ({ path: skill.filePath, sourceInfo: skill.sourceInfo })));
|
|
1153
|
-
const skillList = this.formatScopeGroups(groups, {
|
|
1154
|
-
formatPath: (item) => this.formatDisplayPath(item.path),
|
|
1155
|
-
formatPackagePath: (item) => this.getShortPath(item.path, item.sourceInfo),
|
|
1156
|
-
});
|
|
1157
|
-
const skillCompactList = formatCompactList(skills.map((skill) => skill.name));
|
|
1158
|
-
addLoadedSection("Skills", skillCompactList, skillList);
|
|
1159
|
-
}
|
|
1160
|
-
if (templates.length > 0) {
|
|
1161
|
-
const groups = this.buildScopeGroups(templates.map((template) => ({ path: template.filePath, sourceInfo: template.sourceInfo })));
|
|
1162
|
-
const templateByPath = new Map(templates.map((t) => [t.filePath, t]));
|
|
1163
|
-
const templateList = this.formatScopeGroups(groups, {
|
|
1164
|
-
formatPath: (item) => {
|
|
1165
|
-
const template = templateByPath.get(item.path);
|
|
1166
|
-
return template ? `/${template.name}` : this.formatDisplayPath(item.path);
|
|
1167
|
-
},
|
|
1168
|
-
formatPackagePath: (item) => {
|
|
1169
|
-
const template = templateByPath.get(item.path);
|
|
1170
|
-
return template ? `/${template.name}` : this.formatDisplayPath(item.path);
|
|
1171
|
-
},
|
|
1172
|
-
});
|
|
1173
|
-
const promptCompactList = formatCompactList(templates.map((template) => `/${template.name}`));
|
|
1174
|
-
addLoadedSection("Prompts", promptCompactList, templateList);
|
|
1175
|
-
}
|
|
1176
|
-
if (extensions.length > 0) {
|
|
1177
|
-
const groups = this.buildScopeGroups(extensions);
|
|
1178
|
-
const extList = this.formatScopeGroups(groups, {
|
|
1179
|
-
formatPath: (item) => item.displayName ?? this.formatExtensionDisplayPath(item.path),
|
|
1180
|
-
formatPackagePath: (item) => item.displayName ?? this.formatExtensionDisplayPath(this.getShortPath(item.path, item.sourceInfo)),
|
|
1181
|
-
});
|
|
1182
|
-
const extensionCompactList = formatCompactList(this.getCompactExtensionLabels(extensions));
|
|
1183
|
-
addLoadedSection("Extensions", extensionCompactList, extList, "mdHeading");
|
|
1184
|
-
}
|
|
1185
|
-
if (customThemes.length > 0) {
|
|
1186
|
-
const groups = this.buildScopeGroups(customThemes.map((loadedTheme) => ({
|
|
1187
|
-
path: loadedTheme.sourcePath,
|
|
1188
|
-
sourceInfo: loadedTheme.sourceInfo,
|
|
1189
|
-
})));
|
|
1190
|
-
const themeList = this.formatScopeGroups(groups, {
|
|
1191
|
-
formatPath: (item) => this.formatDisplayPath(item.path),
|
|
1192
|
-
formatPackagePath: (item) => this.getShortPath(item.path, item.sourceInfo),
|
|
1193
|
-
});
|
|
1194
|
-
const themeCompactList = formatCompactList(customThemes.map((loadedTheme) => loadedTheme.name ?? this.getCompactPathLabel(loadedTheme.sourcePath, loadedTheme.sourceInfo)));
|
|
1195
|
-
addLoadedSection("Themes", themeCompactList, themeList);
|
|
1196
|
-
}
|
|
1197
|
-
}
|
|
1198
|
-
if (contextWarnings.length > 0) {
|
|
1199
|
-
for (const warning of contextWarnings) {
|
|
1200
|
-
this.chatContainer.addChild(new Text(theme.fg("warning", warning), 0, 0));
|
|
1201
|
-
}
|
|
1202
|
-
}
|
|
1203
|
-
}
|
|
1204
|
-
if (showDiagnostics) {
|
|
1205
|
-
const skillDiagnostics = skillsResult.diagnostics;
|
|
1206
|
-
if (skillDiagnostics.length > 0) {
|
|
1207
|
-
const warningLines = this.formatDiagnostics(skillDiagnostics, sourceInfos);
|
|
1208
|
-
this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Skill conflicts]")}\n${warningLines}`, 0, 0));
|
|
1209
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
1210
|
-
}
|
|
1211
|
-
const promptDiagnostics = promptsResult.diagnostics;
|
|
1212
|
-
if (promptDiagnostics.length > 0) {
|
|
1213
|
-
const warningLines = this.formatDiagnostics(promptDiagnostics, sourceInfos);
|
|
1214
|
-
this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Prompt conflicts]")}\n${warningLines}`, 0, 0));
|
|
1215
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
1216
|
-
}
|
|
1217
|
-
const extensionDiagnostics = [];
|
|
1218
|
-
const extensionErrors = this.session.resourceLoader.getExtensions().errors;
|
|
1219
|
-
if (extensionErrors.length > 0) {
|
|
1220
|
-
for (const error of extensionErrors) {
|
|
1221
|
-
extensionDiagnostics.push({ type: "error", message: error.error, path: error.path });
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
|
-
const commandDiagnostics = this.session.extensionRunner.getCommandDiagnostics();
|
|
1225
|
-
extensionDiagnostics.push(...commandDiagnostics);
|
|
1226
|
-
extensionDiagnostics.push(...this.getBuiltInCommandConflictDiagnostics(this.session.extensionRunner));
|
|
1227
|
-
const shortcutDiagnostics = this.session.extensionRunner.getShortcutDiagnostics();
|
|
1228
|
-
extensionDiagnostics.push(...shortcutDiagnostics);
|
|
1229
|
-
if (extensionDiagnostics.length > 0) {
|
|
1230
|
-
const warningLines = this.formatDiagnostics(extensionDiagnostics, sourceInfos);
|
|
1231
|
-
this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Extension issues]")}\n${warningLines}`, 0, 0));
|
|
1232
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
1233
|
-
}
|
|
1234
|
-
const themeDiagnostics = themesResult.diagnostics;
|
|
1235
|
-
if (themeDiagnostics.length > 0) {
|
|
1236
|
-
const warningLines = this.formatDiagnostics(themeDiagnostics, sourceInfos);
|
|
1237
|
-
this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Theme conflicts]")}\n${warningLines}`, 0, 0));
|
|
1238
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
1239
|
-
}
|
|
1240
|
-
}
|
|
689
|
+
renderLoadedResources({
|
|
690
|
+
chatContainer: this.chatContainer,
|
|
691
|
+
getCwd: () => this.sessionManager.getCwd(),
|
|
692
|
+
getResourceLoader: () => this.session.resourceLoader,
|
|
693
|
+
getPromptTemplates: () => this.session.promptTemplates,
|
|
694
|
+
getExtensionRunner: () => this.session.extensionRunner,
|
|
695
|
+
getActiveMode: () => this.footerDataProvider.getActiveMode(),
|
|
696
|
+
getSubagentEnabled: () => this.footerDataProvider.getSubagentEnabled(),
|
|
697
|
+
quietStartup: () => this.settingsManager.getQuietStartup(),
|
|
698
|
+
verbose: this.options.verbose ?? false,
|
|
699
|
+
isExpanded: () => this.getStartupExpansionState(),
|
|
700
|
+
getBuiltInCommandConflictDiagnostics: (runner) => this.getBuiltInCommandConflictDiagnostics(runner),
|
|
701
|
+
}, options);
|
|
1241
702
|
}
|
|
1242
703
|
/**
|
|
1243
704
|
* Initialize the extension system with TUI-based UI context.
|
|
@@ -1296,7 +757,7 @@ export class InteractiveMode {
|
|
|
1296
757
|
this.editor.setText(result.editorText);
|
|
1297
758
|
}
|
|
1298
759
|
this.showStatus("Navigated to selected point");
|
|
1299
|
-
void this.flushCompactionQueue({ willRetry: false });
|
|
760
|
+
void this.messageQueue.flushCompactionQueue({ willRetry: false });
|
|
1300
761
|
return { cancelled: false };
|
|
1301
762
|
},
|
|
1302
763
|
switchSession: async (sessionPath, options) => {
|
|
@@ -1345,7 +806,7 @@ export class InteractiveMode {
|
|
|
1345
806
|
this.applyRuntimeSettings();
|
|
1346
807
|
await this.bindCurrentSessionExtensions();
|
|
1347
808
|
this.subscribeToAgent();
|
|
1348
|
-
await this.updateAvailableProviderCount();
|
|
809
|
+
await this.modelController.updateAvailableProviderCount();
|
|
1349
810
|
this.updateEditorBorderColor();
|
|
1350
811
|
this.updateTerminalTitle();
|
|
1351
812
|
}
|
|
@@ -1359,7 +820,7 @@ export class InteractiveMode {
|
|
|
1359
820
|
renderCurrentSessionState() {
|
|
1360
821
|
this.chatContainer.clear();
|
|
1361
822
|
this.pendingMessagesContainer.clear();
|
|
1362
|
-
this.
|
|
823
|
+
this.messageQueue.resetCompactionQueue();
|
|
1363
824
|
this.streamingComponent = undefined;
|
|
1364
825
|
this.streamingMessage = undefined;
|
|
1365
826
|
this.pendingTools.clear();
|
|
@@ -1474,72 +935,11 @@ export class InteractiveMode {
|
|
|
1474
935
|
}
|
|
1475
936
|
this.ui.requestRender();
|
|
1476
937
|
}
|
|
1477
|
-
/**
|
|
1478
|
-
* Set an extension widget (string array or custom component).
|
|
1479
|
-
*/
|
|
1480
|
-
setExtensionWidget(key, content, options) {
|
|
1481
|
-
const placement = options?.placement ?? "aboveEditor";
|
|
1482
|
-
const removeExisting = (map) => {
|
|
1483
|
-
const existing = map.get(key);
|
|
1484
|
-
if (existing?.dispose)
|
|
1485
|
-
existing.dispose();
|
|
1486
|
-
map.delete(key);
|
|
1487
|
-
};
|
|
1488
|
-
removeExisting(this.extensionWidgetsAbove);
|
|
1489
|
-
removeExisting(this.extensionWidgetsBelow);
|
|
1490
|
-
if (content === undefined) {
|
|
1491
|
-
this.renderWidgets();
|
|
1492
|
-
return;
|
|
1493
|
-
}
|
|
1494
|
-
let component;
|
|
1495
|
-
if (Array.isArray(content)) {
|
|
1496
|
-
// Wrap string array in a Container with Text components
|
|
1497
|
-
const container = new Container();
|
|
1498
|
-
for (const line of content.slice(0, InteractiveMode.MAX_WIDGET_LINES)) {
|
|
1499
|
-
container.addChild(new Text(line, 1, 0));
|
|
1500
|
-
}
|
|
1501
|
-
if (content.length > InteractiveMode.MAX_WIDGET_LINES) {
|
|
1502
|
-
container.addChild(new Text(theme.fg("muted", "... (widget truncated)"), 1, 0));
|
|
1503
|
-
}
|
|
1504
|
-
component = container;
|
|
1505
|
-
}
|
|
1506
|
-
else {
|
|
1507
|
-
// Factory function - create component
|
|
1508
|
-
component = content(this.ui, theme);
|
|
1509
|
-
}
|
|
1510
|
-
const targetMap = placement === "belowEditor" ? this.extensionWidgetsBelow : this.extensionWidgetsAbove;
|
|
1511
|
-
targetMap.set(key, component);
|
|
1512
|
-
this.renderWidgets();
|
|
1513
|
-
}
|
|
1514
|
-
clearExtensionWidgets() {
|
|
1515
|
-
for (const widget of this.extensionWidgetsAbove.values()) {
|
|
1516
|
-
widget.dispose?.();
|
|
1517
|
-
}
|
|
1518
|
-
for (const widget of this.extensionWidgetsBelow.values()) {
|
|
1519
|
-
widget.dispose?.();
|
|
1520
|
-
}
|
|
1521
|
-
this.extensionWidgetsAbove.clear();
|
|
1522
|
-
this.extensionWidgetsBelow.clear();
|
|
1523
|
-
this.renderWidgets();
|
|
1524
|
-
}
|
|
1525
938
|
resetExtensionUI() {
|
|
1526
|
-
|
|
1527
|
-
this.hideExtensionSelector();
|
|
1528
|
-
}
|
|
1529
|
-
if (this.extensionInput) {
|
|
1530
|
-
this.hideExtensionInput();
|
|
1531
|
-
}
|
|
1532
|
-
if (this.askOptions) {
|
|
1533
|
-
this.hideAskOptions();
|
|
1534
|
-
}
|
|
1535
|
-
if (this.extensionEditor) {
|
|
1536
|
-
this.hideExtensionEditor();
|
|
1537
|
-
}
|
|
939
|
+
this.dialogs.reset();
|
|
1538
940
|
this.ui.hideOverlay();
|
|
1539
941
|
this.clearExtensionTerminalInputListeners();
|
|
1540
|
-
this.
|
|
1541
|
-
this.setExtensionHeader(undefined);
|
|
1542
|
-
this.clearExtensionWidgets();
|
|
942
|
+
this.chrome.reset();
|
|
1543
943
|
this.footerDataProvider.clearExtensionStatuses();
|
|
1544
944
|
this.footer.invalidate();
|
|
1545
945
|
this.autocompleteProviderWrappers = [];
|
|
@@ -1555,101 +955,6 @@ export class InteractiveMode {
|
|
|
1555
955
|
}
|
|
1556
956
|
this.setHiddenThinkingLabel();
|
|
1557
957
|
}
|
|
1558
|
-
// Maximum total widget lines to prevent viewport overflow
|
|
1559
|
-
static MAX_WIDGET_LINES = 10;
|
|
1560
|
-
/**
|
|
1561
|
-
* Render all extension widgets to the widget container.
|
|
1562
|
-
*/
|
|
1563
|
-
renderWidgets() {
|
|
1564
|
-
if (!this.widgetContainerAbove || !this.widgetContainerBelow)
|
|
1565
|
-
return;
|
|
1566
|
-
this.renderWidgetContainer(this.widgetContainerAbove, this.extensionWidgetsAbove, true, true);
|
|
1567
|
-
this.renderWidgetContainer(this.widgetContainerBelow, this.extensionWidgetsBelow, false, false);
|
|
1568
|
-
this.ui.requestRender();
|
|
1569
|
-
}
|
|
1570
|
-
renderWidgetContainer(container, widgets, spacerWhenEmpty, leadingSpacer) {
|
|
1571
|
-
container.clear();
|
|
1572
|
-
if (widgets.size === 0) {
|
|
1573
|
-
if (spacerWhenEmpty) {
|
|
1574
|
-
container.addChild(new Spacer(1));
|
|
1575
|
-
}
|
|
1576
|
-
return;
|
|
1577
|
-
}
|
|
1578
|
-
if (leadingSpacer) {
|
|
1579
|
-
container.addChild(new Spacer(1));
|
|
1580
|
-
}
|
|
1581
|
-
for (const component of widgets.values()) {
|
|
1582
|
-
container.addChild(component);
|
|
1583
|
-
}
|
|
1584
|
-
}
|
|
1585
|
-
/**
|
|
1586
|
-
* Set a custom footer component, or restore the built-in footer.
|
|
1587
|
-
*/
|
|
1588
|
-
setExtensionFooter(factory) {
|
|
1589
|
-
// Dispose existing custom footer
|
|
1590
|
-
if (this.customFooter?.dispose) {
|
|
1591
|
-
this.customFooter.dispose();
|
|
1592
|
-
}
|
|
1593
|
-
// Remove current footer from UI
|
|
1594
|
-
if (this.customFooter) {
|
|
1595
|
-
this.ui.removeChild(this.customFooter);
|
|
1596
|
-
}
|
|
1597
|
-
else {
|
|
1598
|
-
this.ui.removeChild(this.footer);
|
|
1599
|
-
}
|
|
1600
|
-
if (factory) {
|
|
1601
|
-
// Create and add custom footer, passing the data provider
|
|
1602
|
-
this.customFooter = factory(this.ui, theme, this.footerDataProvider);
|
|
1603
|
-
this.ui.addChild(this.customFooter);
|
|
1604
|
-
}
|
|
1605
|
-
else {
|
|
1606
|
-
// Restore built-in footer
|
|
1607
|
-
this.customFooter = undefined;
|
|
1608
|
-
this.ui.addChild(this.footer);
|
|
1609
|
-
}
|
|
1610
|
-
this.ui.requestRender();
|
|
1611
|
-
}
|
|
1612
|
-
/**
|
|
1613
|
-
* Set a custom header component, or restore the built-in header.
|
|
1614
|
-
*/
|
|
1615
|
-
setExtensionHeader(factory) {
|
|
1616
|
-
// Header may not be initialized yet if called during early initialization
|
|
1617
|
-
if (!this.builtInHeader) {
|
|
1618
|
-
return;
|
|
1619
|
-
}
|
|
1620
|
-
// Dispose existing custom header
|
|
1621
|
-
if (this.customHeader?.dispose) {
|
|
1622
|
-
this.customHeader.dispose();
|
|
1623
|
-
}
|
|
1624
|
-
// Find the index of the current header in the header container
|
|
1625
|
-
const currentHeader = this.customHeader || this.builtInHeader;
|
|
1626
|
-
const index = this.headerContainer.children.indexOf(currentHeader);
|
|
1627
|
-
if (factory) {
|
|
1628
|
-
// Create and add custom header
|
|
1629
|
-
this.customHeader = factory(this.ui, theme);
|
|
1630
|
-
if (isExpandable(this.customHeader)) {
|
|
1631
|
-
this.customHeader.setExpanded(this.toolOutputExpanded);
|
|
1632
|
-
}
|
|
1633
|
-
if (index !== -1) {
|
|
1634
|
-
this.headerContainer.children[index] = this.customHeader;
|
|
1635
|
-
}
|
|
1636
|
-
else {
|
|
1637
|
-
// If not found (e.g. builtInHeader was never added), add at the top
|
|
1638
|
-
this.headerContainer.children.unshift(this.customHeader);
|
|
1639
|
-
}
|
|
1640
|
-
}
|
|
1641
|
-
else {
|
|
1642
|
-
// Restore built-in header
|
|
1643
|
-
this.customHeader = undefined;
|
|
1644
|
-
if (isExpandable(this.builtInHeader)) {
|
|
1645
|
-
this.builtInHeader.setExpanded(this.toolOutputExpanded);
|
|
1646
|
-
}
|
|
1647
|
-
if (index !== -1) {
|
|
1648
|
-
this.headerContainer.children[index] = this.builtInHeader;
|
|
1649
|
-
}
|
|
1650
|
-
}
|
|
1651
|
-
this.ui.requestRender();
|
|
1652
|
-
}
|
|
1653
958
|
addExtensionTerminalInputListener(handler) {
|
|
1654
959
|
const unsubscribe = this.ui.addInputListener(handler);
|
|
1655
960
|
this.extensionTerminalInputUnsubscribers.add(unsubscribe);
|
|
@@ -1669,10 +974,10 @@ export class InteractiveMode {
|
|
|
1669
974
|
*/
|
|
1670
975
|
createExtensionUIContext() {
|
|
1671
976
|
return {
|
|
1672
|
-
select: (title, options, opts) => this.
|
|
1673
|
-
confirm: (title, message, opts) => this.
|
|
1674
|
-
input: (title, placeholder, opts) => this.
|
|
1675
|
-
askOptions: (questions, opts) => this.showAskOptions(questions, opts),
|
|
977
|
+
select: (title, options, opts) => this.dialogs.showSelector(title, options, opts),
|
|
978
|
+
confirm: (title, message, opts) => this.dialogs.confirm(title, message, opts),
|
|
979
|
+
input: (title, placeholder, opts) => this.dialogs.showInput(title, placeholder, opts),
|
|
980
|
+
askOptions: (questions, opts) => this.dialogs.showAskOptions(questions, opts),
|
|
1676
981
|
notify: (message, type) => this.showExtensionNotify(message, type),
|
|
1677
982
|
onTerminalInput: (handler) => this.addExtensionTerminalInputListener(handler),
|
|
1678
983
|
setStatus: (key, text) => this.setExtensionStatus(key, text),
|
|
@@ -1690,15 +995,15 @@ export class InteractiveMode {
|
|
|
1690
995
|
setWorkingVisible: (visible) => this.setWorkingVisible(visible),
|
|
1691
996
|
setWorkingIndicator: (options) => this.setWorkingIndicator(options),
|
|
1692
997
|
setHiddenThinkingLabel: (label) => this.setHiddenThinkingLabel(label),
|
|
1693
|
-
setWidget: (key, content, options) => this.
|
|
1694
|
-
setFooter: (factory) => this.
|
|
1695
|
-
setHeader: (factory) => this.
|
|
998
|
+
setWidget: (key, content, options) => this.chrome.setWidget(key, content, options),
|
|
999
|
+
setFooter: (factory) => this.chrome.setFooter(factory),
|
|
1000
|
+
setHeader: (factory) => this.chrome.setHeader(factory),
|
|
1696
1001
|
setTitle: (title) => this.ui.terminal.setTitle(title),
|
|
1697
|
-
custom: (factory, options) => this.
|
|
1002
|
+
custom: (factory, options) => this.dialogs.showCustom(factory, options),
|
|
1698
1003
|
pasteToEditor: (text) => this.editor.handleInput(`\x1b[200~${text}\x1b[201~`),
|
|
1699
1004
|
setEditorText: (text) => this.editor.setText(text),
|
|
1700
1005
|
getEditorText: () => this.editor.getExpandedText?.() ?? this.editor.getText(),
|
|
1701
|
-
editor: (title, prefill) => this.
|
|
1006
|
+
editor: (title, prefill) => this.dialogs.showEditor(title, prefill),
|
|
1702
1007
|
addAutocompleteProvider: (factory) => {
|
|
1703
1008
|
this.autocompleteProviderWrappers.push(factory);
|
|
1704
1009
|
this.setupAutocompleteProvider();
|
|
@@ -1729,175 +1034,18 @@ export class InteractiveMode {
|
|
|
1729
1034
|
setToolsExpanded: (expanded) => this.setToolsExpanded(expanded),
|
|
1730
1035
|
};
|
|
1731
1036
|
}
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
showExtensionSelector(title, options, opts) {
|
|
1736
|
-
return new Promise((resolve) => {
|
|
1737
|
-
if (opts?.signal?.aborted) {
|
|
1738
|
-
resolve(undefined);
|
|
1739
|
-
return;
|
|
1740
|
-
}
|
|
1741
|
-
const onAbort = () => {
|
|
1742
|
-
this.hideExtensionSelector();
|
|
1743
|
-
resolve(undefined);
|
|
1744
|
-
};
|
|
1745
|
-
opts?.signal?.addEventListener("abort", onAbort, { once: true });
|
|
1746
|
-
this.extensionSelector = new ExtensionSelectorComponent(title, options, (option) => {
|
|
1747
|
-
opts?.signal?.removeEventListener("abort", onAbort);
|
|
1748
|
-
this.hideExtensionSelector();
|
|
1749
|
-
resolve(option);
|
|
1750
|
-
}, () => {
|
|
1751
|
-
opts?.signal?.removeEventListener("abort", onAbort);
|
|
1752
|
-
this.hideExtensionSelector();
|
|
1753
|
-
resolve(undefined);
|
|
1754
|
-
}, { tui: this.ui, timeout: opts?.timeout });
|
|
1755
|
-
this.editorContainer.clear();
|
|
1756
|
-
this.editorContainer.addChild(this.extensionSelector);
|
|
1757
|
-
this.ui.setFocus(this.extensionSelector);
|
|
1758
|
-
this.ui.requestRender();
|
|
1759
|
-
});
|
|
1037
|
+
async promptForMissingSessionCwd(error) {
|
|
1038
|
+
const confirmed = await this.dialogs.confirm("Session cwd not found", formatMissingSessionCwdPrompt(error.issue));
|
|
1039
|
+
return confirmed ? error.issue.fallbackCwd : undefined;
|
|
1760
1040
|
}
|
|
1761
1041
|
/**
|
|
1762
|
-
*
|
|
1042
|
+
* Set a custom editor component from an extension.
|
|
1043
|
+
* Pass undefined to restore the default editor.
|
|
1763
1044
|
*/
|
|
1764
|
-
|
|
1765
|
-
this.
|
|
1766
|
-
|
|
1767
|
-
this.
|
|
1768
|
-
this.extensionSelector = undefined;
|
|
1769
|
-
this.ui.setFocus(this.editor);
|
|
1770
|
-
this.ui.requestRender();
|
|
1771
|
-
}
|
|
1772
|
-
/**
|
|
1773
|
-
* Show the options pane — the agent asking the user one or more questions.
|
|
1774
|
-
* Resolves with one answer per question, or undefined if skipped/aborted.
|
|
1775
|
-
*/
|
|
1776
|
-
showAskOptions(questions, opts) {
|
|
1777
|
-
return new Promise((resolve) => {
|
|
1778
|
-
if (!questions.length || opts?.signal?.aborted) {
|
|
1779
|
-
resolve(undefined);
|
|
1780
|
-
return;
|
|
1781
|
-
}
|
|
1782
|
-
const onAbort = () => {
|
|
1783
|
-
this.hideAskOptions();
|
|
1784
|
-
resolve(undefined);
|
|
1785
|
-
};
|
|
1786
|
-
opts?.signal?.addEventListener("abort", onAbort, { once: true });
|
|
1787
|
-
this.askOptions = new AskOptionsComponent(questions, (answers) => {
|
|
1788
|
-
opts?.signal?.removeEventListener("abort", onAbort);
|
|
1789
|
-
this.hideAskOptions();
|
|
1790
|
-
resolve(answers);
|
|
1791
|
-
}, () => {
|
|
1792
|
-
opts?.signal?.removeEventListener("abort", onAbort);
|
|
1793
|
-
this.hideAskOptions();
|
|
1794
|
-
resolve(undefined);
|
|
1795
|
-
});
|
|
1796
|
-
this.editorContainer.clear();
|
|
1797
|
-
this.editorContainer.addChild(this.askOptions);
|
|
1798
|
-
this.ui.setFocus(this.askOptions);
|
|
1799
|
-
this.ui.requestRender();
|
|
1800
|
-
});
|
|
1801
|
-
}
|
|
1802
|
-
/**
|
|
1803
|
-
* Hide the options pane and restore the editor.
|
|
1804
|
-
*/
|
|
1805
|
-
hideAskOptions() {
|
|
1806
|
-
if (!this.askOptions)
|
|
1807
|
-
return;
|
|
1808
|
-
this.editorContainer.clear();
|
|
1809
|
-
this.editorContainer.addChild(this.editor);
|
|
1810
|
-
this.askOptions = undefined;
|
|
1811
|
-
this.ui.setFocus(this.editor);
|
|
1812
|
-
this.ui.requestRender();
|
|
1813
|
-
}
|
|
1814
|
-
/**
|
|
1815
|
-
* Show a confirmation dialog for extensions.
|
|
1816
|
-
*/
|
|
1817
|
-
async showExtensionConfirm(title, message, opts) {
|
|
1818
|
-
const result = await this.showExtensionSelector(`${title}\n${message}`, ["Yes", "No"], opts);
|
|
1819
|
-
return result === "Yes";
|
|
1820
|
-
}
|
|
1821
|
-
async promptForMissingSessionCwd(error) {
|
|
1822
|
-
const confirmed = await this.showExtensionConfirm("Session cwd not found", formatMissingSessionCwdPrompt(error.issue));
|
|
1823
|
-
return confirmed ? error.issue.fallbackCwd : undefined;
|
|
1824
|
-
}
|
|
1825
|
-
/**
|
|
1826
|
-
* Show a text input for extensions.
|
|
1827
|
-
*/
|
|
1828
|
-
showExtensionInput(title, placeholder, opts) {
|
|
1829
|
-
return new Promise((resolve) => {
|
|
1830
|
-
if (opts?.signal?.aborted) {
|
|
1831
|
-
resolve(undefined);
|
|
1832
|
-
return;
|
|
1833
|
-
}
|
|
1834
|
-
const onAbort = () => {
|
|
1835
|
-
this.hideExtensionInput();
|
|
1836
|
-
resolve(undefined);
|
|
1837
|
-
};
|
|
1838
|
-
opts?.signal?.addEventListener("abort", onAbort, { once: true });
|
|
1839
|
-
this.extensionInput = new ExtensionInputComponent(title, placeholder, (value) => {
|
|
1840
|
-
opts?.signal?.removeEventListener("abort", onAbort);
|
|
1841
|
-
this.hideExtensionInput();
|
|
1842
|
-
resolve(value);
|
|
1843
|
-
}, () => {
|
|
1844
|
-
opts?.signal?.removeEventListener("abort", onAbort);
|
|
1845
|
-
this.hideExtensionInput();
|
|
1846
|
-
resolve(undefined);
|
|
1847
|
-
}, { tui: this.ui, timeout: opts?.timeout });
|
|
1848
|
-
this.editorContainer.clear();
|
|
1849
|
-
this.editorContainer.addChild(this.extensionInput);
|
|
1850
|
-
this.ui.setFocus(this.extensionInput);
|
|
1851
|
-
this.ui.requestRender();
|
|
1852
|
-
});
|
|
1853
|
-
}
|
|
1854
|
-
/**
|
|
1855
|
-
* Hide the extension input.
|
|
1856
|
-
*/
|
|
1857
|
-
hideExtensionInput() {
|
|
1858
|
-
this.extensionInput?.dispose();
|
|
1859
|
-
this.editorContainer.clear();
|
|
1860
|
-
this.editorContainer.addChild(this.editor);
|
|
1861
|
-
this.extensionInput = undefined;
|
|
1862
|
-
this.ui.setFocus(this.editor);
|
|
1863
|
-
this.ui.requestRender();
|
|
1864
|
-
}
|
|
1865
|
-
/**
|
|
1866
|
-
* Show a multi-line editor for extensions (with Ctrl+G support).
|
|
1867
|
-
*/
|
|
1868
|
-
showExtensionEditor(title, prefill) {
|
|
1869
|
-
return new Promise((resolve) => {
|
|
1870
|
-
this.extensionEditor = new ExtensionEditorComponent(this.ui, this.keybindings, title, prefill, (value) => {
|
|
1871
|
-
this.hideExtensionEditor();
|
|
1872
|
-
resolve(value);
|
|
1873
|
-
}, () => {
|
|
1874
|
-
this.hideExtensionEditor();
|
|
1875
|
-
resolve(undefined);
|
|
1876
|
-
});
|
|
1877
|
-
this.editorContainer.clear();
|
|
1878
|
-
this.editorContainer.addChild(this.extensionEditor);
|
|
1879
|
-
this.ui.setFocus(this.extensionEditor);
|
|
1880
|
-
this.ui.requestRender();
|
|
1881
|
-
});
|
|
1882
|
-
}
|
|
1883
|
-
/**
|
|
1884
|
-
* Hide the extension editor.
|
|
1885
|
-
*/
|
|
1886
|
-
hideExtensionEditor() {
|
|
1887
|
-
this.editorContainer.clear();
|
|
1888
|
-
this.editorContainer.addChild(this.editor);
|
|
1889
|
-
this.extensionEditor = undefined;
|
|
1890
|
-
this.ui.setFocus(this.editor);
|
|
1891
|
-
this.ui.requestRender();
|
|
1892
|
-
}
|
|
1893
|
-
/**
|
|
1894
|
-
* Set a custom editor component from an extension.
|
|
1895
|
-
* Pass undefined to restore the default editor.
|
|
1896
|
-
*/
|
|
1897
|
-
setCustomEditorComponent(factory) {
|
|
1898
|
-
this.editorComponentFactory = factory;
|
|
1899
|
-
// Save text from current editor before switching
|
|
1900
|
-
const currentText = this.editor.getText();
|
|
1045
|
+
setCustomEditorComponent(factory) {
|
|
1046
|
+
this.editorComponentFactory = factory;
|
|
1047
|
+
// Save text from current editor before switching
|
|
1048
|
+
const currentText = this.editor.getText();
|
|
1901
1049
|
this.editorContainer.clear();
|
|
1902
1050
|
if (factory) {
|
|
1903
1051
|
// Create the custom editor with tui, theme, and keybindings
|
|
@@ -1964,75 +1112,6 @@ export class InteractiveMode {
|
|
|
1964
1112
|
this.showStatus(message);
|
|
1965
1113
|
}
|
|
1966
1114
|
}
|
|
1967
|
-
/** Show a custom component with keyboard focus. Overlay mode renders on top of existing content. */
|
|
1968
|
-
async showExtensionCustom(factory, options) {
|
|
1969
|
-
const savedText = this.editor.getText();
|
|
1970
|
-
const isOverlay = options?.overlay ?? false;
|
|
1971
|
-
const restoreEditor = () => {
|
|
1972
|
-
this.editorContainer.clear();
|
|
1973
|
-
this.editorContainer.addChild(this.editor);
|
|
1974
|
-
this.editor.setText(savedText);
|
|
1975
|
-
this.ui.setFocus(this.editor);
|
|
1976
|
-
this.ui.requestRender();
|
|
1977
|
-
};
|
|
1978
|
-
return new Promise((resolve, reject) => {
|
|
1979
|
-
let component;
|
|
1980
|
-
let closed = false;
|
|
1981
|
-
const close = (result) => {
|
|
1982
|
-
if (closed)
|
|
1983
|
-
return;
|
|
1984
|
-
closed = true;
|
|
1985
|
-
if (isOverlay)
|
|
1986
|
-
this.ui.hideOverlay();
|
|
1987
|
-
else
|
|
1988
|
-
restoreEditor();
|
|
1989
|
-
// Note: both branches above already call requestRender
|
|
1990
|
-
resolve(result);
|
|
1991
|
-
try {
|
|
1992
|
-
component?.dispose?.();
|
|
1993
|
-
}
|
|
1994
|
-
catch {
|
|
1995
|
-
/* ignore dispose errors */
|
|
1996
|
-
}
|
|
1997
|
-
};
|
|
1998
|
-
Promise.resolve(factory(this.ui, theme, this.keybindings, close))
|
|
1999
|
-
.then((c) => {
|
|
2000
|
-
if (closed)
|
|
2001
|
-
return;
|
|
2002
|
-
component = c;
|
|
2003
|
-
if (isOverlay) {
|
|
2004
|
-
// Resolve overlay options - can be static or dynamic function
|
|
2005
|
-
const resolveOptions = () => {
|
|
2006
|
-
if (options?.overlayOptions) {
|
|
2007
|
-
const opts = typeof options.overlayOptions === "function"
|
|
2008
|
-
? options.overlayOptions()
|
|
2009
|
-
: options.overlayOptions;
|
|
2010
|
-
return opts;
|
|
2011
|
-
}
|
|
2012
|
-
// Fallback: use component's width property if available
|
|
2013
|
-
const w = component.width;
|
|
2014
|
-
return w ? { width: w } : undefined;
|
|
2015
|
-
};
|
|
2016
|
-
const handle = this.ui.showOverlay(component, resolveOptions());
|
|
2017
|
-
// Expose handle to caller for visibility control
|
|
2018
|
-
options?.onHandle?.(handle);
|
|
2019
|
-
}
|
|
2020
|
-
else {
|
|
2021
|
-
this.editorContainer.clear();
|
|
2022
|
-
this.editorContainer.addChild(component);
|
|
2023
|
-
this.ui.setFocus(component);
|
|
2024
|
-
this.ui.requestRender();
|
|
2025
|
-
}
|
|
2026
|
-
})
|
|
2027
|
-
.catch((err) => {
|
|
2028
|
-
if (closed)
|
|
2029
|
-
return;
|
|
2030
|
-
if (!isOverlay)
|
|
2031
|
-
restoreEditor();
|
|
2032
|
-
reject(err);
|
|
2033
|
-
});
|
|
2034
|
-
});
|
|
2035
|
-
}
|
|
2036
1115
|
/**
|
|
2037
1116
|
* Show an extension error in the UI.
|
|
2038
1117
|
*/
|
|
@@ -2061,7 +1140,7 @@ export class InteractiveMode {
|
|
|
2061
1140
|
// so they work correctly regardless of which editor is active
|
|
2062
1141
|
this.defaultEditor.onEscape = () => {
|
|
2063
1142
|
if (this.session.isStreaming) {
|
|
2064
|
-
this.restoreQueuedMessagesToEditor({ abort: true });
|
|
1143
|
+
this.messageQueue.restoreQueuedMessagesToEditor({ abort: true });
|
|
2065
1144
|
}
|
|
2066
1145
|
else if (this.session.isBashRunning) {
|
|
2067
1146
|
this.session.abortBash();
|
|
@@ -2096,19 +1175,19 @@ export class InteractiveMode {
|
|
|
2096
1175
|
this.defaultEditor.onCtrlD = () => this.handleCtrlD();
|
|
2097
1176
|
this.defaultEditor.onAction("app.suspend", () => this.handleCtrlZ());
|
|
2098
1177
|
this.defaultEditor.onAction("app.thinking.cycle", () => this.cycleThinkingLevel());
|
|
2099
|
-
this.defaultEditor.onAction("app.model.cycleForward", () => this.cycleModel("forward"));
|
|
2100
|
-
this.defaultEditor.onAction("app.model.cycleBackward", () => this.cycleModel("backward"));
|
|
1178
|
+
this.defaultEditor.onAction("app.model.cycleForward", () => this.modelController.cycleModel("forward"));
|
|
1179
|
+
this.defaultEditor.onAction("app.model.cycleBackward", () => this.modelController.cycleModel("backward"));
|
|
2101
1180
|
// Global debug handler on TUI (works regardless of focus)
|
|
2102
1181
|
this.ui.onDebug = () => this.commandExecutor.handleDebug();
|
|
2103
|
-
this.defaultEditor.onAction("app.model.select", () => this.showModelSelector());
|
|
1182
|
+
this.defaultEditor.onAction("app.model.select", () => this.modelController.showModelSelector());
|
|
2104
1183
|
this.defaultEditor.onAction("app.tools.expand", () => this.toggleToolOutputExpansion());
|
|
2105
1184
|
this.defaultEditor.onAction("app.thinking.toggle", () => this.toggleThinkingBlockVisibility());
|
|
2106
1185
|
this.defaultEditor.onAction("app.tasks.cycleView", () => {
|
|
2107
1186
|
this.taskPanel.cycleView();
|
|
2108
1187
|
});
|
|
2109
|
-
this.defaultEditor.onAction("app.team.focus", () => this.
|
|
1188
|
+
this.defaultEditor.onAction("app.team.focus", () => this.teamFocus.enterFocus());
|
|
2110
1189
|
this.defaultEditor.onAction("app.editor.external", () => this.openExternalEditor());
|
|
2111
|
-
this.defaultEditor.onAction("app.input.voiceTranscribe", () => this.
|
|
1190
|
+
this.defaultEditor.onAction("app.input.voiceTranscribe", () => this.voice.toggle());
|
|
2112
1191
|
this.defaultEditor.onAction("app.message.followUp", () => this.handleFollowUp());
|
|
2113
1192
|
this.defaultEditor.onAction("app.message.dequeue", () => this.handleDequeue());
|
|
2114
1193
|
this.defaultEditor.onAction("app.session.new", () => this.commandExecutor.handleClear());
|
|
@@ -2153,278 +1232,183 @@ export class InteractiveMode {
|
|
|
2153
1232
|
// =========================================================================
|
|
2154
1233
|
/**
|
|
2155
1234
|
* Wire a hooteams connection into the TUI. Called by main.ts when `--team`
|
|
2156
|
-
* is set, before run().
|
|
2157
|
-
* the attach side panel (a) on the task panel's teams view, and approval
|
|
2158
|
-
* gates: task_paused events (and gates already pending on the server)
|
|
2159
|
-
* surface inline in the attach panel when it shows the paused role,
|
|
2160
|
-
* otherwise in the options pane; the answer goes back over
|
|
2161
|
-
* POST /tasks/:id/resume.
|
|
1235
|
+
* is set, before run(). See TeamFocusController for the feature itself.
|
|
2162
1236
|
*/
|
|
2163
1237
|
attachTeamClient(client) {
|
|
2164
|
-
this.
|
|
2165
|
-
const approvals = new TeamApprovalCoordinator({
|
|
2166
|
-
present: (approval, signal) => this.presentTeamApproval(approval, signal),
|
|
2167
|
-
resume: (taskId, option) => client.resume(taskId, option),
|
|
2168
|
-
info: (message) => this.showStatus(message),
|
|
2169
|
-
warn: (message) => this.showWarning(message),
|
|
2170
|
-
});
|
|
2171
|
-
client.subscribe((event) => approvals.handleEvent(event));
|
|
2172
|
-
// Gates that opened before we attached don't replay as task_paused.
|
|
2173
|
-
void client.pendingApprovals().then((pending) => {
|
|
2174
|
-
for (const gate of pending)
|
|
2175
|
-
approvals.enqueuePending(gate);
|
|
2176
|
-
}, () => {
|
|
2177
|
-
// Best-effort like the rest of the bridge; live gates still arrive via SSE.
|
|
2178
|
-
});
|
|
1238
|
+
this.teamFocus.attachClient(client);
|
|
2179
1239
|
}
|
|
2180
1240
|
/**
|
|
2181
|
-
*
|
|
2182
|
-
*
|
|
2183
|
-
*
|
|
2184
|
-
*
|
|
2185
|
-
* while another ask is on screen. Either way the signal (gate answered
|
|
2186
|
-
* elsewhere) dismisses the prompt.
|
|
1241
|
+
* Built-in slash commands dispatched by the editor submit handler.
|
|
1242
|
+
* `withArgs` commands also match "/name <args>" and receive the full text.
|
|
1243
|
+
* Each handler keeps its own editor-clearing order (before vs after the
|
|
1244
|
+
* await) — some commands must show their UI before the prompt is wiped.
|
|
2187
1245
|
*/
|
|
2188
|
-
|
|
2189
|
-
const
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
if (answer !== undefined || signal.aborted || this.teamAttachPanel === panel)
|
|
2197
|
-
return answer;
|
|
2198
|
-
}
|
|
2199
|
-
while (this.askOptions && !signal.aborted) {
|
|
2200
|
-
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
2201
|
-
}
|
|
2202
|
-
if (signal.aborted)
|
|
2203
|
-
return undefined;
|
|
2204
|
-
const answers = await this.showAskOptions([
|
|
2205
|
-
{
|
|
2206
|
-
question: approval.question,
|
|
2207
|
-
short: approval.taskId,
|
|
2208
|
-
detail: `team task "${approval.taskId}"${approval.role ? ` (${approval.role})` : ""} is paused until answered`,
|
|
2209
|
-
options: approval.options.map((label) => ({ label })),
|
|
2210
|
-
allowCustom: true,
|
|
1246
|
+
createBuiltInSlashCommands() {
|
|
1247
|
+
const clearEditor = () => this.editor.setText("");
|
|
1248
|
+
return new Map(Object.entries({
|
|
1249
|
+
"/settings": {
|
|
1250
|
+
run: () => {
|
|
1251
|
+
this.showSettingsSelector();
|
|
1252
|
+
clearEditor();
|
|
1253
|
+
},
|
|
2211
1254
|
},
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
1255
|
+
"/scoped-models": {
|
|
1256
|
+
run: async () => {
|
|
1257
|
+
clearEditor();
|
|
1258
|
+
await this.modelController.showModelsSelector();
|
|
1259
|
+
},
|
|
1260
|
+
},
|
|
1261
|
+
"/model": {
|
|
1262
|
+
withArgs: true,
|
|
1263
|
+
run: async (text) => {
|
|
1264
|
+
const searchTerm = text.startsWith("/model ") ? text.slice(7).trim() : undefined;
|
|
1265
|
+
clearEditor();
|
|
1266
|
+
await this.commandExecutor.handleModel(searchTerm);
|
|
1267
|
+
},
|
|
1268
|
+
},
|
|
1269
|
+
"/export": {
|
|
1270
|
+
withArgs: true,
|
|
1271
|
+
run: async (text) => {
|
|
1272
|
+
await this.commandExecutor.handleExport(text);
|
|
1273
|
+
clearEditor();
|
|
1274
|
+
},
|
|
1275
|
+
},
|
|
1276
|
+
"/import": {
|
|
1277
|
+
withArgs: true,
|
|
1278
|
+
run: async (text) => {
|
|
1279
|
+
await this.commandExecutor.handleImport(text);
|
|
1280
|
+
clearEditor();
|
|
1281
|
+
},
|
|
1282
|
+
},
|
|
1283
|
+
"/share": {
|
|
1284
|
+
run: async () => {
|
|
1285
|
+
await this.commandExecutor.handleShare();
|
|
1286
|
+
clearEditor();
|
|
1287
|
+
},
|
|
1288
|
+
},
|
|
1289
|
+
"/copy": {
|
|
1290
|
+
run: async () => {
|
|
1291
|
+
await this.commandExecutor.handleCopy();
|
|
1292
|
+
clearEditor();
|
|
1293
|
+
},
|
|
1294
|
+
},
|
|
1295
|
+
"/name": {
|
|
1296
|
+
withArgs: true,
|
|
1297
|
+
run: (text) => {
|
|
1298
|
+
this.commandExecutor.handleName(text);
|
|
1299
|
+
clearEditor();
|
|
1300
|
+
},
|
|
1301
|
+
},
|
|
1302
|
+
"/session": {
|
|
1303
|
+
run: () => {
|
|
1304
|
+
this.commandExecutor.handleSession();
|
|
1305
|
+
clearEditor();
|
|
1306
|
+
},
|
|
1307
|
+
},
|
|
1308
|
+
"/changelog": {
|
|
1309
|
+
run: () => {
|
|
1310
|
+
this.commandExecutor.handleChangelog();
|
|
1311
|
+
clearEditor();
|
|
1312
|
+
},
|
|
1313
|
+
},
|
|
1314
|
+
"/hotkeys": {
|
|
1315
|
+
run: () => {
|
|
1316
|
+
this.commandExecutor.handleHotkeys();
|
|
1317
|
+
clearEditor();
|
|
1318
|
+
},
|
|
1319
|
+
},
|
|
1320
|
+
"/fork": {
|
|
1321
|
+
run: () => {
|
|
1322
|
+
this.showUserMessageSelector();
|
|
1323
|
+
clearEditor();
|
|
1324
|
+
},
|
|
1325
|
+
},
|
|
1326
|
+
"/clone": {
|
|
1327
|
+
run: async () => {
|
|
1328
|
+
clearEditor();
|
|
1329
|
+
await this.commandExecutor.handleClone();
|
|
1330
|
+
},
|
|
1331
|
+
},
|
|
1332
|
+
"/tree": {
|
|
1333
|
+
run: () => {
|
|
1334
|
+
this.showTreeSelector();
|
|
1335
|
+
clearEditor();
|
|
1336
|
+
},
|
|
1337
|
+
},
|
|
1338
|
+
"/login": {
|
|
1339
|
+
run: () => {
|
|
1340
|
+
this.loginController.showOAuthSelector("login");
|
|
1341
|
+
clearEditor();
|
|
1342
|
+
},
|
|
1343
|
+
},
|
|
1344
|
+
"/logout": {
|
|
1345
|
+
run: () => {
|
|
1346
|
+
this.loginController.showOAuthSelector("logout");
|
|
1347
|
+
clearEditor();
|
|
1348
|
+
},
|
|
1349
|
+
},
|
|
1350
|
+
"/new": {
|
|
1351
|
+
run: async () => {
|
|
1352
|
+
clearEditor();
|
|
1353
|
+
await this.commandExecutor.handleClear();
|
|
1354
|
+
},
|
|
1355
|
+
},
|
|
1356
|
+
"/compact": {
|
|
1357
|
+
withArgs: true,
|
|
1358
|
+
run: async (text) => {
|
|
1359
|
+
const prefix = "/compact ";
|
|
1360
|
+
const customInstructions = text.startsWith(prefix)
|
|
1361
|
+
? text.slice(prefix.length).trim() || undefined
|
|
1362
|
+
: undefined;
|
|
1363
|
+
clearEditor();
|
|
1364
|
+
await this.handleCompactCommand(customInstructions);
|
|
1365
|
+
},
|
|
1366
|
+
},
|
|
1367
|
+
"/reload": {
|
|
1368
|
+
run: async () => {
|
|
1369
|
+
clearEditor();
|
|
1370
|
+
await this.handleReloadCommand();
|
|
1371
|
+
},
|
|
1372
|
+
},
|
|
1373
|
+
"/debug": {
|
|
1374
|
+
run: () => {
|
|
1375
|
+
this.commandExecutor.handleDebug();
|
|
1376
|
+
clearEditor();
|
|
1377
|
+
},
|
|
1378
|
+
},
|
|
1379
|
+
"/resume": {
|
|
1380
|
+
run: () => {
|
|
1381
|
+
this.showSessionSelector();
|
|
1382
|
+
clearEditor();
|
|
1383
|
+
},
|
|
1384
|
+
},
|
|
1385
|
+
"/quit": {
|
|
1386
|
+
run: async () => {
|
|
1387
|
+
clearEditor();
|
|
1388
|
+
await this.shutdown();
|
|
1389
|
+
},
|
|
1390
|
+
},
|
|
1391
|
+
"/subagent": {
|
|
1392
|
+
withArgs: true,
|
|
1393
|
+
run: async (text) => {
|
|
1394
|
+
clearEditor();
|
|
1395
|
+
await this.commandExecutor.handleSubagent(text);
|
|
1396
|
+
},
|
|
1397
|
+
},
|
|
1398
|
+
}));
|
|
2298
1399
|
}
|
|
2299
1400
|
setupEditorSubmitHandler() {
|
|
1401
|
+
const slashCommands = this.createBuiltInSlashCommands();
|
|
2300
1402
|
this.defaultEditor.onSubmit = async (text) => {
|
|
2301
1403
|
text = text.trim();
|
|
2302
1404
|
if (!text)
|
|
2303
1405
|
return;
|
|
2304
|
-
// Handle commands
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
if (text === "/scoped-models") {
|
|
2311
|
-
this.editor.setText("");
|
|
2312
|
-
await this.showModelsSelector();
|
|
2313
|
-
return;
|
|
2314
|
-
}
|
|
2315
|
-
if (text === "/model" || text.startsWith("/model ")) {
|
|
2316
|
-
const searchTerm = text.startsWith("/model ") ? text.slice(7).trim() : undefined;
|
|
2317
|
-
this.editor.setText("");
|
|
2318
|
-
await this.commandExecutor.handleModel(searchTerm);
|
|
2319
|
-
return;
|
|
2320
|
-
}
|
|
2321
|
-
if (text === "/export" || text.startsWith("/export ")) {
|
|
2322
|
-
await this.commandExecutor.handleExport(text);
|
|
2323
|
-
this.editor.setText("");
|
|
2324
|
-
return;
|
|
2325
|
-
}
|
|
2326
|
-
if (text === "/import" || text.startsWith("/import ")) {
|
|
2327
|
-
await this.commandExecutor.handleImport(text);
|
|
2328
|
-
this.editor.setText("");
|
|
2329
|
-
return;
|
|
2330
|
-
}
|
|
2331
|
-
if (text === "/share") {
|
|
2332
|
-
await this.commandExecutor.handleShare();
|
|
2333
|
-
this.editor.setText("");
|
|
2334
|
-
return;
|
|
2335
|
-
}
|
|
2336
|
-
if (text === "/copy") {
|
|
2337
|
-
await this.commandExecutor.handleCopy();
|
|
2338
|
-
this.editor.setText("");
|
|
2339
|
-
return;
|
|
2340
|
-
}
|
|
2341
|
-
if (text === "/name" || text.startsWith("/name ")) {
|
|
2342
|
-
this.commandExecutor.handleName(text);
|
|
2343
|
-
this.editor.setText("");
|
|
2344
|
-
return;
|
|
2345
|
-
}
|
|
2346
|
-
if (text === "/session") {
|
|
2347
|
-
this.commandExecutor.handleSession();
|
|
2348
|
-
this.editor.setText("");
|
|
2349
|
-
return;
|
|
2350
|
-
}
|
|
2351
|
-
if (text === "/changelog") {
|
|
2352
|
-
this.commandExecutor.handleChangelog();
|
|
2353
|
-
this.editor.setText("");
|
|
2354
|
-
return;
|
|
2355
|
-
}
|
|
2356
|
-
if (text === "/hotkeys") {
|
|
2357
|
-
this.commandExecutor.handleHotkeys();
|
|
2358
|
-
this.editor.setText("");
|
|
2359
|
-
return;
|
|
2360
|
-
}
|
|
2361
|
-
if (text === "/fork") {
|
|
2362
|
-
this.showUserMessageSelector();
|
|
2363
|
-
this.editor.setText("");
|
|
2364
|
-
return;
|
|
2365
|
-
}
|
|
2366
|
-
if (text === "/clone") {
|
|
2367
|
-
this.editor.setText("");
|
|
2368
|
-
await this.commandExecutor.handleClone();
|
|
2369
|
-
return;
|
|
2370
|
-
}
|
|
2371
|
-
if (text === "/tree") {
|
|
2372
|
-
this.showTreeSelector();
|
|
2373
|
-
this.editor.setText("");
|
|
2374
|
-
return;
|
|
2375
|
-
}
|
|
2376
|
-
if (text === "/login") {
|
|
2377
|
-
this.showOAuthSelector("login");
|
|
2378
|
-
this.editor.setText("");
|
|
2379
|
-
return;
|
|
2380
|
-
}
|
|
2381
|
-
if (text === "/logout") {
|
|
2382
|
-
this.showOAuthSelector("logout");
|
|
2383
|
-
this.editor.setText("");
|
|
2384
|
-
return;
|
|
2385
|
-
}
|
|
2386
|
-
if (text === "/new") {
|
|
2387
|
-
this.editor.setText("");
|
|
2388
|
-
await this.commandExecutor.handleClear();
|
|
2389
|
-
return;
|
|
2390
|
-
}
|
|
2391
|
-
if (text === "/compact" || text.startsWith("/compact ")) {
|
|
2392
|
-
const prefix = "/compact ";
|
|
2393
|
-
const customInstructions = text.startsWith(prefix)
|
|
2394
|
-
? text.slice(prefix.length).trim() || undefined
|
|
2395
|
-
: undefined;
|
|
2396
|
-
this.editor.setText("");
|
|
2397
|
-
await this.handleCompactCommand(customInstructions);
|
|
2398
|
-
return;
|
|
2399
|
-
}
|
|
2400
|
-
if (text === "/reload") {
|
|
2401
|
-
this.editor.setText("");
|
|
2402
|
-
await this.handleReloadCommand();
|
|
2403
|
-
return;
|
|
2404
|
-
}
|
|
2405
|
-
if (text === "/debug") {
|
|
2406
|
-
this.commandExecutor.handleDebug();
|
|
2407
|
-
this.editor.setText("");
|
|
2408
|
-
return;
|
|
2409
|
-
}
|
|
2410
|
-
if (text === "/arminsayshi") {
|
|
2411
|
-
this.commandExecutor.handleArminSaysHi();
|
|
2412
|
-
this.editor.setText("");
|
|
2413
|
-
return;
|
|
2414
|
-
}
|
|
2415
|
-
if (text === "/resume") {
|
|
2416
|
-
this.showSessionSelector();
|
|
2417
|
-
this.editor.setText("");
|
|
2418
|
-
return;
|
|
2419
|
-
}
|
|
2420
|
-
if (text === "/quit") {
|
|
2421
|
-
this.editor.setText("");
|
|
2422
|
-
await this.shutdown();
|
|
2423
|
-
return;
|
|
2424
|
-
}
|
|
2425
|
-
if (text === "/subagent" || text.startsWith("/subagent ")) {
|
|
2426
|
-
this.editor.setText("");
|
|
2427
|
-
await this.commandExecutor.handleSubagent(text);
|
|
1406
|
+
// Handle built-in slash commands
|
|
1407
|
+
const spaceIdx = text.indexOf(" ");
|
|
1408
|
+
const commandName = spaceIdx === -1 ? text : text.slice(0, spaceIdx);
|
|
1409
|
+
const command = slashCommands.get(commandName);
|
|
1410
|
+
if (command && (spaceIdx === -1 || command.withArgs)) {
|
|
1411
|
+
await command.run(text);
|
|
2428
1412
|
return;
|
|
2429
1413
|
}
|
|
2430
1414
|
// Handle bash command (! for normal, !! for excluded from context)
|
|
@@ -2438,7 +1422,7 @@ export class InteractiveMode {
|
|
|
2438
1422
|
return;
|
|
2439
1423
|
}
|
|
2440
1424
|
this.editor.addToHistory?.(text);
|
|
2441
|
-
await this.handleBashCommand(command, isExcluded);
|
|
1425
|
+
await this.bashExecution.handleBashCommand(command, isExcluded);
|
|
2442
1426
|
this.isBashMode = false;
|
|
2443
1427
|
this.updateEditorBorderColor();
|
|
2444
1428
|
return;
|
|
@@ -2446,13 +1430,13 @@ export class InteractiveMode {
|
|
|
2446
1430
|
}
|
|
2447
1431
|
// Queue input during compaction (extension commands execute immediately)
|
|
2448
1432
|
if (this.session.isCompacting) {
|
|
2449
|
-
if (this.isExtensionCommand(text)) {
|
|
1433
|
+
if (this.messageQueue.isExtensionCommand(text)) {
|
|
2450
1434
|
this.editor.addToHistory?.(text);
|
|
2451
1435
|
this.editor.setText("");
|
|
2452
1436
|
await this.session.prompt(text);
|
|
2453
1437
|
}
|
|
2454
1438
|
else {
|
|
2455
|
-
this.queueCompactionMessage(text, "steer");
|
|
1439
|
+
this.messageQueue.queueCompactionMessage(text, "steer");
|
|
2456
1440
|
}
|
|
2457
1441
|
return;
|
|
2458
1442
|
}
|
|
@@ -2462,13 +1446,13 @@ export class InteractiveMode {
|
|
|
2462
1446
|
this.editor.addToHistory?.(text);
|
|
2463
1447
|
this.editor.setText("");
|
|
2464
1448
|
await this.session.prompt(text, { streamingBehavior: "steer" });
|
|
2465
|
-
this.updatePendingMessagesDisplay();
|
|
1449
|
+
this.messageQueue.updatePendingMessagesDisplay();
|
|
2466
1450
|
this.ui.requestRender();
|
|
2467
1451
|
return;
|
|
2468
1452
|
}
|
|
2469
1453
|
// Normal message submission
|
|
2470
1454
|
// First, move any pending bash components to chat
|
|
2471
|
-
this.flushPendingBashComponents();
|
|
1455
|
+
this.bashExecution.flushPendingBashComponents();
|
|
2472
1456
|
if (this.onInputCallback) {
|
|
2473
1457
|
this.onInputCallback(text);
|
|
2474
1458
|
}
|
|
@@ -2513,7 +1497,7 @@ export class InteractiveMode {
|
|
|
2513
1497
|
this.ui.requestRender();
|
|
2514
1498
|
break;
|
|
2515
1499
|
case "queue_update":
|
|
2516
|
-
this.updatePendingMessagesDisplay();
|
|
1500
|
+
this.messageQueue.updatePendingMessagesDisplay();
|
|
2517
1501
|
this.ui.requestRender();
|
|
2518
1502
|
break;
|
|
2519
1503
|
case "session_info_changed":
|
|
@@ -2539,7 +1523,7 @@ export class InteractiveMode {
|
|
|
2539
1523
|
// message can arrive while a subagent is still running.
|
|
2540
1524
|
taskStore.reset();
|
|
2541
1525
|
this.addMessageToChat(event.message);
|
|
2542
|
-
this.updatePendingMessagesDisplay();
|
|
1526
|
+
this.messageQueue.updatePendingMessagesDisplay();
|
|
2543
1527
|
this.ui.requestRender();
|
|
2544
1528
|
}
|
|
2545
1529
|
else if (event.message.role === "assistant") {
|
|
@@ -2720,7 +1704,7 @@ export class InteractiveMode {
|
|
|
2720
1704
|
this.chatContainer.addChild(new Text(theme.fg("error", event.errorMessage), 1, 0));
|
|
2721
1705
|
}
|
|
2722
1706
|
}
|
|
2723
|
-
void this.flushCompactionQueue({ willRetry: event.willRetry });
|
|
1707
|
+
void this.messageQueue.flushCompactionQueue({ willRetry: event.willRetry });
|
|
2724
1708
|
this.ui.requestRender();
|
|
2725
1709
|
break;
|
|
2726
1710
|
}
|
|
@@ -2784,273 +1768,13 @@ export class InteractiveMode {
|
|
|
2784
1768
|
* If multiple status messages are emitted back-to-back (without anything else being added to the chat),
|
|
2785
1769
|
* we update the previous status line instead of appending new ones to avoid log spam.
|
|
2786
1770
|
*/
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
* `voicetools transcribe` per press, same as before. Pressing the shortcut
|
|
2795
|
-
* again while listening (or while still warming up) cancels.
|
|
2796
|
-
*/
|
|
2797
|
-
toggleVoiceTranscribe() {
|
|
2798
|
-
if (this.voiceActive) {
|
|
2799
|
-
if (this.voiceDaemon?.isReady) {
|
|
2800
|
-
this.voiceDaemon.cancel();
|
|
2801
|
-
}
|
|
2802
|
-
else {
|
|
2803
|
-
this.voiceSession?.stop();
|
|
2804
|
-
}
|
|
2805
|
-
this.voiceSession = undefined;
|
|
2806
|
-
this.voiceActive = false;
|
|
2807
|
-
this.resetVoiceUI();
|
|
2808
|
-
return;
|
|
2809
|
-
}
|
|
2810
|
-
if (this.voiceStarting) {
|
|
2811
|
-
// A second press while resolving/warming up: honour the cancel. Any
|
|
2812
|
-
// daemon that finishes loading afterwards is kept warm for next time.
|
|
2813
|
-
this.voiceStarting = false;
|
|
2814
|
-
this.resetVoiceUI();
|
|
2815
|
-
return;
|
|
2816
|
-
}
|
|
2817
|
-
if (this.voiceDaemonUnsupported) {
|
|
2818
|
-
this.voiceStarting = true;
|
|
2819
|
-
this.showVoiceWarming("Starting voice input...");
|
|
2820
|
-
void this.resolveVoiceBin()
|
|
2821
|
-
.then((bin) => {
|
|
2822
|
-
if (!this.voiceStarting)
|
|
2823
|
-
return;
|
|
2824
|
-
this.voiceStarting = false;
|
|
2825
|
-
if (!bin) {
|
|
2826
|
-
this.resetVoiceUI();
|
|
2827
|
-
this.showError(VOICE_UNAVAILABLE_MESSAGE);
|
|
2828
|
-
return;
|
|
2829
|
-
}
|
|
2830
|
-
this.beginLegacyVoiceCapture(bin);
|
|
2831
|
-
})
|
|
2832
|
-
.catch((err) => {
|
|
2833
|
-
this.voiceStarting = false;
|
|
2834
|
-
this.resetVoiceUI();
|
|
2835
|
-
this.showError(`Voice input failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2836
|
-
});
|
|
2837
|
-
return;
|
|
2838
|
-
}
|
|
2839
|
-
if (this.voiceDaemon?.isReady) {
|
|
2840
|
-
this.beginDaemonVoiceCapture();
|
|
2841
|
-
return;
|
|
2842
|
-
}
|
|
2843
|
-
// No daemon yet: resolve the binary, then probe for `serve` support by
|
|
2844
|
-
// spawning it. `VoiceDaemon.spawn` doubles as the probe: it resolves to a
|
|
2845
|
-
// live daemon once READY arrives, to "unsupported" if the process exits
|
|
2846
|
-
// with no output at all (an old binary rejecting the unrecognized `serve`
|
|
2847
|
-
// subcommand), or to "error" if it printed a real ERROR first (e.g. no
|
|
2848
|
-
// model installed yet) — already surfaced via onError, so that case skips
|
|
2849
|
-
// the legacy fallback (it would just hit the same error) but leaves
|
|
2850
|
-
// daemon mode available to retry on the next press.
|
|
2851
|
-
this.voiceStarting = true;
|
|
2852
|
-
this.showVoiceWarming("Warming up voice input...");
|
|
2853
|
-
void this.resolveVoiceBin()
|
|
2854
|
-
.then(async (bin) => {
|
|
2855
|
-
if (!bin) {
|
|
2856
|
-
this.voiceStarting = false;
|
|
2857
|
-
this.resetVoiceUI();
|
|
2858
|
-
this.showError(VOICE_UNAVAILABLE_MESSAGE);
|
|
2859
|
-
return;
|
|
2860
|
-
}
|
|
2861
|
-
const result = await VoiceDaemon.spawn(bin, this.buildVoiceDaemonHandlers(), {
|
|
2862
|
-
silenceMs: VOICE_SILENCE_MS,
|
|
2863
|
-
idleTimeoutMs: VOICE_IDLE_TIMEOUT_MS,
|
|
2864
|
-
});
|
|
2865
|
-
if (!result.ok) {
|
|
2866
|
-
if (result.reason === "unsupported") {
|
|
2867
|
-
this.voiceDaemonUnsupported = true;
|
|
2868
|
-
if (!this.voiceStarting) {
|
|
2869
|
-
this.resetVoiceUI();
|
|
2870
|
-
return;
|
|
2871
|
-
}
|
|
2872
|
-
this.voiceStarting = false;
|
|
2873
|
-
this.beginLegacyVoiceCapture(bin);
|
|
2874
|
-
return;
|
|
2875
|
-
}
|
|
2876
|
-
this.voiceStarting = false;
|
|
2877
|
-
this.resetVoiceUI();
|
|
2878
|
-
return;
|
|
2879
|
-
}
|
|
2880
|
-
this.voiceDaemon = result.daemon;
|
|
2881
|
-
if (!this.voiceStarting) {
|
|
2882
|
-
// Cancelled while warming up: keep the loaded daemon warm for next time.
|
|
2883
|
-
this.resetVoiceUI();
|
|
2884
|
-
return;
|
|
2885
|
-
}
|
|
2886
|
-
this.voiceStarting = false;
|
|
2887
|
-
this.beginDaemonVoiceCapture();
|
|
2888
|
-
})
|
|
2889
|
-
.catch((err) => {
|
|
2890
|
-
this.voiceStarting = false;
|
|
2891
|
-
this.resetVoiceUI();
|
|
2892
|
-
this.showError(`Voice input failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2893
|
-
});
|
|
2894
|
-
}
|
|
2895
|
-
/**
|
|
2896
|
-
* Build the (stable, reused-across-captures) handlers for the daemon.
|
|
2897
|
-
*
|
|
2898
|
-
* Every handler bails out once `voiceActive` is false: CANCEL is a soft
|
|
2899
|
-
* request (unlike the legacy path's `proc.kill()`, it doesn't sever the
|
|
2900
|
-
* pipe), so the daemon can still have a trailing PARTIAL/FINAL/DONE for the
|
|
2901
|
-
* just-cancelled capture in flight when the user presses cancel. Without
|
|
2902
|
-
* this guard that stale text would land in the editor after the panel had
|
|
2903
|
-
* already collapsed.
|
|
2904
|
-
*
|
|
2905
|
-
* v0.1.4 serve streams `PARTIAL <full growing hypothesis>` (live preview,
|
|
2906
|
-
* never committed) and ends with a single `FINAL <complete text>` (the one
|
|
2907
|
-
* commit to the editor). `SEGMENT` is still handled for the legacy
|
|
2908
|
-
* transcribe path and any binary that streams committed chunks directly.
|
|
2909
|
-
*/
|
|
2910
|
-
buildVoiceDaemonHandlers() {
|
|
2911
|
-
return {
|
|
2912
|
-
onSegment: (text) => {
|
|
2913
|
-
if (!this.voiceActive)
|
|
2914
|
-
return;
|
|
2915
|
-
this.commitVoiceText(text);
|
|
2916
|
-
},
|
|
2917
|
-
onPartial: (text) => {
|
|
2918
|
-
if (!this.voiceActive)
|
|
2919
|
-
return;
|
|
2920
|
-
this.voicePanel?.setPartial(text);
|
|
2921
|
-
},
|
|
2922
|
-
onFinal: (text) => {
|
|
2923
|
-
if (!this.voiceActive)
|
|
2924
|
-
return;
|
|
2925
|
-
this.commitVoiceText(text);
|
|
2926
|
-
},
|
|
2927
|
-
onStatus: (status) => {
|
|
2928
|
-
if (!this.voiceActive)
|
|
2929
|
-
return;
|
|
2930
|
-
if (status === "done") {
|
|
2931
|
-
this.voiceActive = false;
|
|
2932
|
-
this.resetVoiceUI();
|
|
2933
|
-
return;
|
|
2934
|
-
}
|
|
2935
|
-
if (status === "transcribing") {
|
|
2936
|
-
this.voicePanel?.setTranscribing();
|
|
2937
|
-
}
|
|
2938
|
-
else if (status === "listening") {
|
|
2939
|
-
this.voicePanel?.startListening();
|
|
2940
|
-
}
|
|
2941
|
-
},
|
|
2942
|
-
onLevel: (rms) => {
|
|
2943
|
-
if (!this.voiceActive)
|
|
2944
|
-
return;
|
|
2945
|
-
this.voicePanel?.pushLevel(rms);
|
|
2946
|
-
},
|
|
2947
|
-
onPhase: (phase) => {
|
|
2948
|
-
if (!this.voiceActive)
|
|
2949
|
-
return;
|
|
2950
|
-
if (phase === "silence") {
|
|
2951
|
-
this.voicePanel?.beginSilence(VOICE_SILENCE_MS);
|
|
2952
|
-
}
|
|
2953
|
-
else {
|
|
2954
|
-
this.voicePanel?.endSilence();
|
|
2955
|
-
}
|
|
2956
|
-
},
|
|
2957
|
-
onError: (message) => {
|
|
2958
|
-
this.voiceActive = false;
|
|
2959
|
-
this.resetVoiceUI();
|
|
2960
|
-
this.showError(`Voice input failed: ${message}`);
|
|
2961
|
-
},
|
|
2962
|
-
onCrash: (message) => {
|
|
2963
|
-
this.voiceActive = false;
|
|
2964
|
-
this.voiceDaemon = undefined;
|
|
2965
|
-
this.resetVoiceUI();
|
|
2966
|
-
this.showError(`Voice input daemon crashed: ${message}. It will restart on next use.`);
|
|
2967
|
-
},
|
|
2968
|
-
onIdle: () => {
|
|
2969
|
-
// Daemon auto-shut down after idle timeout: drop the reference so the
|
|
2970
|
-
// next ctrl+r respawns (cold start). No user-facing message — this is
|
|
2971
|
-
// an expected memory-reclamation event, not an error.
|
|
2972
|
-
this.voiceDaemon = undefined;
|
|
2973
|
-
},
|
|
2974
|
-
};
|
|
2975
|
-
}
|
|
2976
|
-
/** Inject decoded text into the editor via bracketed paste (with a trailing space). */
|
|
2977
|
-
commitVoiceText(text) {
|
|
2978
|
-
this.editor.handleInput(`\x1b[200~${text} \x1b[201~`);
|
|
2979
|
-
}
|
|
2980
|
-
beginDaemonVoiceCapture() {
|
|
2981
|
-
if (!this.voiceDaemon?.isReady)
|
|
2982
|
-
return;
|
|
2983
|
-
this.voiceActive = true;
|
|
2984
|
-
this.showVoicePanel().startListening();
|
|
2985
|
-
this.voiceDaemon.startCapture();
|
|
2986
|
-
}
|
|
2987
|
-
beginLegacyVoiceCapture(bin) {
|
|
2988
|
-
this.voiceActive = true;
|
|
2989
|
-
const panel = this.showVoicePanel();
|
|
2990
|
-
panel.startListening();
|
|
2991
|
-
this.voiceSession = startVoiceTranscribe(bin, {
|
|
2992
|
-
onStatus: (status) => {
|
|
2993
|
-
if (status === "done") {
|
|
2994
|
-
this.voiceActive = false;
|
|
2995
|
-
this.voiceSession = undefined;
|
|
2996
|
-
this.resetVoiceUI();
|
|
2997
|
-
return;
|
|
2998
|
-
}
|
|
2999
|
-
// Old binaries emit no LEVEL/PARTIAL, so the panel shows a spinner
|
|
3000
|
-
// for the batch phases; committed words still stream into the editor.
|
|
3001
|
-
if (status === "transcribing")
|
|
3002
|
-
panel.setTranscribing();
|
|
3003
|
-
},
|
|
3004
|
-
onSegment: (text) => {
|
|
3005
|
-
this.commitVoiceText(text);
|
|
3006
|
-
},
|
|
3007
|
-
onError: (message) => {
|
|
3008
|
-
this.voiceActive = false;
|
|
3009
|
-
this.voiceSession = undefined;
|
|
3010
|
-
this.resetVoiceUI();
|
|
3011
|
-
this.showError(`Voice input failed: ${message}`);
|
|
3012
|
-
},
|
|
3013
|
-
});
|
|
3014
|
-
}
|
|
3015
|
-
/** Resolve the `voicetools` binary path. Prefers an explicit VOICETOOLS_BIN
|
|
3016
|
-
* override, otherwise resolves via the managed tools manager (bin dir / PATH /
|
|
3017
|
-
* download from the published release). Returns undefined when unavailable.
|
|
3018
|
-
*/
|
|
3019
|
-
async resolveVoiceBin() {
|
|
3020
|
-
const override = process.env.VOICETOOLS_BIN?.trim();
|
|
3021
|
-
if (override)
|
|
3022
|
-
return override;
|
|
3023
|
-
return ensureTool("voicetools", true);
|
|
3024
|
-
}
|
|
3025
|
-
/** Create (or reuse) the voice panel and mount it in the status container. */
|
|
3026
|
-
showVoicePanel() {
|
|
3027
|
-
if (!this.voicePanel) {
|
|
3028
|
-
this.statusContainer.clear();
|
|
3029
|
-
this.voicePanel = new VoicePanel(this.ui, keyHint("app.input.voiceTranscribe", "cancel"));
|
|
3030
|
-
this.statusContainer.addChild(this.voicePanel);
|
|
3031
|
-
}
|
|
3032
|
-
this.ui.requestRender();
|
|
3033
|
-
return this.voicePanel;
|
|
3034
|
-
}
|
|
3035
|
-
showVoiceWarming(message) {
|
|
3036
|
-
this.showVoicePanel().setWarming(message);
|
|
3037
|
-
}
|
|
3038
|
-
/** Collapse the voice panel back to nothing (idle) and stop its animation. */
|
|
3039
|
-
resetVoiceUI() {
|
|
3040
|
-
if (this.voicePanel) {
|
|
3041
|
-
this.voicePanel.dispose();
|
|
3042
|
-
this.voicePanel = undefined;
|
|
3043
|
-
}
|
|
3044
|
-
this.statusContainer.clear();
|
|
3045
|
-
this.ui.requestRender();
|
|
3046
|
-
}
|
|
3047
|
-
showStatus(message) {
|
|
3048
|
-
const children = this.chatContainer.children;
|
|
3049
|
-
const last = children.length > 0 ? children[children.length - 1] : undefined;
|
|
3050
|
-
const secondLast = children.length > 1 ? children[children.length - 2] : undefined;
|
|
3051
|
-
if (last && secondLast && last === this.lastStatusText && secondLast === this.lastStatusSpacer) {
|
|
3052
|
-
this.lastStatusText.setText(theme.fg("dim", message));
|
|
3053
|
-
this.ui.requestRender();
|
|
1771
|
+
showStatus(message) {
|
|
1772
|
+
const children = this.chatContainer.children;
|
|
1773
|
+
const last = children.length > 0 ? children[children.length - 1] : undefined;
|
|
1774
|
+
const secondLast = children.length > 1 ? children[children.length - 2] : undefined;
|
|
1775
|
+
if (last && secondLast && last === this.lastStatusText && secondLast === this.lastStatusSpacer) {
|
|
1776
|
+
this.lastStatusText.setText(theme.fg("dim", message));
|
|
1777
|
+
this.ui.requestRender();
|
|
3054
1778
|
return;
|
|
3055
1779
|
}
|
|
3056
1780
|
const spacer = new Spacer(1);
|
|
@@ -3261,7 +1985,7 @@ export class InteractiveMode {
|
|
|
3261
1985
|
// This prevents escape sequences from leaking to the parent shell over slow SSH.
|
|
3262
1986
|
await this.ui.terminal.drainInput(1000);
|
|
3263
1987
|
this.taskStoreUnsubscribe?.();
|
|
3264
|
-
this.
|
|
1988
|
+
this.teamFocus.closeAttach();
|
|
3265
1989
|
this.taskPanel.dispose();
|
|
3266
1990
|
this.stop();
|
|
3267
1991
|
await this.runtimeHost.dispose();
|
|
@@ -3355,13 +2079,13 @@ export class InteractiveMode {
|
|
|
3355
2079
|
return;
|
|
3356
2080
|
// Queue input during compaction (extension commands execute immediately)
|
|
3357
2081
|
if (this.session.isCompacting) {
|
|
3358
|
-
if (this.isExtensionCommand(text)) {
|
|
2082
|
+
if (this.messageQueue.isExtensionCommand(text)) {
|
|
3359
2083
|
this.editor.addToHistory?.(text);
|
|
3360
2084
|
this.editor.setText("");
|
|
3361
2085
|
await this.session.prompt(text);
|
|
3362
2086
|
}
|
|
3363
2087
|
else {
|
|
3364
|
-
this.queueCompactionMessage(text, "followUp");
|
|
2088
|
+
this.messageQueue.queueCompactionMessage(text, "followUp");
|
|
3365
2089
|
}
|
|
3366
2090
|
return;
|
|
3367
2091
|
}
|
|
@@ -3371,7 +2095,7 @@ export class InteractiveMode {
|
|
|
3371
2095
|
this.editor.addToHistory?.(text);
|
|
3372
2096
|
this.editor.setText("");
|
|
3373
2097
|
await this.session.prompt(text, { streamingBehavior: "followUp" });
|
|
3374
|
-
this.updatePendingMessagesDisplay();
|
|
2098
|
+
this.messageQueue.updatePendingMessagesDisplay();
|
|
3375
2099
|
this.ui.requestRender();
|
|
3376
2100
|
}
|
|
3377
2101
|
// If not streaming, Alt+Enter acts like regular Enter (trigger onSubmit)
|
|
@@ -3381,7 +2105,7 @@ export class InteractiveMode {
|
|
|
3381
2105
|
}
|
|
3382
2106
|
}
|
|
3383
2107
|
handleDequeue() {
|
|
3384
|
-
const restored = this.restoreQueuedMessagesToEditor();
|
|
2108
|
+
const restored = this.messageQueue.restoreQueuedMessagesToEditor();
|
|
3385
2109
|
if (restored === 0) {
|
|
3386
2110
|
this.showStatus("No queued messages to restore");
|
|
3387
2111
|
}
|
|
@@ -3421,31 +2145,12 @@ export class InteractiveMode {
|
|
|
3421
2145
|
this.showStatus(`Thinking level: ${newLevel}`);
|
|
3422
2146
|
}
|
|
3423
2147
|
}
|
|
3424
|
-
async cycleModel(direction) {
|
|
3425
|
-
try {
|
|
3426
|
-
const result = await this.session.cycleModel(direction);
|
|
3427
|
-
if (result === undefined) {
|
|
3428
|
-
const msg = this.session.scopedModels.length > 0 ? "Only one model in scope" : "Only one model available";
|
|
3429
|
-
this.showStatus(msg);
|
|
3430
|
-
}
|
|
3431
|
-
else {
|
|
3432
|
-
this.footer.invalidate();
|
|
3433
|
-
this.updateEditorBorderColor();
|
|
3434
|
-
const thinkingStr = result.model.reasoning && result.thinkingLevel !== "off" ? ` (thinking: ${result.thinkingLevel})` : "";
|
|
3435
|
-
this.showStatus(`Switched to ${result.model.name || result.model.id}${thinkingStr}`);
|
|
3436
|
-
void this.maybeWarnAboutAnthropicSubscriptionAuth(result.model);
|
|
3437
|
-
}
|
|
3438
|
-
}
|
|
3439
|
-
catch (error) {
|
|
3440
|
-
this.showError(error instanceof Error ? error.message : String(error));
|
|
3441
|
-
}
|
|
3442
|
-
}
|
|
3443
2148
|
toggleToolOutputExpansion() {
|
|
3444
2149
|
this.setToolsExpanded(!this.toolOutputExpanded);
|
|
3445
2150
|
}
|
|
3446
2151
|
setToolsExpanded(expanded) {
|
|
3447
2152
|
this.toolOutputExpanded = expanded;
|
|
3448
|
-
const activeHeader = this.customHeader ?? this.builtInHeader;
|
|
2153
|
+
const activeHeader = this.chrome.customHeader ?? this.builtInHeader;
|
|
3449
2154
|
if (isExpandable(activeHeader)) {
|
|
3450
2155
|
activeHeader.setExpanded(expanded);
|
|
3451
2156
|
}
|
|
@@ -3552,170 +2257,6 @@ export class InteractiveMode {
|
|
|
3552
2257
|
this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
|
|
3553
2258
|
this.ui.requestRender();
|
|
3554
2259
|
}
|
|
3555
|
-
/**
|
|
3556
|
-
* Get all queued messages (read-only).
|
|
3557
|
-
* Combines session queue and compaction queue.
|
|
3558
|
-
*/
|
|
3559
|
-
getAllQueuedMessages() {
|
|
3560
|
-
return {
|
|
3561
|
-
steering: [
|
|
3562
|
-
...this.session.getSteeringMessages(),
|
|
3563
|
-
...this.compactionQueuedMessages.filter((msg) => msg.mode === "steer").map((msg) => msg.text),
|
|
3564
|
-
],
|
|
3565
|
-
followUp: [
|
|
3566
|
-
...this.session.getFollowUpMessages(),
|
|
3567
|
-
...this.compactionQueuedMessages.filter((msg) => msg.mode === "followUp").map((msg) => msg.text),
|
|
3568
|
-
],
|
|
3569
|
-
};
|
|
3570
|
-
}
|
|
3571
|
-
/**
|
|
3572
|
-
* Clear all queued messages and return their contents.
|
|
3573
|
-
* Clears both session queue and compaction queue.
|
|
3574
|
-
*/
|
|
3575
|
-
clearAllQueues() {
|
|
3576
|
-
const { steering, followUp } = this.session.clearQueue();
|
|
3577
|
-
const compactionSteering = this.compactionQueuedMessages
|
|
3578
|
-
.filter((msg) => msg.mode === "steer")
|
|
3579
|
-
.map((msg) => msg.text);
|
|
3580
|
-
const compactionFollowUp = this.compactionQueuedMessages
|
|
3581
|
-
.filter((msg) => msg.mode === "followUp")
|
|
3582
|
-
.map((msg) => msg.text);
|
|
3583
|
-
this.compactionQueuedMessages = [];
|
|
3584
|
-
return {
|
|
3585
|
-
steering: [...steering, ...compactionSteering],
|
|
3586
|
-
followUp: [...followUp, ...compactionFollowUp],
|
|
3587
|
-
};
|
|
3588
|
-
}
|
|
3589
|
-
updatePendingMessagesDisplay() {
|
|
3590
|
-
this.pendingMessagesContainer.clear();
|
|
3591
|
-
const { steering: steeringMessages, followUp: followUpMessages } = this.getAllQueuedMessages();
|
|
3592
|
-
if (steeringMessages.length > 0 || followUpMessages.length > 0) {
|
|
3593
|
-
this.pendingMessagesContainer.addChild(new Spacer(1));
|
|
3594
|
-
for (const message of steeringMessages) {
|
|
3595
|
-
const text = theme.fg("dim", `Steering: ${message}`);
|
|
3596
|
-
this.pendingMessagesContainer.addChild(new TruncatedText(text, 1, 0));
|
|
3597
|
-
}
|
|
3598
|
-
for (const message of followUpMessages) {
|
|
3599
|
-
const text = theme.fg("dim", `Follow-up: ${message}`);
|
|
3600
|
-
this.pendingMessagesContainer.addChild(new TruncatedText(text, 1, 0));
|
|
3601
|
-
}
|
|
3602
|
-
const dequeueHint = this.getAppKeyDisplay("app.message.dequeue");
|
|
3603
|
-
const hintText = theme.fg("dim", `↳ ${dequeueHint} to edit all queued messages`);
|
|
3604
|
-
this.pendingMessagesContainer.addChild(new TruncatedText(hintText, 1, 0));
|
|
3605
|
-
}
|
|
3606
|
-
}
|
|
3607
|
-
restoreQueuedMessagesToEditor(options) {
|
|
3608
|
-
const { steering, followUp } = this.clearAllQueues();
|
|
3609
|
-
const allQueued = [...steering, ...followUp];
|
|
3610
|
-
if (allQueued.length === 0) {
|
|
3611
|
-
this.updatePendingMessagesDisplay();
|
|
3612
|
-
if (options?.abort) {
|
|
3613
|
-
this.agent.abort();
|
|
3614
|
-
}
|
|
3615
|
-
return 0;
|
|
3616
|
-
}
|
|
3617
|
-
const queuedText = allQueued.join("\n\n");
|
|
3618
|
-
const currentText = options?.currentText ?? this.editor.getText();
|
|
3619
|
-
const combinedText = [queuedText, currentText].filter((t) => t.trim()).join("\n\n");
|
|
3620
|
-
this.editor.setText(combinedText);
|
|
3621
|
-
this.updatePendingMessagesDisplay();
|
|
3622
|
-
if (options?.abort) {
|
|
3623
|
-
this.agent.abort();
|
|
3624
|
-
}
|
|
3625
|
-
return allQueued.length;
|
|
3626
|
-
}
|
|
3627
|
-
queueCompactionMessage(text, mode) {
|
|
3628
|
-
this.compactionQueuedMessages.push({ text, mode });
|
|
3629
|
-
this.editor.addToHistory?.(text);
|
|
3630
|
-
this.editor.setText("");
|
|
3631
|
-
this.updatePendingMessagesDisplay();
|
|
3632
|
-
this.showStatus("Queued message for after compaction");
|
|
3633
|
-
}
|
|
3634
|
-
isExtensionCommand(text) {
|
|
3635
|
-
if (!text.startsWith("/"))
|
|
3636
|
-
return false;
|
|
3637
|
-
const extensionRunner = this.session.extensionRunner;
|
|
3638
|
-
const spaceIndex = text.indexOf(" ");
|
|
3639
|
-
const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
|
|
3640
|
-
return !!extensionRunner.getCommand(commandName);
|
|
3641
|
-
}
|
|
3642
|
-
async flushCompactionQueue(options) {
|
|
3643
|
-
if (this.compactionQueuedMessages.length === 0) {
|
|
3644
|
-
return;
|
|
3645
|
-
}
|
|
3646
|
-
const queuedMessages = [...this.compactionQueuedMessages];
|
|
3647
|
-
this.compactionQueuedMessages = [];
|
|
3648
|
-
this.updatePendingMessagesDisplay();
|
|
3649
|
-
const restoreQueue = (error) => {
|
|
3650
|
-
this.session.clearQueue();
|
|
3651
|
-
this.compactionQueuedMessages = queuedMessages;
|
|
3652
|
-
this.updatePendingMessagesDisplay();
|
|
3653
|
-
this.showError(`Failed to send queued message${queuedMessages.length > 1 ? "s" : ""}: ${error instanceof Error ? error.message : String(error)}`);
|
|
3654
|
-
};
|
|
3655
|
-
try {
|
|
3656
|
-
if (options?.willRetry) {
|
|
3657
|
-
// When retry is pending, queue messages for the retry turn
|
|
3658
|
-
for (const message of queuedMessages) {
|
|
3659
|
-
if (this.isExtensionCommand(message.text)) {
|
|
3660
|
-
await this.session.prompt(message.text);
|
|
3661
|
-
}
|
|
3662
|
-
else if (message.mode === "followUp") {
|
|
3663
|
-
await this.session.followUp(message.text);
|
|
3664
|
-
}
|
|
3665
|
-
else {
|
|
3666
|
-
await this.session.steer(message.text);
|
|
3667
|
-
}
|
|
3668
|
-
}
|
|
3669
|
-
this.updatePendingMessagesDisplay();
|
|
3670
|
-
return;
|
|
3671
|
-
}
|
|
3672
|
-
// Find first non-extension-command message to use as prompt
|
|
3673
|
-
const firstPromptIndex = queuedMessages.findIndex((message) => !this.isExtensionCommand(message.text));
|
|
3674
|
-
if (firstPromptIndex === -1) {
|
|
3675
|
-
// All extension commands - execute them all
|
|
3676
|
-
for (const message of queuedMessages) {
|
|
3677
|
-
await this.session.prompt(message.text);
|
|
3678
|
-
}
|
|
3679
|
-
return;
|
|
3680
|
-
}
|
|
3681
|
-
// Execute any extension commands before the first prompt
|
|
3682
|
-
const preCommands = queuedMessages.slice(0, firstPromptIndex);
|
|
3683
|
-
const firstPrompt = queuedMessages[firstPromptIndex];
|
|
3684
|
-
const rest = queuedMessages.slice(firstPromptIndex + 1);
|
|
3685
|
-
for (const message of preCommands) {
|
|
3686
|
-
await this.session.prompt(message.text);
|
|
3687
|
-
}
|
|
3688
|
-
// Send first prompt (starts streaming)
|
|
3689
|
-
const promptPromise = this.session.prompt(firstPrompt.text).catch((error) => {
|
|
3690
|
-
restoreQueue(error);
|
|
3691
|
-
});
|
|
3692
|
-
// Queue remaining messages
|
|
3693
|
-
for (const message of rest) {
|
|
3694
|
-
if (this.isExtensionCommand(message.text)) {
|
|
3695
|
-
await this.session.prompt(message.text);
|
|
3696
|
-
}
|
|
3697
|
-
else if (message.mode === "followUp") {
|
|
3698
|
-
await this.session.followUp(message.text);
|
|
3699
|
-
}
|
|
3700
|
-
else {
|
|
3701
|
-
await this.session.steer(message.text);
|
|
3702
|
-
}
|
|
3703
|
-
}
|
|
3704
|
-
this.updatePendingMessagesDisplay();
|
|
3705
|
-
void promptPromise;
|
|
3706
|
-
}
|
|
3707
|
-
catch (error) {
|
|
3708
|
-
restoreQueue(error);
|
|
3709
|
-
}
|
|
3710
|
-
}
|
|
3711
|
-
/** Move pending bash components from pending area to chat */
|
|
3712
|
-
flushPendingBashComponents() {
|
|
3713
|
-
for (const component of this.pendingBashComponents) {
|
|
3714
|
-
this.pendingMessagesContainer.removeChild(component);
|
|
3715
|
-
this.chatContainer.addChild(component);
|
|
3716
|
-
}
|
|
3717
|
-
this.pendingBashComponents = [];
|
|
3718
|
-
}
|
|
3719
2260
|
// =========================================================================
|
|
3720
2261
|
// Selectors
|
|
3721
2262
|
// =========================================================================
|
|
@@ -3886,145 +2427,6 @@ export class InteractiveMode {
|
|
|
3886
2427
|
return { component: selector, focus: selector.getSettingsList() };
|
|
3887
2428
|
});
|
|
3888
2429
|
}
|
|
3889
|
-
async findExactModelMatch(searchTerm) {
|
|
3890
|
-
const models = await this.getModelCandidates();
|
|
3891
|
-
return findExactModelReferenceMatch(searchTerm, models);
|
|
3892
|
-
}
|
|
3893
|
-
async getModelCandidates() {
|
|
3894
|
-
if (this.session.scopedModels.length > 0) {
|
|
3895
|
-
return this.session.scopedModels.map((scoped) => scoped.model);
|
|
3896
|
-
}
|
|
3897
|
-
this.session.modelRegistry.refresh();
|
|
3898
|
-
try {
|
|
3899
|
-
return await this.session.modelRegistry.getAvailable();
|
|
3900
|
-
}
|
|
3901
|
-
catch {
|
|
3902
|
-
return [];
|
|
3903
|
-
}
|
|
3904
|
-
}
|
|
3905
|
-
/** Update the footer's available provider count from current model candidates */
|
|
3906
|
-
async updateAvailableProviderCount() {
|
|
3907
|
-
const models = await this.getModelCandidates();
|
|
3908
|
-
const uniqueProviders = new Set(models.map((m) => m.provider));
|
|
3909
|
-
this.footerDataProvider.setAvailableProviderCount(uniqueProviders.size);
|
|
3910
|
-
}
|
|
3911
|
-
async maybeWarnAboutAnthropicSubscriptionAuth(model = this.session.model) {
|
|
3912
|
-
if (this.settingsManager.getWarnings().anthropicExtraUsage === false) {
|
|
3913
|
-
return;
|
|
3914
|
-
}
|
|
3915
|
-
if (this.anthropicSubscriptionWarningShown) {
|
|
3916
|
-
return;
|
|
3917
|
-
}
|
|
3918
|
-
if (!model || model.provider !== "anthropic") {
|
|
3919
|
-
return;
|
|
3920
|
-
}
|
|
3921
|
-
const storedCredential = this.session.modelRegistry.authStorage.get("anthropic");
|
|
3922
|
-
if (storedCredential?.type === "oauth") {
|
|
3923
|
-
this.anthropicSubscriptionWarningShown = true;
|
|
3924
|
-
this.showWarning(ANTHROPIC_SUBSCRIPTION_AUTH_WARNING);
|
|
3925
|
-
return;
|
|
3926
|
-
}
|
|
3927
|
-
try {
|
|
3928
|
-
const apiKey = await this.session.modelRegistry.getApiKeyForProvider(model.provider);
|
|
3929
|
-
if (!isAnthropicSubscriptionAuthKey(apiKey)) {
|
|
3930
|
-
return;
|
|
3931
|
-
}
|
|
3932
|
-
this.anthropicSubscriptionWarningShown = true;
|
|
3933
|
-
this.showWarning(ANTHROPIC_SUBSCRIPTION_AUTH_WARNING);
|
|
3934
|
-
}
|
|
3935
|
-
catch {
|
|
3936
|
-
// Ignore auth lookup failures for warning-only checks.
|
|
3937
|
-
}
|
|
3938
|
-
}
|
|
3939
|
-
showModelSelector(initialSearchInput) {
|
|
3940
|
-
this.showSelector((done) => {
|
|
3941
|
-
const selector = new ModelSelectorComponent(this.ui, this.session.model, this.settingsManager, this.session.modelRegistry, this.session.scopedModels, async (model) => {
|
|
3942
|
-
try {
|
|
3943
|
-
await this.session.setModel(model);
|
|
3944
|
-
this.footer.invalidate();
|
|
3945
|
-
this.updateEditorBorderColor();
|
|
3946
|
-
done();
|
|
3947
|
-
this.showStatus(`Model: ${model.id}`);
|
|
3948
|
-
void this.maybeWarnAboutAnthropicSubscriptionAuth(model);
|
|
3949
|
-
this.checkDaxnutsEasterEgg(model);
|
|
3950
|
-
}
|
|
3951
|
-
catch (error) {
|
|
3952
|
-
done();
|
|
3953
|
-
this.showError(error instanceof Error ? error.message : String(error));
|
|
3954
|
-
}
|
|
3955
|
-
}, () => {
|
|
3956
|
-
done();
|
|
3957
|
-
this.ui.requestRender();
|
|
3958
|
-
}, initialSearchInput);
|
|
3959
|
-
return { component: selector, focus: selector };
|
|
3960
|
-
});
|
|
3961
|
-
}
|
|
3962
|
-
async showModelsSelector() {
|
|
3963
|
-
// Get all available models
|
|
3964
|
-
this.session.modelRegistry.refresh();
|
|
3965
|
-
const allModels = this.session.modelRegistry.getAvailable();
|
|
3966
|
-
if (allModels.length === 0) {
|
|
3967
|
-
this.showStatus("No models available");
|
|
3968
|
-
return;
|
|
3969
|
-
}
|
|
3970
|
-
// Check if session has scoped models (from previous session-only changes or CLI --models)
|
|
3971
|
-
const sessionScopedModels = this.session.scopedModels;
|
|
3972
|
-
const hasSessionScope = sessionScopedModels.length > 0;
|
|
3973
|
-
// Build enabled model IDs from session state or settings
|
|
3974
|
-
let currentEnabledIds = null;
|
|
3975
|
-
if (hasSessionScope) {
|
|
3976
|
-
// Use current session's scoped models
|
|
3977
|
-
currentEnabledIds = sessionScopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`);
|
|
3978
|
-
}
|
|
3979
|
-
else {
|
|
3980
|
-
// Fall back to settings
|
|
3981
|
-
const patterns = this.settingsManager.getEnabledModels();
|
|
3982
|
-
if (patterns !== undefined && patterns.length > 0) {
|
|
3983
|
-
const scopedModels = await resolveModelScope(patterns, this.session.modelRegistry);
|
|
3984
|
-
currentEnabledIds = scopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`);
|
|
3985
|
-
}
|
|
3986
|
-
}
|
|
3987
|
-
// Helper to update session's scoped models (session-only, no persist)
|
|
3988
|
-
const updateSessionModels = async (enabledIds) => {
|
|
3989
|
-
currentEnabledIds = enabledIds === null ? null : [...enabledIds];
|
|
3990
|
-
if (enabledIds && enabledIds.length > 0 && enabledIds.length < allModels.length) {
|
|
3991
|
-
const newScopedModels = await resolveModelScope(enabledIds, this.session.modelRegistry);
|
|
3992
|
-
this.session.setScopedModels(newScopedModels.map((sm) => ({
|
|
3993
|
-
model: sm.model,
|
|
3994
|
-
thinkingLevel: sm.thinkingLevel,
|
|
3995
|
-
})));
|
|
3996
|
-
}
|
|
3997
|
-
else {
|
|
3998
|
-
// All enabled or none enabled = no filter
|
|
3999
|
-
this.session.setScopedModels([]);
|
|
4000
|
-
}
|
|
4001
|
-
await this.updateAvailableProviderCount();
|
|
4002
|
-
this.ui.requestRender();
|
|
4003
|
-
};
|
|
4004
|
-
this.showSelector((done) => {
|
|
4005
|
-
const selector = new ScopedModelsSelectorComponent({
|
|
4006
|
-
allModels,
|
|
4007
|
-
enabledModelIds: currentEnabledIds,
|
|
4008
|
-
}, {
|
|
4009
|
-
onChange: async (enabledIds) => {
|
|
4010
|
-
await updateSessionModels(enabledIds);
|
|
4011
|
-
},
|
|
4012
|
-
onPersist: (enabledIds) => {
|
|
4013
|
-
// Persist to settings
|
|
4014
|
-
const newPatterns = enabledIds === null || enabledIds.length === allModels.length
|
|
4015
|
-
? undefined // All enabled = clear filter
|
|
4016
|
-
: enabledIds;
|
|
4017
|
-
this.settingsManager.setEnabledModels(newPatterns ? [...newPatterns] : undefined);
|
|
4018
|
-
this.showStatus("Model selection saved to settings");
|
|
4019
|
-
},
|
|
4020
|
-
onCancel: () => {
|
|
4021
|
-
done();
|
|
4022
|
-
this.ui.requestRender();
|
|
4023
|
-
},
|
|
4024
|
-
});
|
|
4025
|
-
return { component: selector, focus: selector };
|
|
4026
|
-
});
|
|
4027
|
-
}
|
|
4028
2430
|
showUserMessageSelector() {
|
|
4029
2431
|
const userMessages = this.session.getUserMessagesForForking();
|
|
4030
2432
|
if (userMessages.length === 0) {
|
|
@@ -4081,7 +2483,7 @@ export class InteractiveMode {
|
|
|
4081
2483
|
// Check if we should skip the prompt (user preference to always default to no summary)
|
|
4082
2484
|
if (!this.settingsManager.getBranchSummarySkipPrompt()) {
|
|
4083
2485
|
while (true) {
|
|
4084
|
-
const summaryChoice = await this.
|
|
2486
|
+
const summaryChoice = await this.dialogs.showSelector("Summarize branch?", [
|
|
4085
2487
|
"No summary",
|
|
4086
2488
|
"Summarize",
|
|
4087
2489
|
"Summarize with custom prompt",
|
|
@@ -4093,7 +2495,7 @@ export class InteractiveMode {
|
|
|
4093
2495
|
}
|
|
4094
2496
|
wantsSummary = summaryChoice !== "No summary";
|
|
4095
2497
|
if (summaryChoice === "Summarize with custom prompt") {
|
|
4096
|
-
customInstructions = await this.
|
|
2498
|
+
customInstructions = await this.dialogs.showEditor("Custom summarization instructions");
|
|
4097
2499
|
if (customInstructions === undefined) {
|
|
4098
2500
|
// User cancelled - loop back to summary selector
|
|
4099
2501
|
continue;
|
|
@@ -4137,7 +2539,7 @@ export class InteractiveMode {
|
|
|
4137
2539
|
this.editor.setText(result.editorText);
|
|
4138
2540
|
}
|
|
4139
2541
|
this.showStatus("Navigated to selected point");
|
|
4140
|
-
void this.flushCompactionQueue({ willRetry: false });
|
|
2542
|
+
void this.messageQueue.flushCompactionQueue({ willRetry: false });
|
|
4141
2543
|
}
|
|
4142
2544
|
catch (error) {
|
|
4143
2545
|
this.showError(error instanceof Error ? error.message : String(error));
|
|
@@ -4221,327 +2623,6 @@ export class InteractiveMode {
|
|
|
4221
2623
|
return this.handleFatalRuntimeError("Failed to resume session", error);
|
|
4222
2624
|
}
|
|
4223
2625
|
}
|
|
4224
|
-
getLoginProviderOptions(authType) {
|
|
4225
|
-
const authStorage = this.session.modelRegistry.authStorage;
|
|
4226
|
-
const oauthProviders = authStorage.getOAuthProviders();
|
|
4227
|
-
const oauthProviderIds = new Set(oauthProviders.map((provider) => provider.id));
|
|
4228
|
-
const options = oauthProviders.map((provider) => ({
|
|
4229
|
-
id: provider.id,
|
|
4230
|
-
name: provider.name,
|
|
4231
|
-
authType: "oauth",
|
|
4232
|
-
}));
|
|
4233
|
-
const modelProviders = new Set(this.session.modelRegistry.getAll().map((model) => model.provider));
|
|
4234
|
-
for (const providerId of modelProviders) {
|
|
4235
|
-
if (!isApiKeyLoginProvider(providerId, oauthProviderIds)) {
|
|
4236
|
-
continue;
|
|
4237
|
-
}
|
|
4238
|
-
options.push({
|
|
4239
|
-
id: providerId,
|
|
4240
|
-
name: this.session.modelRegistry.getProviderDisplayName(providerId),
|
|
4241
|
-
authType: "api_key",
|
|
4242
|
-
});
|
|
4243
|
-
}
|
|
4244
|
-
const filteredOptions = authType ? options.filter((option) => option.authType === authType) : options;
|
|
4245
|
-
return filteredOptions.sort((a, b) => a.name.localeCompare(b.name));
|
|
4246
|
-
}
|
|
4247
|
-
getLogoutProviderOptions() {
|
|
4248
|
-
const authStorage = this.session.modelRegistry.authStorage;
|
|
4249
|
-
const options = [];
|
|
4250
|
-
for (const providerId of authStorage.list()) {
|
|
4251
|
-
const credential = authStorage.get(providerId);
|
|
4252
|
-
if (!credential) {
|
|
4253
|
-
continue;
|
|
4254
|
-
}
|
|
4255
|
-
options.push({
|
|
4256
|
-
id: providerId,
|
|
4257
|
-
name: this.session.modelRegistry.getProviderDisplayName(providerId),
|
|
4258
|
-
authType: credential.type,
|
|
4259
|
-
});
|
|
4260
|
-
}
|
|
4261
|
-
return options.sort((a, b) => a.name.localeCompare(b.name));
|
|
4262
|
-
}
|
|
4263
|
-
showLoginAuthTypeSelector() {
|
|
4264
|
-
const subscriptionLabel = "Use a subscription";
|
|
4265
|
-
const apiKeyLabel = "Use an API key";
|
|
4266
|
-
this.showSelector((done) => {
|
|
4267
|
-
const selector = new ExtensionSelectorComponent("Select authentication method:", [subscriptionLabel, apiKeyLabel], (option) => {
|
|
4268
|
-
done();
|
|
4269
|
-
const authType = option === subscriptionLabel ? "oauth" : "api_key";
|
|
4270
|
-
this.showLoginProviderSelector(authType);
|
|
4271
|
-
}, () => {
|
|
4272
|
-
done();
|
|
4273
|
-
this.ui.requestRender();
|
|
4274
|
-
});
|
|
4275
|
-
return { component: selector, focus: selector };
|
|
4276
|
-
});
|
|
4277
|
-
}
|
|
4278
|
-
showLoginProviderSelector(authType) {
|
|
4279
|
-
const providerOptions = this.getLoginProviderOptions(authType);
|
|
4280
|
-
if (providerOptions.length === 0) {
|
|
4281
|
-
this.showStatus(authType === "oauth" ? "No subscription providers available." : "No API key providers available.");
|
|
4282
|
-
return;
|
|
4283
|
-
}
|
|
4284
|
-
this.showSelector((done) => {
|
|
4285
|
-
const selector = new OAuthSelectorComponent("login", this.session.modelRegistry.authStorage, providerOptions, async (providerId) => {
|
|
4286
|
-
done();
|
|
4287
|
-
const providerOption = providerOptions.find((provider) => provider.id === providerId);
|
|
4288
|
-
if (!providerOption) {
|
|
4289
|
-
return;
|
|
4290
|
-
}
|
|
4291
|
-
if (providerOption.authType === "oauth") {
|
|
4292
|
-
await this.showLoginDialog(providerOption.id, providerOption.name);
|
|
4293
|
-
}
|
|
4294
|
-
else if (providerOption.id === BEDROCK_PROVIDER_ID) {
|
|
4295
|
-
this.showBedrockSetupDialog(providerOption.id, providerOption.name);
|
|
4296
|
-
}
|
|
4297
|
-
else {
|
|
4298
|
-
await this.showApiKeyLoginDialog(providerOption.id, providerOption.name);
|
|
4299
|
-
}
|
|
4300
|
-
}, () => {
|
|
4301
|
-
done();
|
|
4302
|
-
this.showLoginAuthTypeSelector();
|
|
4303
|
-
}, (providerId) => this.session.modelRegistry.getProviderAuthStatus(providerId));
|
|
4304
|
-
return { component: selector, focus: selector };
|
|
4305
|
-
});
|
|
4306
|
-
}
|
|
4307
|
-
async showOAuthSelector(mode) {
|
|
4308
|
-
if (mode === "login") {
|
|
4309
|
-
this.showLoginAuthTypeSelector();
|
|
4310
|
-
return;
|
|
4311
|
-
}
|
|
4312
|
-
const providerOptions = this.getLogoutProviderOptions();
|
|
4313
|
-
if (providerOptions.length === 0) {
|
|
4314
|
-
this.showStatus("No stored credentials to remove. /logout only removes credentials saved by /login; environment variables and models.json config are unchanged.");
|
|
4315
|
-
return;
|
|
4316
|
-
}
|
|
4317
|
-
this.showSelector((done) => {
|
|
4318
|
-
const selector = new OAuthSelectorComponent(mode, this.session.modelRegistry.authStorage, providerOptions, async (providerId) => {
|
|
4319
|
-
done();
|
|
4320
|
-
const providerOption = providerOptions.find((provider) => provider.id === providerId);
|
|
4321
|
-
if (!providerOption) {
|
|
4322
|
-
return;
|
|
4323
|
-
}
|
|
4324
|
-
try {
|
|
4325
|
-
this.session.modelRegistry.authStorage.logout(providerOption.id);
|
|
4326
|
-
this.session.modelRegistry.refresh();
|
|
4327
|
-
await this.updateAvailableProviderCount();
|
|
4328
|
-
const message = providerOption.authType === "oauth"
|
|
4329
|
-
? `Logged out of ${providerOption.name}`
|
|
4330
|
-
: `Removed stored API key for ${providerOption.name}. Environment variables and models.json config are unchanged.`;
|
|
4331
|
-
this.showStatus(message);
|
|
4332
|
-
}
|
|
4333
|
-
catch (error) {
|
|
4334
|
-
this.showError(`Logout failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
4335
|
-
}
|
|
4336
|
-
}, () => {
|
|
4337
|
-
done();
|
|
4338
|
-
this.ui.requestRender();
|
|
4339
|
-
});
|
|
4340
|
-
return { component: selector, focus: selector };
|
|
4341
|
-
});
|
|
4342
|
-
}
|
|
4343
|
-
async completeProviderAuthentication(providerId, providerName, authType, previousModel) {
|
|
4344
|
-
this.session.modelRegistry.refresh();
|
|
4345
|
-
const actionLabel = authType === "oauth" ? `Logged in to ${providerName}` : `Saved API key for ${providerName}`;
|
|
4346
|
-
let selectedModel;
|
|
4347
|
-
let selectionError;
|
|
4348
|
-
if (isUnknownModel(previousModel)) {
|
|
4349
|
-
const availableModels = this.session.modelRegistry.getAvailable();
|
|
4350
|
-
const providerModels = availableModels.filter((model) => model.provider === providerId);
|
|
4351
|
-
if (!hasDefaultModelProvider(providerId)) {
|
|
4352
|
-
selectionError = `${actionLabel}, but no default model is configured for provider "${providerId}". Use /model to select a model.`;
|
|
4353
|
-
}
|
|
4354
|
-
else if (providerModels.length === 0) {
|
|
4355
|
-
selectionError = `${actionLabel}, but no models are available for that provider. Use /model to select a model.`;
|
|
4356
|
-
}
|
|
4357
|
-
else {
|
|
4358
|
-
const defaultModelId = defaultModelPerProvider[providerId];
|
|
4359
|
-
selectedModel = providerModels.find((model) => model.id === defaultModelId);
|
|
4360
|
-
if (!selectedModel) {
|
|
4361
|
-
selectionError = `${actionLabel}, but its default model "${defaultModelId}" is not available. Use /model to select a model.`;
|
|
4362
|
-
}
|
|
4363
|
-
else {
|
|
4364
|
-
try {
|
|
4365
|
-
await this.session.setModel(selectedModel);
|
|
4366
|
-
}
|
|
4367
|
-
catch (error) {
|
|
4368
|
-
selectedModel = undefined;
|
|
4369
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4370
|
-
selectionError = `${actionLabel}, but selecting its default model failed: ${errorMessage}. Use /model to select a model.`;
|
|
4371
|
-
}
|
|
4372
|
-
}
|
|
4373
|
-
}
|
|
4374
|
-
}
|
|
4375
|
-
await this.updateAvailableProviderCount();
|
|
4376
|
-
this.footer.invalidate();
|
|
4377
|
-
this.updateEditorBorderColor();
|
|
4378
|
-
if (selectedModel) {
|
|
4379
|
-
this.showStatus(`${actionLabel}. Selected ${selectedModel.id}. Credentials saved to ${getAuthPath()}`);
|
|
4380
|
-
void this.maybeWarnAboutAnthropicSubscriptionAuth(selectedModel);
|
|
4381
|
-
this.checkDaxnutsEasterEgg(selectedModel);
|
|
4382
|
-
}
|
|
4383
|
-
else {
|
|
4384
|
-
this.showStatus(`${actionLabel}. Credentials saved to ${getAuthPath()}`);
|
|
4385
|
-
if (selectionError) {
|
|
4386
|
-
this.showError(selectionError);
|
|
4387
|
-
}
|
|
4388
|
-
else {
|
|
4389
|
-
void this.maybeWarnAboutAnthropicSubscriptionAuth();
|
|
4390
|
-
}
|
|
4391
|
-
}
|
|
4392
|
-
}
|
|
4393
|
-
showBedrockSetupDialog(providerId, providerName) {
|
|
4394
|
-
const restoreEditor = () => {
|
|
4395
|
-
this.editorContainer.clear();
|
|
4396
|
-
this.editorContainer.addChild(this.editor);
|
|
4397
|
-
this.ui.setFocus(this.editor);
|
|
4398
|
-
this.ui.requestRender();
|
|
4399
|
-
};
|
|
4400
|
-
const dialog = new LoginDialogComponent(this.ui, providerId, () => restoreEditor(), providerName, "Amazon Bedrock setup");
|
|
4401
|
-
dialog.showInfo([
|
|
4402
|
-
theme.fg("text", "Amazon Bedrock uses AWS credentials instead of a single API key."),
|
|
4403
|
-
theme.fg("text", "Configure an AWS profile, IAM keys, bearer token, or role-based credentials."),
|
|
4404
|
-
theme.fg("muted", "See:"),
|
|
4405
|
-
theme.fg("accent", ` ${path.join(getDocsPath(), "providers.md")}`),
|
|
4406
|
-
]);
|
|
4407
|
-
this.editorContainer.clear();
|
|
4408
|
-
this.editorContainer.addChild(dialog);
|
|
4409
|
-
this.ui.setFocus(dialog);
|
|
4410
|
-
this.ui.requestRender();
|
|
4411
|
-
}
|
|
4412
|
-
async showApiKeyLoginDialog(providerId, providerName) {
|
|
4413
|
-
const previousModel = this.session.model;
|
|
4414
|
-
const dialog = new LoginDialogComponent(this.ui, providerId, (_success, _message) => {
|
|
4415
|
-
// Completion handled below
|
|
4416
|
-
}, providerName);
|
|
4417
|
-
this.editorContainer.clear();
|
|
4418
|
-
this.editorContainer.addChild(dialog);
|
|
4419
|
-
this.ui.setFocus(dialog);
|
|
4420
|
-
this.ui.requestRender();
|
|
4421
|
-
const restoreEditor = () => {
|
|
4422
|
-
this.editorContainer.clear();
|
|
4423
|
-
this.editorContainer.addChild(this.editor);
|
|
4424
|
-
this.ui.setFocus(this.editor);
|
|
4425
|
-
this.ui.requestRender();
|
|
4426
|
-
};
|
|
4427
|
-
try {
|
|
4428
|
-
const apiKey = (await dialog.showPrompt("Enter API key:")).trim();
|
|
4429
|
-
if (!apiKey) {
|
|
4430
|
-
throw new Error("API key cannot be empty.");
|
|
4431
|
-
}
|
|
4432
|
-
this.session.modelRegistry.authStorage.set(providerId, { type: "api_key", key: apiKey });
|
|
4433
|
-
restoreEditor();
|
|
4434
|
-
await this.completeProviderAuthentication(providerId, providerName, "api_key", previousModel);
|
|
4435
|
-
}
|
|
4436
|
-
catch (error) {
|
|
4437
|
-
restoreEditor();
|
|
4438
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
4439
|
-
if (errorMsg !== "Login cancelled") {
|
|
4440
|
-
this.showError(`Failed to save API key for ${providerName}: ${errorMsg}`);
|
|
4441
|
-
}
|
|
4442
|
-
}
|
|
4443
|
-
}
|
|
4444
|
-
showOAuthLoginSelect(dialog, prompt) {
|
|
4445
|
-
return new Promise((resolve) => {
|
|
4446
|
-
const restoreDialog = () => {
|
|
4447
|
-
this.editorContainer.clear();
|
|
4448
|
-
this.editorContainer.addChild(dialog);
|
|
4449
|
-
this.ui.setFocus(dialog);
|
|
4450
|
-
this.ui.requestRender();
|
|
4451
|
-
};
|
|
4452
|
-
const labels = prompt.options.map((option) => option.label);
|
|
4453
|
-
const selector = new ExtensionSelectorComponent(prompt.message, labels, (optionLabel) => {
|
|
4454
|
-
restoreDialog();
|
|
4455
|
-
resolve(prompt.options.find((option) => option.label === optionLabel)?.id);
|
|
4456
|
-
}, () => {
|
|
4457
|
-
restoreDialog();
|
|
4458
|
-
resolve(undefined);
|
|
4459
|
-
});
|
|
4460
|
-
this.editorContainer.clear();
|
|
4461
|
-
this.editorContainer.addChild(selector);
|
|
4462
|
-
this.ui.setFocus(selector);
|
|
4463
|
-
this.ui.requestRender();
|
|
4464
|
-
});
|
|
4465
|
-
}
|
|
4466
|
-
async showLoginDialog(providerId, providerName) {
|
|
4467
|
-
const providerInfo = this.session.modelRegistry.authStorage
|
|
4468
|
-
.getOAuthProviders()
|
|
4469
|
-
.find((provider) => provider.id === providerId);
|
|
4470
|
-
const previousModel = this.session.model;
|
|
4471
|
-
// Providers that use callback servers (can paste redirect URL)
|
|
4472
|
-
const usesCallbackServer = providerInfo?.usesCallbackServer ?? false;
|
|
4473
|
-
// Create login dialog component
|
|
4474
|
-
const dialog = new LoginDialogComponent(this.ui, providerId, (_success, _message) => {
|
|
4475
|
-
// Completion handled below
|
|
4476
|
-
}, providerName);
|
|
4477
|
-
// Show dialog in editor container
|
|
4478
|
-
this.editorContainer.clear();
|
|
4479
|
-
this.editorContainer.addChild(dialog);
|
|
4480
|
-
this.ui.setFocus(dialog);
|
|
4481
|
-
this.ui.requestRender();
|
|
4482
|
-
// Promise for manual code input (racing with callback server)
|
|
4483
|
-
let manualCodeResolve;
|
|
4484
|
-
let manualCodeReject;
|
|
4485
|
-
const manualCodePromise = new Promise((resolve, reject) => {
|
|
4486
|
-
manualCodeResolve = resolve;
|
|
4487
|
-
manualCodeReject = reject;
|
|
4488
|
-
});
|
|
4489
|
-
// Restore editor helper
|
|
4490
|
-
const restoreEditor = () => {
|
|
4491
|
-
this.editorContainer.clear();
|
|
4492
|
-
this.editorContainer.addChild(this.editor);
|
|
4493
|
-
this.ui.setFocus(this.editor);
|
|
4494
|
-
this.ui.requestRender();
|
|
4495
|
-
};
|
|
4496
|
-
try {
|
|
4497
|
-
await this.session.modelRegistry.authStorage.login(providerId, {
|
|
4498
|
-
onAuth: (info) => {
|
|
4499
|
-
dialog.showAuth(info.url, info.instructions);
|
|
4500
|
-
if (usesCallbackServer) {
|
|
4501
|
-
// Show input for manual paste, racing with callback
|
|
4502
|
-
dialog
|
|
4503
|
-
.showManualInput("Paste redirect URL below, or complete login in browser:")
|
|
4504
|
-
.then((value) => {
|
|
4505
|
-
if (value && manualCodeResolve) {
|
|
4506
|
-
manualCodeResolve(value);
|
|
4507
|
-
manualCodeResolve = undefined;
|
|
4508
|
-
}
|
|
4509
|
-
})
|
|
4510
|
-
.catch(() => {
|
|
4511
|
-
if (manualCodeReject) {
|
|
4512
|
-
manualCodeReject(new Error("Login cancelled"));
|
|
4513
|
-
manualCodeReject = undefined;
|
|
4514
|
-
}
|
|
4515
|
-
});
|
|
4516
|
-
}
|
|
4517
|
-
else if (providerId === "github-copilot") {
|
|
4518
|
-
// GitHub Copilot polls after onAuth
|
|
4519
|
-
dialog.showWaiting("Waiting for browser authentication...");
|
|
4520
|
-
}
|
|
4521
|
-
// For Anthropic: onPrompt is called immediately after
|
|
4522
|
-
},
|
|
4523
|
-
onPrompt: async (prompt) => {
|
|
4524
|
-
return dialog.showPrompt(prompt.message, prompt.placeholder);
|
|
4525
|
-
},
|
|
4526
|
-
onProgress: (message) => {
|
|
4527
|
-
dialog.showProgress(message);
|
|
4528
|
-
},
|
|
4529
|
-
onSelect: (prompt) => this.showOAuthLoginSelect(dialog, prompt),
|
|
4530
|
-
onManualCodeInput: () => manualCodePromise,
|
|
4531
|
-
signal: dialog.signal,
|
|
4532
|
-
});
|
|
4533
|
-
// Success
|
|
4534
|
-
restoreEditor();
|
|
4535
|
-
await this.completeProviderAuthentication(providerId, providerName, "oauth", previousModel);
|
|
4536
|
-
}
|
|
4537
|
-
catch (error) {
|
|
4538
|
-
restoreEditor();
|
|
4539
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
4540
|
-
if (errorMsg !== "Login cancelled") {
|
|
4541
|
-
this.showError(`Failed to login to ${providerName}: ${errorMsg}`);
|
|
4542
|
-
}
|
|
4543
|
-
}
|
|
4544
|
-
}
|
|
4545
2626
|
// =========================================================================
|
|
4546
2627
|
// Command handlers
|
|
4547
2628
|
// =========================================================================
|
|
@@ -4577,7 +2658,7 @@ export class InteractiveMode {
|
|
|
4577
2658
|
try {
|
|
4578
2659
|
await this.session.reload();
|
|
4579
2660
|
this.keybindings.reload();
|
|
4580
|
-
const activeHeader = this.customHeader ?? this.builtInHeader;
|
|
2661
|
+
const activeHeader = this.chrome.customHeader ?? this.builtInHeader;
|
|
4581
2662
|
if (isExpandable(activeHeader)) {
|
|
4582
2663
|
activeHeader.setExpanded(this.toolOutputExpanded);
|
|
4583
2664
|
}
|
|
@@ -4618,87 +2699,6 @@ export class InteractiveMode {
|
|
|
4618
2699
|
this.showError(`Reload failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
4619
2700
|
}
|
|
4620
2701
|
}
|
|
4621
|
-
/**
|
|
4622
|
-
* Get capitalized display string for an app keybinding action.
|
|
4623
|
-
*/
|
|
4624
|
-
getAppKeyDisplay(action) {
|
|
4625
|
-
return keyDisplayText(action);
|
|
4626
|
-
}
|
|
4627
|
-
handleDaxnuts() {
|
|
4628
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
4629
|
-
this.chatContainer.addChild(new DaxnutsComponent(this.ui));
|
|
4630
|
-
this.ui.requestRender();
|
|
4631
|
-
}
|
|
4632
|
-
checkDaxnutsEasterEgg(model) {
|
|
4633
|
-
if (model.provider === "opencode" && model.id.toLowerCase().includes("kimi-k2.5")) {
|
|
4634
|
-
this.handleDaxnuts();
|
|
4635
|
-
}
|
|
4636
|
-
}
|
|
4637
|
-
async handleBashCommand(command, excludeFromContext = false) {
|
|
4638
|
-
const extensionRunner = this.session.extensionRunner;
|
|
4639
|
-
// Emit user_bash event to let extensions intercept
|
|
4640
|
-
const eventResult = await extensionRunner.emitUserBash({
|
|
4641
|
-
type: "user_bash",
|
|
4642
|
-
command,
|
|
4643
|
-
excludeFromContext,
|
|
4644
|
-
cwd: this.sessionManager.getCwd(),
|
|
4645
|
-
});
|
|
4646
|
-
// If extension returned a full result, use it directly
|
|
4647
|
-
if (eventResult?.result) {
|
|
4648
|
-
const result = eventResult.result;
|
|
4649
|
-
// Create UI component for display
|
|
4650
|
-
this.bashComponent = new BashExecutionComponent(command, this.ui, excludeFromContext);
|
|
4651
|
-
if (this.session.isStreaming) {
|
|
4652
|
-
this.pendingMessagesContainer.addChild(this.bashComponent);
|
|
4653
|
-
this.pendingBashComponents.push(this.bashComponent);
|
|
4654
|
-
}
|
|
4655
|
-
else {
|
|
4656
|
-
this.chatContainer.addChild(this.bashComponent);
|
|
4657
|
-
}
|
|
4658
|
-
// Show output and complete
|
|
4659
|
-
if (result.output) {
|
|
4660
|
-
this.bashComponent.appendOutput(result.output);
|
|
4661
|
-
}
|
|
4662
|
-
this.bashComponent.setComplete(result.exitCode, result.cancelled, result.truncated ? { truncated: true, content: result.output } : undefined, result.fullOutputPath);
|
|
4663
|
-
// Record the result in session
|
|
4664
|
-
this.session.recordBashResult(command, result, { excludeFromContext });
|
|
4665
|
-
this.bashComponent = undefined;
|
|
4666
|
-
this.ui.requestRender();
|
|
4667
|
-
return;
|
|
4668
|
-
}
|
|
4669
|
-
// Normal execution path (possibly with custom operations)
|
|
4670
|
-
const isDeferred = this.session.isStreaming;
|
|
4671
|
-
this.bashComponent = new BashExecutionComponent(command, this.ui, excludeFromContext);
|
|
4672
|
-
if (isDeferred) {
|
|
4673
|
-
// Show in pending area when agent is streaming
|
|
4674
|
-
this.pendingMessagesContainer.addChild(this.bashComponent);
|
|
4675
|
-
this.pendingBashComponents.push(this.bashComponent);
|
|
4676
|
-
}
|
|
4677
|
-
else {
|
|
4678
|
-
// Show in chat immediately when agent is idle
|
|
4679
|
-
this.chatContainer.addChild(this.bashComponent);
|
|
4680
|
-
}
|
|
4681
|
-
this.ui.requestRender();
|
|
4682
|
-
try {
|
|
4683
|
-
const result = await this.session.executeBash(command, (chunk) => {
|
|
4684
|
-
if (this.bashComponent) {
|
|
4685
|
-
this.bashComponent.appendOutput(chunk);
|
|
4686
|
-
this.ui.requestRender();
|
|
4687
|
-
}
|
|
4688
|
-
}, { excludeFromContext, operations: eventResult?.operations });
|
|
4689
|
-
if (this.bashComponent) {
|
|
4690
|
-
this.bashComponent.setComplete(result.exitCode, result.cancelled, result.truncated ? { truncated: true, content: result.output } : undefined, result.fullOutputPath);
|
|
4691
|
-
}
|
|
4692
|
-
}
|
|
4693
|
-
catch (error) {
|
|
4694
|
-
if (this.bashComponent) {
|
|
4695
|
-
this.bashComponent.setComplete(undefined, false);
|
|
4696
|
-
}
|
|
4697
|
-
this.showError(`Bash command failed: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
4698
|
-
}
|
|
4699
|
-
this.bashComponent = undefined;
|
|
4700
|
-
this.ui.requestRender();
|
|
4701
|
-
}
|
|
4702
2702
|
async handleCompactCommand(customInstructions) {
|
|
4703
2703
|
// The session is the single source of truth for whether compaction is
|
|
4704
2704
|
// possible (e.g. "Already compacted", "Nothing to compact (session too
|
|
@@ -4718,18 +2718,7 @@ export class InteractiveMode {
|
|
|
4718
2718
|
}
|
|
4719
2719
|
stop() {
|
|
4720
2720
|
this.unregisterSignalHandlers();
|
|
4721
|
-
|
|
4722
|
-
this.voiceSession.stop();
|
|
4723
|
-
this.voiceSession = undefined;
|
|
4724
|
-
}
|
|
4725
|
-
this.voiceDaemon?.shutdown();
|
|
4726
|
-
this.voiceDaemon = undefined;
|
|
4727
|
-
if (this.voicePanel) {
|
|
4728
|
-
this.voicePanel.dispose();
|
|
4729
|
-
this.voicePanel = undefined;
|
|
4730
|
-
}
|
|
4731
|
-
this.voiceActive = false;
|
|
4732
|
-
this.voiceStarting = false;
|
|
2721
|
+
this.voice.dispose();
|
|
4733
2722
|
if (this.settingsManager.getShowTerminalProgress()) {
|
|
4734
2723
|
this.ui.terminal.setProgress(false);
|
|
4735
2724
|
}
|