@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
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Startup environment probes and the changelog-on-update lookup, shown as
|
|
3
|
+
* notices when the interactive mode starts. Extracted from interactive-mode.ts.
|
|
4
|
+
*/
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import { APP_NAME, getAgentDir, VERSION } from "../../config.js";
|
|
7
|
+
import { DefaultPackageManager } from "../../core/package-manager.js";
|
|
8
|
+
import { getChangelogPath, getNewEntries, parseChangelog } from "../../utils/changelog.js";
|
|
9
|
+
export async function checkForPackageUpdates(cwd, settingsManager) {
|
|
10
|
+
if (process.env.HOOCODE_OFFLINE) {
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
const packageManager = new DefaultPackageManager({
|
|
15
|
+
cwd,
|
|
16
|
+
agentDir: getAgentDir(),
|
|
17
|
+
settingsManager,
|
|
18
|
+
});
|
|
19
|
+
const updates = await packageManager.checkForAvailableUpdates();
|
|
20
|
+
return updates.map((update) => update.displayName);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export async function checkTmuxKeyboardSetup() {
|
|
27
|
+
if (!process.env.TMUX)
|
|
28
|
+
return undefined;
|
|
29
|
+
const runTmuxShow = (option) => {
|
|
30
|
+
return new Promise((resolve) => {
|
|
31
|
+
const proc = spawn("tmux", ["show", "-gv", option], {
|
|
32
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
33
|
+
});
|
|
34
|
+
let stdout = "";
|
|
35
|
+
const timer = setTimeout(() => {
|
|
36
|
+
proc.kill();
|
|
37
|
+
resolve(undefined);
|
|
38
|
+
}, 2000);
|
|
39
|
+
proc.stdout?.on("data", (data) => {
|
|
40
|
+
stdout += data.toString();
|
|
41
|
+
});
|
|
42
|
+
proc.on("error", () => {
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
resolve(undefined);
|
|
45
|
+
});
|
|
46
|
+
proc.on("close", (code) => {
|
|
47
|
+
clearTimeout(timer);
|
|
48
|
+
resolve(code === 0 ? stdout.trim() : undefined);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
const [extendedKeys, extendedKeysFormat] = await Promise.all([
|
|
53
|
+
runTmuxShow("extended-keys"),
|
|
54
|
+
runTmuxShow("extended-keys-format"),
|
|
55
|
+
]);
|
|
56
|
+
// If we couldn't query tmux (timeout, sandbox, etc.), don't warn
|
|
57
|
+
if (extendedKeys === undefined)
|
|
58
|
+
return undefined;
|
|
59
|
+
if (extendedKeys !== "on" && extendedKeys !== "always") {
|
|
60
|
+
return "tmux extended-keys is off. Modified Enter keys may not work. Add `set -g extended-keys on` to ~/.tmux.conf and restart tmux.";
|
|
61
|
+
}
|
|
62
|
+
if (extendedKeysFormat === "xterm") {
|
|
63
|
+
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.`;
|
|
64
|
+
}
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
function reportInstallTelemetry(_version) {
|
|
68
|
+
// Disabled in HooCode fork: this is forked from pi (upstream pi.dev install-telemetry endpoint is not run by this fork).
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Get changelog entries to display on startup.
|
|
72
|
+
* Only shows new entries since last seen version, skips for resumed sessions.
|
|
73
|
+
*/
|
|
74
|
+
export function getChangelogForDisplay(options) {
|
|
75
|
+
// Skip changelog for resumed/continued sessions (already have messages)
|
|
76
|
+
if (options.hasMessages) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
const settingsManager = options.settingsManager;
|
|
80
|
+
const lastVersion = settingsManager.getLastChangelogVersion();
|
|
81
|
+
const changelogPath = getChangelogPath();
|
|
82
|
+
const entries = parseChangelog(changelogPath);
|
|
83
|
+
if (!lastVersion) {
|
|
84
|
+
// Fresh install - record the latest entry's version (not VERSION, which may
|
|
85
|
+
// overshoot the latest entry and silently swallow it once it appears).
|
|
86
|
+
// Fall back to VERSION only if no entries exist yet.
|
|
87
|
+
const seedVersion = entries.length > 0 ? `${entries[0].major}.${entries[0].minor}.${entries[0].patch}` : VERSION;
|
|
88
|
+
settingsManager.setLastChangelogVersion(seedVersion);
|
|
89
|
+
reportInstallTelemetry(seedVersion);
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
const newEntries = getNewEntries(entries, lastVersion);
|
|
93
|
+
if (newEntries.length > 0) {
|
|
94
|
+
const latest = newEntries[0];
|
|
95
|
+
const latestVersion = `${latest.major}.${latest.minor}.${latest.patch}`;
|
|
96
|
+
settingsManager.setLastChangelogVersion(latestVersion);
|
|
97
|
+
reportInstallTelemetry(latestVersion);
|
|
98
|
+
return newEntries.map((e) => e.content).join("\n\n");
|
|
99
|
+
}
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=startup-checks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"startup-checks.js","sourceRoot":"","sources":["../../../src/modes/interactive/startup-checks.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Startup environment probes and the changelog-on-update lookup, shown as\n * notices when the interactive mode starts. Extracted from interactive-mode.ts.\n */\n\nimport { spawn } from \"node:child_process\";\nimport { APP_NAME, getAgentDir, VERSION } from \"../../config.js\";\nimport { DefaultPackageManager } from \"../../core/package-manager.js\";\nimport type { SettingsManager } from \"../../core/settings-manager.js\";\nimport { getChangelogPath, getNewEntries, parseChangelog } from \"../../utils/changelog.js\";\n\nexport async function checkForPackageUpdates(cwd: string, settingsManager: SettingsManager): Promise<string[]> {\n\tif (process.env.HOOCODE_OFFLINE) {\n\t\treturn [];\n\t}\n\n\ttry {\n\t\tconst packageManager = new DefaultPackageManager({\n\t\t\tcwd,\n\t\t\tagentDir: getAgentDir(),\n\t\t\tsettingsManager,\n\t\t});\n\t\tconst updates = await packageManager.checkForAvailableUpdates();\n\t\treturn updates.map((update) => update.displayName);\n\t} catch {\n\t\treturn [];\n\t}\n}\n\nexport async function checkTmuxKeyboardSetup(): Promise<string | undefined> {\n\tif (!process.env.TMUX) return undefined;\n\n\tconst runTmuxShow = (option: string): Promise<string | undefined> => {\n\t\treturn new Promise((resolve) => {\n\t\t\tconst proc = spawn(\"tmux\", [\"show\", \"-gv\", option], {\n\t\t\t\tstdio: [\"ignore\", \"pipe\", \"ignore\"],\n\t\t\t});\n\t\t\tlet stdout = \"\";\n\t\t\tconst timer = setTimeout(() => {\n\t\t\t\tproc.kill();\n\t\t\t\tresolve(undefined);\n\t\t\t}, 2000);\n\n\t\t\tproc.stdout?.on(\"data\", (data) => {\n\t\t\t\tstdout += data.toString();\n\t\t\t});\n\t\t\tproc.on(\"error\", () => {\n\t\t\t\tclearTimeout(timer);\n\t\t\t\tresolve(undefined);\n\t\t\t});\n\t\t\tproc.on(\"close\", (code) => {\n\t\t\t\tclearTimeout(timer);\n\t\t\t\tresolve(code === 0 ? stdout.trim() : undefined);\n\t\t\t});\n\t\t});\n\t};\n\n\tconst [extendedKeys, extendedKeysFormat] = await Promise.all([\n\t\trunTmuxShow(\"extended-keys\"),\n\t\trunTmuxShow(\"extended-keys-format\"),\n\t]);\n\n\t// If we couldn't query tmux (timeout, sandbox, etc.), don't warn\n\tif (extendedKeys === undefined) return undefined;\n\n\tif (extendedKeys !== \"on\" && extendedKeys !== \"always\") {\n\t\treturn \"tmux extended-keys is off. Modified Enter keys may not work. Add `set -g extended-keys on` to ~/.tmux.conf and restart tmux.\";\n\t}\n\n\tif (extendedKeysFormat === \"xterm\") {\n\t\treturn `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.`;\n\t}\n\n\treturn undefined;\n}\n\nfunction reportInstallTelemetry(_version: string): void {\n\t// Disabled in HooCode fork: this is forked from pi (upstream pi.dev install-telemetry endpoint is not run by this fork).\n}\n\n/**\n * Get changelog entries to display on startup.\n * Only shows new entries since last seen version, skips for resumed sessions.\n */\nexport function getChangelogForDisplay(options: {\n\t/** True for resumed/continued sessions (which already have messages). */\n\thasMessages: boolean;\n\tsettingsManager: SettingsManager;\n}): string | undefined {\n\t// Skip changelog for resumed/continued sessions (already have messages)\n\tif (options.hasMessages) {\n\t\treturn undefined;\n\t}\n\n\tconst settingsManager = options.settingsManager;\n\tconst lastVersion = settingsManager.getLastChangelogVersion();\n\tconst changelogPath = getChangelogPath();\n\tconst entries = parseChangelog(changelogPath);\n\n\tif (!lastVersion) {\n\t\t// Fresh install - record the latest entry's version (not VERSION, which may\n\t\t// overshoot the latest entry and silently swallow it once it appears).\n\t\t// Fall back to VERSION only if no entries exist yet.\n\t\tconst seedVersion = entries.length > 0 ? `${entries[0].major}.${entries[0].minor}.${entries[0].patch}` : VERSION;\n\t\tsettingsManager.setLastChangelogVersion(seedVersion);\n\t\treportInstallTelemetry(seedVersion);\n\t\treturn undefined;\n\t}\n\n\tconst newEntries = getNewEntries(entries, lastVersion);\n\tif (newEntries.length > 0) {\n\t\tconst latest = newEntries[0];\n\t\tconst latestVersion = `${latest.major}.${latest.minor}.${latest.patch}`;\n\t\tsettingsManager.setLastChangelogVersion(latestVersion);\n\t\treportInstallTelemetry(latestVersion);\n\t\treturn newEntries.map((e) => e.content).join(\"\\n\\n\");\n\t}\n\n\treturn undefined;\n}\n"]}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hoocode team focus (--team): focus role rows, nudge, attach.
|
|
3
|
+
*
|
|
4
|
+
* Wires a hooteams connection into the TUI: team focus (app.team.focus),
|
|
5
|
+
* nudging (n), the attach side panel (a) on the task panel's teams view, and
|
|
6
|
+
* approval gates. task_paused events (and gates already pending on the server)
|
|
7
|
+
* surface inline in the attach panel when it shows the paused role, otherwise
|
|
8
|
+
* in the options pane; the answer goes back over POST /tasks/:id/resume.
|
|
9
|
+
*/
|
|
10
|
+
import type { Component, Container, TUI } from "@kolisachint/hoocode-tui";
|
|
11
|
+
import type { AskQuestion, ExtensionUIDialogOptions } from "../../core/extensions/index.js";
|
|
12
|
+
import type { TeamViewConnection } from "../../core/team-view.js";
|
|
13
|
+
import type { TaskPanelComponent } from "./components/task-panel.js";
|
|
14
|
+
/** The slice of the interactive mode the team feature needs. */
|
|
15
|
+
export interface TeamFocusDeps {
|
|
16
|
+
ui: TUI;
|
|
17
|
+
taskPanel: TaskPanelComponent;
|
|
18
|
+
editorContainer: Container;
|
|
19
|
+
/** The prompt editor (read at call time; the editor can be swapped). */
|
|
20
|
+
getEditor(): Component;
|
|
21
|
+
showStatus(message: string): void;
|
|
22
|
+
showWarning(message: string): void;
|
|
23
|
+
showAskOptions(questions: AskQuestion[], opts?: ExtensionUIDialogOptions): Promise<string[] | undefined>;
|
|
24
|
+
/** True while another ask-options pane is on screen. */
|
|
25
|
+
isAskOptionsOpen(): boolean;
|
|
26
|
+
}
|
|
27
|
+
export declare class TeamFocusController {
|
|
28
|
+
private readonly deps;
|
|
29
|
+
private client;
|
|
30
|
+
private attachPanel;
|
|
31
|
+
private attachHandle;
|
|
32
|
+
constructor(deps: TeamFocusDeps);
|
|
33
|
+
/** True once a team client has been attached (--team). */
|
|
34
|
+
get connected(): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Wire a hooteams connection into the TUI. Called by main.ts when `--team`
|
|
37
|
+
* is set, before run().
|
|
38
|
+
*/
|
|
39
|
+
attachClient(client: TeamViewConnection): void;
|
|
40
|
+
private presentApproval;
|
|
41
|
+
/** Move keyboard focus to the task panel's team roster. */
|
|
42
|
+
enterFocus(): void;
|
|
43
|
+
/** Leave team focus: detach any side panel and return focus to the editor. */
|
|
44
|
+
exitFocus(): void;
|
|
45
|
+
/**
|
|
46
|
+
* Inline nudge editor for a role (n in team focus or while attached).
|
|
47
|
+
* Swaps the prompt editor for a one-line input; submit fires POST /steer in
|
|
48
|
+
* the background (the REPL and team focus are never blocked on the network).
|
|
49
|
+
*/
|
|
50
|
+
showNudgeInput(role: string): void;
|
|
51
|
+
/** Open the attach side panel for a role (a in team focus). */
|
|
52
|
+
showAttach(role: string): void;
|
|
53
|
+
/** Detach the side panel; the role keeps running. Safe to call when closed. */
|
|
54
|
+
closeAttach(): void;
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=team-focus.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"team-focus.d.ts","sourceRoot":"","sources":["../../../src/modes/interactive/team-focus.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAiB,GAAG,EAAE,MAAM,0BAA0B,CAAC;AACzF,OAAO,KAAK,EAAE,WAAW,EAAE,wBAAwB,EAAE,MAAM,gCAAgC,CAAC;AAG5F,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAElE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAGrE,gEAAgE;AAChE,MAAM,WAAW,aAAa;IAC7B,EAAE,EAAE,GAAG,CAAC;IACR,SAAS,EAAE,kBAAkB,CAAC;IAC9B,eAAe,EAAE,SAAS,CAAC;IAC3B,wEAAwE;IACxE,SAAS,IAAI,SAAS,CAAC;IACvB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,cAAc,CAAC,SAAS,EAAE,WAAW,EAAE,EAAE,IAAI,CAAC,EAAE,wBAAwB,GAAG,OAAO,CAAC,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC;IACzG,wDAAwD;IACxD,gBAAgB,IAAI,OAAO,CAAC;CAC5B;AAED,qBAAa,mBAAmB;IAKnB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAJjC,OAAO,CAAC,MAAM,CAA6C;IAC3D,OAAO,CAAC,WAAW,CAAmD;IACtE,OAAO,CAAC,YAAY,CAAwC;IAE5D,YAA6B,IAAI,EAAE,aAAa,EAAI;IAEpD,0DAA0D;IAC1D,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED;;;OAGG;IACH,YAAY,CAAC,MAAM,EAAE,kBAAkB,GAAG,IAAI,CAkB7C;YAUa,eAAe;IA6B7B,2DAA2D;IAC3D,UAAU,IAAI,IAAI,CAYjB;IAED,8EAA8E;IAC9E,SAAS,IAAI,IAAI,CAIhB;IAED;;;;OAIG;IACH,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAsCjC;IAED,+DAA+D;IAC/D,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAuB7B;IAED,+EAA+E;IAC/E,WAAW,IAAI,IAAI,CAKlB;CACD","sourcesContent":["/**\n * hoocode team focus (--team): focus role rows, nudge, attach.\n *\n * Wires a hooteams connection into the TUI: team focus (app.team.focus),\n * nudging (n), the attach side panel (a) on the task panel's teams view, and\n * approval gates. task_paused events (and gates already pending on the server)\n * surface inline in the attach panel when it shows the paused role, otherwise\n * in the options pane; the answer goes back over POST /tasks/:id/resume.\n */\n\nimport type { Component, Container, OverlayHandle, TUI } from \"@kolisachint/hoocode-tui\";\nimport type { AskQuestion, ExtensionUIDialogOptions } from \"../../core/extensions/index.js\";\nimport { taskStore } from \"../../core/task-store.js\";\nimport { type TeamApproval, TeamApprovalCoordinator } from \"../../core/team-approvals.js\";\nimport type { TeamViewConnection } from \"../../core/team-view.js\";\nimport { ExtensionInputComponent } from \"./components/extension-input.js\";\nimport type { TaskPanelComponent } from \"./components/task-panel.js\";\nimport { TeamAttachPanelComponent } from \"./components/team-attach-panel.js\";\n\n/** The slice of the interactive mode the team feature needs. */\nexport interface TeamFocusDeps {\n\tui: TUI;\n\ttaskPanel: TaskPanelComponent;\n\teditorContainer: Container;\n\t/** The prompt editor (read at call time; the editor can be swapped). */\n\tgetEditor(): Component;\n\tshowStatus(message: string): void;\n\tshowWarning(message: string): void;\n\tshowAskOptions(questions: AskQuestion[], opts?: ExtensionUIDialogOptions): Promise<string[] | undefined>;\n\t/** True while another ask-options pane is on screen. */\n\tisAskOptionsOpen(): boolean;\n}\n\nexport class TeamFocusController {\n\tprivate client: TeamViewConnection | undefined = undefined;\n\tprivate attachPanel: TeamAttachPanelComponent | undefined = undefined;\n\tprivate attachHandle: OverlayHandle | undefined = undefined;\n\n\tconstructor(private readonly deps: TeamFocusDeps) {}\n\n\t/** True once a team client has been attached (--team). */\n\tget connected(): boolean {\n\t\treturn this.client !== undefined;\n\t}\n\n\t/**\n\t * Wire a hooteams connection into the TUI. Called by main.ts when `--team`\n\t * is set, before run().\n\t */\n\tattachClient(client: TeamViewConnection): void {\n\t\tthis.client = client;\n\t\tconst approvals = new TeamApprovalCoordinator({\n\t\t\tpresent: (approval, signal) => this.presentApproval(approval, signal),\n\t\t\tresume: (taskId, option) => client.resume(taskId, option),\n\t\t\tinfo: (message) => this.deps.showStatus(message),\n\t\t\twarn: (message) => this.deps.showWarning(message),\n\t\t});\n\t\tclient.subscribe((event) => approvals.handleEvent(event));\n\t\t// Gates that opened before we attached don't replay as task_paused.\n\t\tvoid client.pendingApprovals().then(\n\t\t\t(pending) => {\n\t\t\t\tfor (const gate of pending) approvals.enqueuePending(gate);\n\t\t\t},\n\t\t\t() => {\n\t\t\t\t// Best-effort like the rest of the bridge; live gates still arrive via SSE.\n\t\t\t},\n\t\t);\n\t}\n\n\t/**\n\t * Show one team approval gate and resolve with the chosen (or free-form)\n\t * answer, undefined when skipped. When the attach side panel is open on the\n\t * role that paused, the gate renders inline in the panel — right where its\n\t * stream stopped; otherwise it goes to the options pane, waiting politely\n\t * while another ask is on screen. Either way the signal (gate answered\n\t * elsewhere) dismisses the prompt.\n\t */\n\tprivate async presentApproval(approval: TeamApproval, signal: AbortSignal): Promise<string | undefined> {\n\t\tconst panel = this.attachPanel;\n\t\tif (panel && approval.role === panel.role) {\n\t\t\tthis.attachHandle?.focus();\n\t\t\tconst answer = await panel.presentApproval(approval, signal);\n\t\t\t// Detaching mid-gate settles as skipped — fall through to the options\n\t\t\t// pane so the question isn't silently lost. A skip with the panel\n\t\t\t// still open is a real skip.\n\t\t\tif (answer !== undefined || signal.aborted || this.attachPanel === panel) return answer;\n\t\t}\n\t\twhile (this.deps.isAskOptionsOpen() && !signal.aborted) {\n\t\t\tawait new Promise((resolve) => setTimeout(resolve, 200));\n\t\t}\n\t\tif (signal.aborted) return undefined;\n\t\tconst answers = await this.deps.showAskOptions(\n\t\t\t[\n\t\t\t\t{\n\t\t\t\t\tquestion: approval.question,\n\t\t\t\t\tshort: approval.taskId,\n\t\t\t\t\tdetail: `team task \"${approval.taskId}\"${approval.role ? ` (${approval.role})` : \"\"} is paused until answered`,\n\t\t\t\t\toptions: approval.options.map((label) => ({ label })),\n\t\t\t\t\tallowCustom: true,\n\t\t\t\t},\n\t\t\t],\n\t\t\t{ signal },\n\t\t);\n\t\treturn answers?.[0];\n\t}\n\n\t/** Move keyboard focus to the task panel's team roster. */\n\tenterFocus(): void {\n\t\tif (!this.client) {\n\t\t\tthis.deps.showStatus(\"No team connected. Start with --team <url> to mirror a hooteams server.\");\n\t\t\treturn;\n\t\t}\n\t\tif (!taskStore.agents().some((a) => a.kind === \"role\")) {\n\t\t\tthis.deps.showStatus(\"Team roster is empty — waiting for roles from the team server.\");\n\t\t\treturn;\n\t\t}\n\t\tthis.deps.taskPanel.setView(\"teams\");\n\t\tthis.deps.ui.setFocus(this.deps.taskPanel);\n\t\tthis.deps.ui.requestRender();\n\t}\n\n\t/** Leave team focus: detach any side panel and return focus to the editor. */\n\texitFocus(): void {\n\t\tthis.closeAttach();\n\t\tthis.deps.ui.setFocus(this.deps.getEditor());\n\t\tthis.deps.ui.requestRender();\n\t}\n\n\t/**\n\t * Inline nudge editor for a role (n in team focus or while attached).\n\t * Swaps the prompt editor for a one-line input; submit fires POST /steer in\n\t * the background (the REPL and team focus are never blocked on the network).\n\t */\n\tshowNudgeInput(role: string): void {\n\t\tconst client = this.client;\n\t\tif (!client) return;\n\n\t\tconst restoreFocus = () => {\n\t\t\tthis.deps.editorContainer.clear();\n\t\t\tthis.deps.editorContainer.addChild(this.deps.getEditor());\n\t\t\t// Return focus where the nudge came from: the attach panel if one is\n\t\t\t// open, otherwise the team roster.\n\t\t\tif (this.attachHandle) this.attachHandle.focus();\n\t\t\telse this.deps.ui.setFocus(this.deps.taskPanel);\n\t\t\tthis.deps.ui.requestRender();\n\t\t};\n\n\t\tconst input = new ExtensionInputComponent(\n\t\t\t`Nudge ${role}`,\n\t\t\tundefined,\n\t\t\t(value) => {\n\t\t\t\tinput.dispose();\n\t\t\t\trestoreFocus();\n\t\t\t\tconst message = value.trim();\n\t\t\t\tif (!message) return;\n\t\t\t\tvoid client.steer(role, message).then(\n\t\t\t\t\t() => this.deps.showStatus(`Nudged ${role}`),\n\t\t\t\t\t(error) => this.deps.showWarning(`Failed to nudge ${role}: ${String(error)}`),\n\t\t\t\t);\n\t\t\t},\n\t\t\t() => {\n\t\t\t\tinput.dispose();\n\t\t\t\trestoreFocus();\n\t\t\t},\n\t\t\t{ tui: this.deps.ui },\n\t\t);\n\n\t\tthis.deps.editorContainer.clear();\n\t\tthis.deps.editorContainer.addChild(input);\n\t\tthis.deps.ui.setFocus(input);\n\t\tthis.deps.ui.requestRender();\n\t}\n\n\t/** Open the attach side panel for a role (a in team focus). */\n\tshowAttach(role: string): void {\n\t\tconst client = this.client;\n\t\tif (!client) return;\n\t\t// One attached role at a time: re-attaching swaps the panel.\n\t\tthis.closeAttach();\n\t\tconst panel = new TeamAttachPanelComponent(\n\t\t\trole,\n\t\t\tclient,\n\t\t\t{\n\t\t\t\tonDetach: () => this.closeAttach(),\n\t\t\t\tonNudge: (attachedRole) => this.showNudgeInput(attachedRole),\n\t\t\t},\n\t\t\tthis.deps.ui,\n\t\t);\n\t\tthis.attachPanel = panel;\n\t\t// preFocus is the task panel (attach is triggered from team focus), so\n\t\t// hiding the overlay drops the user back on the role roster.\n\t\tthis.attachHandle = this.deps.ui.showOverlay(panel, {\n\t\t\tanchor: \"top-right\",\n\t\t\twidth: \"45%\",\n\t\t\tminWidth: 36,\n\t\t\tmargin: { top: 1, right: 1 },\n\t\t});\n\t}\n\n\t/** Detach the side panel; the role keeps running. Safe to call when closed. */\n\tcloseAttach(): void {\n\t\tthis.attachPanel?.dispose();\n\t\tthis.attachHandle?.hide();\n\t\tthis.attachPanel = undefined;\n\t\tthis.attachHandle = undefined;\n\t}\n}\n"]}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hoocode team focus (--team): focus role rows, nudge, attach.
|
|
3
|
+
*
|
|
4
|
+
* Wires a hooteams connection into the TUI: team focus (app.team.focus),
|
|
5
|
+
* nudging (n), the attach side panel (a) on the task panel's teams view, and
|
|
6
|
+
* approval gates. task_paused events (and gates already pending on the server)
|
|
7
|
+
* surface inline in the attach panel when it shows the paused role, otherwise
|
|
8
|
+
* in the options pane; the answer goes back over POST /tasks/:id/resume.
|
|
9
|
+
*/
|
|
10
|
+
import { taskStore } from "../../core/task-store.js";
|
|
11
|
+
import { TeamApprovalCoordinator } from "../../core/team-approvals.js";
|
|
12
|
+
import { ExtensionInputComponent } from "./components/extension-input.js";
|
|
13
|
+
import { TeamAttachPanelComponent } from "./components/team-attach-panel.js";
|
|
14
|
+
export class TeamFocusController {
|
|
15
|
+
deps;
|
|
16
|
+
client = undefined;
|
|
17
|
+
attachPanel = undefined;
|
|
18
|
+
attachHandle = undefined;
|
|
19
|
+
constructor(deps) {
|
|
20
|
+
this.deps = deps;
|
|
21
|
+
}
|
|
22
|
+
/** True once a team client has been attached (--team). */
|
|
23
|
+
get connected() {
|
|
24
|
+
return this.client !== undefined;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Wire a hooteams connection into the TUI. Called by main.ts when `--team`
|
|
28
|
+
* is set, before run().
|
|
29
|
+
*/
|
|
30
|
+
attachClient(client) {
|
|
31
|
+
this.client = client;
|
|
32
|
+
const approvals = new TeamApprovalCoordinator({
|
|
33
|
+
present: (approval, signal) => this.presentApproval(approval, signal),
|
|
34
|
+
resume: (taskId, option) => client.resume(taskId, option),
|
|
35
|
+
info: (message) => this.deps.showStatus(message),
|
|
36
|
+
warn: (message) => this.deps.showWarning(message),
|
|
37
|
+
});
|
|
38
|
+
client.subscribe((event) => approvals.handleEvent(event));
|
|
39
|
+
// Gates that opened before we attached don't replay as task_paused.
|
|
40
|
+
void client.pendingApprovals().then((pending) => {
|
|
41
|
+
for (const gate of pending)
|
|
42
|
+
approvals.enqueuePending(gate);
|
|
43
|
+
}, () => {
|
|
44
|
+
// Best-effort like the rest of the bridge; live gates still arrive via SSE.
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Show one team approval gate and resolve with the chosen (or free-form)
|
|
49
|
+
* answer, undefined when skipped. When the attach side panel is open on the
|
|
50
|
+
* role that paused, the gate renders inline in the panel — right where its
|
|
51
|
+
* stream stopped; otherwise it goes to the options pane, waiting politely
|
|
52
|
+
* while another ask is on screen. Either way the signal (gate answered
|
|
53
|
+
* elsewhere) dismisses the prompt.
|
|
54
|
+
*/
|
|
55
|
+
async presentApproval(approval, signal) {
|
|
56
|
+
const panel = this.attachPanel;
|
|
57
|
+
if (panel && approval.role === panel.role) {
|
|
58
|
+
this.attachHandle?.focus();
|
|
59
|
+
const answer = await panel.presentApproval(approval, signal);
|
|
60
|
+
// Detaching mid-gate settles as skipped — fall through to the options
|
|
61
|
+
// pane so the question isn't silently lost. A skip with the panel
|
|
62
|
+
// still open is a real skip.
|
|
63
|
+
if (answer !== undefined || signal.aborted || this.attachPanel === panel)
|
|
64
|
+
return answer;
|
|
65
|
+
}
|
|
66
|
+
while (this.deps.isAskOptionsOpen() && !signal.aborted) {
|
|
67
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
68
|
+
}
|
|
69
|
+
if (signal.aborted)
|
|
70
|
+
return undefined;
|
|
71
|
+
const answers = await this.deps.showAskOptions([
|
|
72
|
+
{
|
|
73
|
+
question: approval.question,
|
|
74
|
+
short: approval.taskId,
|
|
75
|
+
detail: `team task "${approval.taskId}"${approval.role ? ` (${approval.role})` : ""} is paused until answered`,
|
|
76
|
+
options: approval.options.map((label) => ({ label })),
|
|
77
|
+
allowCustom: true,
|
|
78
|
+
},
|
|
79
|
+
], { signal });
|
|
80
|
+
return answers?.[0];
|
|
81
|
+
}
|
|
82
|
+
/** Move keyboard focus to the task panel's team roster. */
|
|
83
|
+
enterFocus() {
|
|
84
|
+
if (!this.client) {
|
|
85
|
+
this.deps.showStatus("No team connected. Start with --team <url> to mirror a hooteams server.");
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (!taskStore.agents().some((a) => a.kind === "role")) {
|
|
89
|
+
this.deps.showStatus("Team roster is empty — waiting for roles from the team server.");
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
this.deps.taskPanel.setView("teams");
|
|
93
|
+
this.deps.ui.setFocus(this.deps.taskPanel);
|
|
94
|
+
this.deps.ui.requestRender();
|
|
95
|
+
}
|
|
96
|
+
/** Leave team focus: detach any side panel and return focus to the editor. */
|
|
97
|
+
exitFocus() {
|
|
98
|
+
this.closeAttach();
|
|
99
|
+
this.deps.ui.setFocus(this.deps.getEditor());
|
|
100
|
+
this.deps.ui.requestRender();
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Inline nudge editor for a role (n in team focus or while attached).
|
|
104
|
+
* Swaps the prompt editor for a one-line input; submit fires POST /steer in
|
|
105
|
+
* the background (the REPL and team focus are never blocked on the network).
|
|
106
|
+
*/
|
|
107
|
+
showNudgeInput(role) {
|
|
108
|
+
const client = this.client;
|
|
109
|
+
if (!client)
|
|
110
|
+
return;
|
|
111
|
+
const restoreFocus = () => {
|
|
112
|
+
this.deps.editorContainer.clear();
|
|
113
|
+
this.deps.editorContainer.addChild(this.deps.getEditor());
|
|
114
|
+
// Return focus where the nudge came from: the attach panel if one is
|
|
115
|
+
// open, otherwise the team roster.
|
|
116
|
+
if (this.attachHandle)
|
|
117
|
+
this.attachHandle.focus();
|
|
118
|
+
else
|
|
119
|
+
this.deps.ui.setFocus(this.deps.taskPanel);
|
|
120
|
+
this.deps.ui.requestRender();
|
|
121
|
+
};
|
|
122
|
+
const input = new ExtensionInputComponent(`Nudge ${role}`, undefined, (value) => {
|
|
123
|
+
input.dispose();
|
|
124
|
+
restoreFocus();
|
|
125
|
+
const message = value.trim();
|
|
126
|
+
if (!message)
|
|
127
|
+
return;
|
|
128
|
+
void client.steer(role, message).then(() => this.deps.showStatus(`Nudged ${role}`), (error) => this.deps.showWarning(`Failed to nudge ${role}: ${String(error)}`));
|
|
129
|
+
}, () => {
|
|
130
|
+
input.dispose();
|
|
131
|
+
restoreFocus();
|
|
132
|
+
}, { tui: this.deps.ui });
|
|
133
|
+
this.deps.editorContainer.clear();
|
|
134
|
+
this.deps.editorContainer.addChild(input);
|
|
135
|
+
this.deps.ui.setFocus(input);
|
|
136
|
+
this.deps.ui.requestRender();
|
|
137
|
+
}
|
|
138
|
+
/** Open the attach side panel for a role (a in team focus). */
|
|
139
|
+
showAttach(role) {
|
|
140
|
+
const client = this.client;
|
|
141
|
+
if (!client)
|
|
142
|
+
return;
|
|
143
|
+
// One attached role at a time: re-attaching swaps the panel.
|
|
144
|
+
this.closeAttach();
|
|
145
|
+
const panel = new TeamAttachPanelComponent(role, client, {
|
|
146
|
+
onDetach: () => this.closeAttach(),
|
|
147
|
+
onNudge: (attachedRole) => this.showNudgeInput(attachedRole),
|
|
148
|
+
}, this.deps.ui);
|
|
149
|
+
this.attachPanel = panel;
|
|
150
|
+
// preFocus is the task panel (attach is triggered from team focus), so
|
|
151
|
+
// hiding the overlay drops the user back on the role roster.
|
|
152
|
+
this.attachHandle = this.deps.ui.showOverlay(panel, {
|
|
153
|
+
anchor: "top-right",
|
|
154
|
+
width: "45%",
|
|
155
|
+
minWidth: 36,
|
|
156
|
+
margin: { top: 1, right: 1 },
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
/** Detach the side panel; the role keeps running. Safe to call when closed. */
|
|
160
|
+
closeAttach() {
|
|
161
|
+
this.attachPanel?.dispose();
|
|
162
|
+
this.attachHandle?.hide();
|
|
163
|
+
this.attachPanel = undefined;
|
|
164
|
+
this.attachHandle = undefined;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
//# sourceMappingURL=team-focus.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"team-focus.js","sourceRoot":"","sources":["../../../src/modes/interactive/team-focus.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * hoocode team focus (--team): focus role rows, nudge, attach.\n *\n * Wires a hooteams connection into the TUI: team focus (app.team.focus),\n * nudging (n), the attach side panel (a) on the task panel's teams view, and\n * approval gates. task_paused events (and gates already pending on the server)\n * surface inline in the attach panel when it shows the paused role, otherwise\n * in the options pane; the answer goes back over POST /tasks/:id/resume.\n */\n\nimport type { Component, Container, OverlayHandle, TUI } from \"@kolisachint/hoocode-tui\";\nimport type { AskQuestion, ExtensionUIDialogOptions } from \"../../core/extensions/index.js\";\nimport { taskStore } from \"../../core/task-store.js\";\nimport { type TeamApproval, TeamApprovalCoordinator } from \"../../core/team-approvals.js\";\nimport type { TeamViewConnection } from \"../../core/team-view.js\";\nimport { ExtensionInputComponent } from \"./components/extension-input.js\";\nimport type { TaskPanelComponent } from \"./components/task-panel.js\";\nimport { TeamAttachPanelComponent } from \"./components/team-attach-panel.js\";\n\n/** The slice of the interactive mode the team feature needs. */\nexport interface TeamFocusDeps {\n\tui: TUI;\n\ttaskPanel: TaskPanelComponent;\n\teditorContainer: Container;\n\t/** The prompt editor (read at call time; the editor can be swapped). */\n\tgetEditor(): Component;\n\tshowStatus(message: string): void;\n\tshowWarning(message: string): void;\n\tshowAskOptions(questions: AskQuestion[], opts?: ExtensionUIDialogOptions): Promise<string[] | undefined>;\n\t/** True while another ask-options pane is on screen. */\n\tisAskOptionsOpen(): boolean;\n}\n\nexport class TeamFocusController {\n\tprivate client: TeamViewConnection | undefined = undefined;\n\tprivate attachPanel: TeamAttachPanelComponent | undefined = undefined;\n\tprivate attachHandle: OverlayHandle | undefined = undefined;\n\n\tconstructor(private readonly deps: TeamFocusDeps) {}\n\n\t/** True once a team client has been attached (--team). */\n\tget connected(): boolean {\n\t\treturn this.client !== undefined;\n\t}\n\n\t/**\n\t * Wire a hooteams connection into the TUI. Called by main.ts when `--team`\n\t * is set, before run().\n\t */\n\tattachClient(client: TeamViewConnection): void {\n\t\tthis.client = client;\n\t\tconst approvals = new TeamApprovalCoordinator({\n\t\t\tpresent: (approval, signal) => this.presentApproval(approval, signal),\n\t\t\tresume: (taskId, option) => client.resume(taskId, option),\n\t\t\tinfo: (message) => this.deps.showStatus(message),\n\t\t\twarn: (message) => this.deps.showWarning(message),\n\t\t});\n\t\tclient.subscribe((event) => approvals.handleEvent(event));\n\t\t// Gates that opened before we attached don't replay as task_paused.\n\t\tvoid client.pendingApprovals().then(\n\t\t\t(pending) => {\n\t\t\t\tfor (const gate of pending) approvals.enqueuePending(gate);\n\t\t\t},\n\t\t\t() => {\n\t\t\t\t// Best-effort like the rest of the bridge; live gates still arrive via SSE.\n\t\t\t},\n\t\t);\n\t}\n\n\t/**\n\t * Show one team approval gate and resolve with the chosen (or free-form)\n\t * answer, undefined when skipped. When the attach side panel is open on the\n\t * role that paused, the gate renders inline in the panel — right where its\n\t * stream stopped; otherwise it goes to the options pane, waiting politely\n\t * while another ask is on screen. Either way the signal (gate answered\n\t * elsewhere) dismisses the prompt.\n\t */\n\tprivate async presentApproval(approval: TeamApproval, signal: AbortSignal): Promise<string | undefined> {\n\t\tconst panel = this.attachPanel;\n\t\tif (panel && approval.role === panel.role) {\n\t\t\tthis.attachHandle?.focus();\n\t\t\tconst answer = await panel.presentApproval(approval, signal);\n\t\t\t// Detaching mid-gate settles as skipped — fall through to the options\n\t\t\t// pane so the question isn't silently lost. A skip with the panel\n\t\t\t// still open is a real skip.\n\t\t\tif (answer !== undefined || signal.aborted || this.attachPanel === panel) return answer;\n\t\t}\n\t\twhile (this.deps.isAskOptionsOpen() && !signal.aborted) {\n\t\t\tawait new Promise((resolve) => setTimeout(resolve, 200));\n\t\t}\n\t\tif (signal.aborted) return undefined;\n\t\tconst answers = await this.deps.showAskOptions(\n\t\t\t[\n\t\t\t\t{\n\t\t\t\t\tquestion: approval.question,\n\t\t\t\t\tshort: approval.taskId,\n\t\t\t\t\tdetail: `team task \"${approval.taskId}\"${approval.role ? ` (${approval.role})` : \"\"} is paused until answered`,\n\t\t\t\t\toptions: approval.options.map((label) => ({ label })),\n\t\t\t\t\tallowCustom: true,\n\t\t\t\t},\n\t\t\t],\n\t\t\t{ signal },\n\t\t);\n\t\treturn answers?.[0];\n\t}\n\n\t/** Move keyboard focus to the task panel's team roster. */\n\tenterFocus(): void {\n\t\tif (!this.client) {\n\t\t\tthis.deps.showStatus(\"No team connected. Start with --team <url> to mirror a hooteams server.\");\n\t\t\treturn;\n\t\t}\n\t\tif (!taskStore.agents().some((a) => a.kind === \"role\")) {\n\t\t\tthis.deps.showStatus(\"Team roster is empty — waiting for roles from the team server.\");\n\t\t\treturn;\n\t\t}\n\t\tthis.deps.taskPanel.setView(\"teams\");\n\t\tthis.deps.ui.setFocus(this.deps.taskPanel);\n\t\tthis.deps.ui.requestRender();\n\t}\n\n\t/** Leave team focus: detach any side panel and return focus to the editor. */\n\texitFocus(): void {\n\t\tthis.closeAttach();\n\t\tthis.deps.ui.setFocus(this.deps.getEditor());\n\t\tthis.deps.ui.requestRender();\n\t}\n\n\t/**\n\t * Inline nudge editor for a role (n in team focus or while attached).\n\t * Swaps the prompt editor for a one-line input; submit fires POST /steer in\n\t * the background (the REPL and team focus are never blocked on the network).\n\t */\n\tshowNudgeInput(role: string): void {\n\t\tconst client = this.client;\n\t\tif (!client) return;\n\n\t\tconst restoreFocus = () => {\n\t\t\tthis.deps.editorContainer.clear();\n\t\t\tthis.deps.editorContainer.addChild(this.deps.getEditor());\n\t\t\t// Return focus where the nudge came from: the attach panel if one is\n\t\t\t// open, otherwise the team roster.\n\t\t\tif (this.attachHandle) this.attachHandle.focus();\n\t\t\telse this.deps.ui.setFocus(this.deps.taskPanel);\n\t\t\tthis.deps.ui.requestRender();\n\t\t};\n\n\t\tconst input = new ExtensionInputComponent(\n\t\t\t`Nudge ${role}`,\n\t\t\tundefined,\n\t\t\t(value) => {\n\t\t\t\tinput.dispose();\n\t\t\t\trestoreFocus();\n\t\t\t\tconst message = value.trim();\n\t\t\t\tif (!message) return;\n\t\t\t\tvoid client.steer(role, message).then(\n\t\t\t\t\t() => this.deps.showStatus(`Nudged ${role}`),\n\t\t\t\t\t(error) => this.deps.showWarning(`Failed to nudge ${role}: ${String(error)}`),\n\t\t\t\t);\n\t\t\t},\n\t\t\t() => {\n\t\t\t\tinput.dispose();\n\t\t\t\trestoreFocus();\n\t\t\t},\n\t\t\t{ tui: this.deps.ui },\n\t\t);\n\n\t\tthis.deps.editorContainer.clear();\n\t\tthis.deps.editorContainer.addChild(input);\n\t\tthis.deps.ui.setFocus(input);\n\t\tthis.deps.ui.requestRender();\n\t}\n\n\t/** Open the attach side panel for a role (a in team focus). */\n\tshowAttach(role: string): void {\n\t\tconst client = this.client;\n\t\tif (!client) return;\n\t\t// One attached role at a time: re-attaching swaps the panel.\n\t\tthis.closeAttach();\n\t\tconst panel = new TeamAttachPanelComponent(\n\t\t\trole,\n\t\t\tclient,\n\t\t\t{\n\t\t\t\tonDetach: () => this.closeAttach(),\n\t\t\t\tonNudge: (attachedRole) => this.showNudgeInput(attachedRole),\n\t\t\t},\n\t\t\tthis.deps.ui,\n\t\t);\n\t\tthis.attachPanel = panel;\n\t\t// preFocus is the task panel (attach is triggered from team focus), so\n\t\t// hiding the overlay drops the user back on the role roster.\n\t\tthis.attachHandle = this.deps.ui.showOverlay(panel, {\n\t\t\tanchor: \"top-right\",\n\t\t\twidth: \"45%\",\n\t\t\tminWidth: 36,\n\t\t\tmargin: { top: 1, right: 1 },\n\t\t});\n\t}\n\n\t/** Detach the side panel; the role keeps running. Safe to call when closed. */\n\tcloseAttach(): void {\n\t\tthis.attachPanel?.dispose();\n\t\tthis.attachHandle?.hide();\n\t\tthis.attachPanel = undefined;\n\t\tthis.attachHandle = undefined;\n\t}\n}\n"]}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Voice-to-text capture control (ctrl+r / app.input.voiceTranscribe).
|
|
3
|
+
*
|
|
4
|
+
* Prefers a persistent `voicetools serve` daemon: the first press probes for
|
|
5
|
+
* support and (if found) loads models once, showing a "warming up" spinner;
|
|
6
|
+
* every later press reuses the already-warm daemon and jumps straight to
|
|
7
|
+
* Listening. Binaries without `serve` support fall back to spawning
|
|
8
|
+
* `voicetools transcribe` per press. Pressing the shortcut again while
|
|
9
|
+
* listening (or while still warming up) cancels.
|
|
10
|
+
*/
|
|
11
|
+
import type { Container, TUI } from "@kolisachint/hoocode-tui";
|
|
12
|
+
/** The slice of the interactive mode the voice feature needs. */
|
|
13
|
+
export interface VoiceControllerDeps {
|
|
14
|
+
ui: TUI;
|
|
15
|
+
/** The status area below the chat; the voice panel mounts here. */
|
|
16
|
+
statusContainer: Container;
|
|
17
|
+
/** Feed raw input (bracketed paste) to the prompt editor. */
|
|
18
|
+
sendEditorInput(data: string): void;
|
|
19
|
+
showError(message: string): void;
|
|
20
|
+
}
|
|
21
|
+
export declare class VoiceController {
|
|
22
|
+
private readonly deps;
|
|
23
|
+
/** In-flight legacy (`voicetools transcribe`) capture. */
|
|
24
|
+
private session;
|
|
25
|
+
private daemon;
|
|
26
|
+
/** Set when the binary rejected `serve`; future presses use the legacy path. */
|
|
27
|
+
private daemonUnsupported;
|
|
28
|
+
/** True while the voicetools binary is being resolved/downloaded before a session starts. */
|
|
29
|
+
private starting;
|
|
30
|
+
/** True while a capture is active (listening or transcribing). */
|
|
31
|
+
private active;
|
|
32
|
+
/** The status panel while voice input is active. */
|
|
33
|
+
private panel;
|
|
34
|
+
constructor(deps: VoiceControllerDeps);
|
|
35
|
+
/** Toggle voice-to-text capture. */
|
|
36
|
+
toggle(): void;
|
|
37
|
+
/** Stop any capture, shut down the daemon, and drop the panel. */
|
|
38
|
+
dispose(): void;
|
|
39
|
+
/**
|
|
40
|
+
* Build the (stable, reused-across-captures) handlers for the daemon.
|
|
41
|
+
*
|
|
42
|
+
* Every handler bails out once `active` is false: CANCEL is a soft
|
|
43
|
+
* request (unlike the legacy path's `proc.kill()`, it doesn't sever the
|
|
44
|
+
* pipe), so the daemon can still have a trailing PARTIAL/FINAL/DONE for the
|
|
45
|
+
* just-cancelled capture in flight when the user presses cancel. Without
|
|
46
|
+
* this guard that stale text would land in the editor after the panel had
|
|
47
|
+
* already collapsed.
|
|
48
|
+
*
|
|
49
|
+
* v0.1.4 serve streams `PARTIAL <full growing hypothesis>` (live preview,
|
|
50
|
+
* never committed) and ends with a single `FINAL <complete text>` (the one
|
|
51
|
+
* commit to the editor). `SEGMENT` is still handled for the legacy
|
|
52
|
+
* transcribe path and any binary that streams committed chunks directly.
|
|
53
|
+
*/
|
|
54
|
+
private buildDaemonHandlers;
|
|
55
|
+
/** Inject decoded text into the editor via bracketed paste (with a trailing space). */
|
|
56
|
+
private commitText;
|
|
57
|
+
private beginDaemonCapture;
|
|
58
|
+
private beginLegacyCapture;
|
|
59
|
+
private resolveBin;
|
|
60
|
+
/** Create (or reuse) the voice panel and mount it in the status container. */
|
|
61
|
+
private showPanel;
|
|
62
|
+
private showWarming;
|
|
63
|
+
/** Collapse the voice panel back to nothing (idle) and stop its animation. */
|
|
64
|
+
private resetUI;
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=voice-controller.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"voice-controller.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/voice/voice-controller.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,0BAA0B,CAAC;AAoB/D,iEAAiE;AACjE,MAAM,WAAW,mBAAmB;IACnC,EAAE,EAAE,GAAG,CAAC;IACR,mEAAmE;IACnE,eAAe,EAAE,SAAS,CAAC;IAC3B,6DAA6D;IAC7D,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAED,qBAAa,eAAe;IAef,OAAO,CAAC,QAAQ,CAAC,IAAI;IAdjC,0DAA0D;IAC1D,OAAO,CAAC,OAAO,CAA2B;IAG1C,OAAO,CAAC,MAAM,CAA0B;IACxC,gFAAgF;IAChF,OAAO,CAAC,iBAAiB,CAAS;IAClC,6FAA6F;IAC7F,OAAO,CAAC,QAAQ,CAAS;IACzB,kEAAkE;IAClE,OAAO,CAAC,MAAM,CAAS;IACvB,oDAAoD;IACpD,OAAO,CAAC,KAAK,CAAyB;IAEtC,YAA6B,IAAI,EAAE,mBAAmB,EAAI;IAE1D,oCAAoC;IACpC,MAAM,IAAI,IAAI,CAmGb;IAED,kEAAkE;IAClE,OAAO,IAAI,IAAI,CAad;IAED;;;;;;;;;;;;;;OAcG;IACH,OAAO,CAAC,mBAAmB;IA2D3B,uFAAuF;IACvF,OAAO,CAAC,UAAU;IAIlB,OAAO,CAAC,kBAAkB;IAO1B,OAAO,CAAC,kBAAkB;YAgCZ,UAAU;IAMxB,8EAA8E;IAC9E,OAAO,CAAC,SAAS;IAUjB,OAAO,CAAC,WAAW;IAInB,8EAA8E;IAC9E,OAAO,CAAC,OAAO;CAQf","sourcesContent":["/**\n * Voice-to-text capture control (ctrl+r / app.input.voiceTranscribe).\n *\n * Prefers a persistent `voicetools serve` daemon: the first press probes for\n * support and (if found) loads models once, showing a \"warming up\" spinner;\n * every later press reuses the already-warm daemon and jumps straight to\n * Listening. Binaries without `serve` support fall back to spawning\n * `voicetools transcribe` per press. Pressing the shortcut again while\n * listening (or while still warming up) cancels.\n */\n\nimport type { Container, TUI } from \"@kolisachint/hoocode-tui\";\nimport { ensureTool } from \"../../../utils/tools-manager.js\";\nimport { keyHint } from \"../components/keybinding-hints.js\";\nimport { VoicePanel } from \"./voice-panel.js\";\nimport { startVoiceTranscribe, VoiceDaemon, type VoiceDaemonHandlers, type VoiceSession } from \"./voice-transcribe.js\";\n\n// Trailing-silence window: how long a pause while speaking lasts before the\n// capture auto-stops. Passed to `voicetools serve` via `--silence-ms` (see\n// VoiceDaemon.spawn) so the binary's real cutoff matches the on-screen\n// countdown this same value drives. Kept generous so a thinking pause mid-\n// sentence doesn't cut the user off (the binary's own default is 600ms).\nconst VOICE_SILENCE_MS = 3000;\n/** How long to keep the warm voice model in memory after the last capture\n * completes. The daemon auto-shuts down after this window, releasing the\n * ~900 MB resident model; the next ctrl+r pays a cold-start respawn cost. */\nconst VOICE_IDLE_TIMEOUT_MS = 60_000;\nconst VOICE_UNAVAILABLE_MESSAGE =\n\t\"Voice input failed: voicetools binary unavailable and could not be downloaded. \" +\n\t\"Install it, set VOICETOOLS_BIN, or ensure a published release exists for this platform.\";\n\n/** The slice of the interactive mode the voice feature needs. */\nexport interface VoiceControllerDeps {\n\tui: TUI;\n\t/** The status area below the chat; the voice panel mounts here. */\n\tstatusContainer: Container;\n\t/** Feed raw input (bracketed paste) to the prompt editor. */\n\tsendEditorInput(data: string): void;\n\tshowError(message: string): void;\n}\n\nexport class VoiceController {\n\t/** In-flight legacy (`voicetools transcribe`) capture. */\n\tprivate session: VoiceSession | undefined;\n\t// Persistent `voicetools serve` process, once probed successfully. Stays alive\n\t// between captures so the model stays warm.\n\tprivate daemon: VoiceDaemon | undefined;\n\t/** Set when the binary rejected `serve`; future presses use the legacy path. */\n\tprivate daemonUnsupported = false;\n\t/** True while the voicetools binary is being resolved/downloaded before a session starts. */\n\tprivate starting = false;\n\t/** True while a capture is active (listening or transcribing). */\n\tprivate active = false;\n\t/** The status panel while voice input is active. */\n\tprivate panel: VoicePanel | undefined;\n\n\tconstructor(private readonly deps: VoiceControllerDeps) {}\n\n\t/** Toggle voice-to-text capture. */\n\ttoggle(): void {\n\t\tif (this.active) {\n\t\t\tif (this.daemon?.isReady) {\n\t\t\t\tthis.daemon.cancel();\n\t\t\t} else {\n\t\t\t\tthis.session?.stop();\n\t\t\t}\n\t\t\tthis.session = undefined;\n\t\t\tthis.active = false;\n\t\t\tthis.resetUI();\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.starting) {\n\t\t\t// A second press while resolving/warming up: honour the cancel. Any\n\t\t\t// daemon that finishes loading afterwards is kept warm for next time.\n\t\t\tthis.starting = false;\n\t\t\tthis.resetUI();\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.daemonUnsupported) {\n\t\t\tthis.starting = true;\n\t\t\tthis.showWarming(\"Starting voice input...\");\n\t\t\tvoid this.resolveBin()\n\t\t\t\t.then((bin) => {\n\t\t\t\t\tif (!this.starting) return;\n\t\t\t\t\tthis.starting = false;\n\t\t\t\t\tif (!bin) {\n\t\t\t\t\t\tthis.resetUI();\n\t\t\t\t\t\tthis.deps.showError(VOICE_UNAVAILABLE_MESSAGE);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tthis.beginLegacyCapture(bin);\n\t\t\t\t})\n\t\t\t\t.catch((err: unknown) => {\n\t\t\t\t\tthis.starting = false;\n\t\t\t\t\tthis.resetUI();\n\t\t\t\t\tthis.deps.showError(`Voice input failed: ${err instanceof Error ? err.message : String(err)}`);\n\t\t\t\t});\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.daemon?.isReady) {\n\t\t\tthis.beginDaemonCapture();\n\t\t\treturn;\n\t\t}\n\n\t\t// No daemon yet: resolve the binary, then probe for `serve` support by\n\t\t// spawning it. `VoiceDaemon.spawn` doubles as the probe: it resolves to a\n\t\t// live daemon once READY arrives, to \"unsupported\" if the process exits\n\t\t// with no output at all (an old binary rejecting the unrecognized `serve`\n\t\t// subcommand), or to \"error\" if it printed a real ERROR first (e.g. no\n\t\t// model installed yet) — already surfaced via onError, so that case skips\n\t\t// the legacy fallback (it would just hit the same error) but leaves\n\t\t// daemon mode available to retry on the next press.\n\t\tthis.starting = true;\n\t\tthis.showWarming(\"Warming up voice input...\");\n\t\tvoid this.resolveBin()\n\t\t\t.then(async (bin) => {\n\t\t\t\tif (!bin) {\n\t\t\t\t\tthis.starting = false;\n\t\t\t\t\tthis.resetUI();\n\t\t\t\t\tthis.deps.showError(VOICE_UNAVAILABLE_MESSAGE);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst result = await VoiceDaemon.spawn(bin, this.buildDaemonHandlers(), {\n\t\t\t\t\tsilenceMs: VOICE_SILENCE_MS,\n\t\t\t\t\tidleTimeoutMs: VOICE_IDLE_TIMEOUT_MS,\n\t\t\t\t});\n\t\t\t\tif (!result.ok) {\n\t\t\t\t\tif (result.reason === \"unsupported\") {\n\t\t\t\t\t\tthis.daemonUnsupported = true;\n\t\t\t\t\t\tif (!this.starting) {\n\t\t\t\t\t\t\tthis.resetUI();\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthis.starting = false;\n\t\t\t\t\t\tthis.beginLegacyCapture(bin);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tthis.starting = false;\n\t\t\t\t\tthis.resetUI();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tthis.daemon = result.daemon;\n\t\t\t\tif (!this.starting) {\n\t\t\t\t\t// Cancelled while warming up: keep the loaded daemon warm for next time.\n\t\t\t\t\tthis.resetUI();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tthis.starting = false;\n\t\t\t\tthis.beginDaemonCapture();\n\t\t\t})\n\t\t\t.catch((err: unknown) => {\n\t\t\t\tthis.starting = false;\n\t\t\t\tthis.resetUI();\n\t\t\t\tthis.deps.showError(`Voice input failed: ${err instanceof Error ? err.message : String(err)}`);\n\t\t\t});\n\t}\n\n\t/** Stop any capture, shut down the daemon, and drop the panel. */\n\tdispose(): void {\n\t\tif (this.session?.running) {\n\t\t\tthis.session.stop();\n\t\t\tthis.session = undefined;\n\t\t}\n\t\tthis.daemon?.shutdown();\n\t\tthis.daemon = undefined;\n\t\tif (this.panel) {\n\t\t\tthis.panel.dispose();\n\t\t\tthis.panel = undefined;\n\t\t}\n\t\tthis.active = false;\n\t\tthis.starting = false;\n\t}\n\n\t/**\n\t * Build the (stable, reused-across-captures) handlers for the daemon.\n\t *\n\t * Every handler bails out once `active` is false: CANCEL is a soft\n\t * request (unlike the legacy path's `proc.kill()`, it doesn't sever the\n\t * pipe), so the daemon can still have a trailing PARTIAL/FINAL/DONE for the\n\t * just-cancelled capture in flight when the user presses cancel. Without\n\t * this guard that stale text would land in the editor after the panel had\n\t * already collapsed.\n\t *\n\t * v0.1.4 serve streams `PARTIAL <full growing hypothesis>` (live preview,\n\t * never committed) and ends with a single `FINAL <complete text>` (the one\n\t * commit to the editor). `SEGMENT` is still handled for the legacy\n\t * transcribe path and any binary that streams committed chunks directly.\n\t */\n\tprivate buildDaemonHandlers(): VoiceDaemonHandlers {\n\t\treturn {\n\t\t\tonSegment: (text) => {\n\t\t\t\tif (!this.active) return;\n\t\t\t\tthis.commitText(text);\n\t\t\t},\n\t\t\tonPartial: (text) => {\n\t\t\t\tif (!this.active) return;\n\t\t\t\tthis.panel?.setPartial(text);\n\t\t\t},\n\t\t\tonFinal: (text) => {\n\t\t\t\tif (!this.active) return;\n\t\t\t\tthis.commitText(text);\n\t\t\t},\n\t\t\tonStatus: (status) => {\n\t\t\t\tif (!this.active) return;\n\t\t\t\tif (status === \"done\") {\n\t\t\t\t\tthis.active = false;\n\t\t\t\t\tthis.resetUI();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (status === \"transcribing\") {\n\t\t\t\t\tthis.panel?.setTranscribing();\n\t\t\t\t} else if (status === \"listening\") {\n\t\t\t\t\tthis.panel?.startListening();\n\t\t\t\t}\n\t\t\t},\n\t\t\tonLevel: (rms) => {\n\t\t\t\tif (!this.active) return;\n\t\t\t\tthis.panel?.pushLevel(rms);\n\t\t\t},\n\t\t\tonPhase: (phase) => {\n\t\t\t\tif (!this.active) return;\n\t\t\t\tif (phase === \"silence\") {\n\t\t\t\t\tthis.panel?.beginSilence(VOICE_SILENCE_MS);\n\t\t\t\t} else {\n\t\t\t\t\tthis.panel?.endSilence();\n\t\t\t\t}\n\t\t\t},\n\t\t\tonError: (message) => {\n\t\t\t\tthis.active = false;\n\t\t\t\tthis.resetUI();\n\t\t\t\tthis.deps.showError(`Voice input failed: ${message}`);\n\t\t\t},\n\t\t\tonCrash: (message) => {\n\t\t\t\tthis.active = false;\n\t\t\t\tthis.daemon = undefined;\n\t\t\t\tthis.resetUI();\n\t\t\t\tthis.deps.showError(`Voice input daemon crashed: ${message}. It will restart on next use.`);\n\t\t\t},\n\t\t\tonIdle: () => {\n\t\t\t\t// Daemon auto-shut down after idle timeout: drop the reference so the\n\t\t\t\t// next ctrl+r respawns (cold start). No user-facing message — this is\n\t\t\t\t// an expected memory-reclamation event, not an error.\n\t\t\t\tthis.daemon = undefined;\n\t\t\t},\n\t\t};\n\t}\n\n\t/** Inject decoded text into the editor via bracketed paste (with a trailing space). */\n\tprivate commitText(text: string): void {\n\t\tthis.deps.sendEditorInput(`\\x1b[200~${text} \\x1b[201~`);\n\t}\n\n\tprivate beginDaemonCapture(): void {\n\t\tif (!this.daemon?.isReady) return;\n\t\tthis.active = true;\n\t\tthis.showPanel().startListening();\n\t\tthis.daemon.startCapture();\n\t}\n\n\tprivate beginLegacyCapture(bin: string): void {\n\t\tthis.active = true;\n\t\tconst panel = this.showPanel();\n\t\tpanel.startListening();\n\t\tthis.session = startVoiceTranscribe(bin, {\n\t\t\tonStatus: (status) => {\n\t\t\t\tif (status === \"done\") {\n\t\t\t\t\tthis.active = false;\n\t\t\t\t\tthis.session = undefined;\n\t\t\t\t\tthis.resetUI();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t// Old binaries emit no LEVEL/PARTIAL, so the panel shows a spinner\n\t\t\t\t// for the batch phases; committed words still stream into the editor.\n\t\t\t\tif (status === \"transcribing\") panel.setTranscribing();\n\t\t\t},\n\t\t\tonSegment: (text) => {\n\t\t\t\tthis.commitText(text);\n\t\t\t},\n\t\t\tonError: (message) => {\n\t\t\t\tthis.active = false;\n\t\t\t\tthis.session = undefined;\n\t\t\t\tthis.resetUI();\n\t\t\t\tthis.deps.showError(`Voice input failed: ${message}`);\n\t\t\t},\n\t\t});\n\t}\n\n\t/** Resolve the `voicetools` binary path. Prefers an explicit VOICETOOLS_BIN\n\t * override, otherwise resolves via the managed tools manager (bin dir / PATH /\n\t * download from the published release). Returns undefined when unavailable.\n\t */\n\tprivate async resolveBin(): Promise<string | undefined> {\n\t\tconst override = process.env.VOICETOOLS_BIN?.trim();\n\t\tif (override) return override;\n\t\treturn ensureTool(\"voicetools\", true);\n\t}\n\n\t/** Create (or reuse) the voice panel and mount it in the status container. */\n\tprivate showPanel(): VoicePanel {\n\t\tif (!this.panel) {\n\t\t\tthis.deps.statusContainer.clear();\n\t\t\tthis.panel = new VoicePanel(this.deps.ui, keyHint(\"app.input.voiceTranscribe\", \"cancel\"));\n\t\t\tthis.deps.statusContainer.addChild(this.panel);\n\t\t}\n\t\tthis.deps.ui.requestRender();\n\t\treturn this.panel;\n\t}\n\n\tprivate showWarming(message: string): void {\n\t\tthis.showPanel().setWarming(message);\n\t}\n\n\t/** Collapse the voice panel back to nothing (idle) and stop its animation. */\n\tprivate resetUI(): void {\n\t\tif (this.panel) {\n\t\t\tthis.panel.dispose();\n\t\t\tthis.panel = undefined;\n\t\t}\n\t\tthis.deps.statusContainer.clear();\n\t\tthis.deps.ui.requestRender();\n\t}\n}\n"]}
|