@kolisachint/hoocode-agent 0.4.112 → 0.4.114
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 +41 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +1 -1
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/extensions/plugins/authoring.d.ts +51 -0
- package/dist/core/extensions/plugins/authoring.d.ts.map +1 -0
- package/dist/core/extensions/plugins/authoring.js +91 -0
- package/dist/core/extensions/plugins/authoring.js.map +1 -0
- package/dist/core/extensions/plugins/default-marketplace/.agents-plugin/marketplace.json +12 -0
- package/dist/core/extensions/plugins/default-marketplace/plugins/hello-world/.agents-plugin/plugin.json +6 -0
- package/dist/core/extensions/plugins/default-marketplace/plugins/hello-world/commands/hello.md +5 -0
- package/dist/core/extensions/plugins/default-marketplace/plugins/hello-world/skills/greet/SKILL.md +7 -0
- package/dist/core/extensions/plugins/formats/agents.d.ts +7 -0
- package/dist/core/extensions/plugins/formats/agents.d.ts.map +1 -0
- package/dist/core/extensions/plugins/formats/agents.js +14 -0
- package/dist/core/extensions/plugins/formats/agents.js.map +1 -0
- package/dist/core/extensions/plugins/formats/claude.d.ts +10 -0
- package/dist/core/extensions/plugins/formats/claude.d.ts.map +1 -0
- package/dist/core/extensions/plugins/formats/claude.js +17 -0
- package/dist/core/extensions/plugins/formats/claude.js.map +1 -0
- package/dist/core/extensions/plugins/formats/copilot.d.ts +23 -0
- package/dist/core/extensions/plugins/formats/copilot.d.ts.map +1 -0
- package/dist/core/extensions/plugins/formats/copilot.js +130 -0
- package/dist/core/extensions/plugins/formats/copilot.js.map +1 -0
- package/dist/core/extensions/plugins/formats/index.d.ts +41 -0
- package/dist/core/extensions/plugins/formats/index.d.ts.map +1 -0
- package/dist/core/extensions/plugins/formats/index.js +76 -0
- package/dist/core/extensions/plugins/formats/index.js.map +1 -0
- package/dist/core/extensions/plugins/formats/jsonManifest.d.ts +21 -0
- package/dist/core/extensions/plugins/formats/jsonManifest.d.ts.map +1 -0
- package/dist/core/extensions/plugins/formats/jsonManifest.js +97 -0
- package/dist/core/extensions/plugins/formats/jsonManifest.js.map +1 -0
- package/dist/core/extensions/plugins/formats/shared.d.ts +44 -0
- package/dist/core/extensions/plugins/formats/shared.d.ts.map +1 -0
- package/dist/core/extensions/plugins/formats/shared.js +107 -0
- package/dist/core/extensions/plugins/formats/shared.js.map +1 -0
- package/dist/core/extensions/plugins/formats/types.d.ts +118 -0
- package/dist/core/extensions/plugins/formats/types.d.ts.map +1 -0
- package/dist/core/extensions/plugins/formats/types.js +19 -0
- package/dist/core/extensions/plugins/formats/types.js.map +1 -0
- package/dist/core/extensions/plugins/install.d.ts +73 -0
- package/dist/core/extensions/plugins/install.d.ts.map +1 -0
- package/dist/core/extensions/plugins/install.js +173 -0
- package/dist/core/extensions/plugins/install.js.map +1 -0
- package/dist/core/extensions/plugins/manifest.d.ts +25 -7
- package/dist/core/extensions/plugins/manifest.d.ts.map +1 -1
- package/dist/core/extensions/plugins/manifest.js +16 -91
- package/dist/core/extensions/plugins/manifest.js.map +1 -1
- package/dist/core/extensions/plugins/marketplace.d.ts +5 -4
- package/dist/core/extensions/plugins/marketplace.d.ts.map +1 -1
- package/dist/core/extensions/plugins/marketplace.js +8 -13
- package/dist/core/extensions/plugins/marketplace.js.map +1 -1
- package/dist/core/settings-defaults.d.ts +2 -0
- package/dist/core/settings-defaults.d.ts.map +1 -1
- package/dist/core/settings-defaults.js +2 -0
- package/dist/core/settings-defaults.js.map +1 -1
- package/dist/core/settings-manager.d.ts +4 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +16 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/settings-types.d.ts +2 -0
- package/dist/core/settings-types.d.ts.map +1 -1
- package/dist/core/settings-types.js.map +1 -1
- package/dist/core/subagent-depth.d.ts +13 -0
- package/dist/core/subagent-depth.d.ts.map +1 -1
- package/dist/core/subagent-depth.js +15 -0
- package/dist/core/subagent-depth.js.map +1 -1
- package/dist/core/subagent-events.d.ts +21 -0
- package/dist/core/subagent-events.d.ts.map +1 -0
- package/dist/core/subagent-events.js +25 -0
- package/dist/core/subagent-events.js.map +1 -0
- package/dist/core/subagent-pool.d.ts +5 -4
- package/dist/core/subagent-pool.d.ts.map +1 -1
- package/dist/core/subagent-pool.js +11 -10
- package/dist/core/subagent-pool.js.map +1 -1
- package/dist/core/tools/find.d.ts +8 -3
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js +189 -122
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/index.d.ts +0 -4
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +2 -5
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/plugin-tool-names.d.ts +21 -0
- package/dist/core/tools/plugin-tool-names.d.ts.map +1 -0
- package/dist/core/tools/plugin-tool-names.js +31 -0
- package/dist/core/tools/plugin-tool-names.js.map +1 -0
- package/dist/core/tools/plugins.d.ts +49 -0
- package/dist/core/tools/plugins.d.ts.map +1 -0
- package/dist/core/tools/plugins.js +186 -0
- package/dist/core/tools/plugins.js.map +1 -0
- package/dist/core/tools/propose-plugin.d.ts +36 -0
- package/dist/core/tools/propose-plugin.d.ts.map +1 -0
- package/dist/core/tools/propose-plugin.js +219 -0
- package/dist/core/tools/propose-plugin.js.map +1 -0
- package/dist/core/warm-subagent-pool.d.ts.map +1 -1
- package/dist/core/warm-subagent-pool.js +4 -1
- package/dist/core/warm-subagent-pool.js.map +1 -1
- package/dist/extensions/core/marketplace.d.ts +7 -3
- package/dist/extensions/core/marketplace.d.ts.map +1 -1
- package/dist/extensions/core/marketplace.js +23 -67
- package/dist/extensions/core/marketplace.js.map +1 -1
- package/dist/extensions/core/mcp-deferred.d.ts +28 -0
- package/dist/extensions/core/mcp-deferred.d.ts.map +1 -0
- package/dist/extensions/core/mcp-deferred.js +53 -0
- package/dist/extensions/core/mcp-deferred.js.map +1 -0
- package/dist/extensions/core/mcp-loader.d.ts.map +1 -1
- package/dist/extensions/core/mcp-loader.js +132 -78
- package/dist/extensions/core/mcp-loader.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +19 -1
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts +27 -1
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +150 -7
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-editor.js +18 -0
- package/dist/modes/interactive/components/custom-editor.js.map +1 -1
- package/dist/modes/interactive/components/task-panel.d.ts.map +1 -1
- package/dist/modes/interactive/components/task-panel.js +5 -2
- package/dist/modes/interactive/components/task-panel.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +17 -0
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +78 -5
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/components/user-message.d.ts +4 -0
- package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/user-message.js +12 -3
- package/dist/modes/interactive/components/user-message.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +12 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +75 -4
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/voice/voice-controller.d.ts.map +1 -1
- package/dist/modes/interactive/voice/voice-controller.js +9 -5
- package/dist/modes/interactive/voice/voice-controller.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +10 -3
- package/dist/modes/print-mode.js.map +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/minimal-mode.ts +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +5 -5
- package/dist/core/tools/glob.d.ts +0 -42
- package/dist/core/tools/glob.d.ts.map +0 -1
- package/dist/core/tools/glob.js +0 -432
- package/dist/core/tools/glob.js.map +0 -1
|
@@ -1 +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"]}
|
|
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;AAwB/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. Every utterance pays this in full before\n// finalization can start, so keep it close to the binary's 600ms default —\n// a slightly longer window tolerates brief mid-sentence pauses without\n// taxing every dictation with seconds of dead air.\nconst VOICE_SILENCE_MS = 800;\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.\n * Kept long enough that normal pacing between dictations (reading a reply,\n * typing) doesn't repeatedly hit the multi-second cold start. */\nconst VOICE_IDLE_TIMEOUT_MS = 300_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"]}
|
|
@@ -15,13 +15,17 @@ import { startVoiceTranscribe, VoiceDaemon } from "./voice-transcribe.js";
|
|
|
15
15
|
// Trailing-silence window: how long a pause while speaking lasts before the
|
|
16
16
|
// capture auto-stops. Passed to `voicetools serve` via `--silence-ms` (see
|
|
17
17
|
// VoiceDaemon.spawn) so the binary's real cutoff matches the on-screen
|
|
18
|
-
// countdown this same value drives.
|
|
19
|
-
//
|
|
20
|
-
|
|
18
|
+
// countdown this same value drives. Every utterance pays this in full before
|
|
19
|
+
// finalization can start, so keep it close to the binary's 600ms default —
|
|
20
|
+
// a slightly longer window tolerates brief mid-sentence pauses without
|
|
21
|
+
// taxing every dictation with seconds of dead air.
|
|
22
|
+
const VOICE_SILENCE_MS = 800;
|
|
21
23
|
/** How long to keep the warm voice model in memory after the last capture
|
|
22
24
|
* completes. The daemon auto-shuts down after this window, releasing the
|
|
23
|
-
* ~900 MB resident model; the next ctrl+r pays a cold-start respawn cost.
|
|
24
|
-
|
|
25
|
+
* ~900 MB resident model; the next ctrl+r pays a cold-start respawn cost.
|
|
26
|
+
* Kept long enough that normal pacing between dictations (reading a reply,
|
|
27
|
+
* typing) doesn't repeatedly hit the multi-second cold start. */
|
|
28
|
+
const VOICE_IDLE_TIMEOUT_MS = 300_000;
|
|
25
29
|
const VOICE_UNAVAILABLE_MESSAGE = "Voice input failed: voicetools binary unavailable and could not be downloaded. " +
|
|
26
30
|
"Install it, set VOICETOOLS_BIN, or ensure a published release exists for this platform.";
|
|
27
31
|
export class VoiceController {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"voice-controller.js","sourceRoot":"","sources":["../../../../src/modes/interactive/voice/voice-controller.ts"],"names":[],"mappings":"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","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"]}
|
|
1
|
+
{"version":3,"file":"voice-controller.js","sourceRoot":"","sources":["../../../../src/modes/interactive/voice/voice-controller.ts"],"names":[],"mappings":"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","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. Every utterance pays this in full before\n// finalization can start, so keep it close to the binary's 600ms default —\n// a slightly longer window tolerates brief mid-sentence pauses without\n// taxing every dictation with seconds of dead air.\nconst VOICE_SILENCE_MS = 800;\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.\n * Kept long enough that normal pacing between dictations (reading a reply,\n * typing) doesn't repeatedly hit the multi-second cold start. */\nconst VOICE_IDLE_TIMEOUT_MS = 300_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"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"print-mode.d.ts","sourceRoot":"","sources":["../../src/modes/print-mode.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAoB,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC9E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;
|
|
1
|
+
{"version":3,"file":"print-mode.d.ts","sourceRoot":"","sources":["../../src/modes/print-mode.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAoB,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC9E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAU5E;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAChC,yEAAyE;IACzE,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IACtB,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,wDAAwD;IACxD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,8CAA8C;IAC9C,aAAa,CAAC,EAAE,YAAY,EAAE,CAAC;IAC/B,iGAAiG;IACjG,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yGAAyG;IACzG,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAAC,WAAW,EAAE,mBAAmB,EAAE,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,CAqM/G","sourcesContent":["/**\n * Print mode (single-shot): Send prompts, output result, exit.\n *\n * Used for:\n * - `hoocode -p \"prompt\"` - text output\n * - `hoocode --mode json \"prompt\"` - JSON event stream\n */\n\nimport type { AssistantMessage, ImageContent } from \"@kolisachint/hoocode-ai\";\nimport type { AgentSessionRuntime } from \"../core/agent-session-runtime.js\";\nimport { flushRawStdout, writeRawStdout } from \"../core/output-guard.js\";\nimport { SUBAGENT_STDOUT_EVENT_TYPES } from \"../core/subagent-events.js\";\nimport { buildSubagentResult, buildTaskForest, writeSubagentResult } from \"../core/subagent-result.js\";\nimport { taskStore } from \"../core/task-store.js\";\nimport { killTrackedDetachedChildren } from \"../utils/shell.js\";\n\n/** Heartbeat cadence for spawned subagents (parent lifeguard stalls after 60s of silence). */\nconst SUBAGENT_HEARTBEAT_MS = 30000;\n\n/**\n * Options for print mode.\n */\nexport interface PrintModeOptions {\n\t/** Output mode: \"text\" for final response only, \"json\" for all events */\n\tmode: \"text\" | \"json\";\n\t/** Array of additional prompts to send after initialMessage */\n\tmessages?: string[];\n\t/** First message to send (may contain @file content) */\n\tinitialMessage?: string;\n\t/** Images to attach to the initial message */\n\tinitialImages?: ImageContent[];\n\t/** Internal: set when this process is a spawned subagent. Enables heartbeats and result.json. */\n\ttaskId?: string;\n\t/** Hard cap on assistant turns. Near the cap the agent is asked to wrap up; at the cap it is aborted. */\n\tmaxTurns?: number;\n}\n\n/**\n * Run in print (single-shot) mode.\n * Sends prompts to the agent and outputs the result.\n */\nexport async function runPrintMode(runtimeHost: AgentSessionRuntime, options: PrintModeOptions): Promise<number> {\n\tconst { mode, messages = [], initialMessage, initialImages, taskId, maxTurns } = options;\n\tlet exitCode = 0;\n\tlet session = runtimeHost.session;\n\tlet unsubscribe: (() => void) | undefined;\n\tlet disposed = false;\n\n\t// Spawned subagents (json mode + task id) emit a periodic heartbeat so the\n\t// parent lifeguard does not treat a long-thinking turn as a stall.\n\tconst isSubagent = mode === \"json\" && typeof taskId === \"string\" && taskId.length > 0;\n\tlet heartbeat: NodeJS.Timeout | undefined;\n\tif (isSubagent) {\n\t\t// Emit an immediate ping so a child that crashes during startup surfaces its\n\t\t// real error instead of stalling silently until the first delayed heartbeat.\n\t\twriteRawStdout(`${JSON.stringify({ ping: true })}\\n`);\n\t\theartbeat = setInterval(() => {\n\t\t\twriteRawStdout(`${JSON.stringify({ ping: true })}\\n`);\n\t\t}, SUBAGENT_HEARTBEAT_MS);\n\t\theartbeat.unref();\n\t}\n\t// Turn-limit enforcement for spawned subagents: ask the agent to wrap up near\n\t// the cap, then hard-stop at the cap so a runaway agent always terminates.\n\tlet reachedMaxTurns = false;\n\tlet turnLimitUnsub: (() => void) | undefined;\n\tconst signalCleanupHandlers: Array<() => void> = [];\n\n\tconst disposeRuntime = async (): Promise<void> => {\n\t\tif (disposed) return;\n\t\tdisposed = true;\n\t\tunsubscribe?.();\n\t\tawait runtimeHost.dispose();\n\t};\n\n\tconst registerSignalHandlers = (): void => {\n\t\tconst signals: NodeJS.Signals[] = [\"SIGTERM\"];\n\t\tif (process.platform !== \"win32\") {\n\t\t\tsignals.push(\"SIGHUP\");\n\t\t}\n\n\t\tfor (const signal of signals) {\n\t\t\tconst handler = () => {\n\t\t\t\tkillTrackedDetachedChildren();\n\t\t\t\tvoid disposeRuntime().finally(() => {\n\t\t\t\t\tprocess.exit(signal === \"SIGHUP\" ? 129 : 143);\n\t\t\t\t});\n\t\t\t};\n\t\t\tprocess.on(signal, handler);\n\t\t\tsignalCleanupHandlers.push(() => process.off(signal, handler));\n\t\t}\n\t};\n\n\tregisterSignalHandlers();\n\n\truntimeHost.setRebindSession(async () => {\n\t\tawait rebindSession();\n\t});\n\n\tconst rebindSession = async (): Promise<void> => {\n\t\tsession = runtimeHost.session;\n\t\tawait session.bindExtensions({\n\t\t\tcommandContextActions: {\n\t\t\t\twaitForIdle: () => session.agent.waitForIdle(),\n\t\t\t\tnewSession: async (newSessionOptions) => runtimeHost.newSession(newSessionOptions),\n\t\t\t\tfork: async (entryId, forkOptions) => {\n\t\t\t\t\tconst result = await runtimeHost.fork(entryId, forkOptions);\n\t\t\t\t\treturn { cancelled: result.cancelled };\n\t\t\t\t},\n\t\t\t\tnavigateTree: async (targetId, navigateOptions) => {\n\t\t\t\t\tconst result = await session.navigateTree(targetId, {\n\t\t\t\t\t\tsummarize: navigateOptions?.summarize,\n\t\t\t\t\t\tcustomInstructions: navigateOptions?.customInstructions,\n\t\t\t\t\t\treplaceInstructions: navigateOptions?.replaceInstructions,\n\t\t\t\t\t\tlabel: navigateOptions?.label,\n\t\t\t\t\t});\n\t\t\t\t\treturn { cancelled: result.cancelled };\n\t\t\t\t},\n\t\t\t\tswitchSession: async (sessionPath, switchOptions) => {\n\t\t\t\t\treturn runtimeHost.switchSession(sessionPath, switchOptions);\n\t\t\t\t},\n\t\t\t\treload: async () => {\n\t\t\t\t\tawait session.reload();\n\t\t\t\t},\n\t\t\t},\n\t\t\tonError: (err) => {\n\t\t\t\tconsole.error(`Extension error (${err.extensionPath}): ${err.error}`);\n\t\t\t},\n\t\t});\n\n\t\tunsubscribe?.();\n\t\tunsubscribe = session.subscribe((event) => {\n\t\t\tif (mode !== \"json\") return;\n\t\t\t// A spawned subagent's parent only consumes progress events + message_end\n\t\t\t// usage from this stream (see SUBAGENT_STDOUT_EVENT_TYPES); dropping the\n\t\t\t// per-delta firehose at the source keeps it off the parent's UI event\n\t\t\t// loop. A top-level `--json` consumer still receives the full stream.\n\t\t\tif (isSubagent && !SUBAGENT_STDOUT_EVENT_TYPES.has(event.type)) return;\n\t\t\twriteRawStdout(`${JSON.stringify(event)}\\n`);\n\t\t});\n\t};\n\n\ttry {\n\t\tif (mode === \"json\") {\n\t\t\tconst header = session.sessionManager.getHeader();\n\t\t\tif (header) {\n\t\t\t\twriteRawStdout(`${JSON.stringify(header)}\\n`);\n\t\t\t}\n\t\t}\n\n\t\tawait rebindSession();\n\n\t\tif (isSubagent && typeof maxTurns === \"number\" && maxTurns > 0) {\n\t\t\tconst cap = maxTurns;\n\t\t\tconst wrapUpAt = Math.floor(cap * 0.9);\n\t\t\tlet turns = 0;\n\t\t\tlet warned = false;\n\t\t\tturnLimitUnsub = session.subscribe((event) => {\n\t\t\t\tif (event.type !== \"turn_end\") return;\n\t\t\t\tturns += 1;\n\t\t\t\tif (turns >= cap) {\n\t\t\t\t\tif (!reachedMaxTurns) {\n\t\t\t\t\t\treachedMaxTurns = true;\n\t\t\t\t\t\tvoid session.abort();\n\t\t\t\t\t}\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (!warned && wrapUpAt >= 1 && wrapUpAt < cap && turns >= wrapUpAt) {\n\t\t\t\t\twarned = true;\n\t\t\t\t\tvoid session.steer(\n\t\t\t\t\t\t`You are at turn ${turns} of your ${cap}-turn limit. Stop investigating or making changes now and write your final summary of findings and results in your next message.`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\n\t\tif (initialMessage) {\n\t\t\tawait session.prompt(initialMessage, { images: initialImages });\n\t\t}\n\n\t\tfor (const message of messages) {\n\t\t\tawait session.prompt(message);\n\t\t}\n\n\t\t// Spawned subagents write the audit file the parent pool verifies.\n\t\tif (isSubagent && taskId) {\n\t\t\tconst stats = session.getSessionStats();\n\t\t\tconst result = buildSubagentResult(\n\t\t\t\tsession.state.messages,\n\t\t\t\t{\n\t\t\t\t\tinput: stats.tokens.input,\n\t\t\t\t\toutput: stats.tokens.output,\n\t\t\t\t\tcacheRead: stats.tokens.cacheRead,\n\t\t\t\t\tcacheWrite: stats.tokens.cacheWrite,\n\t\t\t\t\tcost: stats.cost,\n\t\t\t\t},\n\t\t\t\t{ reachedMaxTurns },\n\t\t\t);\n\t\t\t// Propagate this subagent's own task subtree (its TodoWrite plan plus any\n\t\t\t// nested delegations/MCP calls) so the parent can render work deeper than\n\t\t\t// one level under the dispatching task.\n\t\t\tconst tree = buildTaskForest(taskStore.list());\n\t\t\tif (tree.length > 0) result.task_tree = tree;\n\t\t\twriteSubagentResult(session.sessionManager.getCwd(), taskId, result);\n\t\t\tif (result.status === \"failed\") exitCode = 1;\n\t\t}\n\n\t\tif (mode === \"text\") {\n\t\t\tconst state = session.state;\n\t\t\tconst lastMessage = state.messages[state.messages.length - 1];\n\n\t\t\tif (lastMessage?.role === \"assistant\") {\n\t\t\t\tconst assistantMsg = lastMessage as AssistantMessage;\n\t\t\t\tif (assistantMsg.stopReason === \"error\" || assistantMsg.stopReason === \"aborted\") {\n\t\t\t\t\tconsole.error(assistantMsg.errorMessage || `Request ${assistantMsg.stopReason}`);\n\t\t\t\t\texitCode = 1;\n\t\t\t\t} else {\n\t\t\t\t\tfor (const content of assistantMsg.content) {\n\t\t\t\t\t\tif (content.type === \"text\") {\n\t\t\t\t\t\t\twriteRawStdout(`${content.text}\\n`);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn exitCode;\n\t} catch (error: unknown) {\n\t\tconsole.error(error instanceof Error ? error.message : String(error));\n\t\treturn 1;\n\t} finally {\n\t\tif (heartbeat) clearInterval(heartbeat);\n\t\tturnLimitUnsub?.();\n\t\tfor (const cleanup of signalCleanupHandlers) {\n\t\t\tcleanup();\n\t\t}\n\t\tawait disposeRuntime();\n\t\tawait flushRawStdout();\n\t}\n}\n"]}
|
package/dist/modes/print-mode.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* - `hoocode --mode json "prompt"` - JSON event stream
|
|
7
7
|
*/
|
|
8
8
|
import { flushRawStdout, writeRawStdout } from "../core/output-guard.js";
|
|
9
|
+
import { SUBAGENT_STDOUT_EVENT_TYPES } from "../core/subagent-events.js";
|
|
9
10
|
import { buildSubagentResult, buildTaskForest, writeSubagentResult } from "../core/subagent-result.js";
|
|
10
11
|
import { taskStore } from "../core/task-store.js";
|
|
11
12
|
import { killTrackedDetachedChildren } from "../utils/shell.js";
|
|
@@ -98,9 +99,15 @@ export async function runPrintMode(runtimeHost, options) {
|
|
|
98
99
|
});
|
|
99
100
|
unsubscribe?.();
|
|
100
101
|
unsubscribe = session.subscribe((event) => {
|
|
101
|
-
if (mode
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
if (mode !== "json")
|
|
103
|
+
return;
|
|
104
|
+
// A spawned subagent's parent only consumes progress events + message_end
|
|
105
|
+
// usage from this stream (see SUBAGENT_STDOUT_EVENT_TYPES); dropping the
|
|
106
|
+
// per-delta firehose at the source keeps it off the parent's UI event
|
|
107
|
+
// loop. A top-level `--json` consumer still receives the full stream.
|
|
108
|
+
if (isSubagent && !SUBAGENT_STDOUT_EVENT_TYPES.has(event.type))
|
|
109
|
+
return;
|
|
110
|
+
writeRawStdout(`${JSON.stringify(event)}\n`);
|
|
104
111
|
});
|
|
105
112
|
};
|
|
106
113
|
try {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"print-mode.js","sourceRoot":"","sources":["../../src/modes/print-mode.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Print mode (single-shot): Send prompts, output result, exit.\n *\n * Used for:\n * - `hoocode -p \"prompt\"` - text output\n * - `hoocode --mode json \"prompt\"` - JSON event stream\n */\n\nimport type { AssistantMessage, ImageContent } from \"@kolisachint/hoocode-ai\";\nimport type { AgentSessionRuntime } from \"../core/agent-session-runtime.js\";\nimport { flushRawStdout, writeRawStdout } from \"../core/output-guard.js\";\nimport { buildSubagentResult, buildTaskForest, writeSubagentResult } from \"../core/subagent-result.js\";\nimport { taskStore } from \"../core/task-store.js\";\nimport { killTrackedDetachedChildren } from \"../utils/shell.js\";\n\n/** Heartbeat cadence for spawned subagents (parent lifeguard stalls after 60s of silence). */\nconst SUBAGENT_HEARTBEAT_MS = 30000;\n\n/**\n * Options for print mode.\n */\nexport interface PrintModeOptions {\n\t/** Output mode: \"text\" for final response only, \"json\" for all events */\n\tmode: \"text\" | \"json\";\n\t/** Array of additional prompts to send after initialMessage */\n\tmessages?: string[];\n\t/** First message to send (may contain @file content) */\n\tinitialMessage?: string;\n\t/** Images to attach to the initial message */\n\tinitialImages?: ImageContent[];\n\t/** Internal: set when this process is a spawned subagent. Enables heartbeats and result.json. */\n\ttaskId?: string;\n\t/** Hard cap on assistant turns. Near the cap the agent is asked to wrap up; at the cap it is aborted. */\n\tmaxTurns?: number;\n}\n\n/**\n * Run in print (single-shot) mode.\n * Sends prompts to the agent and outputs the result.\n */\nexport async function runPrintMode(runtimeHost: AgentSessionRuntime, options: PrintModeOptions): Promise<number> {\n\tconst { mode, messages = [], initialMessage, initialImages, taskId, maxTurns } = options;\n\tlet exitCode = 0;\n\tlet session = runtimeHost.session;\n\tlet unsubscribe: (() => void) | undefined;\n\tlet disposed = false;\n\n\t// Spawned subagents (json mode + task id) emit a periodic heartbeat so the\n\t// parent lifeguard does not treat a long-thinking turn as a stall.\n\tconst isSubagent = mode === \"json\" && typeof taskId === \"string\" && taskId.length > 0;\n\tlet heartbeat: NodeJS.Timeout | undefined;\n\tif (isSubagent) {\n\t\t// Emit an immediate ping so a child that crashes during startup surfaces its\n\t\t// real error instead of stalling silently until the first delayed heartbeat.\n\t\twriteRawStdout(`${JSON.stringify({ ping: true })}\\n`);\n\t\theartbeat = setInterval(() => {\n\t\t\twriteRawStdout(`${JSON.stringify({ ping: true })}\\n`);\n\t\t}, SUBAGENT_HEARTBEAT_MS);\n\t\theartbeat.unref();\n\t}\n\t// Turn-limit enforcement for spawned subagents: ask the agent to wrap up near\n\t// the cap, then hard-stop at the cap so a runaway agent always terminates.\n\tlet reachedMaxTurns = false;\n\tlet turnLimitUnsub: (() => void) | undefined;\n\tconst signalCleanupHandlers: Array<() => void> = [];\n\n\tconst disposeRuntime = async (): Promise<void> => {\n\t\tif (disposed) return;\n\t\tdisposed = true;\n\t\tunsubscribe?.();\n\t\tawait runtimeHost.dispose();\n\t};\n\n\tconst registerSignalHandlers = (): void => {\n\t\tconst signals: NodeJS.Signals[] = [\"SIGTERM\"];\n\t\tif (process.platform !== \"win32\") {\n\t\t\tsignals.push(\"SIGHUP\");\n\t\t}\n\n\t\tfor (const signal of signals) {\n\t\t\tconst handler = () => {\n\t\t\t\tkillTrackedDetachedChildren();\n\t\t\t\tvoid disposeRuntime().finally(() => {\n\t\t\t\t\tprocess.exit(signal === \"SIGHUP\" ? 129 : 143);\n\t\t\t\t});\n\t\t\t};\n\t\t\tprocess.on(signal, handler);\n\t\t\tsignalCleanupHandlers.push(() => process.off(signal, handler));\n\t\t}\n\t};\n\n\tregisterSignalHandlers();\n\n\truntimeHost.setRebindSession(async () => {\n\t\tawait rebindSession();\n\t});\n\n\tconst rebindSession = async (): Promise<void> => {\n\t\tsession = runtimeHost.session;\n\t\tawait session.bindExtensions({\n\t\t\tcommandContextActions: {\n\t\t\t\twaitForIdle: () => session.agent.waitForIdle(),\n\t\t\t\tnewSession: async (newSessionOptions) => runtimeHost.newSession(newSessionOptions),\n\t\t\t\tfork: async (entryId, forkOptions) => {\n\t\t\t\t\tconst result = await runtimeHost.fork(entryId, forkOptions);\n\t\t\t\t\treturn { cancelled: result.cancelled };\n\t\t\t\t},\n\t\t\t\tnavigateTree: async (targetId, navigateOptions) => {\n\t\t\t\t\tconst result = await session.navigateTree(targetId, {\n\t\t\t\t\t\tsummarize: navigateOptions?.summarize,\n\t\t\t\t\t\tcustomInstructions: navigateOptions?.customInstructions,\n\t\t\t\t\t\treplaceInstructions: navigateOptions?.replaceInstructions,\n\t\t\t\t\t\tlabel: navigateOptions?.label,\n\t\t\t\t\t});\n\t\t\t\t\treturn { cancelled: result.cancelled };\n\t\t\t\t},\n\t\t\t\tswitchSession: async (sessionPath, switchOptions) => {\n\t\t\t\t\treturn runtimeHost.switchSession(sessionPath, switchOptions);\n\t\t\t\t},\n\t\t\t\treload: async () => {\n\t\t\t\t\tawait session.reload();\n\t\t\t\t},\n\t\t\t},\n\t\t\tonError: (err) => {\n\t\t\t\tconsole.error(`Extension error (${err.extensionPath}): ${err.error}`);\n\t\t\t},\n\t\t});\n\n\t\tunsubscribe?.();\n\t\tunsubscribe = session.subscribe((event) => {\n\t\t\tif (mode === \"json\") {\n\t\t\t\twriteRawStdout(`${JSON.stringify(event)}\\n`);\n\t\t\t}\n\t\t});\n\t};\n\n\ttry {\n\t\tif (mode === \"json\") {\n\t\t\tconst header = session.sessionManager.getHeader();\n\t\t\tif (header) {\n\t\t\t\twriteRawStdout(`${JSON.stringify(header)}\\n`);\n\t\t\t}\n\t\t}\n\n\t\tawait rebindSession();\n\n\t\tif (isSubagent && typeof maxTurns === \"number\" && maxTurns > 0) {\n\t\t\tconst cap = maxTurns;\n\t\t\tconst wrapUpAt = Math.floor(cap * 0.9);\n\t\t\tlet turns = 0;\n\t\t\tlet warned = false;\n\t\t\tturnLimitUnsub = session.subscribe((event) => {\n\t\t\t\tif (event.type !== \"turn_end\") return;\n\t\t\t\tturns += 1;\n\t\t\t\tif (turns >= cap) {\n\t\t\t\t\tif (!reachedMaxTurns) {\n\t\t\t\t\t\treachedMaxTurns = true;\n\t\t\t\t\t\tvoid session.abort();\n\t\t\t\t\t}\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (!warned && wrapUpAt >= 1 && wrapUpAt < cap && turns >= wrapUpAt) {\n\t\t\t\t\twarned = true;\n\t\t\t\t\tvoid session.steer(\n\t\t\t\t\t\t`You are at turn ${turns} of your ${cap}-turn limit. Stop investigating or making changes now and write your final summary of findings and results in your next message.`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\n\t\tif (initialMessage) {\n\t\t\tawait session.prompt(initialMessage, { images: initialImages });\n\t\t}\n\n\t\tfor (const message of messages) {\n\t\t\tawait session.prompt(message);\n\t\t}\n\n\t\t// Spawned subagents write the audit file the parent pool verifies.\n\t\tif (isSubagent && taskId) {\n\t\t\tconst stats = session.getSessionStats();\n\t\t\tconst result = buildSubagentResult(\n\t\t\t\tsession.state.messages,\n\t\t\t\t{\n\t\t\t\t\tinput: stats.tokens.input,\n\t\t\t\t\toutput: stats.tokens.output,\n\t\t\t\t\tcacheRead: stats.tokens.cacheRead,\n\t\t\t\t\tcacheWrite: stats.tokens.cacheWrite,\n\t\t\t\t\tcost: stats.cost,\n\t\t\t\t},\n\t\t\t\t{ reachedMaxTurns },\n\t\t\t);\n\t\t\t// Propagate this subagent's own task subtree (its TodoWrite plan plus any\n\t\t\t// nested delegations/MCP calls) so the parent can render work deeper than\n\t\t\t// one level under the dispatching task.\n\t\t\tconst tree = buildTaskForest(taskStore.list());\n\t\t\tif (tree.length > 0) result.task_tree = tree;\n\t\t\twriteSubagentResult(session.sessionManager.getCwd(), taskId, result);\n\t\t\tif (result.status === \"failed\") exitCode = 1;\n\t\t}\n\n\t\tif (mode === \"text\") {\n\t\t\tconst state = session.state;\n\t\t\tconst lastMessage = state.messages[state.messages.length - 1];\n\n\t\t\tif (lastMessage?.role === \"assistant\") {\n\t\t\t\tconst assistantMsg = lastMessage as AssistantMessage;\n\t\t\t\tif (assistantMsg.stopReason === \"error\" || assistantMsg.stopReason === \"aborted\") {\n\t\t\t\t\tconsole.error(assistantMsg.errorMessage || `Request ${assistantMsg.stopReason}`);\n\t\t\t\t\texitCode = 1;\n\t\t\t\t} else {\n\t\t\t\t\tfor (const content of assistantMsg.content) {\n\t\t\t\t\t\tif (content.type === \"text\") {\n\t\t\t\t\t\t\twriteRawStdout(`${content.text}\\n`);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn exitCode;\n\t} catch (error: unknown) {\n\t\tconsole.error(error instanceof Error ? error.message : String(error));\n\t\treturn 1;\n\t} finally {\n\t\tif (heartbeat) clearInterval(heartbeat);\n\t\tturnLimitUnsub?.();\n\t\tfor (const cleanup of signalCleanupHandlers) {\n\t\t\tcleanup();\n\t\t}\n\t\tawait disposeRuntime();\n\t\tawait flushRawStdout();\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"print-mode.js","sourceRoot":"","sources":["../../src/modes/print-mode.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Print mode (single-shot): Send prompts, output result, exit.\n *\n * Used for:\n * - `hoocode -p \"prompt\"` - text output\n * - `hoocode --mode json \"prompt\"` - JSON event stream\n */\n\nimport type { AssistantMessage, ImageContent } from \"@kolisachint/hoocode-ai\";\nimport type { AgentSessionRuntime } from \"../core/agent-session-runtime.js\";\nimport { flushRawStdout, writeRawStdout } from \"../core/output-guard.js\";\nimport { SUBAGENT_STDOUT_EVENT_TYPES } from \"../core/subagent-events.js\";\nimport { buildSubagentResult, buildTaskForest, writeSubagentResult } from \"../core/subagent-result.js\";\nimport { taskStore } from \"../core/task-store.js\";\nimport { killTrackedDetachedChildren } from \"../utils/shell.js\";\n\n/** Heartbeat cadence for spawned subagents (parent lifeguard stalls after 60s of silence). */\nconst SUBAGENT_HEARTBEAT_MS = 30000;\n\n/**\n * Options for print mode.\n */\nexport interface PrintModeOptions {\n\t/** Output mode: \"text\" for final response only, \"json\" for all events */\n\tmode: \"text\" | \"json\";\n\t/** Array of additional prompts to send after initialMessage */\n\tmessages?: string[];\n\t/** First message to send (may contain @file content) */\n\tinitialMessage?: string;\n\t/** Images to attach to the initial message */\n\tinitialImages?: ImageContent[];\n\t/** Internal: set when this process is a spawned subagent. Enables heartbeats and result.json. */\n\ttaskId?: string;\n\t/** Hard cap on assistant turns. Near the cap the agent is asked to wrap up; at the cap it is aborted. */\n\tmaxTurns?: number;\n}\n\n/**\n * Run in print (single-shot) mode.\n * Sends prompts to the agent and outputs the result.\n */\nexport async function runPrintMode(runtimeHost: AgentSessionRuntime, options: PrintModeOptions): Promise<number> {\n\tconst { mode, messages = [], initialMessage, initialImages, taskId, maxTurns } = options;\n\tlet exitCode = 0;\n\tlet session = runtimeHost.session;\n\tlet unsubscribe: (() => void) | undefined;\n\tlet disposed = false;\n\n\t// Spawned subagents (json mode + task id) emit a periodic heartbeat so the\n\t// parent lifeguard does not treat a long-thinking turn as a stall.\n\tconst isSubagent = mode === \"json\" && typeof taskId === \"string\" && taskId.length > 0;\n\tlet heartbeat: NodeJS.Timeout | undefined;\n\tif (isSubagent) {\n\t\t// Emit an immediate ping so a child that crashes during startup surfaces its\n\t\t// real error instead of stalling silently until the first delayed heartbeat.\n\t\twriteRawStdout(`${JSON.stringify({ ping: true })}\\n`);\n\t\theartbeat = setInterval(() => {\n\t\t\twriteRawStdout(`${JSON.stringify({ ping: true })}\\n`);\n\t\t}, SUBAGENT_HEARTBEAT_MS);\n\t\theartbeat.unref();\n\t}\n\t// Turn-limit enforcement for spawned subagents: ask the agent to wrap up near\n\t// the cap, then hard-stop at the cap so a runaway agent always terminates.\n\tlet reachedMaxTurns = false;\n\tlet turnLimitUnsub: (() => void) | undefined;\n\tconst signalCleanupHandlers: Array<() => void> = [];\n\n\tconst disposeRuntime = async (): Promise<void> => {\n\t\tif (disposed) return;\n\t\tdisposed = true;\n\t\tunsubscribe?.();\n\t\tawait runtimeHost.dispose();\n\t};\n\n\tconst registerSignalHandlers = (): void => {\n\t\tconst signals: NodeJS.Signals[] = [\"SIGTERM\"];\n\t\tif (process.platform !== \"win32\") {\n\t\t\tsignals.push(\"SIGHUP\");\n\t\t}\n\n\t\tfor (const signal of signals) {\n\t\t\tconst handler = () => {\n\t\t\t\tkillTrackedDetachedChildren();\n\t\t\t\tvoid disposeRuntime().finally(() => {\n\t\t\t\t\tprocess.exit(signal === \"SIGHUP\" ? 129 : 143);\n\t\t\t\t});\n\t\t\t};\n\t\t\tprocess.on(signal, handler);\n\t\t\tsignalCleanupHandlers.push(() => process.off(signal, handler));\n\t\t}\n\t};\n\n\tregisterSignalHandlers();\n\n\truntimeHost.setRebindSession(async () => {\n\t\tawait rebindSession();\n\t});\n\n\tconst rebindSession = async (): Promise<void> => {\n\t\tsession = runtimeHost.session;\n\t\tawait session.bindExtensions({\n\t\t\tcommandContextActions: {\n\t\t\t\twaitForIdle: () => session.agent.waitForIdle(),\n\t\t\t\tnewSession: async (newSessionOptions) => runtimeHost.newSession(newSessionOptions),\n\t\t\t\tfork: async (entryId, forkOptions) => {\n\t\t\t\t\tconst result = await runtimeHost.fork(entryId, forkOptions);\n\t\t\t\t\treturn { cancelled: result.cancelled };\n\t\t\t\t},\n\t\t\t\tnavigateTree: async (targetId, navigateOptions) => {\n\t\t\t\t\tconst result = await session.navigateTree(targetId, {\n\t\t\t\t\t\tsummarize: navigateOptions?.summarize,\n\t\t\t\t\t\tcustomInstructions: navigateOptions?.customInstructions,\n\t\t\t\t\t\treplaceInstructions: navigateOptions?.replaceInstructions,\n\t\t\t\t\t\tlabel: navigateOptions?.label,\n\t\t\t\t\t});\n\t\t\t\t\treturn { cancelled: result.cancelled };\n\t\t\t\t},\n\t\t\t\tswitchSession: async (sessionPath, switchOptions) => {\n\t\t\t\t\treturn runtimeHost.switchSession(sessionPath, switchOptions);\n\t\t\t\t},\n\t\t\t\treload: async () => {\n\t\t\t\t\tawait session.reload();\n\t\t\t\t},\n\t\t\t},\n\t\t\tonError: (err) => {\n\t\t\t\tconsole.error(`Extension error (${err.extensionPath}): ${err.error}`);\n\t\t\t},\n\t\t});\n\n\t\tunsubscribe?.();\n\t\tunsubscribe = session.subscribe((event) => {\n\t\t\tif (mode !== \"json\") return;\n\t\t\t// A spawned subagent's parent only consumes progress events + message_end\n\t\t\t// usage from this stream (see SUBAGENT_STDOUT_EVENT_TYPES); dropping the\n\t\t\t// per-delta firehose at the source keeps it off the parent's UI event\n\t\t\t// loop. A top-level `--json` consumer still receives the full stream.\n\t\t\tif (isSubagent && !SUBAGENT_STDOUT_EVENT_TYPES.has(event.type)) return;\n\t\t\twriteRawStdout(`${JSON.stringify(event)}\\n`);\n\t\t});\n\t};\n\n\ttry {\n\t\tif (mode === \"json\") {\n\t\t\tconst header = session.sessionManager.getHeader();\n\t\t\tif (header) {\n\t\t\t\twriteRawStdout(`${JSON.stringify(header)}\\n`);\n\t\t\t}\n\t\t}\n\n\t\tawait rebindSession();\n\n\t\tif (isSubagent && typeof maxTurns === \"number\" && maxTurns > 0) {\n\t\t\tconst cap = maxTurns;\n\t\t\tconst wrapUpAt = Math.floor(cap * 0.9);\n\t\t\tlet turns = 0;\n\t\t\tlet warned = false;\n\t\t\tturnLimitUnsub = session.subscribe((event) => {\n\t\t\t\tif (event.type !== \"turn_end\") return;\n\t\t\t\tturns += 1;\n\t\t\t\tif (turns >= cap) {\n\t\t\t\t\tif (!reachedMaxTurns) {\n\t\t\t\t\t\treachedMaxTurns = true;\n\t\t\t\t\t\tvoid session.abort();\n\t\t\t\t\t}\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (!warned && wrapUpAt >= 1 && wrapUpAt < cap && turns >= wrapUpAt) {\n\t\t\t\t\twarned = true;\n\t\t\t\t\tvoid session.steer(\n\t\t\t\t\t\t`You are at turn ${turns} of your ${cap}-turn limit. Stop investigating or making changes now and write your final summary of findings and results in your next message.`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\n\t\tif (initialMessage) {\n\t\t\tawait session.prompt(initialMessage, { images: initialImages });\n\t\t}\n\n\t\tfor (const message of messages) {\n\t\t\tawait session.prompt(message);\n\t\t}\n\n\t\t// Spawned subagents write the audit file the parent pool verifies.\n\t\tif (isSubagent && taskId) {\n\t\t\tconst stats = session.getSessionStats();\n\t\t\tconst result = buildSubagentResult(\n\t\t\t\tsession.state.messages,\n\t\t\t\t{\n\t\t\t\t\tinput: stats.tokens.input,\n\t\t\t\t\toutput: stats.tokens.output,\n\t\t\t\t\tcacheRead: stats.tokens.cacheRead,\n\t\t\t\t\tcacheWrite: stats.tokens.cacheWrite,\n\t\t\t\t\tcost: stats.cost,\n\t\t\t\t},\n\t\t\t\t{ reachedMaxTurns },\n\t\t\t);\n\t\t\t// Propagate this subagent's own task subtree (its TodoWrite plan plus any\n\t\t\t// nested delegations/MCP calls) so the parent can render work deeper than\n\t\t\t// one level under the dispatching task.\n\t\t\tconst tree = buildTaskForest(taskStore.list());\n\t\t\tif (tree.length > 0) result.task_tree = tree;\n\t\t\twriteSubagentResult(session.sessionManager.getCwd(), taskId, result);\n\t\t\tif (result.status === \"failed\") exitCode = 1;\n\t\t}\n\n\t\tif (mode === \"text\") {\n\t\t\tconst state = session.state;\n\t\t\tconst lastMessage = state.messages[state.messages.length - 1];\n\n\t\t\tif (lastMessage?.role === \"assistant\") {\n\t\t\t\tconst assistantMsg = lastMessage as AssistantMessage;\n\t\t\t\tif (assistantMsg.stopReason === \"error\" || assistantMsg.stopReason === \"aborted\") {\n\t\t\t\t\tconsole.error(assistantMsg.errorMessage || `Request ${assistantMsg.stopReason}`);\n\t\t\t\t\texitCode = 1;\n\t\t\t\t} else {\n\t\t\t\t\tfor (const content of assistantMsg.content) {\n\t\t\t\t\t\tif (content.type === \"text\") {\n\t\t\t\t\t\t\twriteRawStdout(`${content.text}\\n`);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn exitCode;\n\t} catch (error: unknown) {\n\t\tconsole.error(error instanceof Error ? error.message : String(error));\n\t\treturn 1;\n\t} finally {\n\t\tif (heartbeat) clearInterval(heartbeat);\n\t\tturnLimitUnsub?.();\n\t\tfor (const cleanup of signalCleanupHandlers) {\n\t\t\tcleanup();\n\t\t}\n\t\tawait disposeRuntime();\n\t\tawait flushRawStdout();\n\t}\n}\n"]}
|
|
@@ -264,7 +264,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
264
264
|
},
|
|
265
265
|
|
|
266
266
|
renderCall(args, theme, _context) {
|
|
267
|
-
const pattern = args.pattern || "";
|
|
267
|
+
const pattern = Array.isArray(args.pattern) ? args.pattern.join(", ") : args.pattern || "";
|
|
268
268
|
const path = shortenPath(args.path || ".");
|
|
269
269
|
const limit = args.limit;
|
|
270
270
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolisachint/hoocode-agent",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.114",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"hoocodeConfig": {
|
|
@@ -39,15 +39,15 @@
|
|
|
39
39
|
"build:full": "npm run embed-templates && npm run build",
|
|
40
40
|
"build:binary": "npm --prefix ../tui run build && npm --prefix ../ai run build && npm --prefix ../agent run build && npm run build && npm run copy-binary-assets && pkg . --entry bin/hoocode.js",
|
|
41
41
|
"build:bun-binary": "node scripts/build-bun-binary.mjs",
|
|
42
|
-
"copy-assets": "shx mkdir -p dist/modes/interactive/theme && shx cp src/modes/interactive/theme/*.json dist/modes/interactive/theme/ && shx mkdir -p dist/core/export-html/vendor && shx cp src/core/export-html/template.html src/core/export-html/template.css src/core/export-html/template.js dist/core/export-html/ && shx cp src/core/export-html/vendor/*.js dist/core/export-html/vendor/",
|
|
42
|
+
"copy-assets": "shx mkdir -p dist/modes/interactive/theme && shx cp src/modes/interactive/theme/*.json dist/modes/interactive/theme/ && shx mkdir -p dist/core/export-html/vendor && shx cp src/core/export-html/template.html src/core/export-html/template.css src/core/export-html/template.js dist/core/export-html/ && shx cp src/core/export-html/vendor/*.js dist/core/export-html/vendor/ && shx mkdir -p dist/core/extensions/plugins && shx cp -r src/core/extensions/plugins/default-marketplace dist/core/extensions/plugins/",
|
|
43
43
|
"copy-binary-assets": "shx cp package.json dist/ && shx cp README.md dist/ && shx cp CHANGELOG.md dist/ && shx mkdir -p dist/theme && shx cp src/modes/interactive/theme/*.json dist/theme/ && shx mkdir -p dist/export-html/vendor && shx cp src/core/export-html/template.html dist/export-html/ && shx cp src/core/export-html/vendor/*.js dist/export-html/vendor/ && shx cp -r docs dist/ && shx cp -r examples dist/ && shx cp ../../node_modules/@silvia-odwyer/photon-node/photon_rs_bg.wasm dist/",
|
|
44
44
|
"test": "vitest --run",
|
|
45
45
|
"prepublishOnly": "npm run clean && npm run build"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@kolisachint/hoocode-agent-core": "^0.4.
|
|
49
|
-
"@kolisachint/hoocode-ai": "^0.4.
|
|
50
|
-
"@kolisachint/hoocode-tui": "^0.4.
|
|
48
|
+
"@kolisachint/hoocode-agent-core": "^0.4.114",
|
|
49
|
+
"@kolisachint/hoocode-ai": "^0.4.114",
|
|
50
|
+
"@kolisachint/hoocode-tui": "^0.4.114",
|
|
51
51
|
"@silvia-odwyer/photon-node": "^0.3.4",
|
|
52
52
|
"chalk": "^5.5.0",
|
|
53
53
|
"cli-highlight": "^2.1.11",
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import type { AgentTool } from "@kolisachint/hoocode-agent-core";
|
|
2
|
-
import { type Static, Type } from "typebox";
|
|
3
|
-
import type { ToolDefinition } from "../extensions/types.js";
|
|
4
|
-
import { type TruncationResult } from "./truncate.js";
|
|
5
|
-
declare const globSchema: Type.TObject<{
|
|
6
|
-
patterns: Type.TUnion<[Type.TString, Type.TArray<Type.TString>]>;
|
|
7
|
-
path: Type.TOptional<Type.TString>;
|
|
8
|
-
exclude: Type.TOptional<Type.TUnion<[Type.TString, Type.TArray<Type.TString>]>>;
|
|
9
|
-
type: Type.TOptional<Type.TUnion<[Type.TLiteral<"f">, Type.TLiteral<"d">, Type.TLiteral<"l">]>>;
|
|
10
|
-
depth: Type.TOptional<Type.TNumber>;
|
|
11
|
-
limit: Type.TOptional<Type.TNumber>;
|
|
12
|
-
compress: Type.TOptional<Type.TBoolean>;
|
|
13
|
-
}>;
|
|
14
|
-
export type GlobToolInput = Static<typeof globSchema>;
|
|
15
|
-
export interface GlobToolDetails {
|
|
16
|
-
truncation?: TruncationResult;
|
|
17
|
-
resultLimitReached?: number;
|
|
18
|
-
totalMatches?: number;
|
|
19
|
-
uniqueDirectories?: number;
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* Pluggable operations for the glob tool.
|
|
23
|
-
* Override these to delegate file search to remote systems (for example SSH).
|
|
24
|
-
*/
|
|
25
|
-
export interface GlobOperations {
|
|
26
|
-
/** Check if path exists */
|
|
27
|
-
exists: (absolutePath: string) => Promise<boolean> | boolean;
|
|
28
|
-
/** Optional custom glob resolver (used for SSH/remote) */
|
|
29
|
-
glob?: (patterns: string[], cwd: string, options: {
|
|
30
|
-
ignore: string[];
|
|
31
|
-
limit: number;
|
|
32
|
-
type?: string;
|
|
33
|
-
}) => Promise<string[]> | string[];
|
|
34
|
-
}
|
|
35
|
-
export interface GlobToolOptions {
|
|
36
|
-
/** Custom operations for glob. Default: local filesystem plus fd */
|
|
37
|
-
operations?: GlobOperations;
|
|
38
|
-
}
|
|
39
|
-
export declare function createGlobToolDefinition(cwd: string, options?: GlobToolOptions): ToolDefinition<typeof globSchema, GlobToolDetails | undefined>;
|
|
40
|
-
export declare function createGlobTool(cwd: string, options?: GlobToolOptions): AgentTool<typeof globSchema>;
|
|
41
|
-
export {};
|
|
42
|
-
//# sourceMappingURL=glob.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"glob.d.ts","sourceRoot":"","sources":["../../../src/core/tools/glob.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAC;AAKjE,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAG5C,OAAO,KAAK,EAAE,cAAc,EAA2B,MAAM,wBAAwB,CAAC;AAKtF,OAAO,EAAiC,KAAK,gBAAgB,EAAgB,MAAM,eAAe,CAAC;AAEnG,QAAA,MAAM,UAAU;;;;;;;;EAmBd,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,OAAO,UAAU,CAAC,CAAC;AAItD,MAAM,WAAW,eAAe;IAC/B,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC9B,2BAA2B;IAC3B,MAAM,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;IAC7D,0DAA0D;IAC1D,IAAI,CAAC,EAAE,CACN,QAAQ,EAAE,MAAM,EAAE,EAClB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE;QAAE,MAAM,EAAE,MAAM,EAAE,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,KACvD,OAAO,CAAC,MAAM,EAAE,CAAC,GAAG,MAAM,EAAE,CAAC;CAClC;AAMD,MAAM,WAAW,eAAe;IAC/B,oEAAoE;IACpE,UAAU,CAAC,EAAE,cAAc,CAAC;CAC5B;AAsKD,wBAAgB,wBAAwB,CACvC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,eAAe,GACvB,cAAc,CAAC,OAAO,UAAU,EAAE,eAAe,GAAG,SAAS,CAAC,CAuUhE;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,SAAS,CAAC,OAAO,UAAU,CAAC,CAEnG","sourcesContent":["import { createInterface } from \"node:readline\";\nimport type { AgentTool } from \"@kolisachint/hoocode-agent-core\";\nimport { Text } from \"@kolisachint/hoocode-tui\";\nimport { spawn } from \"child_process\";\nimport { existsSync } from \"fs\";\nimport path from \"path\";\nimport { type Static, Type } from \"typebox\";\nimport { keyHint } from \"../../modes/interactive/components/keybinding-hints.js\";\nimport { ensureTool } from \"../../utils/tools-manager.js\";\nimport type { ToolDefinition, ToolRenderResultOptions } from \"../extensions/types.js\";\nimport { applyFdGlobPattern, relativizeFdLine, toPosixPath } from \"./fd-utils.js\";\nimport { resolveToCwd } from \"./path-utils.js\";\nimport { getTextOutput, invalidArgText, shortenPath, str } from \"./render-utils.js\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.js\";\nimport { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from \"./truncate.js\";\n\nconst globSchema = Type.Object({\n\tpatterns: Type.Union([Type.String(), Type.Array(Type.String())], {\n\t\tdescription:\n\t\t\t\"One or more glob patterns to match files (OR logic), e.g. '*.ts', '**/*.json', or ['src/**/*.ts', 'test/**/*.ts']\",\n\t}),\n\tpath: Type.Optional(Type.String({ description: \"Root directory to search from (default: current directory)\" })),\n\texclude: Type.Optional(\n\t\tType.Union([Type.String(), Type.Array(Type.String())], {\n\t\t\tdescription: \"Additional exclusion patterns, e.g. '**/*.test.ts' or ['**/dist/**', '**/build/**']\",\n\t\t}),\n\t),\n\ttype: Type.Optional(\n\t\tType.Union([Type.Literal(\"f\"), Type.Literal(\"d\"), Type.Literal(\"l\")], {\n\t\t\tdescription: \"Filter by file type: 'f' for files, 'd' for directories, 'l' for symlinks (default: 'f')\",\n\t\t}),\n\t),\n\tdepth: Type.Optional(Type.Number({ description: \"Maximum directory depth to search\" })),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of results (default: 2000)\" })),\n\tcompress: Type.Optional(Type.Boolean({ description: \"Compress output by grouping files (default: true)\" })),\n});\n\nexport type GlobToolInput = Static<typeof globSchema>;\n\nconst DEFAULT_LIMIT = 2000;\n\nexport interface GlobToolDetails {\n\ttruncation?: TruncationResult;\n\tresultLimitReached?: number;\n\ttotalMatches?: number;\n\tuniqueDirectories?: number;\n}\n\n/**\n * Pluggable operations for the glob tool.\n * Override these to delegate file search to remote systems (for example SSH).\n */\nexport interface GlobOperations {\n\t/** Check if path exists */\n\texists: (absolutePath: string) => Promise<boolean> | boolean;\n\t/** Optional custom glob resolver (used for SSH/remote) */\n\tglob?: (\n\t\tpatterns: string[],\n\t\tcwd: string,\n\t\toptions: { ignore: string[]; limit: number; type?: string },\n\t) => Promise<string[]> | string[];\n}\n\nconst defaultGlobOperations: GlobOperations = {\n\texists: existsSync,\n};\n\nexport interface GlobToolOptions {\n\t/** Custom operations for glob. Default: local filesystem plus fd */\n\toperations?: GlobOperations;\n}\n\n/**\n * Compress a list of file paths by grouping files in the same directory.\n *\n * Compression rules:\n * - If a directory has >=3 entries all sharing the same extension: fold into `dir/{stem1,stem2,stem3}.ext`\n * - If >=3 entries with mixed extensions: sub-group by extension; fold sub-groups >=3\n * - If directory has >6 total files: summarize as `dir/ [N files]`\n * - 1-2 entries: emit verbatim\n */\nfunction compressPaths(paths: string[]): string {\n\tif (paths.length === 0) return \"\";\n\n\t// Sort paths for stable output\n\tconst sorted = [...paths].sort();\n\n\t// Group by directory\n\tconst dirMap = new Map<string, string[]>();\n\tfor (const p of sorted) {\n\t\tconst dir = path.dirname(p);\n\t\tconst base = path.basename(p);\n\t\tif (!dirMap.has(dir)) {\n\t\t\tdirMap.set(dir, []);\n\t\t}\n\t\tdirMap.get(dir)!.push(base);\n\t}\n\n\tconst result: string[] = [];\n\n\tfor (const [dir, files] of dirMap) {\n\t\tif (files.length <= 2) {\n\t\t\t// 1-2 entries: emit verbatim\n\t\t\tfor (const f of files) {\n\t\t\t\tresult.push(dir === \".\" ? f : `${dir}/${f}`);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Check if all files share the same extension\n\t\tconst extMap = new Map<string, string[]>();\n\t\tfor (const f of files) {\n\t\t\tconst ext = path.extname(f);\n\t\t\tconst stem = path.basename(f, ext);\n\t\t\tif (!extMap.has(ext)) {\n\t\t\t\textMap.set(ext, []);\n\t\t\t}\n\t\t\textMap.get(ext)!.push(stem);\n\t\t}\n\n\t\t// If all files have the same extension and >=3, fold\n\t\tif (extMap.size === 1 && files.length >= 3) {\n\t\t\tconst [ext, stems] = extMap.entries().next().value!;\n\t\t\tconst displayDir = dir === \".\" ? \"\" : `${dir}/`;\n\t\t\tresult.push(`${displayDir}{${stems.join(\",\")}}${ext}`);\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Mixed extensions: try to sub-group\n\t\tlet anyFolded = false;\n\t\tconst subResults: string[] = [];\n\n\t\tfor (const [ext, stems] of extMap) {\n\t\t\tif (stems.length >= 3) {\n\t\t\t\t// Fold this sub-group\n\t\t\t\tconst displayDir = dir === \".\" ? \"\" : `${dir}/`;\n\t\t\t\tsubResults.push(`${displayDir}{${stems.join(\",\")}}${ext}`);\n\t\t\t\tanyFolded = true;\n\t\t\t} else {\n\t\t\t\t// Emit verbatim\n\t\t\t\tfor (const stem of stems) {\n\t\t\t\t\tsubResults.push(dir === \".\" ? `${stem}${ext}` : `${dir}/${stem}${ext}`);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (anyFolded || files.length > 6) {\n\t\t\tresult.push(...subResults);\n\t\t} else {\n\t\t\t// Too mixed, emit verbatim\n\t\t\tfor (const f of files) {\n\t\t\t\tresult.push(dir === \".\" ? f : `${dir}/${f}`);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn result.join(\"\\n\");\n}\n\nfunction formatGlobCall(\n\targs: { patterns?: string | string[]; path?: string; type?: string; depth?: number; limit?: number } | undefined,\n\ttheme: typeof import(\"../../modes/interactive/theme/theme.js\").theme,\n): string {\n\tconst patterns = args?.patterns;\n\tconst patternStr = Array.isArray(patterns) ? patterns.join(\", \") : (str(patterns) ?? \"\");\n\tconst rawPath = str(args?.path);\n\tconst displayPath = rawPath !== null ? shortenPath(rawPath || \".\") : null;\n\tconst type = args?.type;\n\tconst depth = args?.depth;\n\tconst limit = args?.limit;\n\tconst invalidArg = invalidArgText(theme);\n\n\tlet text =\n\t\ttheme.fg(\"toolTitle\", theme.bold(\"glob\")) +\n\t\t\" \" +\n\t\t(patternStr === \"\" ? invalidArg : theme.fg(\"accent\", patternStr)) +\n\t\ttheme.fg(\"toolOutput\", ` in ${displayPath === null ? invalidArg : displayPath}`);\n\n\tif (type && type !== \"f\") {\n\t\tconst typeLabel = type === \"d\" ? \"dirs\" : type === \"l\" ? \"symlinks\" : type;\n\t\ttext += theme.fg(\"toolOutput\", ` (${typeLabel})`);\n\t}\n\tif (depth !== undefined) {\n\t\ttext += theme.fg(\"toolOutput\", ` (depth ${depth})`);\n\t}\n\tif (limit !== undefined) {\n\t\ttext += theme.fg(\"toolOutput\", ` (limit ${limit})`);\n\t}\n\treturn text;\n}\n\nfunction formatGlobResult(\n\tresult: {\n\t\tcontent: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;\n\t\tdetails?: GlobToolDetails;\n\t},\n\toptions: ToolRenderResultOptions,\n\ttheme: typeof import(\"../../modes/interactive/theme/theme.js\").theme,\n\tshowImages: boolean,\n): string {\n\tconst output = getTextOutput(result, showImages).trim();\n\tlet text = \"\";\n\tif (output) {\n\t\tconst lines = output.split(\"\\n\");\n\t\tconst maxLines = options.expanded ? lines.length : 25;\n\t\tconst displayLines = lines.slice(0, maxLines);\n\t\tconst remaining = lines.length - maxLines;\n\t\ttext += `\\n${displayLines.map((line) => theme.fg(\"toolOutput\", line)).join(\"\\n\")}`;\n\t\tif (remaining > 0) {\n\t\t\ttext += `${theme.fg(\"muted\", `\\n... (${remaining} more lines,`)} ${keyHint(\"app.tools.expand\", \"to expand\")})`;\n\t\t}\n\t}\n\n\t// Stats footer\n\tconst totalMatches = result.details?.totalMatches;\n\tconst uniqueDirectories = result.details?.uniqueDirectories;\n\tif (totalMatches !== undefined && uniqueDirectories !== undefined) {\n\t\ttext += `\\n${theme.fg(\"muted\", `[${totalMatches} matches, ${uniqueDirectories} dirs]`)}`;\n\t}\n\n\t// Truncation/limit warnings\n\tconst resultLimit = result.details?.resultLimitReached;\n\tconst truncation = result.details?.truncation;\n\tif (resultLimit || truncation?.truncated) {\n\t\tconst warnings: string[] = [];\n\t\tif (resultLimit) {\n\t\t\twarnings.push(`${resultLimit} results limit reached. Use limit=${resultLimit * 2} for more`);\n\t\t}\n\t\tif (truncation?.truncated) {\n\t\t\twarnings.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);\n\t\t}\n\t\ttext += `\\n${theme.fg(\"warning\", `[Truncated: ${warnings.join(\". \")}]`)}`;\n\t}\n\treturn text;\n}\n\nexport function createGlobToolDefinition(\n\tcwd: string,\n\toptions?: GlobToolOptions,\n): ToolDefinition<typeof globSchema, GlobToolDetails | undefined> {\n\tconst customOps = options?.operations;\n\treturn {\n\t\tname: \"glob\",\n\t\tlabel: \"glob\",\n\t\tdescription: `Search for files matching one or more glob patterns. Supports OR logic across patterns. Returns matching file paths grouped and compressed for readability. Respects .gitignore. Output is truncated to ${DEFAULT_LIMIT} results or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first).`,\n\t\tpromptSnippet: \"Search for files by glob pattern (supports multiple patterns, respects .gitignore)\",\n\t\tparameters: globSchema,\n\t\tasync execute(\n\t\t\t_toolCallId,\n\t\t\t{\n\t\t\t\tpatterns: rawPatterns,\n\t\t\t\tpath: searchDir,\n\t\t\t\texclude: rawExclude,\n\t\t\t\ttype,\n\t\t\t\tdepth,\n\t\t\t\tlimit,\n\t\t\t\tcompress = true,\n\t\t\t}: {\n\t\t\t\tpatterns: string | string[];\n\t\t\t\tpath?: string;\n\t\t\t\texclude?: string | string[];\n\t\t\t\ttype?: \"f\" | \"d\" | \"l\";\n\t\t\t\tdepth?: number;\n\t\t\t\tlimit?: number;\n\t\t\t\tcompress?: boolean;\n\t\t\t},\n\t\t\tsignal?: AbortSignal,\n\t\t\t_onUpdate?,\n\t\t\t_ctx?,\n\t\t) {\n\t\t\treturn new Promise((resolve, reject) => {\n\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tlet settled = false;\n\t\t\t\tlet stopChild: (() => void) | undefined;\n\t\t\t\tconst settle = (fn: () => void) => {\n\t\t\t\t\tif (settled) return;\n\t\t\t\t\tsettled = true;\n\t\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\tstopChild = undefined;\n\t\t\t\t\tfn();\n\t\t\t\t};\n\t\t\t\tconst onAbort = () => {\n\t\t\t\t\tstopChild?.();\n\t\t\t\t\tsettle(() => reject(new Error(\"Operation aborted\")));\n\t\t\t\t};\n\t\t\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\n\t\t\t\t(async () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst searchPath = resolveToCwd(searchDir || \".\", cwd);\n\t\t\t\t\t\tconst effectiveLimit = limit ?? DEFAULT_LIMIT;\n\t\t\t\t\t\tconst ops = customOps ?? defaultGlobOperations;\n\n\t\t\t\t\t\t// Normalize patterns to array\n\t\t\t\t\t\tconst patterns = Array.isArray(rawPatterns) ? rawPatterns : [rawPatterns];\n\n\t\t\t\t\t\t// Normalize exclude to array\n\t\t\t\t\t\tconst excludePatterns = rawExclude ? (Array.isArray(rawExclude) ? rawExclude : [rawExclude]) : [];\n\n\t\t\t\t\t\t// Always exclude node_modules and .git\n\t\t\t\t\t\tconst allIgnore = [\"**/node_modules/**\", \"**/.git/**\", ...excludePatterns];\n\n\t\t\t\t\t\t// Build type filter for fd\n\t\t\t\t\t\tconst typeFilter = type === \"d\" ? \"d\" : type === \"l\" ? \"l\" : \"f\";\n\n\t\t\t\t\t\t// If custom operations provide glob(), use that instead of fd.\n\t\t\t\t\t\tif (customOps?.glob) {\n\t\t\t\t\t\t\tif (!(await ops.exists(searchPath))) {\n\t\t\t\t\t\t\t\tsettle(() => reject(new Error(`Path not found: ${searchPath}`)));\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\t\t\t\tsettle(() => reject(new Error(\"Operation aborted\")));\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tconst results = await ops.glob!(patterns, searchPath, {\n\t\t\t\t\t\t\t\tignore: allIgnore,\n\t\t\t\t\t\t\t\tlimit: effectiveLimit,\n\t\t\t\t\t\t\t\ttype: typeFilter,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\t\t\t\tsettle(() => reject(new Error(\"Operation aborted\")));\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (results.length === 0) {\n\t\t\t\t\t\t\t\tsettle(() =>\n\t\t\t\t\t\t\t\t\tresolve({\n\t\t\t\t\t\t\t\t\t\tcontent: [{ type: \"text\", text: \"No files found matching patterns\" }],\n\t\t\t\t\t\t\t\t\t\tdetails: undefined,\n\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Relativize paths against the search root\n\t\t\t\t\t\t\tconst relativized = results.map((p) => {\n\t\t\t\t\t\t\t\tif (p.startsWith(searchPath)) return toPosixPath(p.slice(searchPath.length + 1));\n\t\t\t\t\t\t\t\treturn toPosixPath(path.relative(searchPath, p));\n\t\t\t\t\t\t\t});\n\n\t\t\t\t\t\t\t// Deduplicate\n\t\t\t\t\t\t\tconst unique = [...new Set(relativized)];\n\n\t\t\t\t\t\t\tconst totalMatches = unique.length;\n\t\t\t\t\t\t\tconst resultLimitReached = totalMatches >= effectiveLimit;\n\t\t\t\t\t\t\tconst truncated = unique.slice(0, effectiveLimit);\n\n\t\t\t\t\t\t\t// Compress output\n\t\t\t\t\t\t\tlet resultOutput: string;\n\t\t\t\t\t\t\tif (compress) {\n\t\t\t\t\t\t\t\tresultOutput = compressPaths(truncated);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tresultOutput = truncated.join(\"\\n\");\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Apply byte-level truncation\n\t\t\t\t\t\t\tconst truncationResult = truncateHead(resultOutput, {\n\t\t\t\t\t\t\t\tmaxLines: Number.MAX_SAFE_INTEGER,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\tresultOutput = truncationResult.content;\n\n\t\t\t\t\t\t\t// Calculate unique directories\n\t\t\t\t\t\t\tconst uniqueDirs = new Set(truncated.map((p) => path.dirname(p)));\n\n\t\t\t\t\t\t\tconst details: GlobToolDetails = {\n\t\t\t\t\t\t\t\ttotalMatches,\n\t\t\t\t\t\t\t\tuniqueDirectories: uniqueDirs.size,\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\tconst notices: string[] = [];\n\t\t\t\t\t\t\tif (resultLimitReached) {\n\t\t\t\t\t\t\t\tnotices.push(\n\t\t\t\t\t\t\t\t\t`${effectiveLimit} results limit reached. Use limit=${effectiveLimit * 2} for more`,\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\tdetails.resultLimitReached = effectiveLimit;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (truncationResult.truncated) {\n\t\t\t\t\t\t\t\tnotices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);\n\t\t\t\t\t\t\t\tdetails.truncation = truncationResult;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (notices.length > 0) {\n\t\t\t\t\t\t\t\tresultOutput += `\\n\\n[${notices.join(\". \")}]`;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tsettle(() =>\n\t\t\t\t\t\t\t\tresolve({\n\t\t\t\t\t\t\t\t\tcontent: [{ type: \"text\", text: resultOutput }],\n\t\t\t\t\t\t\t\t\tdetails: Object.keys(details).length > 0 ? details : undefined,\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Default implementation uses fd.\n\t\t\t\t\t\tconst fdPath = await ensureTool(\"fd\", true);\n\t\t\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\t\t\tsettle(() => reject(new Error(\"Operation aborted\")));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (!fdPath) {\n\t\t\t\t\t\t\tsettle(() =>\n\t\t\t\t\t\t\t\treject(\n\t\t\t\t\t\t\t\t\tnew Error(\n\t\t\t\t\t\t\t\t\t\t\"fd unavailable and could not be downloaded — use the bash tool to run find instead\",\n\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Run all patterns in parallel\n\t\t\t\t\t\tconst allResults: string[] = [];\n\n\t\t\t\t\t\tconst runPattern = async (pattern: string): Promise<string[]> => {\n\t\t\t\t\t\t\tconst args: string[] = [\n\t\t\t\t\t\t\t\t\"--glob\",\n\t\t\t\t\t\t\t\t\"--color=never\",\n\t\t\t\t\t\t\t\t\"--hidden\",\n\t\t\t\t\t\t\t\t\"--no-require-git\",\n\t\t\t\t\t\t\t\t\"--max-results\",\n\t\t\t\t\t\t\t\tString(effectiveLimit),\n\t\t\t\t\t\t\t];\n\n\t\t\t\t\t\t\t// Add type filter\n\t\t\t\t\t\t\tif (typeFilter) {\n\t\t\t\t\t\t\t\targs.push(\"--type\", typeFilter);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Add exclusions\n\t\t\t\t\t\t\tfor (const excl of allIgnore) {\n\t\t\t\t\t\t\t\targs.push(\"--exclude\", excl);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Add depth\n\t\t\t\t\t\t\tif (depth !== undefined) {\n\t\t\t\t\t\t\t\targs.push(\"--max-depth\", String(depth));\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tconst effectivePattern = applyFdGlobPattern(args, pattern);\n\t\t\t\t\t\t\targs.push(\"--\", effectivePattern, searchPath);\n\n\t\t\t\t\t\t\treturn new Promise<string[]>((res, rej) => {\n\t\t\t\t\t\t\t\tconst child = spawn(fdPath, args, { stdio: [\"ignore\", \"pipe\", \"pipe\"] });\n\t\t\t\t\t\t\t\tconst rl = createInterface({ input: child.stdout });\n\t\t\t\t\t\t\t\tconst lines: string[] = [];\n\n\t\t\t\t\t\t\t\tconst cleanup = () => {\n\t\t\t\t\t\t\t\t\trl.close();\n\t\t\t\t\t\t\t\t};\n\n\t\t\t\t\t\t\t\trl.on(\"line\", (line) => {\n\t\t\t\t\t\t\t\t\tlines.push(line);\n\t\t\t\t\t\t\t\t});\n\n\t\t\t\t\t\t\t\tchild.on(\"error\", (error) => {\n\t\t\t\t\t\t\t\t\tcleanup();\n\t\t\t\t\t\t\t\t\trej(new Error(`Failed to run fd: ${error.message}`));\n\t\t\t\t\t\t\t\t});\n\n\t\t\t\t\t\t\t\tchild.on(\"close\", () => {\n\t\t\t\t\t\t\t\t\tcleanup();\n\t\t\t\t\t\t\t\t\tres(lines);\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t};\n\n\t\t\t\t\t\t// Execute all patterns in parallel\n\t\t\t\t\t\tconst patternResults = await Promise.all(patterns.map(runPattern));\n\n\t\t\t\t\t\t// Merge and deduplicate results\n\t\t\t\t\t\tconst seen = new Set<string>();\n\t\t\t\t\t\tfor (const results of patternResults) {\n\t\t\t\t\t\t\tfor (const rawLine of results) {\n\t\t\t\t\t\t\t\tconst line = rawLine.replace(/\\r$/, \"\").trim();\n\t\t\t\t\t\t\t\tif (!line) continue;\n\n\t\t\t\t\t\t\t\tconst posixPath = relativizeFdLine(line, searchPath);\n\t\t\t\t\t\t\t\tif (!seen.has(posixPath)) {\n\t\t\t\t\t\t\t\t\tseen.add(posixPath);\n\t\t\t\t\t\t\t\t\tallResults.push(posixPath);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (allResults.length === 0) {\n\t\t\t\t\t\t\tsettle(() =>\n\t\t\t\t\t\t\t\tresolve({\n\t\t\t\t\t\t\t\t\tcontent: [{ type: \"text\", text: \"No files found matching patterns\" }],\n\t\t\t\t\t\t\t\t\tdetails: undefined,\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Sort results for stable output\n\t\t\t\t\t\tallResults.sort();\n\n\t\t\t\t\t\tconst totalMatches = allResults.length;\n\t\t\t\t\t\tconst resultLimitReached = totalMatches >= effectiveLimit;\n\t\t\t\t\t\tconst truncated = allResults.slice(0, effectiveLimit);\n\n\t\t\t\t\t\t// Compress output\n\t\t\t\t\t\tlet resultOutput: string;\n\t\t\t\t\t\tif (compress) {\n\t\t\t\t\t\t\tresultOutput = compressPaths(truncated);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tresultOutput = truncated.join(\"\\n\");\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Apply byte-level truncation\n\t\t\t\t\t\tconst truncationResult = truncateHead(resultOutput, {\n\t\t\t\t\t\t\tmaxLines: Number.MAX_SAFE_INTEGER,\n\t\t\t\t\t\t});\n\t\t\t\t\t\tresultOutput = truncationResult.content;\n\n\t\t\t\t\t\t// Calculate unique directories\n\t\t\t\t\t\tconst uniqueDirs = new Set(truncated.map((p) => path.dirname(p)));\n\n\t\t\t\t\t\tconst details: GlobToolDetails = {\n\t\t\t\t\t\t\ttotalMatches,\n\t\t\t\t\t\t\tuniqueDirectories: uniqueDirs.size,\n\t\t\t\t\t\t};\n\t\t\t\t\t\tconst notices: string[] = [];\n\t\t\t\t\t\tif (resultLimitReached) {\n\t\t\t\t\t\t\tnotices.push(`${effectiveLimit} results limit reached. Use limit=${effectiveLimit * 2} for more`);\n\t\t\t\t\t\t\tdetails.resultLimitReached = effectiveLimit;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (truncationResult.truncated) {\n\t\t\t\t\t\t\tnotices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);\n\t\t\t\t\t\t\tdetails.truncation = truncationResult;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (notices.length > 0) {\n\t\t\t\t\t\t\tresultOutput += `\\n\\n[${notices.join(\". \")}]`;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tsettle(() =>\n\t\t\t\t\t\t\tresolve({\n\t\t\t\t\t\t\t\tcontent: [{ type: \"text\", text: resultOutput }],\n\t\t\t\t\t\t\t\tdetails: Object.keys(details).length > 0 ? details : undefined,\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t);\n\t\t\t\t\t} catch (e) {\n\t\t\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\t\t\tsettle(() => reject(new Error(\"Operation aborted\")));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst error = e instanceof Error ? e : new Error(String(e));\n\t\t\t\t\t\tsettle(() => reject(error));\n\t\t\t\t\t}\n\t\t\t\t})();\n\t\t\t});\n\t\t},\n\t\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatGlobCall(args, theme));\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, options, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatGlobResult(result as any, options, theme, context.showImages));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createGlobTool(cwd: string, options?: GlobToolOptions): AgentTool<typeof globSchema> {\n\treturn wrapToolDefinition(createGlobToolDefinition(cwd, options));\n}\n"]}
|