@pentoshi/clai 2.0.62 → 3.5.1
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/README.md +262 -368
- package/dist/agent/classic-renderer.js +9 -1
- package/dist/agent/classic-renderer.js.map +1 -1
- package/dist/agent/context-manager.d.ts +5 -0
- package/dist/agent/context-manager.js +14 -2
- package/dist/agent/context-manager.js.map +1 -1
- package/dist/agent/events.d.ts +9 -1
- package/dist/agent/plan-tool.js +80 -8
- package/dist/agent/plan-tool.js.map +1 -1
- package/dist/agent/runner.js +261 -122
- package/dist/agent/runner.js.map +1 -1
- package/dist/agent/tool-call-parser.d.ts +20 -0
- package/dist/agent/tool-call-parser.js +186 -2
- package/dist/agent/tool-call-parser.js.map +1 -1
- package/dist/app/adapters/agent-event-adapter.d.ts +29 -0
- package/dist/app/adapters/agent-event-adapter.js +148 -0
- package/dist/app/adapters/agent-event-adapter.js.map +1 -0
- package/dist/app/adapters/current-agent-adapter.d.ts +7 -0
- package/dist/app/adapters/current-agent-adapter.js +27 -0
- package/dist/app/adapters/current-agent-adapter.js.map +1 -0
- package/dist/app/adapters/current-jobs-adapter.d.ts +4 -0
- package/dist/app/adapters/current-jobs-adapter.js +13 -0
- package/dist/app/adapters/current-jobs-adapter.js.map +1 -0
- package/dist/app/adapters/current-store-adapter.d.ts +3 -0
- package/dist/app/adapters/current-store-adapter.js +22 -0
- package/dist/app/adapters/current-store-adapter.js.map +1 -0
- package/dist/app/adapters/current-terminal-adapter.d.ts +9 -0
- package/dist/app/adapters/current-terminal-adapter.js +31 -0
- package/dist/app/adapters/current-terminal-adapter.js.map +1 -0
- package/dist/app/adapters/current-updates-adapter.d.ts +7 -0
- package/dist/app/adapters/current-updates-adapter.js +31 -0
- package/dist/app/adapters/current-updates-adapter.js.map +1 -0
- package/dist/app/adapters/in-memory-clipboard-adapter.d.ts +10 -0
- package/dist/app/adapters/in-memory-clipboard-adapter.js +20 -0
- package/dist/app/adapters/in-memory-clipboard-adapter.js.map +1 -0
- package/dist/app/commands/command.d.ts +24 -0
- package/dist/app/commands/command.js +4 -0
- package/dist/app/commands/command.js.map +1 -0
- package/dist/app/commands/registry.d.ts +45 -0
- package/dist/app/commands/registry.js +164 -0
- package/dist/app/commands/registry.js.map +1 -0
- package/dist/app/controllers/disposable.d.ts +15 -0
- package/dist/app/controllers/disposable.js +38 -0
- package/dist/app/controllers/disposable.js.map +1 -0
- package/dist/app/controllers/job-controller.d.ts +22 -0
- package/dist/app/controllers/job-controller.js +35 -0
- package/dist/app/controllers/job-controller.js.map +1 -0
- package/dist/app/controllers/plan-controller.d.ts +31 -0
- package/dist/app/controllers/plan-controller.js +69 -0
- package/dist/app/controllers/plan-controller.js.map +1 -0
- package/dist/app/controllers/session-controller.d.ts +167 -0
- package/dist/app/controllers/session-controller.js +467 -0
- package/dist/app/controllers/session-controller.js.map +1 -0
- package/dist/app/controllers/turn-controller.d.ts +52 -0
- package/dist/app/controllers/turn-controller.js +118 -0
- package/dist/app/controllers/turn-controller.js.map +1 -0
- package/dist/app/events/app-event.d.ts +122 -0
- package/dist/app/events/app-event.js +25 -0
- package/dist/app/events/app-event.js.map +1 -0
- package/dist/app/events/event-buffer.d.ts +51 -0
- package/dist/app/events/event-buffer.js +109 -0
- package/dist/app/events/event-buffer.js.map +1 -0
- package/dist/app/events/sequencer.d.ts +32 -0
- package/dist/app/events/sequencer.js +55 -0
- package/dist/app/events/sequencer.js.map +1 -0
- package/dist/app/ports/agent-port.d.ts +29 -0
- package/dist/app/ports/agent-port.js +2 -0
- package/dist/app/ports/agent-port.js.map +1 -0
- package/dist/app/ports/clipboard-port.d.ts +9 -0
- package/dist/app/ports/clipboard-port.js +2 -0
- package/dist/app/ports/clipboard-port.js.map +1 -0
- package/dist/app/ports/confirm-port.d.ts +17 -0
- package/dist/app/ports/confirm-port.js +2 -0
- package/dist/app/ports/confirm-port.js.map +1 -0
- package/dist/app/ports/jobs-port.d.ts +18 -0
- package/dist/app/ports/jobs-port.js +2 -0
- package/dist/app/ports/jobs-port.js.map +1 -0
- package/dist/app/ports/persistence-port.d.ts +21 -0
- package/dist/app/ports/persistence-port.js +2 -0
- package/dist/app/ports/persistence-port.js.map +1 -0
- package/dist/app/ports/secret-port.d.ts +12 -0
- package/dist/app/ports/secret-port.js +2 -0
- package/dist/app/ports/secret-port.js.map +1 -0
- package/dist/app/ports/terminal-port.d.ts +18 -0
- package/dist/app/ports/terminal-port.js +2 -0
- package/dist/app/ports/terminal-port.js.map +1 -0
- package/dist/app/ports/updates-port.d.ts +12 -0
- package/dist/app/ports/updates-port.js +2 -0
- package/dist/app/ports/updates-port.js.map +1 -0
- package/dist/commands/doctor.js +19 -0
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/update.js +1 -1
- package/dist/commands/update.js.map +1 -1
- package/dist/index.js +47 -12
- package/dist/index.js.map +1 -1
- package/dist/llm/router.js +4 -2
- package/dist/llm/router.js.map +1 -1
- package/dist/prompts/index.d.ts +1 -1
- package/dist/prompts/index.js +11 -8
- package/dist/prompts/index.js.map +1 -1
- package/dist/repl/slash-commands.js +1 -1
- package/dist/repl/slash-commands.js.map +1 -1
- package/dist/repl.js +35 -107
- package/dist/repl.js.map +1 -1
- package/dist/store/config.js +3 -1
- package/dist/store/config.js.map +1 -1
- package/dist/store/history.d.ts +26 -2
- package/dist/store/history.js +383 -72
- package/dist/store/history.js.map +1 -1
- package/dist/store/logs.js +3 -3
- package/dist/store/logs.js.map +1 -1
- package/dist/store/paths.d.ts +6 -0
- package/dist/store/paths.js +38 -0
- package/dist/store/paths.js.map +1 -0
- package/dist/store/plan.js +8 -5
- package/dist/store/plan.js.map +1 -1
- package/dist/tools/fs.d.ts +6 -0
- package/dist/tools/fs.js +56 -6
- package/dist/tools/fs.js.map +1 -1
- package/dist/tools/jobs.js +2 -2
- package/dist/tools/jobs.js.map +1 -1
- package/dist/tools/registry.js +189 -67
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/shell.js +2 -2
- package/dist/tools/shell.js.map +1 -1
- package/dist/tui/runtime.d.ts +19 -0
- package/dist/tui/runtime.js +92 -0
- package/dist/tui/runtime.js.map +1 -0
- package/dist/tui/state.js +22 -17
- package/dist/tui/state.js.map +1 -1
- package/dist/tui-v2/actions/action-id.d.ts +14 -0
- package/dist/tui-v2/actions/action-id.js +109 -0
- package/dist/tui-v2/actions/action-id.js.map +1 -0
- package/dist/tui-v2/actions/action-router.d.ts +18 -0
- package/dist/tui-v2/actions/action-router.js +62 -0
- package/dist/tui-v2/actions/action-router.js.map +1 -0
- package/dist/tui-v2/actions/chord-from-key.d.ts +26 -0
- package/dist/tui-v2/actions/chord-from-key.js +45 -0
- package/dist/tui-v2/actions/chord-from-key.js.map +1 -0
- package/dist/tui-v2/actions/keymap.d.ts +24 -0
- package/dist/tui-v2/actions/keymap.js +134 -0
- package/dist/tui-v2/actions/keymap.js.map +1 -0
- package/dist/tui-v2/app/App.d.ts +13 -0
- package/dist/tui-v2/app/App.js +332 -0
- package/dist/tui-v2/app/App.js.map +1 -0
- package/dist/tui-v2/app/command-handlers.d.ts +8 -0
- package/dist/tui-v2/app/command-handlers.js +66 -0
- package/dist/tui-v2/app/command-handlers.js.map +1 -0
- package/dist/tui-v2/app/commands/config-commands.d.ts +14 -0
- package/dist/tui-v2/app/commands/config-commands.js +161 -0
- package/dist/tui-v2/app/commands/config-commands.js.map +1 -0
- package/dist/tui-v2/app/commands/key-commands.d.ts +9 -0
- package/dist/tui-v2/app/commands/key-commands.js +263 -0
- package/dist/tui-v2/app/commands/key-commands.js.map +1 -0
- package/dist/tui-v2/app/commands/picker-commands.d.ts +24 -0
- package/dist/tui-v2/app/commands/picker-commands.js +423 -0
- package/dist/tui-v2/app/commands/picker-commands.js.map +1 -0
- package/dist/tui-v2/app/commands/session-commands.d.ts +19 -0
- package/dist/tui-v2/app/commands/session-commands.js +162 -0
- package/dist/tui-v2/app/commands/session-commands.js.map +1 -0
- package/dist/tui-v2/app/plan-lifecycle.d.ts +22 -0
- package/dist/tui-v2/app/plan-lifecycle.js +95 -0
- package/dist/tui-v2/app/plan-lifecycle.js.map +1 -0
- package/dist/tui-v2/app/providers.d.ts +17 -0
- package/dist/tui-v2/app/providers.js +32 -0
- package/dist/tui-v2/app/providers.js.map +1 -0
- package/dist/tui-v2/bootstrap/capabilities.d.ts +35 -0
- package/dist/tui-v2/bootstrap/capabilities.js +93 -0
- package/dist/tui-v2/bootstrap/capabilities.js.map +1 -0
- package/dist/tui-v2/bootstrap/composition-root.d.ts +85 -0
- package/dist/tui-v2/bootstrap/composition-root.js +131 -0
- package/dist/tui-v2/bootstrap/composition-root.js.map +1 -0
- package/dist/tui-v2/bootstrap/disable-native-selection.d.ts +11 -0
- package/dist/tui-v2/bootstrap/disable-native-selection.js +13 -0
- package/dist/tui-v2/bootstrap/disable-native-selection.js.map +1 -0
- package/dist/tui-v2/bootstrap/lifecycle.d.ts +62 -0
- package/dist/tui-v2/bootstrap/lifecycle.js +123 -0
- package/dist/tui-v2/bootstrap/lifecycle.js.map +1 -0
- package/dist/tui-v2/bootstrap/osc52-clipboard.d.ts +28 -0
- package/dist/tui-v2/bootstrap/osc52-clipboard.js +44 -0
- package/dist/tui-v2/bootstrap/osc52-clipboard.js.map +1 -0
- package/dist/tui-v2/bootstrap/overlay-ports.d.ts +12 -0
- package/dist/{tui/confirm.js → tui-v2/bootstrap/overlay-ports.js} +17 -25
- package/dist/tui-v2/bootstrap/overlay-ports.js.map +1 -0
- package/dist/tui-v2/bootstrap/pager-export.d.ts +24 -0
- package/dist/tui-v2/bootstrap/pager-export.js +87 -0
- package/dist/tui-v2/bootstrap/pager-export.js.map +1 -0
- package/dist/tui-v2/bootstrap/start-tui-v2.d.ts +15 -0
- package/dist/tui-v2/bootstrap/start-tui-v2.js +95 -0
- package/dist/tui-v2/bootstrap/start-tui-v2.js.map +1 -0
- package/dist/tui-v2/bootstrap/ui-selection.d.ts +30 -0
- package/dist/tui-v2/bootstrap/ui-selection.js +59 -0
- package/dist/tui-v2/bootstrap/ui-selection.js.map +1 -0
- package/dist/tui-v2/components/completion/completion-menu.d.ts +23 -0
- package/dist/tui-v2/components/completion/completion-menu.js +84 -0
- package/dist/tui-v2/components/completion/completion-menu.js.map +1 -0
- package/dist/tui-v2/components/jobs/jobs-panel.d.ts +16 -0
- package/dist/tui-v2/components/jobs/jobs-panel.js +92 -0
- package/dist/tui-v2/components/jobs/jobs-panel.js.map +1 -0
- package/dist/tui-v2/components/modal/confirm-modal.d.ts +20 -0
- package/dist/tui-v2/components/modal/confirm-modal.js +110 -0
- package/dist/tui-v2/components/modal/confirm-modal.js.map +1 -0
- package/dist/tui-v2/components/modal/prompt-actions-modal.d.ts +15 -0
- package/dist/tui-v2/components/modal/prompt-actions-modal.js +80 -0
- package/dist/tui-v2/components/modal/prompt-actions-modal.js.map +1 -0
- package/dist/tui-v2/components/modal/secret-modal.d.ts +18 -0
- package/dist/tui-v2/components/modal/secret-modal.js +50 -0
- package/dist/tui-v2/components/modal/secret-modal.js.map +1 -0
- package/dist/tui-v2/components/overlay/overlay-host.d.ts +26 -0
- package/dist/tui-v2/components/overlay/overlay-host.js +43 -0
- package/dist/tui-v2/components/overlay/overlay-host.js.map +1 -0
- package/dist/tui-v2/components/pager/pager.d.ts +20 -0
- package/dist/tui-v2/components/pager/pager.js +396 -0
- package/dist/tui-v2/components/pager/pager.js.map +1 -0
- package/dist/tui-v2/components/picker/picker.d.ts +17 -0
- package/dist/tui-v2/components/picker/picker.js +222 -0
- package/dist/tui-v2/components/picker/picker.js.map +1 -0
- package/dist/tui-v2/components/plan/plan-view.d.ts +20 -0
- package/dist/tui-v2/components/plan/plan-view.js +120 -0
- package/dist/tui-v2/components/plan/plan-view.js.map +1 -0
- package/dist/tui-v2/components/queue/queue-panel.d.ts +23 -0
- package/dist/tui-v2/components/queue/queue-panel.js +58 -0
- package/dist/tui-v2/components/queue/queue-panel.js.map +1 -0
- package/dist/tui-v2/components/status/status-line.d.ts +27 -0
- package/dist/tui-v2/components/status/status-line.js +167 -0
- package/dist/tui-v2/components/status/status-line.js.map +1 -0
- package/dist/tui-v2/components/toast/toast-host.d.ts +15 -0
- package/dist/tui-v2/components/toast/toast-host.js +48 -0
- package/dist/tui-v2/components/toast/toast-host.js.map +1 -0
- package/dist/tui-v2/components/transcript/assistant-message.d.ts +20 -0
- package/dist/tui-v2/components/transcript/assistant-message.js +41 -0
- package/dist/tui-v2/components/transcript/assistant-message.js.map +1 -0
- package/dist/tui-v2/components/transcript/compacted-row.d.ts +19 -0
- package/dist/tui-v2/components/transcript/compacted-row.js +69 -0
- package/dist/tui-v2/components/transcript/compacted-row.js.map +1 -0
- package/dist/tui-v2/components/transcript/intro-card.d.ts +19 -0
- package/dist/tui-v2/components/transcript/intro-card.js +50 -0
- package/dist/tui-v2/components/transcript/intro-card.js.map +1 -0
- package/dist/tui-v2/components/transcript/linkable-text.d.ts +21 -0
- package/dist/tui-v2/components/transcript/linkable-text.js +46 -0
- package/dist/tui-v2/components/transcript/linkable-text.js.map +1 -0
- package/dist/tui-v2/components/transcript/notice-row.d.ts +14 -0
- package/dist/tui-v2/components/transcript/notice-row.js +40 -0
- package/dist/tui-v2/components/transcript/notice-row.js.map +1 -0
- package/dist/tui-v2/components/transcript/search-bar.d.ts +19 -0
- package/dist/tui-v2/components/transcript/search-bar.js +7 -0
- package/dist/tui-v2/components/transcript/search-bar.js.map +1 -0
- package/dist/tui-v2/components/transcript/thinking-block.d.ts +22 -0
- package/dist/tui-v2/components/transcript/thinking-block.js +27 -0
- package/dist/tui-v2/components/transcript/thinking-block.js.map +1 -0
- package/dist/tui-v2/components/transcript/tool-card.d.ts +26 -0
- package/dist/tui-v2/components/transcript/tool-card.js +205 -0
- package/dist/tui-v2/components/transcript/tool-card.js.map +1 -0
- package/dist/tui-v2/components/transcript/transcript-row.d.ts +25 -0
- package/dist/tui-v2/components/transcript/transcript-row.js +37 -0
- package/dist/tui-v2/components/transcript/transcript-row.js.map +1 -0
- package/dist/tui-v2/components/transcript/transcript-scroll-port.d.ts +52 -0
- package/dist/tui-v2/components/transcript/transcript-scroll-port.js +86 -0
- package/dist/tui-v2/components/transcript/transcript-scroll-port.js.map +1 -0
- package/dist/tui-v2/components/transcript/transcript-view.d.ts +24 -0
- package/dist/tui-v2/components/transcript/transcript-view.js +403 -0
- package/dist/tui-v2/components/transcript/transcript-view.js.map +1 -0
- package/dist/tui-v2/components/transcript/use-click-without-drag.d.ts +13 -0
- package/dist/tui-v2/components/transcript/use-click-without-drag.js +35 -0
- package/dist/tui-v2/components/transcript/use-click-without-drag.js.map +1 -0
- package/dist/tui-v2/components/transcript/use-native-selection-copy.d.ts +10 -0
- package/dist/tui-v2/components/transcript/use-native-selection-copy.js +37 -0
- package/dist/tui-v2/components/transcript/use-native-selection-copy.js.map +1 -0
- package/dist/tui-v2/components/transcript/use-transcript-selection.d.ts +23 -0
- package/dist/tui-v2/components/transcript/use-transcript-selection.js +136 -0
- package/dist/tui-v2/components/transcript/use-transcript-selection.js.map +1 -0
- package/dist/tui-v2/components/transcript/user-message.d.ts +10 -0
- package/dist/tui-v2/components/transcript/user-message.js +23 -0
- package/dist/tui-v2/components/transcript/user-message.js.map +1 -0
- package/dist/tui-v2/composer/arrow-intent.d.ts +23 -0
- package/dist/tui-v2/composer/arrow-intent.js +35 -0
- package/dist/tui-v2/composer/arrow-intent.js.map +1 -0
- package/dist/tui-v2/composer/completion.d.ts +37 -0
- package/dist/tui-v2/composer/completion.js +67 -0
- package/dist/tui-v2/composer/completion.js.map +1 -0
- package/dist/tui-v2/composer/composer-editor.d.ts +33 -0
- package/dist/tui-v2/composer/composer-editor.js +620 -0
- package/dist/tui-v2/composer/composer-editor.js.map +1 -0
- package/dist/tui-v2/composer/composer-height.d.ts +21 -0
- package/dist/tui-v2/composer/composer-height.js +29 -0
- package/dist/tui-v2/composer/composer-height.js.map +1 -0
- package/dist/tui-v2/composer/composer-meta.d.ts +9 -0
- package/dist/tui-v2/composer/composer-meta.js +38 -0
- package/dist/tui-v2/composer/composer-meta.js.map +1 -0
- package/dist/tui-v2/composer/history-nav.d.ts +15 -0
- package/dist/tui-v2/composer/history-nav.js +16 -0
- package/dist/tui-v2/composer/history-nav.js.map +1 -0
- package/dist/tui-v2/composer/newline-hint.d.ts +13 -0
- package/dist/tui-v2/composer/newline-hint.js +13 -0
- package/dist/tui-v2/composer/newline-hint.js.map +1 -0
- package/dist/tui-v2/composer/paste-placeholder.d.ts +35 -0
- package/dist/tui-v2/composer/paste-placeholder.js +55 -0
- package/dist/tui-v2/composer/paste-placeholder.js.map +1 -0
- package/dist/tui-v2/composer/prompt-history.d.ts +22 -0
- package/dist/tui-v2/composer/prompt-history.js +51 -0
- package/dist/tui-v2/composer/prompt-history.js.map +1 -0
- package/dist/tui-v2/composer/secret-buffer.d.ts +20 -0
- package/dist/tui-v2/composer/secret-buffer.js +42 -0
- package/dist/tui-v2/composer/secret-buffer.js.map +1 -0
- package/dist/tui-v2/composer/textarea-keybindings.d.ts +22 -0
- package/dist/tui-v2/composer/textarea-keybindings.js +27 -0
- package/dist/tui-v2/composer/textarea-keybindings.js.map +1 -0
- package/dist/tui-v2/controllers/focus-controller.d.ts +30 -0
- package/dist/tui-v2/controllers/focus-controller.js +80 -0
- package/dist/tui-v2/controllers/focus-controller.js.map +1 -0
- package/dist/tui-v2/controllers/overlay-controller.d.ts +99 -0
- package/dist/tui-v2/controllers/overlay-controller.js +195 -0
- package/dist/tui-v2/controllers/overlay-controller.js.map +1 -0
- package/dist/tui-v2/controllers/selection-controller.d.ts +76 -0
- package/dist/tui-v2/controllers/selection-controller.js +238 -0
- package/dist/tui-v2/controllers/selection-controller.js.map +1 -0
- package/dist/tui-v2/controllers/toast-controller.d.ts +35 -0
- package/dist/tui-v2/controllers/toast-controller.js +88 -0
- package/dist/tui-v2/controllers/toast-controller.js.map +1 -0
- package/dist/tui-v2/layout/compute-layout.d.ts +59 -0
- package/dist/tui-v2/layout/compute-layout.js +170 -0
- package/dist/tui-v2/layout/compute-layout.js.map +1 -0
- package/dist/tui-v2/rendering/ansi-to-styled.d.ts +17 -0
- package/dist/tui-v2/rendering/ansi-to-styled.js +217 -0
- package/dist/tui-v2/rendering/ansi-to-styled.js.map +1 -0
- package/dist/tui-v2/rendering/batch-sections.d.ts +57 -0
- package/dist/tui-v2/rendering/batch-sections.js +177 -0
- package/dist/tui-v2/rendering/batch-sections.js.map +1 -0
- package/dist/tui-v2/rendering/link-detector.d.ts +16 -0
- package/dist/tui-v2/rendering/link-detector.js +43 -0
- package/dist/tui-v2/rendering/link-detector.js.map +1 -0
- package/dist/tui-v2/rendering/open-tool-output.d.ts +28 -0
- package/dist/tui-v2/rendering/open-tool-output.js +138 -0
- package/dist/tui-v2/rendering/open-tool-output.js.map +1 -0
- package/dist/tui-v2/rendering/picker-filter.d.ts +22 -0
- package/dist/tui-v2/rendering/picker-filter.js +68 -0
- package/dist/tui-v2/rendering/picker-filter.js.map +1 -0
- package/dist/tui-v2/rendering/plan-view.d.ts +32 -0
- package/dist/tui-v2/rendering/plan-view.js +196 -0
- package/dist/tui-v2/rendering/plan-view.js.map +1 -0
- package/dist/tui-v2/rendering/render-markdown-lines.d.ts +29 -0
- package/dist/tui-v2/rendering/render-markdown-lines.js +46 -0
- package/dist/tui-v2/rendering/render-markdown-lines.js.map +1 -0
- package/dist/tui-v2/rendering/sanitize-display.d.ts +13 -0
- package/dist/tui-v2/rendering/sanitize-display.js +24 -0
- package/dist/tui-v2/rendering/sanitize-display.js.map +1 -0
- package/dist/tui-v2/rendering/strip-tool-surfaces.d.ts +10 -0
- package/dist/tui-v2/rendering/strip-tool-surfaces.js +32 -0
- package/dist/tui-v2/rendering/strip-tool-surfaces.js.map +1 -0
- package/dist/tui-v2/rendering/theme.d.ts +67 -0
- package/dist/tui-v2/rendering/theme.js +74 -0
- package/dist/tui-v2/rendering/theme.js.map +1 -0
- package/dist/tui-v2/rendering/tool-presenter.d.ts +39 -0
- package/dist/tui-v2/rendering/tool-presenter.js +209 -0
- package/dist/tui-v2/rendering/tool-presenter.js.map +1 -0
- package/dist/tui-v2/rendering/transcript-export.d.ts +11 -0
- package/dist/tui-v2/rendering/transcript-export.js +18 -0
- package/dist/tui-v2/rendering/transcript-export.js.map +1 -0
- package/dist/tui-v2/rendering/transcript-semantic.d.ts +17 -0
- package/dist/tui-v2/rendering/transcript-semantic.js +67 -0
- package/dist/tui-v2/rendering/transcript-semantic.js.map +1 -0
- package/dist/tui-v2/state/pager-search.d.ts +22 -0
- package/dist/tui-v2/state/pager-search.js +78 -0
- package/dist/tui-v2/state/pager-search.js.map +1 -0
- package/dist/tui-v2/state/semantic-document.d.ts +34 -0
- package/dist/tui-v2/state/semantic-document.js +201 -0
- package/dist/tui-v2/state/semantic-document.js.map +1 -0
- package/dist/tui-v2/state/transcript-compaction.d.ts +24 -0
- package/dist/tui-v2/state/transcript-compaction.js +79 -0
- package/dist/tui-v2/state/transcript-compaction.js.map +1 -0
- package/dist/tui-v2/state/transcript-hydrate.d.ts +27 -0
- package/dist/tui-v2/state/transcript-hydrate.js +276 -0
- package/dist/tui-v2/state/transcript-hydrate.js.map +1 -0
- package/dist/tui-v2/state/transcript-reducer.d.ts +10 -0
- package/dist/tui-v2/state/transcript-reducer.js +339 -0
- package/dist/tui-v2/state/transcript-reducer.js.map +1 -0
- package/dist/tui-v2/state/transcript-search.d.ts +17 -0
- package/dist/tui-v2/state/transcript-search.js +38 -0
- package/dist/tui-v2/state/transcript-search.js.map +1 -0
- package/dist/tui-v2/state/transcript-store.d.ts +32 -0
- package/dist/tui-v2/state/transcript-store.js +66 -0
- package/dist/tui-v2/state/transcript-store.js.map +1 -0
- package/dist/tui-v2/state/transcript-struct.d.ts +15 -0
- package/dist/tui-v2/state/transcript-struct.js +133 -0
- package/dist/tui-v2/state/transcript-struct.js.map +1 -0
- package/dist/tui-v2/state/transcript-types.d.ts +76 -0
- package/dist/tui-v2/state/transcript-types.js +65 -0
- package/dist/tui-v2/state/transcript-types.js.map +1 -0
- package/dist/tui-v2/state/use-overlay.d.ts +3 -0
- package/dist/tui-v2/state/use-overlay.js +6 -0
- package/dist/tui-v2/state/use-overlay.js.map +1 -0
- package/dist/tui-v2/state/use-plan.d.ts +4 -0
- package/dist/tui-v2/state/use-plan.js +6 -0
- package/dist/tui-v2/state/use-plan.js.map +1 -0
- package/dist/tui-v2/state/use-session-state.d.ts +2 -0
- package/dist/tui-v2/state/use-session-state.js +7 -0
- package/dist/tui-v2/state/use-session-state.js.map +1 -0
- package/dist/tui-v2/state/use-toast.d.ts +3 -0
- package/dist/tui-v2/state/use-toast.js +15 -0
- package/dist/tui-v2/state/use-toast.js.map +1 -0
- package/dist/tui-v2/state/use-transcript-store.d.ts +10 -0
- package/dist/tui-v2/state/use-transcript-store.js +12 -0
- package/dist/tui-v2/state/use-transcript-store.js.map +1 -0
- package/dist/ui/intro-card.js +3 -3
- package/dist/ui/intro-card.js.map +1 -1
- package/dist/ui/intro-header.d.ts +23 -0
- package/dist/ui/intro-header.js +232 -0
- package/dist/ui/intro-header.js.map +1 -0
- package/dist/ui/markdown.js +0 -1
- package/dist/ui/markdown.js.map +1 -1
- package/dist/ui/mentions.d.ts +9 -2
- package/dist/ui/mentions.js +102 -4
- package/dist/ui/mentions.js.map +1 -1
- package/dist/ui/output-pane.js +1 -1
- package/dist/ui/output-pane.js.map +1 -1
- package/dist/ui/plan-pane.d.ts +8 -0
- package/dist/ui/plan-pane.js +38 -0
- package/dist/ui/plan-pane.js.map +1 -1
- package/dist/ui/wordmark.d.ts +5 -0
- package/dist/ui/wordmark.js +6 -1
- package/dist/ui/wordmark.js.map +1 -1
- package/package.json +25 -12
- package/dist/tui/App.d.ts +0 -12
- package/dist/tui/App.js +0 -2174
- package/dist/tui/App.js.map +0 -1
- package/dist/tui/components/ConfirmModal.d.ts +0 -9
- package/dist/tui/components/ConfirmModal.js +0 -55
- package/dist/tui/components/ConfirmModal.js.map +0 -1
- package/dist/tui/components/JobsPanel.d.ts +0 -11
- package/dist/tui/components/JobsPanel.js +0 -55
- package/dist/tui/components/JobsPanel.js.map +0 -1
- package/dist/tui/components/Pager.d.ts +0 -14
- package/dist/tui/components/Pager.js +0 -56
- package/dist/tui/components/Pager.js.map +0 -1
- package/dist/tui/components/PickerPanel.d.ts +0 -15
- package/dist/tui/components/PickerPanel.js +0 -126
- package/dist/tui/components/PickerPanel.js.map +0 -1
- package/dist/tui/components/PlanSidebar.d.ts +0 -6
- package/dist/tui/components/PlanSidebar.js +0 -99
- package/dist/tui/components/PlanSidebar.js.map +0 -1
- package/dist/tui/components/SecretInputPanel.d.ts +0 -7
- package/dist/tui/components/SecretInputPanel.js +0 -38
- package/dist/tui/components/SecretInputPanel.js.map +0 -1
- package/dist/tui/confirm.d.ts +0 -16
- package/dist/tui/confirm.js.map +0 -1
- package/dist/tui/default.d.ts +0 -6
- package/dist/tui/default.js +0 -11
- package/dist/tui/default.js.map +0 -1
- package/dist/tui/hooks/useAgentRunner.d.ts +0 -73
- package/dist/tui/hooks/useAgentRunner.js +0 -277
- package/dist/tui/hooks/useAgentRunner.js.map +0 -1
- package/dist/tui/hooks/useJobs.d.ts +0 -7
- package/dist/tui/hooks/useJobs.js +0 -39
- package/dist/tui/hooks/useJobs.js.map +0 -1
- package/dist/tui/hooks/useSpinner.d.ts +0 -5
- package/dist/tui/hooks/useSpinner.js +0 -21
- package/dist/tui/hooks/useSpinner.js.map +0 -1
- package/dist/tui/hooks/useTerminalSize.d.ts +0 -9
- package/dist/tui/hooks/useTerminalSize.js +0 -26
- package/dist/tui/hooks/useTerminalSize.js.map +0 -1
- package/dist/tui/index.d.ts +0 -10
- package/dist/tui/index.js +0 -37
- package/dist/tui/index.js.map +0 -1
- package/dist/tui/key-intent.d.ts +0 -36
- package/dist/tui/key-intent.js +0 -54
- package/dist/tui/key-intent.js.map +0 -1
- package/dist/tui/mouse.d.ts +0 -8
- package/dist/tui/mouse.js +0 -34
- package/dist/tui/mouse.js.map +0 -1
- package/dist/tui/plan-sidebar-lines.d.ts +0 -2
- package/dist/tui/plan-sidebar-lines.js +0 -140
- package/dist/tui/plan-sidebar-lines.js.map +0 -1
- package/dist/tui/render-lines.d.ts +0 -22
- package/dist/tui/render-lines.js +0 -611
- package/dist/tui/render-lines.js.map +0 -1
package/dist/tui/App.js
DELETED
|
@@ -1,2174 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { Box, Text, useApp, useInput, useStdin } from "ink";
|
|
3
|
-
import chalk from "chalk";
|
|
4
|
-
import { useCallback, useEffect, useMemo, useReducer, useRef, useState, } from "react";
|
|
5
|
-
import { providerIds } from "../types.js";
|
|
6
|
-
import { assertProvider, getProviderInfoText } from "../llm/provider.js";
|
|
7
|
-
import { getProvider } from "../llm/router.js";
|
|
8
|
-
import { envValue, getProviderSecret, getSearchProviderKey, listProviderStatuses, maskSecret, setProviderSecret, setSecret, unsetProviderSecret, } from "../store/keys.js";
|
|
9
|
-
import { setActiveSearchProvider } from "../store/config.js";
|
|
10
|
-
import { searchProviderIds, } from "../tools/web/types.js";
|
|
11
|
-
import { assertSearchProvider, searchProviders, } from "../tools/web/providers/provider.js";
|
|
12
|
-
import "../tools/web/providers/duckduckgo.js";
|
|
13
|
-
import "../tools/web/providers/brave.js";
|
|
14
|
-
import "../tools/web/providers/tavily.js";
|
|
15
|
-
import { modelSupportsThinking, modelSupportsVision, } from "../llm/capabilities.js";
|
|
16
|
-
import { getConfig, getProviderModel, setDefaultMode, setDefaultProvider, setProviderModel, setThinking, updateConfig, } from "../store/config.js";
|
|
17
|
-
import { estimateMessagesTokens } from "../agent/context-manager.js";
|
|
18
|
-
import { clearAllHistory, getSession, listSessions, upsertSession, } from "../store/history.js";
|
|
19
|
-
import { generateSessionTitle } from "../agent/session-title.js";
|
|
20
|
-
import { safeCwd } from "../os/cwd.js";
|
|
21
|
-
import { resolve } from "node:path";
|
|
22
|
-
import { existsSync } from "node:fs";
|
|
23
|
-
import { readFile } from "node:fs/promises";
|
|
24
|
-
import { expandMentions, findFileSuggestions, getMentionQuery, loadImageAttachments, } from "../ui/mentions.js";
|
|
25
|
-
import { deletePlan, loadPlan, savePlan } from "../store/plan.js";
|
|
26
|
-
import { renderPlanDocument } from "../ui/plan-pane.js";
|
|
27
|
-
import { visibleWidth } from "../ui/markdown.js";
|
|
28
|
-
import { getSlashCommandSuggestions, inferProviderForModel, isKnownSlashCommand, knownModels, slashCommands, } from "../repl.js";
|
|
29
|
-
import { initialState, reducer, serializeTranscriptForCompaction, } from "./state.js";
|
|
30
|
-
import { renderTranscriptLines } from "./render-lines.js";
|
|
31
|
-
import { createTuiConfirmPort } from "./confirm.js";
|
|
32
|
-
import { useAgentRunner } from "./hooks/useAgentRunner.js";
|
|
33
|
-
import { useJobs } from "./hooks/useJobs.js";
|
|
34
|
-
import { useSpinner } from "./hooks/useSpinner.js";
|
|
35
|
-
import { useTerminalSize } from "./hooks/useTerminalSize.js";
|
|
36
|
-
import { ConfirmModal } from "./components/ConfirmModal.js";
|
|
37
|
-
import { Pager } from "./components/Pager.js";
|
|
38
|
-
import { JobsPanel } from "./components/JobsPanel.js";
|
|
39
|
-
import { PickerPanel } from "./components/PickerPanel.js";
|
|
40
|
-
import { SecretInputPanel } from "./components/SecretInputPanel.js";
|
|
41
|
-
import { renderPlanSidebarLines } from "./plan-sidebar-lines.js";
|
|
42
|
-
import { clearArtifacts, clearAuditLogs } from "../store/logs.js";
|
|
43
|
-
import { addScopeTargets, clearScope, loadScope, saveScope, } from "../store/scope.js";
|
|
44
|
-
import { formatKeyStatus } from "./format-keys.js";
|
|
45
|
-
import { shouldStoreInPromptHistory } from "./input-history.js";
|
|
46
|
-
import { DISABLE_MOUSE_REPORTING, ENABLE_MOUSE_REPORTING, isMouseReport, mouseWheelDirection, stripMouseReports, } from "./mouse.js";
|
|
47
|
-
import { truncateMiddle, wrapPlainString, relativeTime, shortCwd, } from "./text-format.js";
|
|
48
|
-
import { isComposerNewline, newlineHint } from "./key-intent.js";
|
|
49
|
-
const IMPLEMENT_PROMPT = "I approve the plan. Execute it now in STRICT ORDER. Task 1 (explore) is ALREADY COMPLETE from the planning phase — " +
|
|
50
|
-
"do NOT re-list or re-read the directory. Start with the FIRST pending task that still needs implementation work. " +
|
|
51
|
-
"For each task: call task.update {taskId, state:'in_progress'} → do the real work → VERIFY it succeeded → " +
|
|
52
|
-
"call task.update {taskId, state:'done'}, then move to the NEXT task. " +
|
|
53
|
-
"If a tool call FAILS, mark the task 'failed', fix the problem, and retry. Do NOT mark a task done when it failed. " +
|
|
54
|
-
"Build the project for real with fs.writeMany (create all files in as few calls as possible). " +
|
|
55
|
-
"Do NOT call web.search — you already know everything needed. " +
|
|
56
|
-
"Run real commands (installs, servers, verification) — do not claim anything ran without a successful tool call.";
|
|
57
|
-
const MAX_FILE_SUGGESTIONS = 6;
|
|
58
|
-
function truncateAnsi(line, maxWidth) {
|
|
59
|
-
let visibleLen = 0;
|
|
60
|
-
let result = "";
|
|
61
|
-
let i = 0;
|
|
62
|
-
while (i < line.length) {
|
|
63
|
-
if (line.startsWith("\x1b[", i)) {
|
|
64
|
-
let j = i + 2;
|
|
65
|
-
while (j < line.length && !/[a-zA-Z]/.test(line[j])) {
|
|
66
|
-
j++;
|
|
67
|
-
}
|
|
68
|
-
result += line.slice(i, j + 1);
|
|
69
|
-
i = j + 1;
|
|
70
|
-
}
|
|
71
|
-
else {
|
|
72
|
-
const codePoint = line.codePointAt(i);
|
|
73
|
-
const charLength = codePoint > 0xffff ? 2 : 1;
|
|
74
|
-
const char = line.slice(i, i + charLength);
|
|
75
|
-
const w = Math.max(1, visibleWidth(char));
|
|
76
|
-
if (visibleLen + w > maxWidth) {
|
|
77
|
-
let k = i;
|
|
78
|
-
while (k < line.length) {
|
|
79
|
-
if (line.startsWith("\x1b[", k)) {
|
|
80
|
-
let j = k + 2;
|
|
81
|
-
while (j < line.length && !/[a-zA-Z]/.test(line[j])) {
|
|
82
|
-
j++;
|
|
83
|
-
}
|
|
84
|
-
result += line.slice(k, j + 1);
|
|
85
|
-
k = j + 1;
|
|
86
|
-
}
|
|
87
|
-
else {
|
|
88
|
-
k++;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
break;
|
|
92
|
-
}
|
|
93
|
-
result += char;
|
|
94
|
-
visibleLen += w;
|
|
95
|
-
i += charLength;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
return result;
|
|
99
|
-
}
|
|
100
|
-
/** Pad an ANSI string to a visible width (measuring wide glyphs correctly). */
|
|
101
|
-
function padVisible(line, width) {
|
|
102
|
-
const w = visibleWidth(line);
|
|
103
|
-
return w < width ? line + " ".repeat(width - w) : line;
|
|
104
|
-
}
|
|
105
|
-
export function App({ version, initialMode, provider: initialProvider, initialModel, noHistory = false, }) {
|
|
106
|
-
const { exit } = useApp();
|
|
107
|
-
const { stdin } = useStdin();
|
|
108
|
-
const { columns: cols, rows } = useTerminalSize();
|
|
109
|
-
const [state, dispatch] = useReducer(reducer, undefined, initialState);
|
|
110
|
-
const [mode, setMode] = useState(initialMode);
|
|
111
|
-
const [provider, setProvider] = useState(initialProvider);
|
|
112
|
-
const [model, setModel] = useState(initialModel);
|
|
113
|
-
const [overlay, setOverlay] = useState({ kind: "none" });
|
|
114
|
-
const [input, setInput] = useState("");
|
|
115
|
-
const [cursor, setCursor] = useState(0);
|
|
116
|
-
const [selected, setSelected] = useState(0);
|
|
117
|
-
const [secretRequest, setSecretRequest] = useState();
|
|
118
|
-
const secretResolver = useRef();
|
|
119
|
-
// Provider picker selection is asynchronous when a key is required. Keep
|
|
120
|
-
// repeated Enter events (or a fast double-submit) from opening a second
|
|
121
|
-
// secret prompt before the first activation has finished persisting.
|
|
122
|
-
const providerActivationRef = useRef(undefined);
|
|
123
|
-
const exitRef = useRef(undefined);
|
|
124
|
-
const [scroll, setScroll] = useState(0); // lines scrolled up from bottom
|
|
125
|
-
const [compacting, setCompacting] = useState(false);
|
|
126
|
-
const [compactStartedAt, setCompactStartedAt] = useState(undefined);
|
|
127
|
-
const [mouseMode, setMouseMode] = useState(true);
|
|
128
|
-
const [planPaneVisible, setPlanPaneVisible] = useState(true);
|
|
129
|
-
const history = useRef([]);
|
|
130
|
-
const historyIdx = useRef(-1);
|
|
131
|
-
const historyDraft = useRef("");
|
|
132
|
-
const lastCtrlC = useRef(0);
|
|
133
|
-
const lastDragTime = useRef(0);
|
|
134
|
-
const isCtrlHPressed = useRef(false);
|
|
135
|
-
const jobs = useJobs(overlay.kind === "jobs");
|
|
136
|
-
const spinner = useSpinner(state.status.running || compacting);
|
|
137
|
-
const autosaveTimer = useRef(undefined);
|
|
138
|
-
const latestItems = useRef(state.items);
|
|
139
|
-
latestItems.current = state.items;
|
|
140
|
-
// Tracks model-generated session titles. `titledAtUserCount` records the
|
|
141
|
-
// number of user turns the current title was generated from, so we can
|
|
142
|
-
// refresh the title as the conversation grows past it without regenerating
|
|
143
|
-
// on every single turn. Reset whenever a fresh session starts.
|
|
144
|
-
const titleRef = useRef({
|
|
145
|
-
titledAtUserCount: 0,
|
|
146
|
-
inFlight: false,
|
|
147
|
-
requestId: 0,
|
|
148
|
-
});
|
|
149
|
-
const compactAbortRef = useRef(undefined);
|
|
150
|
-
const cancelCompaction = useCallback(() => {
|
|
151
|
-
if (compactAbortRef.current) {
|
|
152
|
-
compactAbortRef.current.abort();
|
|
153
|
-
compactAbortRef.current = undefined;
|
|
154
|
-
}
|
|
155
|
-
setCompacting(false);
|
|
156
|
-
setCompactStartedAt(undefined);
|
|
157
|
-
}, []);
|
|
158
|
-
useEffect(() => {
|
|
159
|
-
if (!process.stdout.isTTY)
|
|
160
|
-
return;
|
|
161
|
-
process.stdout.write(mouseMode ? ENABLE_MOUSE_REPORTING : DISABLE_MOUSE_REPORTING);
|
|
162
|
-
return () => {
|
|
163
|
-
process.stdout.write(DISABLE_MOUSE_REPORTING);
|
|
164
|
-
};
|
|
165
|
-
}, [mouseMode]);
|
|
166
|
-
// Confirm port → in-app modal
|
|
167
|
-
const confirmController = useMemo(() => createTuiConfirmPort(), []);
|
|
168
|
-
const confirmResolver = useRef(undefined);
|
|
169
|
-
useEffect(() => {
|
|
170
|
-
confirmController.setHandler((req) => new Promise((res) => {
|
|
171
|
-
confirmResolver.current = res;
|
|
172
|
-
dispatch({
|
|
173
|
-
type: "event",
|
|
174
|
-
event: { type: "confirm-request", id: "c", ...req },
|
|
175
|
-
});
|
|
176
|
-
}));
|
|
177
|
-
}, [confirmController]);
|
|
178
|
-
const ctxRef = useRef({ mode, provider, model });
|
|
179
|
-
ctxRef.current = { mode, provider, model };
|
|
180
|
-
const requestSecret = useCallback((request) => new Promise((resolveSecret) => {
|
|
181
|
-
secretResolver.current = resolveSecret;
|
|
182
|
-
setSecretRequest(request);
|
|
183
|
-
}), []);
|
|
184
|
-
const runner = useAgentRunner({
|
|
185
|
-
dispatchEvent: (event) => dispatch({ type: "event", event }),
|
|
186
|
-
confirm: confirmController.port,
|
|
187
|
-
getContext: useCallback(() => ctxRef.current, []),
|
|
188
|
-
requestSecret,
|
|
189
|
-
onSwitchToAgent: useCallback(() => {
|
|
190
|
-
setMode("agent");
|
|
191
|
-
setDefaultMode("agent");
|
|
192
|
-
dispatch({ type: "notice", level: "info", text: "mode → agent" });
|
|
193
|
-
}, []),
|
|
194
|
-
onForceExit: useCallback(() => exitRef.current?.(), []),
|
|
195
|
-
});
|
|
196
|
-
const exitTui = useCallback(() => {
|
|
197
|
-
cancelCompaction();
|
|
198
|
-
runner.abort();
|
|
199
|
-
if (autosaveTimer.current) {
|
|
200
|
-
clearTimeout(autosaveTimer.current);
|
|
201
|
-
autosaveTimer.current = undefined;
|
|
202
|
-
}
|
|
203
|
-
const messages = runner.getMessages();
|
|
204
|
-
if (!noHistory && !getConfig().privateMode && messages.length > 0) {
|
|
205
|
-
// Shutdown must not wait for a slow history backend.
|
|
206
|
-
void upsertSession(runner.getSession().sessionId, messages, undefined, latestItems.current).catch(() => undefined);
|
|
207
|
-
}
|
|
208
|
-
exit();
|
|
209
|
-
}, [cancelCompaction, exit, noHistory, runner]);
|
|
210
|
-
exitRef.current = exitTui;
|
|
211
|
-
// Generate (and later refresh) a concise, model-written title for the
|
|
212
|
-
// active session. Runs only when a real exchange exists, throttles by user
|
|
213
|
-
// turn count, and guards against overlapping requests and session resets.
|
|
214
|
-
const maybeUpdateTitle = useCallback((sessionId) => {
|
|
215
|
-
if (noHistory || getConfig().privateMode)
|
|
216
|
-
return;
|
|
217
|
-
if (titleRef.current.inFlight)
|
|
218
|
-
return;
|
|
219
|
-
const messages = runner.getMessages();
|
|
220
|
-
const userCount = messages.filter((m) => m.role === "user").length;
|
|
221
|
-
const hasAssistant = messages.some((m) => m.role === "assistant" && m.content.trim().length > 0);
|
|
222
|
-
if (userCount === 0 || !hasAssistant)
|
|
223
|
-
return;
|
|
224
|
-
const titledAt = titleRef.current.titledAtUserCount;
|
|
225
|
-
// Title on the first completed exchange, then refresh once two more
|
|
226
|
-
// user turns have accumulated so the name tracks the evolving topic.
|
|
227
|
-
const shouldGenerate = titledAt === 0 || userCount - titledAt >= 2;
|
|
228
|
-
if (!shouldGenerate)
|
|
229
|
-
return;
|
|
230
|
-
titleRef.current.inFlight = true;
|
|
231
|
-
const requestId = ++titleRef.current.requestId;
|
|
232
|
-
const targetCount = userCount;
|
|
233
|
-
const ctx = ctxRef.current;
|
|
234
|
-
// Keep immutable snapshots. The user may start `/new`, resume another
|
|
235
|
-
// session, or exit while the title request is in flight.
|
|
236
|
-
const titleMessages = messages.map((message) => ({ ...message }));
|
|
237
|
-
const titleTranscript = latestItems.current.map((item) => ({ ...item }));
|
|
238
|
-
void generateSessionTitle(messages, {
|
|
239
|
-
provider: ctx.provider,
|
|
240
|
-
model: ctx.model,
|
|
241
|
-
})
|
|
242
|
-
.then((title) => {
|
|
243
|
-
if (titleRef.current.requestId === requestId) {
|
|
244
|
-
titleRef.current.inFlight = false;
|
|
245
|
-
}
|
|
246
|
-
if (!title)
|
|
247
|
-
return;
|
|
248
|
-
if (titleRef.current.requestId === requestId) {
|
|
249
|
-
titleRef.current.titledAtUserCount = targetCount;
|
|
250
|
-
}
|
|
251
|
-
// Persist against the original session, not whatever session is
|
|
252
|
-
// currently visible in the TUI.
|
|
253
|
-
void upsertSession(sessionId, titleMessages, title, titleTranscript).catch(() => undefined);
|
|
254
|
-
})
|
|
255
|
-
.catch(() => {
|
|
256
|
-
if (titleRef.current.requestId === requestId) {
|
|
257
|
-
titleRef.current.inFlight = false;
|
|
258
|
-
}
|
|
259
|
-
});
|
|
260
|
-
}, [noHistory, runner]);
|
|
261
|
-
useEffect(() => {
|
|
262
|
-
if (noHistory || getConfig().privateMode)
|
|
263
|
-
return;
|
|
264
|
-
if (state.items.length === 0)
|
|
265
|
-
return;
|
|
266
|
-
if (autosaveTimer.current)
|
|
267
|
-
clearTimeout(autosaveTimer.current);
|
|
268
|
-
autosaveTimer.current = setTimeout(() => {
|
|
269
|
-
const messages = runner.getMessages();
|
|
270
|
-
// Only persist sessions that contain an actual conversation. This keeps
|
|
271
|
-
// `/new` with no input, or a cleared-then-exited session (notices only,
|
|
272
|
-
// no user turns), out of the history list.
|
|
273
|
-
if (!messages.some((m) => m.role === "user"))
|
|
274
|
-
return;
|
|
275
|
-
const sessionId = runner.getSession().sessionId;
|
|
276
|
-
void upsertSession(sessionId, messages, undefined, state.items).catch(() => undefined);
|
|
277
|
-
maybeUpdateTitle(sessionId);
|
|
278
|
-
}, 250);
|
|
279
|
-
return () => {
|
|
280
|
-
if (autosaveTimer.current)
|
|
281
|
-
clearTimeout(autosaveTimer.current);
|
|
282
|
-
};
|
|
283
|
-
}, [noHistory, runner, state.items, maybeUpdateTitle]);
|
|
284
|
-
useEffect(() => () => {
|
|
285
|
-
if (autosaveTimer.current)
|
|
286
|
-
clearTimeout(autosaveTimer.current);
|
|
287
|
-
const messages = runner.getMessages();
|
|
288
|
-
if (!noHistory &&
|
|
289
|
-
!getConfig().privateMode &&
|
|
290
|
-
messages.some((m) => m.role === "user")) {
|
|
291
|
-
void upsertSession(runner.getSession().sessionId, messages, undefined, latestItems.current).catch(() => undefined);
|
|
292
|
-
}
|
|
293
|
-
}, [noHistory, runner]);
|
|
294
|
-
const startTurn = useCallback(async (display, modelInput, images) => {
|
|
295
|
-
setScroll(0);
|
|
296
|
-
dispatch({ type: "submit", text: display });
|
|
297
|
-
await runner.run(modelInput, images && images.length > 0 ? { images } : undefined);
|
|
298
|
-
}, [runner]);
|
|
299
|
-
const beginTurn = useCallback((rawText) => {
|
|
300
|
-
const vision = modelSupportsVision(provider, model);
|
|
301
|
-
const expansion = expandMentions(rawText, safeCwd(), vision);
|
|
302
|
-
const images = vision ? loadImageAttachments(rawText, safeCwd()) : [];
|
|
303
|
-
const modelInput = expansion.contextBlock
|
|
304
|
-
? `${rawText}\n\n${expansion.contextBlock}`
|
|
305
|
-
: rawText;
|
|
306
|
-
void startTurn(rawText, modelInput, images);
|
|
307
|
-
}, [provider, model, startTurn]);
|
|
308
|
-
useEffect(() => {
|
|
309
|
-
if (!state.status.running &&
|
|
310
|
-
!state.pendingConfirm &&
|
|
311
|
-
state.queued.length > 0 &&
|
|
312
|
-
!runner.isRunning()) {
|
|
313
|
-
const next = state.queued[0];
|
|
314
|
-
dispatch({ type: "dequeue" });
|
|
315
|
-
beginTurn(next);
|
|
316
|
-
}
|
|
317
|
-
}, [
|
|
318
|
-
state.status.running,
|
|
319
|
-
state.pendingConfirm,
|
|
320
|
-
state.queued,
|
|
321
|
-
runner,
|
|
322
|
-
beginTurn,
|
|
323
|
-
]);
|
|
324
|
-
const lastToolOutput = useCallback(() => {
|
|
325
|
-
for (let i = state.items.length - 1; i >= 0; i -= 1) {
|
|
326
|
-
const item = state.items[i];
|
|
327
|
-
if (item.kind === "tool" && item.output)
|
|
328
|
-
return item;
|
|
329
|
-
}
|
|
330
|
-
return undefined;
|
|
331
|
-
}, [state.items]);
|
|
332
|
-
const openToolOutput = useCallback(async (item) => {
|
|
333
|
-
let body = item.output;
|
|
334
|
-
if (item.artifactPath) {
|
|
335
|
-
body = await readFile(item.artifactPath, "utf8").catch(() => item.output);
|
|
336
|
-
}
|
|
337
|
-
setOverlay({ kind: "pager", title: `${item.name} · full output`, body });
|
|
338
|
-
}, []);
|
|
339
|
-
const runImplement = useCallback(async () => {
|
|
340
|
-
const session = runner.getSession();
|
|
341
|
-
const plan = await loadPlan(session.sessionId).catch(() => undefined);
|
|
342
|
-
if (!plan) {
|
|
343
|
-
dispatch({
|
|
344
|
-
type: "notice",
|
|
345
|
-
level: "info",
|
|
346
|
-
text: "no plan to implement — ask clai to plan a multi-step task first",
|
|
347
|
-
});
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
if (plan.tasks.every((t) => t.state === "done")) {
|
|
351
|
-
dispatch({
|
|
352
|
-
type: "notice",
|
|
353
|
-
level: "info",
|
|
354
|
-
text: "this plan is already complete ✓",
|
|
355
|
-
});
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
|
-
plan.status = "approved";
|
|
359
|
-
await savePlan(plan).catch(() => undefined);
|
|
360
|
-
session.planApproved.value = true;
|
|
361
|
-
dispatch({
|
|
362
|
-
type: "notice",
|
|
363
|
-
level: "info",
|
|
364
|
-
text: "✦ plan approved — executing it now",
|
|
365
|
-
});
|
|
366
|
-
void startTurn("/implement", IMPLEMENT_PROMPT);
|
|
367
|
-
}, [runner, startTurn]);
|
|
368
|
-
// After a turn ends, if the agent just created (or revised) a plan that is
|
|
369
|
-
// still a draft awaiting approval, prompt the user with a yes/no modal to
|
|
370
|
-
// implement it now or discard it. /implement and /discard remain available.
|
|
371
|
-
const maybePromptPlanApproval = useCallback(async () => {
|
|
372
|
-
const session = runner.getSession();
|
|
373
|
-
if (session.planApproved.value)
|
|
374
|
-
return;
|
|
375
|
-
if (confirmResolver.current)
|
|
376
|
-
return; // a confirm is already on screen
|
|
377
|
-
const plan = await loadPlan(session.sessionId).catch(() => undefined);
|
|
378
|
-
if (!plan || plan.status !== "draft" || plan.tasks.length === 0)
|
|
379
|
-
return;
|
|
380
|
-
const ok = await new Promise((res) => {
|
|
381
|
-
confirmResolver.current = res;
|
|
382
|
-
dispatch({
|
|
383
|
-
type: "event",
|
|
384
|
-
event: {
|
|
385
|
-
type: "confirm-request",
|
|
386
|
-
id: "plan",
|
|
387
|
-
kind: "plan",
|
|
388
|
-
prompt: `Implement this plan now? "${plan.goal}" — ${plan.tasks.length} task(s). (Y to implement · N to discard)`,
|
|
389
|
-
},
|
|
390
|
-
});
|
|
391
|
-
});
|
|
392
|
-
if (ok) {
|
|
393
|
-
await runImplement();
|
|
394
|
-
}
|
|
395
|
-
else {
|
|
396
|
-
await deletePlan(session.sessionId).catch(() => undefined);
|
|
397
|
-
session.planApproved.value = false;
|
|
398
|
-
dispatch({
|
|
399
|
-
type: "notice",
|
|
400
|
-
level: "info",
|
|
401
|
-
text: `plan discarded · ${plan.goal}`,
|
|
402
|
-
});
|
|
403
|
-
}
|
|
404
|
-
}, [runner, runImplement]);
|
|
405
|
-
// Detect a turn finishing (running true → false) and offer plan approval.
|
|
406
|
-
const prevRunningRef = useRef(false);
|
|
407
|
-
useEffect(() => {
|
|
408
|
-
const wasRunning = prevRunningRef.current;
|
|
409
|
-
prevRunningRef.current = state.status.running;
|
|
410
|
-
if (wasRunning && !state.status.running) {
|
|
411
|
-
void maybePromptPlanApproval();
|
|
412
|
-
}
|
|
413
|
-
}, [state.status.running, maybePromptPlanApproval]);
|
|
414
|
-
const chooseModel = useCallback(async () => {
|
|
415
|
-
const providerImpl = getProvider(provider);
|
|
416
|
-
let models = [];
|
|
417
|
-
if (providerImpl.listModels) {
|
|
418
|
-
try {
|
|
419
|
-
const secret = await getProviderSecret(provider);
|
|
420
|
-
models = await providerImpl.listModels({
|
|
421
|
-
apiKey: secret.value,
|
|
422
|
-
baseUrl: provider === "ollama" ? secret.value : undefined,
|
|
423
|
-
});
|
|
424
|
-
}
|
|
425
|
-
catch (error) {
|
|
426
|
-
dispatch({
|
|
427
|
-
type: "notice",
|
|
428
|
-
level: "warn",
|
|
429
|
-
text: `could not refresh ${provider} models: ${error instanceof Error ? error.message : String(error)} · showing known models`,
|
|
430
|
-
});
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
if (models.length === 0)
|
|
434
|
-
models = knownModels[provider] ?? [];
|
|
435
|
-
if (!models.includes(model))
|
|
436
|
-
models.unshift(model);
|
|
437
|
-
setOverlay({
|
|
438
|
-
kind: "picker",
|
|
439
|
-
title: `Models · ${provider}`,
|
|
440
|
-
options: models.map((value) => ({
|
|
441
|
-
value,
|
|
442
|
-
label: value,
|
|
443
|
-
active: value === model,
|
|
444
|
-
})),
|
|
445
|
-
onSelect: (value) => {
|
|
446
|
-
setModel(value);
|
|
447
|
-
setProviderModel(provider, value);
|
|
448
|
-
setOverlay({ kind: "none" });
|
|
449
|
-
// If the selected model belongs to a different provider, auto-switch
|
|
450
|
-
// so requests are sent to the right endpoint (e.g. minimaxai/minimax-m3
|
|
451
|
-
// is an NVIDIA model and must not be sent to the Gemini API).
|
|
452
|
-
const inferred = inferProviderForModel(value);
|
|
453
|
-
if (inferred && inferred !== provider) {
|
|
454
|
-
void activateProvider(assertProvider(inferred));
|
|
455
|
-
}
|
|
456
|
-
else {
|
|
457
|
-
dispatch({ type: "notice", level: "info", text: `model → ${value}` });
|
|
458
|
-
}
|
|
459
|
-
},
|
|
460
|
-
});
|
|
461
|
-
}, [model, provider]);
|
|
462
|
-
const activateProvider = useCallback(async (next) => {
|
|
463
|
-
if (providerActivationRef.current)
|
|
464
|
-
return;
|
|
465
|
-
providerActivationRef.current = next;
|
|
466
|
-
// Unmount the picker before opening the secret prompt. This makes the
|
|
467
|
-
// prompt the sole owner of stdin while activation is in progress.
|
|
468
|
-
setOverlay({ kind: "none" });
|
|
469
|
-
try {
|
|
470
|
-
const configured = next === "ollama" ||
|
|
471
|
-
Boolean(envValue(next)) ||
|
|
472
|
-
Boolean((await getProviderSecret(next)).value);
|
|
473
|
-
if (!configured) {
|
|
474
|
-
const key = await requestSecret({
|
|
475
|
-
title: `${next} API key`,
|
|
476
|
-
prompt: `No API key is configured for ${next}. Enter it now to activate this provider.`,
|
|
477
|
-
});
|
|
478
|
-
if (!key) {
|
|
479
|
-
dispatch({
|
|
480
|
-
type: "notice",
|
|
481
|
-
level: "warn",
|
|
482
|
-
text: `provider unchanged · ${next} needs an API key`,
|
|
483
|
-
});
|
|
484
|
-
return;
|
|
485
|
-
}
|
|
486
|
-
const trimmedKey = key.trim();
|
|
487
|
-
if (!getProvider(next).validateKey(trimmedKey)) {
|
|
488
|
-
dispatch({
|
|
489
|
-
type: "notice",
|
|
490
|
-
level: "warn",
|
|
491
|
-
text: `invalid API key format for ${next}`,
|
|
492
|
-
});
|
|
493
|
-
return;
|
|
494
|
-
}
|
|
495
|
-
await setProviderSecret(next, trimmedKey);
|
|
496
|
-
const saved = await getProviderSecret(next);
|
|
497
|
-
if (saved.value !== trimmedKey) {
|
|
498
|
-
throw new Error(`could not persist the ${next} API key; provider was not changed`);
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
const nextModel = getProviderModel(next);
|
|
502
|
-
setDefaultProvider(next);
|
|
503
|
-
setProvider(next);
|
|
504
|
-
setModel(nextModel);
|
|
505
|
-
dispatch({
|
|
506
|
-
type: "notice",
|
|
507
|
-
level: "info",
|
|
508
|
-
text: `provider → ${next} · model → ${nextModel}`,
|
|
509
|
-
});
|
|
510
|
-
}
|
|
511
|
-
catch (error) {
|
|
512
|
-
dispatch({
|
|
513
|
-
type: "notice",
|
|
514
|
-
level: "warn",
|
|
515
|
-
text: error instanceof Error ? error.message : String(error),
|
|
516
|
-
});
|
|
517
|
-
}
|
|
518
|
-
finally {
|
|
519
|
-
providerActivationRef.current = undefined;
|
|
520
|
-
}
|
|
521
|
-
}, [requestSecret]);
|
|
522
|
-
const chooseProvider = useCallback(() => {
|
|
523
|
-
setOverlay({
|
|
524
|
-
kind: "picker",
|
|
525
|
-
title: "Providers",
|
|
526
|
-
searchDescription: false,
|
|
527
|
-
options: providerIds.map((value) => ({
|
|
528
|
-
value,
|
|
529
|
-
label: value,
|
|
530
|
-
description: getProviderModel(value),
|
|
531
|
-
active: value === provider,
|
|
532
|
-
})),
|
|
533
|
-
onSelect: (value) => {
|
|
534
|
-
const next = assertProvider(value);
|
|
535
|
-
void activateProvider(next);
|
|
536
|
-
},
|
|
537
|
-
});
|
|
538
|
-
}, [activateProvider, provider]);
|
|
539
|
-
const activateSearchProvider = useCallback(async (next) => {
|
|
540
|
-
const adapter = searchProviders[next];
|
|
541
|
-
if (adapter?.needsApiKey) {
|
|
542
|
-
const current = await getSearchProviderKey(next);
|
|
543
|
-
if (!current.value) {
|
|
544
|
-
const key = await requestSecret({
|
|
545
|
-
title: `${next} search API key`,
|
|
546
|
-
prompt: `No API key is configured for ${adapter.displayName}. Enter it now to use this search provider.`,
|
|
547
|
-
});
|
|
548
|
-
if (!key) {
|
|
549
|
-
dispatch({
|
|
550
|
-
type: "notice",
|
|
551
|
-
level: "warn",
|
|
552
|
-
text: `search provider unchanged · ${next} needs an API key`,
|
|
553
|
-
});
|
|
554
|
-
return;
|
|
555
|
-
}
|
|
556
|
-
await setSecret("search", next, key);
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
setActiveSearchProvider(next);
|
|
560
|
-
setOverlay({ kind: "none" });
|
|
561
|
-
dispatch({
|
|
562
|
-
type: "notice",
|
|
563
|
-
level: "info",
|
|
564
|
-
text: `search provider → ${next}`,
|
|
565
|
-
});
|
|
566
|
-
}, [requestSecret]);
|
|
567
|
-
const chooseSearchProvider = useCallback(() => {
|
|
568
|
-
const active = getConfig().activeSearchProvider;
|
|
569
|
-
setOverlay({
|
|
570
|
-
kind: "picker",
|
|
571
|
-
title: "Search providers",
|
|
572
|
-
options: searchProviderIds.map((id) => {
|
|
573
|
-
const adapter = searchProviders[id];
|
|
574
|
-
return {
|
|
575
|
-
value: id,
|
|
576
|
-
label: id === active ? `${id} · active` : id,
|
|
577
|
-
description: adapter?.needsApiKey
|
|
578
|
-
? `${adapter.displayName} · API key required`
|
|
579
|
-
: `${adapter?.displayName ?? id} · keyless`,
|
|
580
|
-
};
|
|
581
|
-
}),
|
|
582
|
-
onSelect: (value) => {
|
|
583
|
-
void activateSearchProvider(assertSearchProvider(value));
|
|
584
|
-
},
|
|
585
|
-
});
|
|
586
|
-
}, [activateSearchProvider]);
|
|
587
|
-
const setReasoning = useCallback((value) => {
|
|
588
|
-
if (value === "off" || value === "none")
|
|
589
|
-
setThinking({ enabled: false });
|
|
590
|
-
else
|
|
591
|
-
setThinking({ enabled: true, effort: value });
|
|
592
|
-
dispatch({
|
|
593
|
-
type: "notice",
|
|
594
|
-
level: "info",
|
|
595
|
-
text: value === "off" || value === "none"
|
|
596
|
-
? "thinking → off"
|
|
597
|
-
: `thinking → ${value}`,
|
|
598
|
-
});
|
|
599
|
-
}, []);
|
|
600
|
-
const chooseReasoning = useCallback(() => {
|
|
601
|
-
const current = getConfig().thinking;
|
|
602
|
-
const descriptions = {
|
|
603
|
-
off: "disable reasoning",
|
|
604
|
-
minimal: "lowest latency",
|
|
605
|
-
low: "light reasoning",
|
|
606
|
-
medium: "balanced",
|
|
607
|
-
high: "deep reasoning",
|
|
608
|
-
xhigh: "maximum depth",
|
|
609
|
-
};
|
|
610
|
-
setOverlay({
|
|
611
|
-
kind: "picker",
|
|
612
|
-
title: `Reasoning · ${modelSupportsThinking(provider, model) ? "supported" : "model may ignore it"}`,
|
|
613
|
-
options: Object.entries(descriptions).map(([value, description]) => ({
|
|
614
|
-
value,
|
|
615
|
-
label: value,
|
|
616
|
-
description,
|
|
617
|
-
active: value === (current.enabled ? current.effort : "off"),
|
|
618
|
-
})),
|
|
619
|
-
onSelect: (value) => {
|
|
620
|
-
setReasoning(value);
|
|
621
|
-
setOverlay({ kind: "none" });
|
|
622
|
-
},
|
|
623
|
-
});
|
|
624
|
-
}, [model, provider, setReasoning]);
|
|
625
|
-
const handleLocalSlash = useCallback((text) => {
|
|
626
|
-
const [cmd, ...rest] = text.trim().split(/\s+/);
|
|
627
|
-
const arg = rest.join(" ").trim();
|
|
628
|
-
const info = (t) => dispatch({ type: "notice", level: "info", text: t });
|
|
629
|
-
const warn = (t) => dispatch({ type: "notice", level: "warn", text: t });
|
|
630
|
-
switch (cmd) {
|
|
631
|
-
case "/ask":
|
|
632
|
-
setMode("ask");
|
|
633
|
-
setDefaultMode("ask");
|
|
634
|
-
info("mode → ask");
|
|
635
|
-
return true;
|
|
636
|
-
case "/agent":
|
|
637
|
-
setMode("agent");
|
|
638
|
-
setDefaultMode("agent");
|
|
639
|
-
info("mode → agent");
|
|
640
|
-
return true;
|
|
641
|
-
case "/clear":
|
|
642
|
-
cancelCompaction();
|
|
643
|
-
runner.reset();
|
|
644
|
-
dispatch({ type: "reset" });
|
|
645
|
-
info("context cleared");
|
|
646
|
-
return true;
|
|
647
|
-
case "/new":
|
|
648
|
-
cancelCompaction();
|
|
649
|
-
runner.abort();
|
|
650
|
-
if (autosaveTimer.current) {
|
|
651
|
-
clearTimeout(autosaveTimer.current);
|
|
652
|
-
autosaveTimer.current = undefined;
|
|
653
|
-
}
|
|
654
|
-
const messages = runner.getMessages();
|
|
655
|
-
if (!noHistory &&
|
|
656
|
-
!getConfig().privateMode &&
|
|
657
|
-
messages.some((m) => m.role === "user")) {
|
|
658
|
-
// Persist in the background; the new session must be interactive
|
|
659
|
-
// immediately even if the history backend is slow or locked.
|
|
660
|
-
void upsertSession(runner.getSession().sessionId, messages, undefined, latestItems.current).catch(() => undefined);
|
|
661
|
-
}
|
|
662
|
-
runner.reset();
|
|
663
|
-
titleRef.current = {
|
|
664
|
-
titledAtUserCount: 0,
|
|
665
|
-
inFlight: false,
|
|
666
|
-
requestId: titleRef.current.requestId + 1,
|
|
667
|
-
};
|
|
668
|
-
dispatch({ type: "reset" });
|
|
669
|
-
info("fresh session started");
|
|
670
|
-
return true;
|
|
671
|
-
case "/clean":
|
|
672
|
-
cancelCompaction();
|
|
673
|
-
runner.reset();
|
|
674
|
-
titleRef.current = {
|
|
675
|
-
titledAtUserCount: 0,
|
|
676
|
-
inFlight: false,
|
|
677
|
-
requestId: titleRef.current.requestId + 1,
|
|
678
|
-
};
|
|
679
|
-
dispatch({ type: "reset" });
|
|
680
|
-
info("fresh session started");
|
|
681
|
-
return true;
|
|
682
|
-
case "/think":
|
|
683
|
-
case "/thinking":
|
|
684
|
-
dispatch({ type: "toggle-thinking" });
|
|
685
|
-
return true;
|
|
686
|
-
case "/implement":
|
|
687
|
-
if (runner.isRunning())
|
|
688
|
-
warn("a turn is already running");
|
|
689
|
-
else
|
|
690
|
-
void runImplement();
|
|
691
|
-
return true;
|
|
692
|
-
case "/plan":
|
|
693
|
-
void (async () => {
|
|
694
|
-
const plan = await loadPlan(runner.getSession().sessionId).catch(() => undefined);
|
|
695
|
-
if (!plan)
|
|
696
|
-
info("no active plan yet");
|
|
697
|
-
else
|
|
698
|
-
setOverlay({
|
|
699
|
-
kind: "pager",
|
|
700
|
-
title: "Plan",
|
|
701
|
-
body: renderPlanDocument(plan),
|
|
702
|
-
});
|
|
703
|
-
})();
|
|
704
|
-
return true;
|
|
705
|
-
case "/jobs":
|
|
706
|
-
setOverlay({ kind: "jobs" });
|
|
707
|
-
return true;
|
|
708
|
-
case "/output": {
|
|
709
|
-
const outputs = state.items.filter((item) => item.kind === "tool" && Boolean(item.output));
|
|
710
|
-
if (!arg) {
|
|
711
|
-
if (outputs.length)
|
|
712
|
-
dispatch({ type: "toggle-output" });
|
|
713
|
-
else
|
|
714
|
-
info("no tool output yet");
|
|
715
|
-
return true;
|
|
716
|
-
}
|
|
717
|
-
if (arg === "list" || arg === "ls") {
|
|
718
|
-
info(outputs.length
|
|
719
|
-
? outputs.map((item) => `${item.id} · ${item.name}`).join("\n")
|
|
720
|
-
: "no tool output yet");
|
|
721
|
-
return true;
|
|
722
|
-
}
|
|
723
|
-
const selectedOutput = arg && arg !== "last"
|
|
724
|
-
? outputs.find((item) => item.id === arg)
|
|
725
|
-
: lastToolOutput();
|
|
726
|
-
if (!selectedOutput)
|
|
727
|
-
info(arg ? `no tool output: ${arg}` : "no tool output yet");
|
|
728
|
-
else
|
|
729
|
-
void openToolOutput(selectedOutput);
|
|
730
|
-
return true;
|
|
731
|
-
}
|
|
732
|
-
case "/model":
|
|
733
|
-
if (!arg || arg === "list" || arg === "ls") {
|
|
734
|
-
void chooseModel();
|
|
735
|
-
return true;
|
|
736
|
-
}
|
|
737
|
-
{
|
|
738
|
-
const options = knownModels[provider] ?? [];
|
|
739
|
-
const index = Number.parseInt(arg, 10);
|
|
740
|
-
const nextModel = Number.isInteger(index) && index >= 1 && index <= options.length
|
|
741
|
-
? options[index - 1]
|
|
742
|
-
: arg;
|
|
743
|
-
setModel(nextModel);
|
|
744
|
-
setProviderModel(provider, nextModel);
|
|
745
|
-
// If the model lives under a different provider's list, switch to
|
|
746
|
-
// that provider so the request goes to the right endpoint.
|
|
747
|
-
const inferred = inferProviderForModel(nextModel);
|
|
748
|
-
if (inferred && inferred !== provider) {
|
|
749
|
-
void activateProvider(assertProvider(inferred));
|
|
750
|
-
}
|
|
751
|
-
else {
|
|
752
|
-
info(`model → ${nextModel}`);
|
|
753
|
-
}
|
|
754
|
-
return true;
|
|
755
|
-
}
|
|
756
|
-
case "/provider":
|
|
757
|
-
case "/use":
|
|
758
|
-
if (!arg) {
|
|
759
|
-
chooseProvider();
|
|
760
|
-
return true;
|
|
761
|
-
}
|
|
762
|
-
try {
|
|
763
|
-
const next = assertProvider(arg);
|
|
764
|
-
void activateProvider(next);
|
|
765
|
-
}
|
|
766
|
-
catch {
|
|
767
|
-
warn(`unknown provider: ${arg}`);
|
|
768
|
-
}
|
|
769
|
-
return true;
|
|
770
|
-
case "/search":
|
|
771
|
-
case "/search-provider":
|
|
772
|
-
if (!arg || arg === "list" || arg === "ls") {
|
|
773
|
-
chooseSearchProvider();
|
|
774
|
-
return true;
|
|
775
|
-
}
|
|
776
|
-
try {
|
|
777
|
-
const next = assertSearchProvider(arg);
|
|
778
|
-
void activateSearchProvider(next);
|
|
779
|
-
}
|
|
780
|
-
catch {
|
|
781
|
-
warn(`unknown search provider: ${arg}`);
|
|
782
|
-
}
|
|
783
|
-
return true;
|
|
784
|
-
case "/mouse": {
|
|
785
|
-
const normalized = arg.toLowerCase();
|
|
786
|
-
if (!normalized) {
|
|
787
|
-
info(`mouse=${mouseMode ? "on" : "off"} · on: touchpad scrolls chat; off: native select/copy`);
|
|
788
|
-
return true;
|
|
789
|
-
}
|
|
790
|
-
if (/^(on|true|1|enable)$/i.test(normalized)) {
|
|
791
|
-
setMouseMode(true);
|
|
792
|
-
info("mouse=on · touchpad scrolls chat; use Option/Shift selection if your terminal requires it");
|
|
793
|
-
return true;
|
|
794
|
-
}
|
|
795
|
-
if (/^(off|false|0|disable)$/i.test(normalized)) {
|
|
796
|
-
setMouseMode(false);
|
|
797
|
-
info("mouse=off · native select/copy restored; use PageUp/PageDown/Ctrl+U/Ctrl+D or j/k to scroll chat");
|
|
798
|
-
return true;
|
|
799
|
-
}
|
|
800
|
-
warn("usage: /mouse [on|off]");
|
|
801
|
-
return true;
|
|
802
|
-
}
|
|
803
|
-
case "/variants":
|
|
804
|
-
case "/reasoning": {
|
|
805
|
-
if (!arg) {
|
|
806
|
-
chooseReasoning();
|
|
807
|
-
return true;
|
|
808
|
-
}
|
|
809
|
-
const value = arg.toLowerCase();
|
|
810
|
-
if (/^(on|enable|true)$/.test(value)) {
|
|
811
|
-
setThinking({ enabled: true });
|
|
812
|
-
info(`thinking → ${getConfig().thinking.effort}`);
|
|
813
|
-
}
|
|
814
|
-
else if (["off", "none", "disable", "false"].includes(value)) {
|
|
815
|
-
setReasoning("off");
|
|
816
|
-
}
|
|
817
|
-
else if (["minimal", "low", "medium", "high", "xhigh"].includes(value)) {
|
|
818
|
-
setReasoning(value);
|
|
819
|
-
}
|
|
820
|
-
else
|
|
821
|
-
warn("usage: /variants [on|off|minimal|low|medium|high|xhigh]");
|
|
822
|
-
return true;
|
|
823
|
-
}
|
|
824
|
-
case "/cwd": {
|
|
825
|
-
if (!arg) {
|
|
826
|
-
info(`cwd: ${safeCwd()}`);
|
|
827
|
-
return true;
|
|
828
|
-
}
|
|
829
|
-
const target = resolve(safeCwd(), arg);
|
|
830
|
-
if (!existsSync(target)) {
|
|
831
|
-
warn(`no such directory: ${target}`);
|
|
832
|
-
return true;
|
|
833
|
-
}
|
|
834
|
-
try {
|
|
835
|
-
process.chdir(target);
|
|
836
|
-
info(`cwd → ${target}`);
|
|
837
|
-
}
|
|
838
|
-
catch (e) {
|
|
839
|
-
warn(`could not chdir: ${e instanceof Error ? e.message : String(e)}`);
|
|
840
|
-
}
|
|
841
|
-
return true;
|
|
842
|
-
}
|
|
843
|
-
case "/allow":
|
|
844
|
-
if (!arg || arg === "list" || arg === "ls") {
|
|
845
|
-
const list = [...runner.getSession().allow];
|
|
846
|
-
info(list.length
|
|
847
|
-
? `allowed: ${list.join(", ")}`
|
|
848
|
-
: "no session allowances");
|
|
849
|
-
return true;
|
|
850
|
-
}
|
|
851
|
-
runner.getSession().allow.add(arg);
|
|
852
|
-
info(`allowed for session: ${arg}`);
|
|
853
|
-
return true;
|
|
854
|
-
case "/disallow":
|
|
855
|
-
if (arg)
|
|
856
|
-
runner.getSession().allow.delete(arg);
|
|
857
|
-
info(arg ? `disallowed: ${arg}` : "usage: /disallow <tool>");
|
|
858
|
-
return true;
|
|
859
|
-
case "/context": {
|
|
860
|
-
const msgs = runner.getMessages();
|
|
861
|
-
info(`context: ${msgs.length} messages · ~${estimateMessagesTokens(msgs)} tokens`);
|
|
862
|
-
return true;
|
|
863
|
-
}
|
|
864
|
-
case "/compact": {
|
|
865
|
-
if (runner.isRunning()) {
|
|
866
|
-
warn("wait for the current operation to finish");
|
|
867
|
-
return true;
|
|
868
|
-
}
|
|
869
|
-
cancelCompaction();
|
|
870
|
-
const ac = new AbortController();
|
|
871
|
-
compactAbortRef.current = ac;
|
|
872
|
-
setCompacting(true);
|
|
873
|
-
setCompactStartedAt(Date.now());
|
|
874
|
-
info("compacting conversation…");
|
|
875
|
-
const fullSession = serializeTranscriptForCompaction(state.items);
|
|
876
|
-
void runner
|
|
877
|
-
.compact(fullSession, 2, ac.signal)
|
|
878
|
-
.then((result) => {
|
|
879
|
-
if (ac.signal.aborted)
|
|
880
|
-
return;
|
|
881
|
-
if (!result.summarized || result.after === result.before) {
|
|
882
|
-
info("nothing to compact yet — more conversation is needed");
|
|
883
|
-
return;
|
|
884
|
-
}
|
|
885
|
-
const memoMsg = result.messages.find((m) => m.role === "system" &&
|
|
886
|
-
m.content.startsWith("Session memory")) ??
|
|
887
|
-
result.messages.find((m, i) => i > 0 && m.role === "system");
|
|
888
|
-
const summary = memoMsg ? memoMsg.content : "Compacted context";
|
|
889
|
-
dispatch({ type: "compacted", summary, keepRecent: 2 });
|
|
890
|
-
const freed = Math.max(0, result.beforeTokens - result.afterTokens);
|
|
891
|
-
const pct = result.beforeTokens > 0
|
|
892
|
-
? Math.round((freed / result.beforeTokens) * 100)
|
|
893
|
-
: 0;
|
|
894
|
-
info(`context compacted — earlier turns summarized into a memory · ` +
|
|
895
|
-
`freed ~${freed.toLocaleString()} tokens (${pct}% smaller, ` +
|
|
896
|
-
`~${result.beforeTokens.toLocaleString()} → ~${result.afterTokens.toLocaleString()} est.)`);
|
|
897
|
-
})
|
|
898
|
-
.catch((error) => {
|
|
899
|
-
if (ac.signal.aborted ||
|
|
900
|
-
(error instanceof Error && error.name === "AbortError")) {
|
|
901
|
-
info("compaction cancelled");
|
|
902
|
-
}
|
|
903
|
-
else {
|
|
904
|
-
warn(`compaction failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
905
|
-
}
|
|
906
|
-
})
|
|
907
|
-
.finally(() => {
|
|
908
|
-
if (compactAbortRef.current === ac) {
|
|
909
|
-
compactAbortRef.current = undefined;
|
|
910
|
-
setCompacting(false);
|
|
911
|
-
setCompactStartedAt(undefined);
|
|
912
|
-
}
|
|
913
|
-
});
|
|
914
|
-
return true;
|
|
915
|
-
}
|
|
916
|
-
case "/save":
|
|
917
|
-
void (async () => {
|
|
918
|
-
const msgs = runner.getMessages();
|
|
919
|
-
if (msgs.length === 0) {
|
|
920
|
-
info("nothing to save yet");
|
|
921
|
-
return;
|
|
922
|
-
}
|
|
923
|
-
// Continuous autosave already persists this conversation under
|
|
924
|
-
// its live sessionId — upsert that same row (optionally renaming
|
|
925
|
-
// it) instead of minting a separate duplicate entry.
|
|
926
|
-
const rec = await upsertSession(runner.getSession().sessionId, msgs, arg || undefined, state.items).catch(() => undefined);
|
|
927
|
-
info(rec ? `saved session ${rec.id}` : "save failed");
|
|
928
|
-
})();
|
|
929
|
-
return true;
|
|
930
|
-
case "/history":
|
|
931
|
-
void (async () => {
|
|
932
|
-
const sessions = await listSessions(50);
|
|
933
|
-
const currentMessages = runner.getMessages();
|
|
934
|
-
if (sessions.length === 0 && currentMessages.length === 0) {
|
|
935
|
-
info("no session history yet — start chatting to create one");
|
|
936
|
-
return;
|
|
937
|
-
}
|
|
938
|
-
setOverlay({
|
|
939
|
-
kind: "picker",
|
|
940
|
-
title: "Session history",
|
|
941
|
-
twoLine: true,
|
|
942
|
-
options: [
|
|
943
|
-
// Always show where the user currently is, even a brand-new
|
|
944
|
-
// /new session with nothing sent yet — otherwise the picker
|
|
945
|
-
// can look like it's viewing whichever past session happens
|
|
946
|
-
// to sort first, with no indication of "you are here".
|
|
947
|
-
{
|
|
948
|
-
value: "__current__",
|
|
949
|
-
label: "Current session",
|
|
950
|
-
description: currentMessages.length
|
|
951
|
-
? `active now · ${currentMessages.length} messages`
|
|
952
|
-
: "active now · new, empty session",
|
|
953
|
-
active: true,
|
|
954
|
-
},
|
|
955
|
-
...sessions.map((session) => {
|
|
956
|
-
const date = session.updatedAt ?? session.createdAt;
|
|
957
|
-
const when = relativeTime(date);
|
|
958
|
-
const stamp = date.slice(0, 16).replace("T", " ");
|
|
959
|
-
const count = session.transcript?.length ?? session.messages.length;
|
|
960
|
-
const where = shortCwd(session.cwd);
|
|
961
|
-
const meta = [
|
|
962
|
-
when ? `${when} · ${stamp}` : stamp,
|
|
963
|
-
`${count} msgs`,
|
|
964
|
-
where,
|
|
965
|
-
]
|
|
966
|
-
.filter(Boolean)
|
|
967
|
-
.join(" · ");
|
|
968
|
-
return {
|
|
969
|
-
value: session.id,
|
|
970
|
-
label: session.name ?? session.id,
|
|
971
|
-
description: meta,
|
|
972
|
-
};
|
|
973
|
-
}),
|
|
974
|
-
],
|
|
975
|
-
onSelect: (id) => {
|
|
976
|
-
void (async () => {
|
|
977
|
-
if (id === "__current__") {
|
|
978
|
-
setOverlay({ kind: "none" });
|
|
979
|
-
info("showing current session");
|
|
980
|
-
return;
|
|
981
|
-
}
|
|
982
|
-
const session = await getSession(id);
|
|
983
|
-
if (!session) {
|
|
984
|
-
warn("session not found");
|
|
985
|
-
return;
|
|
986
|
-
}
|
|
987
|
-
cancelCompaction();
|
|
988
|
-
runner.setMessages(session.messages, session.id);
|
|
989
|
-
// Keep the resumed session's existing title; only refresh
|
|
990
|
-
// once the user adds new turns on top of it.
|
|
991
|
-
titleRef.current = {
|
|
992
|
-
titledAtUserCount: session.messages.filter((m) => m.role === "user").length,
|
|
993
|
-
inFlight: false,
|
|
994
|
-
requestId: titleRef.current.requestId + 1,
|
|
995
|
-
};
|
|
996
|
-
setOverlay({ kind: "none" });
|
|
997
|
-
dispatch({
|
|
998
|
-
type: "load-history",
|
|
999
|
-
messages: session.messages,
|
|
1000
|
-
transcript: session.transcript,
|
|
1001
|
-
});
|
|
1002
|
-
setScroll(0);
|
|
1003
|
-
info(`session resumed · ${session.transcript?.length ?? session.messages.length} items`);
|
|
1004
|
-
})();
|
|
1005
|
-
},
|
|
1006
|
-
});
|
|
1007
|
-
})();
|
|
1008
|
-
return true;
|
|
1009
|
-
case "/reset":
|
|
1010
|
-
void clearAllHistory().then((result) => info(`history cleared · ${result.detail || "ok"}`));
|
|
1011
|
-
return true;
|
|
1012
|
-
case "/discard":
|
|
1013
|
-
void (async () => {
|
|
1014
|
-
const session = runner.getSession();
|
|
1015
|
-
const plan = await loadPlan(session.sessionId).catch(() => undefined);
|
|
1016
|
-
if (!plan) {
|
|
1017
|
-
info("no active plan to discard");
|
|
1018
|
-
return;
|
|
1019
|
-
}
|
|
1020
|
-
await deletePlan(session.sessionId);
|
|
1021
|
-
session.planApproved.value = false;
|
|
1022
|
-
info(`plan discarded · ${plan.goal}`);
|
|
1023
|
-
})();
|
|
1024
|
-
return true;
|
|
1025
|
-
case "/scope":
|
|
1026
|
-
void (async () => {
|
|
1027
|
-
const [sub = "show", ...parts] = arg.split(/\s+/).filter(Boolean);
|
|
1028
|
-
if (["clear", "reset", "off"].includes(sub)) {
|
|
1029
|
-
await clearScope();
|
|
1030
|
-
info("engagement scope cleared");
|
|
1031
|
-
return;
|
|
1032
|
-
}
|
|
1033
|
-
if (sub === "show" || sub === "list" || sub === "ls") {
|
|
1034
|
-
const scope = await loadScope();
|
|
1035
|
-
info(scope
|
|
1036
|
-
? `scope: ${scope.name ?? "unnamed"} · ${scope.authorizedTargets.join(", ")}`
|
|
1037
|
-
: "no engagement scope configured");
|
|
1038
|
-
return;
|
|
1039
|
-
}
|
|
1040
|
-
if (sub === "add") {
|
|
1041
|
-
const targets = parts
|
|
1042
|
-
.join(" ")
|
|
1043
|
-
.split(/[\s,]+/)
|
|
1044
|
-
.filter(Boolean);
|
|
1045
|
-
if (!targets.length) {
|
|
1046
|
-
warn("usage: /scope add <target1,target2>");
|
|
1047
|
-
return;
|
|
1048
|
-
}
|
|
1049
|
-
const scope = await addScopeTargets(targets);
|
|
1050
|
-
info(`scope updated · ${scope.authorizedTargets.join(", ")}`);
|
|
1051
|
-
return;
|
|
1052
|
-
}
|
|
1053
|
-
if (sub === "new" || sub === "set") {
|
|
1054
|
-
const targets = parts
|
|
1055
|
-
.join(" ")
|
|
1056
|
-
.split(/[\s,]+/)
|
|
1057
|
-
.filter(Boolean);
|
|
1058
|
-
if (!targets.length) {
|
|
1059
|
-
warn("usage: /scope new <target1,target2>");
|
|
1060
|
-
return;
|
|
1061
|
-
}
|
|
1062
|
-
await saveScope({
|
|
1063
|
-
authorizedTargets: targets,
|
|
1064
|
-
createdAt: new Date().toISOString(),
|
|
1065
|
-
});
|
|
1066
|
-
info(`scope created · ${targets.join(", ")}`);
|
|
1067
|
-
return;
|
|
1068
|
-
}
|
|
1069
|
-
warn("usage: /scope [show|clear|new <targets>|add <targets>]");
|
|
1070
|
-
})().catch((error) => warn(error instanceof Error ? error.message : String(error)));
|
|
1071
|
-
return true;
|
|
1072
|
-
case "/privacy":
|
|
1073
|
-
void (async () => {
|
|
1074
|
-
const sub = (arg || "status").toLowerCase();
|
|
1075
|
-
if (["on", "enable"].includes(sub)) {
|
|
1076
|
-
updateConfig({ privateMode: true });
|
|
1077
|
-
info("private mode → on");
|
|
1078
|
-
return;
|
|
1079
|
-
}
|
|
1080
|
-
if (["off", "disable"].includes(sub)) {
|
|
1081
|
-
updateConfig({ privateMode: false });
|
|
1082
|
-
info("private mode → off");
|
|
1083
|
-
return;
|
|
1084
|
-
}
|
|
1085
|
-
if (sub === "status") {
|
|
1086
|
-
info(`private mode: ${getConfig().privateMode ? "on" : "off"}`);
|
|
1087
|
-
return;
|
|
1088
|
-
}
|
|
1089
|
-
if (sub === "clear-history") {
|
|
1090
|
-
const result = await clearAllHistory();
|
|
1091
|
-
info(`history cleared · ${result.detail || "ok"}`);
|
|
1092
|
-
return;
|
|
1093
|
-
}
|
|
1094
|
-
if (sub === "clear-logs") {
|
|
1095
|
-
const result = await clearAuditLogs();
|
|
1096
|
-
info(`audit logs cleared · ${result.removed} files`);
|
|
1097
|
-
return;
|
|
1098
|
-
}
|
|
1099
|
-
if (sub === "clear-artifacts") {
|
|
1100
|
-
const result = await clearArtifacts();
|
|
1101
|
-
info(`artifacts cleared · ${result.removed} files`);
|
|
1102
|
-
return;
|
|
1103
|
-
}
|
|
1104
|
-
if (sub === "clear-all") {
|
|
1105
|
-
const [historyResult, logResult, artifactResult] = await Promise.all([
|
|
1106
|
-
clearAllHistory(),
|
|
1107
|
-
clearAuditLogs(),
|
|
1108
|
-
clearArtifacts(),
|
|
1109
|
-
]);
|
|
1110
|
-
info(`cleared history (${historyResult.detail || "ok"}), logs (${logResult.removed}), artifacts (${artifactResult.removed})`);
|
|
1111
|
-
return;
|
|
1112
|
-
}
|
|
1113
|
-
warn("usage: /privacy [status|on|off|clear-history|clear-logs|clear-artifacts|clear-all]");
|
|
1114
|
-
})();
|
|
1115
|
-
return true;
|
|
1116
|
-
case "/freeonly": {
|
|
1117
|
-
const on = /^(on|true|1|enable)$/i.test(arg), off = /^(off|false|0|disable)$/i.test(arg);
|
|
1118
|
-
if (!on && !off) {
|
|
1119
|
-
info(`freeOnly=${getConfig().freeOnly}`);
|
|
1120
|
-
return true;
|
|
1121
|
-
}
|
|
1122
|
-
updateConfig({ freeOnly: on });
|
|
1123
|
-
info(`freeOnly=${on}`);
|
|
1124
|
-
return true;
|
|
1125
|
-
}
|
|
1126
|
-
case "/fallback": {
|
|
1127
|
-
const on = /^(on|true|1|enable)$/i.test(arg), off = /^(off|false|0|disable)$/i.test(arg);
|
|
1128
|
-
if (!on && !off) {
|
|
1129
|
-
info(`providerFallback=${getConfig().providerFallback}`);
|
|
1130
|
-
return true;
|
|
1131
|
-
}
|
|
1132
|
-
updateConfig({ providerFallback: on });
|
|
1133
|
-
info(`providerFallback=${on}`);
|
|
1134
|
-
return true;
|
|
1135
|
-
}
|
|
1136
|
-
case "/info":
|
|
1137
|
-
void (async () => {
|
|
1138
|
-
const providerVal = arg.trim().toLowerCase();
|
|
1139
|
-
let targetProvider = provider;
|
|
1140
|
-
if (providerVal) {
|
|
1141
|
-
try {
|
|
1142
|
-
targetProvider = assertProvider(providerVal);
|
|
1143
|
-
}
|
|
1144
|
-
catch {
|
|
1145
|
-
warn(`unknown provider: ${providerVal}`);
|
|
1146
|
-
return;
|
|
1147
|
-
}
|
|
1148
|
-
}
|
|
1149
|
-
const infoText = getProviderInfoText(targetProvider);
|
|
1150
|
-
setOverlay({
|
|
1151
|
-
kind: "pager",
|
|
1152
|
-
title: `${targetProvider} Info`,
|
|
1153
|
-
body: infoText,
|
|
1154
|
-
});
|
|
1155
|
-
})();
|
|
1156
|
-
return true;
|
|
1157
|
-
case "/keys":
|
|
1158
|
-
void (async () => {
|
|
1159
|
-
const llm = await listProviderStatuses(provider);
|
|
1160
|
-
const activeSearch = getConfig().activeSearchProvider;
|
|
1161
|
-
const search = await Promise.all(searchProviderIds.map(async (id) => {
|
|
1162
|
-
const secret = await getSearchProviderKey(id);
|
|
1163
|
-
const keyless = id === "duckduckgo";
|
|
1164
|
-
return {
|
|
1165
|
-
provider: id,
|
|
1166
|
-
active: id === activeSearch,
|
|
1167
|
-
configured: keyless || Boolean(secret.value),
|
|
1168
|
-
source: keyless ? "keyless" : secret.source,
|
|
1169
|
-
maskedKey: secret.value
|
|
1170
|
-
? maskSecret(secret.value)
|
|
1171
|
-
: undefined,
|
|
1172
|
-
};
|
|
1173
|
-
}));
|
|
1174
|
-
setOverlay({
|
|
1175
|
-
kind: "pager",
|
|
1176
|
-
title: "Credential status",
|
|
1177
|
-
body: formatKeyStatus(llm, search),
|
|
1178
|
-
});
|
|
1179
|
-
})().catch((error) => warn(`could not read keys: ${error instanceof Error ? error.message : String(error)}`));
|
|
1180
|
-
return true;
|
|
1181
|
-
case "/set": {
|
|
1182
|
-
const parts = arg.split(/\s+/).filter(Boolean);
|
|
1183
|
-
const providerVal = parts[0];
|
|
1184
|
-
const keyVal = parts[1];
|
|
1185
|
-
void (async () => {
|
|
1186
|
-
if (!providerVal) {
|
|
1187
|
-
const llm = await listProviderStatuses(provider);
|
|
1188
|
-
const activeSearch = getConfig().activeSearchProvider;
|
|
1189
|
-
const search = await Promise.all(searchProviderIds.map(async (id) => {
|
|
1190
|
-
const secret = await getSearchProviderKey(id);
|
|
1191
|
-
const keyless = id === "duckduckgo";
|
|
1192
|
-
return {
|
|
1193
|
-
provider: id,
|
|
1194
|
-
configured: keyless || Boolean(secret.value),
|
|
1195
|
-
maskedKey: secret.value
|
|
1196
|
-
? maskSecret(secret.value)
|
|
1197
|
-
: undefined,
|
|
1198
|
-
};
|
|
1199
|
-
}));
|
|
1200
|
-
setOverlay({
|
|
1201
|
-
kind: "picker",
|
|
1202
|
-
title: "Set API key for provider",
|
|
1203
|
-
options: [
|
|
1204
|
-
...llm.map((status) => ({
|
|
1205
|
-
value: `llm:${status.provider}`,
|
|
1206
|
-
label: `${status.provider.padEnd(12)} ${status.configured ? chalk.green("✓ key set") : chalk.red("✗ no key")}${status.active ? " (active)" : ""}`,
|
|
1207
|
-
description: status.model,
|
|
1208
|
-
})),
|
|
1209
|
-
...search.map((status) => ({
|
|
1210
|
-
value: `search:${status.provider}`,
|
|
1211
|
-
label: `${status.provider.padEnd(12)} ${status.configured ? chalk.green("✓ key set") : chalk.red("✗ no key")}`,
|
|
1212
|
-
description: `Search provider${status.provider === activeSearch ? " (active)" : ""}`,
|
|
1213
|
-
})),
|
|
1214
|
-
],
|
|
1215
|
-
onSelect: (val) => {
|
|
1216
|
-
setOverlay({ kind: "none" });
|
|
1217
|
-
void (async () => {
|
|
1218
|
-
const isSearch = val.startsWith("search:");
|
|
1219
|
-
const id = val.split(":")[1];
|
|
1220
|
-
if (isSearch) {
|
|
1221
|
-
const next = id;
|
|
1222
|
-
if (next === "duckduckgo") {
|
|
1223
|
-
info("duckduckgo is keyless and requires no setup");
|
|
1224
|
-
return;
|
|
1225
|
-
}
|
|
1226
|
-
const secret = await getSearchProviderKey(next);
|
|
1227
|
-
if (secret.value) {
|
|
1228
|
-
const reset = await new Promise((resolveConfirm) => {
|
|
1229
|
-
confirmResolver.current = resolveConfirm;
|
|
1230
|
-
dispatch({
|
|
1231
|
-
type: "event",
|
|
1232
|
-
event: {
|
|
1233
|
-
type: "confirm-request",
|
|
1234
|
-
id: "c",
|
|
1235
|
-
kind: "reset",
|
|
1236
|
-
prompt: `${next} already has a key (${maskSecret(secret.value)}). Reset it?`,
|
|
1237
|
-
},
|
|
1238
|
-
});
|
|
1239
|
-
});
|
|
1240
|
-
if (!reset) {
|
|
1241
|
-
info("cancelled");
|
|
1242
|
-
return;
|
|
1243
|
-
}
|
|
1244
|
-
}
|
|
1245
|
-
const key = await requestSecret({
|
|
1246
|
-
title: `${next} API key`,
|
|
1247
|
-
prompt: `Enter API key for ${next}:`,
|
|
1248
|
-
});
|
|
1249
|
-
if (!key) {
|
|
1250
|
-
info("cancelled");
|
|
1251
|
-
return;
|
|
1252
|
-
}
|
|
1253
|
-
await setSecret("search", next, key.trim());
|
|
1254
|
-
info(`saved ${next} ${maskSecret(key.trim())}`);
|
|
1255
|
-
}
|
|
1256
|
-
else {
|
|
1257
|
-
const next = id;
|
|
1258
|
-
if (next === "ollama") {
|
|
1259
|
-
const key = await requestSecret({
|
|
1260
|
-
title: "Ollama host URL",
|
|
1261
|
-
prompt: `Enter host URL for Ollama:`,
|
|
1262
|
-
});
|
|
1263
|
-
if (!key) {
|
|
1264
|
-
info("cancelled");
|
|
1265
|
-
return;
|
|
1266
|
-
}
|
|
1267
|
-
updateConfig({ ollamaHost: key.trim() });
|
|
1268
|
-
info(`saved ollama host → ${key.trim()}`);
|
|
1269
|
-
return;
|
|
1270
|
-
}
|
|
1271
|
-
const secret = await getProviderSecret(next);
|
|
1272
|
-
if (secret.value) {
|
|
1273
|
-
const reset = await new Promise((resolveConfirm) => {
|
|
1274
|
-
confirmResolver.current = resolveConfirm;
|
|
1275
|
-
dispatch({
|
|
1276
|
-
type: "event",
|
|
1277
|
-
event: {
|
|
1278
|
-
type: "confirm-request",
|
|
1279
|
-
id: "c",
|
|
1280
|
-
kind: "reset",
|
|
1281
|
-
prompt: `${next} already has a key (${maskSecret(secret.value)}). Reset it?`,
|
|
1282
|
-
},
|
|
1283
|
-
});
|
|
1284
|
-
});
|
|
1285
|
-
if (!reset) {
|
|
1286
|
-
info("cancelled");
|
|
1287
|
-
return;
|
|
1288
|
-
}
|
|
1289
|
-
}
|
|
1290
|
-
const key = await requestSecret({
|
|
1291
|
-
title: `${next} API key`,
|
|
1292
|
-
prompt: `Enter API key for ${next}:`,
|
|
1293
|
-
});
|
|
1294
|
-
if (!key) {
|
|
1295
|
-
info("cancelled");
|
|
1296
|
-
return;
|
|
1297
|
-
}
|
|
1298
|
-
if (!getProvider(next).validateKey(key.trim())) {
|
|
1299
|
-
warn(`invalid API key format for ${next}`);
|
|
1300
|
-
return;
|
|
1301
|
-
}
|
|
1302
|
-
await setProviderSecret(next, key.trim());
|
|
1303
|
-
info(`saved ${next} ${maskSecret(key.trim())}`);
|
|
1304
|
-
}
|
|
1305
|
-
})();
|
|
1306
|
-
},
|
|
1307
|
-
});
|
|
1308
|
-
}
|
|
1309
|
-
else {
|
|
1310
|
-
try {
|
|
1311
|
-
const isSearch = ["brave", "tavily", "duckduckgo"].includes(providerVal);
|
|
1312
|
-
if (isSearch) {
|
|
1313
|
-
const next = providerVal;
|
|
1314
|
-
if (next === "duckduckgo") {
|
|
1315
|
-
info("duckduckgo is keyless and requires no setup");
|
|
1316
|
-
return;
|
|
1317
|
-
}
|
|
1318
|
-
let key = keyVal;
|
|
1319
|
-
if (!key) {
|
|
1320
|
-
const secret = await getSearchProviderKey(next);
|
|
1321
|
-
if (secret.value) {
|
|
1322
|
-
const reset = await new Promise((resolveConfirm) => {
|
|
1323
|
-
confirmResolver.current = resolveConfirm;
|
|
1324
|
-
dispatch({
|
|
1325
|
-
type: "event",
|
|
1326
|
-
event: {
|
|
1327
|
-
type: "confirm-request",
|
|
1328
|
-
id: "c",
|
|
1329
|
-
kind: "reset",
|
|
1330
|
-
prompt: `${next} already has a key (${maskSecret(secret.value)}). Reset it?`,
|
|
1331
|
-
},
|
|
1332
|
-
});
|
|
1333
|
-
});
|
|
1334
|
-
if (!reset) {
|
|
1335
|
-
info("cancelled");
|
|
1336
|
-
return;
|
|
1337
|
-
}
|
|
1338
|
-
}
|
|
1339
|
-
key = await requestSecret({
|
|
1340
|
-
title: `${next} API key`,
|
|
1341
|
-
prompt: `Enter API key for ${next}:`,
|
|
1342
|
-
});
|
|
1343
|
-
if (!key) {
|
|
1344
|
-
info("cancelled");
|
|
1345
|
-
return;
|
|
1346
|
-
}
|
|
1347
|
-
}
|
|
1348
|
-
await setSecret("search", next, key.trim());
|
|
1349
|
-
info(`saved ${next} ${maskSecret(key.trim())}`);
|
|
1350
|
-
}
|
|
1351
|
-
else {
|
|
1352
|
-
const next = assertProvider(providerVal);
|
|
1353
|
-
if (next === "ollama") {
|
|
1354
|
-
let key = keyVal;
|
|
1355
|
-
if (!key) {
|
|
1356
|
-
key = await requestSecret({
|
|
1357
|
-
title: "Ollama host URL",
|
|
1358
|
-
prompt: `Enter host URL for Ollama:`,
|
|
1359
|
-
});
|
|
1360
|
-
if (!key) {
|
|
1361
|
-
info("cancelled");
|
|
1362
|
-
return;
|
|
1363
|
-
}
|
|
1364
|
-
}
|
|
1365
|
-
updateConfig({ ollamaHost: key.trim() });
|
|
1366
|
-
info(`saved ollama host → ${key.trim()}`);
|
|
1367
|
-
return;
|
|
1368
|
-
}
|
|
1369
|
-
let key = keyVal;
|
|
1370
|
-
if (!key) {
|
|
1371
|
-
const secret = await getProviderSecret(next);
|
|
1372
|
-
if (secret.value) {
|
|
1373
|
-
const reset = await new Promise((resolveConfirm) => {
|
|
1374
|
-
confirmResolver.current = resolveConfirm;
|
|
1375
|
-
dispatch({
|
|
1376
|
-
type: "event",
|
|
1377
|
-
event: {
|
|
1378
|
-
type: "confirm-request",
|
|
1379
|
-
id: "c",
|
|
1380
|
-
kind: "reset",
|
|
1381
|
-
prompt: `${next} already has a key (${maskSecret(secret.value)}). Reset it?`,
|
|
1382
|
-
},
|
|
1383
|
-
});
|
|
1384
|
-
});
|
|
1385
|
-
if (!reset) {
|
|
1386
|
-
info("cancelled");
|
|
1387
|
-
return;
|
|
1388
|
-
}
|
|
1389
|
-
}
|
|
1390
|
-
key = await requestSecret({
|
|
1391
|
-
title: `${next} API key`,
|
|
1392
|
-
prompt: `Enter API key for ${next}:`,
|
|
1393
|
-
});
|
|
1394
|
-
if (!key) {
|
|
1395
|
-
info("cancelled");
|
|
1396
|
-
return;
|
|
1397
|
-
}
|
|
1398
|
-
}
|
|
1399
|
-
if (!getProvider(next).validateKey(key.trim())) {
|
|
1400
|
-
warn(`invalid API key format for ${next}`);
|
|
1401
|
-
return;
|
|
1402
|
-
}
|
|
1403
|
-
await setProviderSecret(next, key.trim());
|
|
1404
|
-
info(`saved ${next} ${maskSecret(key.trim())}`);
|
|
1405
|
-
}
|
|
1406
|
-
}
|
|
1407
|
-
catch (e) {
|
|
1408
|
-
warn(e.message);
|
|
1409
|
-
}
|
|
1410
|
-
}
|
|
1411
|
-
})();
|
|
1412
|
-
return true;
|
|
1413
|
-
}
|
|
1414
|
-
case "/unset": {
|
|
1415
|
-
const parts = arg.split(/\s+/).filter(Boolean);
|
|
1416
|
-
const providerVal = parts[0];
|
|
1417
|
-
void (async () => {
|
|
1418
|
-
if (!providerVal) {
|
|
1419
|
-
const llm = await listProviderStatuses(provider);
|
|
1420
|
-
const activeSearch = getConfig().activeSearchProvider;
|
|
1421
|
-
const search = await Promise.all(searchProviderIds.map(async (id) => {
|
|
1422
|
-
const secret = await getSearchProviderKey(id);
|
|
1423
|
-
const keyless = id === "duckduckgo";
|
|
1424
|
-
return {
|
|
1425
|
-
provider: id,
|
|
1426
|
-
configured: keyless || Boolean(secret.value),
|
|
1427
|
-
maskedKey: secret.value
|
|
1428
|
-
? maskSecret(secret.value)
|
|
1429
|
-
: undefined,
|
|
1430
|
-
};
|
|
1431
|
-
}));
|
|
1432
|
-
setOverlay({
|
|
1433
|
-
kind: "picker",
|
|
1434
|
-
title: "Unset API key for provider",
|
|
1435
|
-
options: [
|
|
1436
|
-
...llm.map((status) => ({
|
|
1437
|
-
value: `llm:${status.provider}`,
|
|
1438
|
-
label: `${status.provider.padEnd(12)} ${status.configured ? chalk.green("✓ ") + (status.maskedKey ?? "key set") : chalk.red("✗ no key")}${status.active ? " (active)" : ""}`,
|
|
1439
|
-
description: status.model,
|
|
1440
|
-
})),
|
|
1441
|
-
...search.map((status) => ({
|
|
1442
|
-
value: `search:${status.provider}`,
|
|
1443
|
-
label: `${status.provider.padEnd(12)} ${status.configured ? chalk.green("✓ ") + (status.maskedKey ?? "keyless") : chalk.red("✗ no key")}`,
|
|
1444
|
-
description: `Search provider${status.provider === activeSearch ? " (active)" : ""}`,
|
|
1445
|
-
})),
|
|
1446
|
-
],
|
|
1447
|
-
onSelect: (val) => {
|
|
1448
|
-
setOverlay({ kind: "none" });
|
|
1449
|
-
void (async () => {
|
|
1450
|
-
const isSearch = val.startsWith("search:");
|
|
1451
|
-
const id = val.split(":")[1];
|
|
1452
|
-
if (isSearch) {
|
|
1453
|
-
const next = id;
|
|
1454
|
-
if (next === "duckduckgo") {
|
|
1455
|
-
info("duckduckgo requires no credentials and cannot be unset");
|
|
1456
|
-
return;
|
|
1457
|
-
}
|
|
1458
|
-
const secret = await getSearchProviderKey(next);
|
|
1459
|
-
if (!secret.value) {
|
|
1460
|
-
warn(`${next} has no key to unset`);
|
|
1461
|
-
return;
|
|
1462
|
-
}
|
|
1463
|
-
const { unsetSearchProviderKey } = await import("../commands/search-providers.js");
|
|
1464
|
-
await unsetSearchProviderKey(next);
|
|
1465
|
-
info(`unset ${next}`);
|
|
1466
|
-
}
|
|
1467
|
-
else {
|
|
1468
|
-
const next = id;
|
|
1469
|
-
if (next === "ollama") {
|
|
1470
|
-
info("ollama does not store an API key");
|
|
1471
|
-
return;
|
|
1472
|
-
}
|
|
1473
|
-
const secret = await getProviderSecret(next);
|
|
1474
|
-
if (!secret.value) {
|
|
1475
|
-
warn(`${next} has no key to unset`);
|
|
1476
|
-
return;
|
|
1477
|
-
}
|
|
1478
|
-
await unsetProviderSecret(next);
|
|
1479
|
-
info(`unset ${next}`);
|
|
1480
|
-
}
|
|
1481
|
-
})();
|
|
1482
|
-
},
|
|
1483
|
-
});
|
|
1484
|
-
}
|
|
1485
|
-
else {
|
|
1486
|
-
try {
|
|
1487
|
-
const isSearch = ["brave", "tavily", "duckduckgo"].includes(providerVal);
|
|
1488
|
-
if (isSearch) {
|
|
1489
|
-
const next = providerVal;
|
|
1490
|
-
if (next === "duckduckgo") {
|
|
1491
|
-
info("duckduckgo requires no credentials and cannot be unset");
|
|
1492
|
-
return;
|
|
1493
|
-
}
|
|
1494
|
-
const secret = await getSearchProviderKey(next);
|
|
1495
|
-
if (!secret.value) {
|
|
1496
|
-
warn(`${next} has no key to unset`);
|
|
1497
|
-
return;
|
|
1498
|
-
}
|
|
1499
|
-
const { unsetSearchProviderKey } = await import("../commands/search-providers.js");
|
|
1500
|
-
await unsetSearchProviderKey(next);
|
|
1501
|
-
info(`unset ${next}`);
|
|
1502
|
-
}
|
|
1503
|
-
else {
|
|
1504
|
-
const next = assertProvider(providerVal);
|
|
1505
|
-
if (next === "ollama") {
|
|
1506
|
-
info("ollama does not store an API key");
|
|
1507
|
-
return;
|
|
1508
|
-
}
|
|
1509
|
-
const secret = await getProviderSecret(next);
|
|
1510
|
-
if (!secret.value) {
|
|
1511
|
-
warn(`${next} has no key to unset`);
|
|
1512
|
-
return;
|
|
1513
|
-
}
|
|
1514
|
-
await unsetProviderSecret(next);
|
|
1515
|
-
info(`unset ${next}`);
|
|
1516
|
-
}
|
|
1517
|
-
}
|
|
1518
|
-
catch (e) {
|
|
1519
|
-
warn(e.message);
|
|
1520
|
-
}
|
|
1521
|
-
}
|
|
1522
|
-
})();
|
|
1523
|
-
return true;
|
|
1524
|
-
}
|
|
1525
|
-
case "/permissions": {
|
|
1526
|
-
const current = getConfig().permissions ?? "default";
|
|
1527
|
-
if (!arg) {
|
|
1528
|
-
setOverlay({
|
|
1529
|
-
kind: "picker",
|
|
1530
|
-
title: "Permissions mode",
|
|
1531
|
-
options: [
|
|
1532
|
-
{
|
|
1533
|
-
value: "default",
|
|
1534
|
-
label: "default",
|
|
1535
|
-
description: "normal behavior (prompts for confirmation on mutating actions)",
|
|
1536
|
-
active: current === "default",
|
|
1537
|
-
},
|
|
1538
|
-
{
|
|
1539
|
-
value: "allow-all",
|
|
1540
|
-
label: "allow-all",
|
|
1541
|
-
description: "run everything without confirmation (except passwords)",
|
|
1542
|
-
active: current === "allow-all",
|
|
1543
|
-
},
|
|
1544
|
-
],
|
|
1545
|
-
onSelect: (val) => {
|
|
1546
|
-
updateConfig({ permissions: val });
|
|
1547
|
-
info(`permissions → ${val}`);
|
|
1548
|
-
setOverlay({ kind: "none" });
|
|
1549
|
-
},
|
|
1550
|
-
});
|
|
1551
|
-
return true;
|
|
1552
|
-
}
|
|
1553
|
-
const value = arg.toLowerCase().trim();
|
|
1554
|
-
if (value === "default" || value === "normal") {
|
|
1555
|
-
updateConfig({ permissions: "default" });
|
|
1556
|
-
info("permissions → default");
|
|
1557
|
-
}
|
|
1558
|
-
else if (["allow-all", "allowall", "all"].includes(value)) {
|
|
1559
|
-
updateConfig({ permissions: "allow-all" });
|
|
1560
|
-
info("permissions → allow-all");
|
|
1561
|
-
}
|
|
1562
|
-
else {
|
|
1563
|
-
warn("usage: /permissions [default|allow-all]");
|
|
1564
|
-
}
|
|
1565
|
-
return true;
|
|
1566
|
-
}
|
|
1567
|
-
case "/update":
|
|
1568
|
-
info(`${cmd} manages updates; use the equivalent \`clai update\` command outside the TUI`);
|
|
1569
|
-
return true;
|
|
1570
|
-
case "/help":
|
|
1571
|
-
setOverlay({
|
|
1572
|
-
kind: "picker",
|
|
1573
|
-
title: "Command reference",
|
|
1574
|
-
options: slashCommands.map((item) => ({
|
|
1575
|
-
value: item.command,
|
|
1576
|
-
label: `${item.command}${item.usage ? ` ${item.usage}` : ""}`,
|
|
1577
|
-
description: item.description,
|
|
1578
|
-
})),
|
|
1579
|
-
onSelect: (value) => {
|
|
1580
|
-
const next = `${value} `;
|
|
1581
|
-
setInput(next);
|
|
1582
|
-
setCursor(next.length);
|
|
1583
|
-
setSelected(0);
|
|
1584
|
-
setOverlay({ kind: "none" });
|
|
1585
|
-
},
|
|
1586
|
-
});
|
|
1587
|
-
return true;
|
|
1588
|
-
case "/exit":
|
|
1589
|
-
case "/quit":
|
|
1590
|
-
exitTui();
|
|
1591
|
-
return true;
|
|
1592
|
-
default:
|
|
1593
|
-
return false;
|
|
1594
|
-
}
|
|
1595
|
-
}, [
|
|
1596
|
-
activateProvider,
|
|
1597
|
-
activateSearchProvider,
|
|
1598
|
-
chooseModel,
|
|
1599
|
-
chooseProvider,
|
|
1600
|
-
chooseReasoning,
|
|
1601
|
-
chooseSearchProvider,
|
|
1602
|
-
compacting,
|
|
1603
|
-
exitTui,
|
|
1604
|
-
mouseMode,
|
|
1605
|
-
noHistory,
|
|
1606
|
-
provider,
|
|
1607
|
-
runner,
|
|
1608
|
-
runImplement,
|
|
1609
|
-
lastToolOutput,
|
|
1610
|
-
openToolOutput,
|
|
1611
|
-
setReasoning,
|
|
1612
|
-
state.items,
|
|
1613
|
-
requestSecret,
|
|
1614
|
-
setOverlay,
|
|
1615
|
-
]);
|
|
1616
|
-
const submitText = useCallback((text) => {
|
|
1617
|
-
const trimmed = text.trim();
|
|
1618
|
-
if (!trimmed)
|
|
1619
|
-
return;
|
|
1620
|
-
historyIdx.current = -1;
|
|
1621
|
-
historyDraft.current = "";
|
|
1622
|
-
setInput("");
|
|
1623
|
-
setCursor(0);
|
|
1624
|
-
setSelected(0);
|
|
1625
|
-
// A macOS/Linux drag-and-drop commonly starts with an absolute path.
|
|
1626
|
-
// Only route a leading slash through the command handler when its first
|
|
1627
|
-
// token is an actual clai command; file paths remain normal prompts.
|
|
1628
|
-
if (trimmed.startsWith("/") && isKnownSlashCommand(trimmed)) {
|
|
1629
|
-
if (!handleLocalSlash(trimmed))
|
|
1630
|
-
dispatch({
|
|
1631
|
-
type: "notice",
|
|
1632
|
-
level: "warn",
|
|
1633
|
-
text: `unknown command: ${trimmed}`,
|
|
1634
|
-
});
|
|
1635
|
-
return;
|
|
1636
|
-
}
|
|
1637
|
-
if (shouldStoreInPromptHistory(trimmed))
|
|
1638
|
-
history.current.push(trimmed);
|
|
1639
|
-
if (runner.isRunning())
|
|
1640
|
-
dispatch({ type: "queue", text: trimmed });
|
|
1641
|
-
else
|
|
1642
|
-
beginTurn(trimmed);
|
|
1643
|
-
}, [handleLocalSlash, runner, beginTurn]);
|
|
1644
|
-
const answerConfirm = useCallback((ok) => {
|
|
1645
|
-
confirmResolver.current?.(ok);
|
|
1646
|
-
confirmResolver.current = undefined;
|
|
1647
|
-
dispatch({ type: "confirm-resolved" });
|
|
1648
|
-
}, []);
|
|
1649
|
-
const answerSecret = useCallback((value) => {
|
|
1650
|
-
secretResolver.current?.(value);
|
|
1651
|
-
secretResolver.current = undefined;
|
|
1652
|
-
setSecretRequest(undefined);
|
|
1653
|
-
}, []);
|
|
1654
|
-
// Layout math (keep the composer pinned to the bottom)
|
|
1655
|
-
const suggestions = input.startsWith("/")
|
|
1656
|
-
? getSlashCommandSuggestions(input)
|
|
1657
|
-
: [];
|
|
1658
|
-
const mention = getMentionQuery(input, cursor);
|
|
1659
|
-
const fileSuggestions = mention
|
|
1660
|
-
? findFileSuggestions(mention.query, safeCwd(), MAX_FILE_SUGGESTIONS)
|
|
1661
|
-
: [];
|
|
1662
|
-
const slashMenuOpen = suggestions.length > 0;
|
|
1663
|
-
const fileMenuOpen = !slashMenuOpen && Boolean(mention) && fileSuggestions.length > 0;
|
|
1664
|
-
const menuOpen = slashMenuOpen || fileMenuOpen;
|
|
1665
|
-
const overlayOpen = overlay.kind !== "none";
|
|
1666
|
-
const modalActive = Boolean(state.pendingConfirm) || Boolean(secretRequest) || overlayOpen;
|
|
1667
|
-
// Leave the terminal's final row unused. Painting through the last cell can
|
|
1668
|
-
// trigger an implicit scroll in several terminals, which looks like a full
|
|
1669
|
-
// screen flash on every keypress/spinner frame.
|
|
1670
|
-
const usableRows = Math.max(8, rows - 1);
|
|
1671
|
-
const headerH = 0;
|
|
1672
|
-
const inputWidth = Math.max(10, cols - 10);
|
|
1673
|
-
let statusH = 0;
|
|
1674
|
-
if (secretRequest) {
|
|
1675
|
-
const wrappedPromptLines = wrapPlainString(secretRequest.prompt, cols - 6);
|
|
1676
|
-
statusH = 5 + wrappedPromptLines.length; // 1 (header) + 1 (password) + 1 (help) + 2 (borders) = 5, plus prompt lines
|
|
1677
|
-
}
|
|
1678
|
-
else if (state.pendingConfirm) {
|
|
1679
|
-
const wrappedPromptLines = wrapPlainString(state.pendingConfirm.prompt, cols - 6);
|
|
1680
|
-
statusH = 4 + wrappedPromptLines.length; // 1 (header) + 1 (help) + 2 (borders) = 4, plus prompt lines
|
|
1681
|
-
}
|
|
1682
|
-
const chromeH = !secretRequest && !state.pendingConfirm ? 1 : 0;
|
|
1683
|
-
const gapH = 1;
|
|
1684
|
-
const wrappedInputLines = wrapPlainString(input, inputWidth);
|
|
1685
|
-
const maxAvailableForComposer = Math.max(3, usableRows - headerH - statusH - chromeH - gapH - 8);
|
|
1686
|
-
const maxComposerTextRows = Math.max(1, maxAvailableForComposer - 2);
|
|
1687
|
-
const composerTextH = Math.min(wrappedInputLines.length, maxComposerTextRows);
|
|
1688
|
-
const composerH = composerTextH + 2;
|
|
1689
|
-
const maxMenuRows = Math.max(3, usableRows - headerH - statusH - composerH - chromeH - gapH - 3);
|
|
1690
|
-
const menuH = slashMenuOpen
|
|
1691
|
-
? Math.min(suggestions.length, maxMenuRows)
|
|
1692
|
-
: fileMenuOpen
|
|
1693
|
-
? fileSuggestions.length
|
|
1694
|
-
: 0;
|
|
1695
|
-
const viewportH = Math.max(3, usableRows - headerH - statusH - composerH - menuH - chromeH - gapH);
|
|
1696
|
-
const slashWindowStart = slashMenuOpen
|
|
1697
|
-
? Math.min(Math.max(0, selected - Math.floor(menuH / 2)), Math.max(0, suggestions.length - menuH))
|
|
1698
|
-
: 0;
|
|
1699
|
-
const visibleSlashSuggestions = suggestions.slice(slashWindowStart, slashWindowStart + menuH);
|
|
1700
|
-
const livePlan = useMemo(() => {
|
|
1701
|
-
for (let i = state.items.length - 1; i >= 0; i -= 1) {
|
|
1702
|
-
const item = state.items[i];
|
|
1703
|
-
if (item?.kind === "plan")
|
|
1704
|
-
return item.plan;
|
|
1705
|
-
}
|
|
1706
|
-
return undefined;
|
|
1707
|
-
}, [state.items]);
|
|
1708
|
-
// Two columns only when both remain genuinely readable. Below this content-
|
|
1709
|
-
// based breakpoint the plan stays available inline and through Ctrl+P.
|
|
1710
|
-
const showPlanSidebar = Boolean(planPaneVisible && livePlan && cols >= 100 && overlay.kind === "none");
|
|
1711
|
-
const planSidebarWidth = showPlanSidebar
|
|
1712
|
-
? Math.min(48, Math.max(32, Math.floor((cols - 7) * 0.32)))
|
|
1713
|
-
: 0;
|
|
1714
|
-
const transcriptWidth = showPlanSidebar
|
|
1715
|
-
? cols - 6 - planSidebarWidth - 1
|
|
1716
|
-
: cols - 6;
|
|
1717
|
-
const transcriptLines = renderTranscriptLines(state, {
|
|
1718
|
-
width: transcriptWidth,
|
|
1719
|
-
thinkingExpanded: state.thinkingExpanded,
|
|
1720
|
-
outputExpanded: state.outputExpanded,
|
|
1721
|
-
running: state.status.running,
|
|
1722
|
-
version,
|
|
1723
|
-
mode,
|
|
1724
|
-
provider,
|
|
1725
|
-
model,
|
|
1726
|
-
permissions: getConfig().permissions ?? "default",
|
|
1727
|
-
hidePlanItems: Boolean(livePlan),
|
|
1728
|
-
});
|
|
1729
|
-
const total = transcriptLines.length;
|
|
1730
|
-
const maxOffset = Math.max(0, total - viewportH);
|
|
1731
|
-
const offset = Math.min(scroll, maxOffset);
|
|
1732
|
-
const end = total - offset;
|
|
1733
|
-
const start = Math.max(0, end - viewportH);
|
|
1734
|
-
let visible = transcriptLines.slice(start, end);
|
|
1735
|
-
if (visible.length < viewportH) {
|
|
1736
|
-
visible = [...visible, ...Array(viewportH - visible.length).fill("")];
|
|
1737
|
-
}
|
|
1738
|
-
// Sidebar rendered to strings (not an Ink flex column) so each viewport row
|
|
1739
|
-
// can be composed as ONE Text node — see renderPlanSidebarLines for why.
|
|
1740
|
-
const planSidebarLines = showPlanSidebar && livePlan
|
|
1741
|
-
? renderPlanSidebarLines(livePlan, planSidebarWidth, viewportH)
|
|
1742
|
-
: [];
|
|
1743
|
-
// SGR mouse reporting keeps wheel/trackpad events distinct from cursor-key
|
|
1744
|
-
// sequences, so scrolling the transcript never walks prompt history.
|
|
1745
|
-
// Mouse wheel scroll stays enabled while a confirm box is showing (so the
|
|
1746
|
-
// user can review context before answering); it's disabled for overlays/
|
|
1747
|
-
// secret prompts, which own their own content and scrolling.
|
|
1748
|
-
useEffect(() => {
|
|
1749
|
-
const confirmOnlyModal = Boolean(state.pendingConfirm) && !secretRequest && !overlayOpen;
|
|
1750
|
-
if (!stdin || (modalActive && !confirmOnlyModal))
|
|
1751
|
-
return;
|
|
1752
|
-
const onData = (chunk) => {
|
|
1753
|
-
const data = String(chunk);
|
|
1754
|
-
if (data === "\x08" || data === "\b") {
|
|
1755
|
-
isCtrlHPressed.current = true;
|
|
1756
|
-
setPlanPaneVisible((visible) => !visible);
|
|
1757
|
-
return;
|
|
1758
|
-
}
|
|
1759
|
-
const direction = mouseWheelDirection(data);
|
|
1760
|
-
if (direction < 0) {
|
|
1761
|
-
setScroll((value) => Math.min(maxOffset, value + 3));
|
|
1762
|
-
return;
|
|
1763
|
-
}
|
|
1764
|
-
if (direction > 0) {
|
|
1765
|
-
setScroll((value) => Math.max(0, value - 3));
|
|
1766
|
-
return;
|
|
1767
|
-
}
|
|
1768
|
-
// Handle drag to scroll when mouse mode is active
|
|
1769
|
-
if (mouseMode) {
|
|
1770
|
-
const match = data.match(/\x1b\[<(\d+);(\d+);(\d+)([mM])/);
|
|
1771
|
-
if (match) {
|
|
1772
|
-
const button = parseInt(match[1], 10);
|
|
1773
|
-
const y = parseInt(match[3], 10);
|
|
1774
|
-
const action = match[4];
|
|
1775
|
-
// Drag event (button 32, pressed/moving action M)
|
|
1776
|
-
if (button === 32 && action === "M") {
|
|
1777
|
-
const now = Date.now();
|
|
1778
|
-
if (now - lastDragTime.current > 100) {
|
|
1779
|
-
if (y <= 4) {
|
|
1780
|
-
setScroll((value) => Math.min(maxOffset, value + 1));
|
|
1781
|
-
lastDragTime.current = now;
|
|
1782
|
-
}
|
|
1783
|
-
else if (y >= viewportH) {
|
|
1784
|
-
setScroll((value) => Math.max(0, value - 1));
|
|
1785
|
-
lastDragTime.current = now;
|
|
1786
|
-
}
|
|
1787
|
-
}
|
|
1788
|
-
}
|
|
1789
|
-
}
|
|
1790
|
-
}
|
|
1791
|
-
};
|
|
1792
|
-
stdin.prependListener("data", onData);
|
|
1793
|
-
return () => {
|
|
1794
|
-
stdin.off("data", onData);
|
|
1795
|
-
};
|
|
1796
|
-
}, [stdin, modalActive, maxOffset, mouseMode, viewportH, state.pendingConfirm, secretRequest, overlayOpen]);
|
|
1797
|
-
// Key handling
|
|
1798
|
-
const onInputRef = useRef(undefined);
|
|
1799
|
-
onInputRef.current = (ch, key) => {
|
|
1800
|
-
if (key.backspace && isCtrlHPressed.current) {
|
|
1801
|
-
isCtrlHPressed.current = false;
|
|
1802
|
-
return;
|
|
1803
|
-
}
|
|
1804
|
-
// While a confirmation box (sudo/mutating command/plan implement/etc.) is
|
|
1805
|
-
// showing, ConfirmModal owns y/n/Esc — but the main transcript behind it
|
|
1806
|
-
// should still be scrollable so the user can review context before
|
|
1807
|
-
// deciding. Only scroll keys pass through; everything else (including
|
|
1808
|
-
// plain text) stays blocked so it can't leak into the confirm prompt.
|
|
1809
|
-
// Overlays (pager/picker/jobs) and the secret prompt are excluded here:
|
|
1810
|
-
// they render their own content and already handle their own scrolling.
|
|
1811
|
-
if (modalActive) {
|
|
1812
|
-
if (state.pendingConfirm && !secretRequest && !overlayOpen) {
|
|
1813
|
-
if (key.pageUp) {
|
|
1814
|
-
setScroll((s) => Math.min(maxOffset, s + viewportH));
|
|
1815
|
-
return;
|
|
1816
|
-
}
|
|
1817
|
-
if (key.pageDown) {
|
|
1818
|
-
setScroll((s) => Math.max(0, s - viewportH));
|
|
1819
|
-
return;
|
|
1820
|
-
}
|
|
1821
|
-
if (key.ctrl && ch === "u") {
|
|
1822
|
-
setScroll((s) => Math.min(maxOffset, s + Math.floor(viewportH / 2)));
|
|
1823
|
-
return;
|
|
1824
|
-
}
|
|
1825
|
-
if (key.ctrl && ch === "d") {
|
|
1826
|
-
setScroll((s) => Math.max(0, s - Math.floor(viewportH / 2)));
|
|
1827
|
-
return;
|
|
1828
|
-
}
|
|
1829
|
-
if (ch === "j" || (key.downArrow && (key.shift || key.ctrl || key.meta))) {
|
|
1830
|
-
setScroll((s) => Math.max(0, s - 1));
|
|
1831
|
-
return;
|
|
1832
|
-
}
|
|
1833
|
-
if (ch === "k" || (key.upArrow && (key.shift || key.ctrl || key.meta))) {
|
|
1834
|
-
setScroll((s) => Math.min(maxOffset, s + 1));
|
|
1835
|
-
return;
|
|
1836
|
-
}
|
|
1837
|
-
}
|
|
1838
|
-
return; // everything else: overlay/modal owns input
|
|
1839
|
-
}
|
|
1840
|
-
const cleanedChunk = stripMouseReports(ch)
|
|
1841
|
-
.replace(/\r\n/g, "\n")
|
|
1842
|
-
.replace(/\r/g, "\n");
|
|
1843
|
-
if (isMouseReport(ch) && cleanedChunk.length === 0)
|
|
1844
|
-
return;
|
|
1845
|
-
// Global shortcuts
|
|
1846
|
-
if (key.ctrl && ch === "t") {
|
|
1847
|
-
dispatch({ type: "toggle-thinking" });
|
|
1848
|
-
return;
|
|
1849
|
-
}
|
|
1850
|
-
if (key.ctrl && ch === "o") {
|
|
1851
|
-
const hasOutput = state.items.some((item) => (item.kind === "tool" && Boolean(item.output)) ||
|
|
1852
|
-
item.kind === "compacted");
|
|
1853
|
-
if (hasOutput)
|
|
1854
|
-
dispatch({ type: "toggle-output" });
|
|
1855
|
-
else
|
|
1856
|
-
dispatch({
|
|
1857
|
-
type: "notice",
|
|
1858
|
-
level: "info",
|
|
1859
|
-
text: "no tool output or compacted context yet",
|
|
1860
|
-
});
|
|
1861
|
-
return;
|
|
1862
|
-
}
|
|
1863
|
-
// Ctrl+H is ASCII backspace (0x08). Depending on the terminal/Ink
|
|
1864
|
-
// version it arrives either as ctrl+h or as a backspace key event.
|
|
1865
|
-
// A normal Backspace is usually DEL (0x7f), so do not consume that.
|
|
1866
|
-
const isPlanToggle = (key.ctrl && (ch.toLowerCase() === "h" || ch.toLowerCase() === "b" || key.backspace)) ||
|
|
1867
|
-
ch === "\b" ||
|
|
1868
|
-
key.sequence === "\x08" ||
|
|
1869
|
-
key.sequence === "\b";
|
|
1870
|
-
if (isPlanToggle) {
|
|
1871
|
-
setPlanPaneVisible((visible) => !visible);
|
|
1872
|
-
return;
|
|
1873
|
-
}
|
|
1874
|
-
if (key.ctrl && ch === "p") {
|
|
1875
|
-
void (async () => {
|
|
1876
|
-
const plan = await loadPlan(runner.getSession().sessionId).catch(() => undefined);
|
|
1877
|
-
if (plan)
|
|
1878
|
-
setOverlay({
|
|
1879
|
-
kind: "pager",
|
|
1880
|
-
title: "Plan",
|
|
1881
|
-
body: renderPlanDocument(plan),
|
|
1882
|
-
});
|
|
1883
|
-
else
|
|
1884
|
-
dispatch({
|
|
1885
|
-
type: "notice",
|
|
1886
|
-
level: "info",
|
|
1887
|
-
text: "no active plan yet",
|
|
1888
|
-
});
|
|
1889
|
-
})();
|
|
1890
|
-
return;
|
|
1891
|
-
}
|
|
1892
|
-
if (key.ctrl && ch === "j") {
|
|
1893
|
-
setOverlay({ kind: "jobs" });
|
|
1894
|
-
return;
|
|
1895
|
-
}
|
|
1896
|
-
// Scrolling
|
|
1897
|
-
if (key.pageUp) {
|
|
1898
|
-
setScroll((s) => Math.min(maxOffset, s + viewportH));
|
|
1899
|
-
return;
|
|
1900
|
-
}
|
|
1901
|
-
if (key.pageDown) {
|
|
1902
|
-
setScroll((s) => Math.max(0, s - viewportH));
|
|
1903
|
-
return;
|
|
1904
|
-
}
|
|
1905
|
-
if (key.ctrl && ch === "u") {
|
|
1906
|
-
setScroll((s) => Math.min(maxOffset, s + Math.floor(viewportH / 2)));
|
|
1907
|
-
return;
|
|
1908
|
-
}
|
|
1909
|
-
if (key.ctrl && ch === "d") {
|
|
1910
|
-
setScroll((s) => Math.max(0, s - Math.floor(viewportH / 2)));
|
|
1911
|
-
return;
|
|
1912
|
-
}
|
|
1913
|
-
if (key.upArrow && (key.shift || key.ctrl || key.meta)) {
|
|
1914
|
-
setScroll((s) => Math.min(maxOffset, s + 1));
|
|
1915
|
-
return;
|
|
1916
|
-
}
|
|
1917
|
-
if (key.downArrow && (key.shift || key.ctrl || key.meta)) {
|
|
1918
|
-
setScroll((s) => Math.max(0, s - 1));
|
|
1919
|
-
return;
|
|
1920
|
-
}
|
|
1921
|
-
if (!input && maxOffset > 0 && ch === "k") {
|
|
1922
|
-
setScroll((s) => Math.min(maxOffset, s + 1));
|
|
1923
|
-
return;
|
|
1924
|
-
}
|
|
1925
|
-
if (!input && maxOffset > 0 && ch === "j") {
|
|
1926
|
-
setScroll((s) => Math.max(0, s - 1));
|
|
1927
|
-
return;
|
|
1928
|
-
}
|
|
1929
|
-
if (key.escape) {
|
|
1930
|
-
if (compacting) {
|
|
1931
|
-
cancelCompaction();
|
|
1932
|
-
return;
|
|
1933
|
-
}
|
|
1934
|
-
if (state.outputExpanded)
|
|
1935
|
-
dispatch({ type: "toggle-output" });
|
|
1936
|
-
else if (runner.isRunning())
|
|
1937
|
-
runner.abort();
|
|
1938
|
-
else if (input) {
|
|
1939
|
-
setInput("");
|
|
1940
|
-
setCursor(0);
|
|
1941
|
-
setSelected(0);
|
|
1942
|
-
}
|
|
1943
|
-
return;
|
|
1944
|
-
}
|
|
1945
|
-
if (key.ctrl && ch === "c") {
|
|
1946
|
-
const now = Date.now();
|
|
1947
|
-
if (now - lastCtrlC.current < 1500) {
|
|
1948
|
-
exitTui();
|
|
1949
|
-
return;
|
|
1950
|
-
}
|
|
1951
|
-
lastCtrlC.current = now;
|
|
1952
|
-
if (runner.isRunning())
|
|
1953
|
-
runner.abort();
|
|
1954
|
-
return;
|
|
1955
|
-
}
|
|
1956
|
-
// Slash menu navigation
|
|
1957
|
-
if (menuOpen && (key.upArrow || key.downArrow)) {
|
|
1958
|
-
setSelected((s) => {
|
|
1959
|
-
const n = slashMenuOpen ? suggestions.length : fileSuggestions.length;
|
|
1960
|
-
return key.upArrow ? (s - 1 + n) % n : (s + 1) % n;
|
|
1961
|
-
});
|
|
1962
|
-
return;
|
|
1963
|
-
}
|
|
1964
|
-
if (menuOpen && key.tab) {
|
|
1965
|
-
if (slashMenuOpen) {
|
|
1966
|
-
setInput(suggestions[selected].command + " ");
|
|
1967
|
-
setCursor(suggestions[selected].command.length + 1);
|
|
1968
|
-
}
|
|
1969
|
-
else if (mention && fileSuggestions[selected]) {
|
|
1970
|
-
const suggestion = fileSuggestions[selected];
|
|
1971
|
-
const inserted = `@${suggestion.value}${suggestion.isDir ? "" : " "}`;
|
|
1972
|
-
const next = input.slice(0, mention.start) + inserted + input.slice(cursor);
|
|
1973
|
-
setInput(next);
|
|
1974
|
-
setCursor(mention.start + inserted.length);
|
|
1975
|
-
}
|
|
1976
|
-
setSelected(0);
|
|
1977
|
-
return;
|
|
1978
|
-
}
|
|
1979
|
-
// Insert a newline instead of submitting. Works on every OS/terminal:
|
|
1980
|
-
// Ctrl+J (universal line feed) and Alt/Option+Enter, plus Shift/Meta+Enter
|
|
1981
|
-
// where the terminal reports the modifier. Plain Enter still submits below.
|
|
1982
|
-
if (isComposerNewline(ch, key)) {
|
|
1983
|
-
const next = input.slice(0, cursor) + "\n" + input.slice(cursor);
|
|
1984
|
-
setInput(next);
|
|
1985
|
-
setCursor(cursor + 1);
|
|
1986
|
-
setSelected(0);
|
|
1987
|
-
historyIdx.current = -1;
|
|
1988
|
-
historyDraft.current = "";
|
|
1989
|
-
return;
|
|
1990
|
-
}
|
|
1991
|
-
if (key.return) {
|
|
1992
|
-
if (slashMenuOpen) {
|
|
1993
|
-
submitText(suggestions[selected].command);
|
|
1994
|
-
return;
|
|
1995
|
-
}
|
|
1996
|
-
if (fileMenuOpen && mention && fileSuggestions[selected]) {
|
|
1997
|
-
const suggestion = fileSuggestions[selected];
|
|
1998
|
-
const inserted = `@${suggestion.value}${suggestion.isDir ? "" : " "}`;
|
|
1999
|
-
const next = input.slice(0, mention.start) + inserted + input.slice(cursor);
|
|
2000
|
-
setInput(next);
|
|
2001
|
-
setCursor(mention.start + inserted.length);
|
|
2002
|
-
setSelected(0);
|
|
2003
|
-
return;
|
|
2004
|
-
}
|
|
2005
|
-
submitText(input);
|
|
2006
|
-
return;
|
|
2007
|
-
}
|
|
2008
|
-
// History (when menu closed)
|
|
2009
|
-
if (!menuOpen && key.upArrow) {
|
|
2010
|
-
if (history.current.length === 0)
|
|
2011
|
-
return;
|
|
2012
|
-
if (historyIdx.current < 0)
|
|
2013
|
-
historyDraft.current = input;
|
|
2014
|
-
const idx = historyIdx.current < 0
|
|
2015
|
-
? history.current.length - 1
|
|
2016
|
-
: Math.max(0, historyIdx.current - 1);
|
|
2017
|
-
historyIdx.current = idx;
|
|
2018
|
-
const v = history.current[idx] ?? "";
|
|
2019
|
-
setInput(v);
|
|
2020
|
-
setCursor(v.length);
|
|
2021
|
-
return;
|
|
2022
|
-
}
|
|
2023
|
-
if (!menuOpen && key.downArrow) {
|
|
2024
|
-
if (historyIdx.current < 0)
|
|
2025
|
-
return;
|
|
2026
|
-
const idx = historyIdx.current + 1;
|
|
2027
|
-
if (idx >= history.current.length) {
|
|
2028
|
-
historyIdx.current = -1;
|
|
2029
|
-
const draft = historyDraft.current;
|
|
2030
|
-
setInput(draft);
|
|
2031
|
-
setCursor(draft.length);
|
|
2032
|
-
return;
|
|
2033
|
-
}
|
|
2034
|
-
historyIdx.current = idx;
|
|
2035
|
-
const v = history.current[idx] ?? "";
|
|
2036
|
-
setInput(v);
|
|
2037
|
-
setCursor(v.length);
|
|
2038
|
-
return;
|
|
2039
|
-
}
|
|
2040
|
-
if (key.leftArrow) {
|
|
2041
|
-
setCursor((c) => Math.max(0, c - 1));
|
|
2042
|
-
return;
|
|
2043
|
-
}
|
|
2044
|
-
if (key.rightArrow) {
|
|
2045
|
-
setCursor((c) => Math.min(input.length, c + 1));
|
|
2046
|
-
return;
|
|
2047
|
-
}
|
|
2048
|
-
if (key.backspace && (key.ctrl || key.meta)) {
|
|
2049
|
-
setInput("");
|
|
2050
|
-
setCursor(0);
|
|
2051
|
-
setSelected(0);
|
|
2052
|
-
historyIdx.current = -1;
|
|
2053
|
-
historyDraft.current = "";
|
|
2054
|
-
return;
|
|
2055
|
-
}
|
|
2056
|
-
if (key.backspace || key.delete) {
|
|
2057
|
-
if (cursor === 0)
|
|
2058
|
-
return;
|
|
2059
|
-
setInput(input.slice(0, cursor - 1) + input.slice(cursor));
|
|
2060
|
-
setCursor(cursor - 1);
|
|
2061
|
-
setSelected(0);
|
|
2062
|
-
historyIdx.current = -1;
|
|
2063
|
-
historyDraft.current = "";
|
|
2064
|
-
return;
|
|
2065
|
-
}
|
|
2066
|
-
if (key.ctrl || key.meta)
|
|
2067
|
-
return;
|
|
2068
|
-
if (cleanedChunk) {
|
|
2069
|
-
setInput(input.slice(0, cursor) + cleanedChunk + input.slice(cursor));
|
|
2070
|
-
setCursor(cursor + cleanedChunk.length);
|
|
2071
|
-
setSelected(0);
|
|
2072
|
-
historyIdx.current = -1;
|
|
2073
|
-
historyDraft.current = "";
|
|
2074
|
-
}
|
|
2075
|
-
};
|
|
2076
|
-
const handleInputStable = useCallback((ch, key) => {
|
|
2077
|
-
onInputRef.current?.(ch, key);
|
|
2078
|
-
}, []);
|
|
2079
|
-
useInput(handleInputStable);
|
|
2080
|
-
const closeOverlay = useCallback(() => setOverlay({ kind: "none" }), []);
|
|
2081
|
-
// Render
|
|
2082
|
-
const timerStart = compacting ? compactStartedAt : state.status.startedAt;
|
|
2083
|
-
const elapsed = timerStart
|
|
2084
|
-
? Math.max(0, Math.floor((Date.now() - timerStart) / 1000))
|
|
2085
|
-
: 0;
|
|
2086
|
-
const before = input.slice(0, cursor);
|
|
2087
|
-
const at = input.slice(cursor, cursor + 1) || " ";
|
|
2088
|
-
const after = input.slice(cursor + 1);
|
|
2089
|
-
const footerInnerWidth = Math.max(20, cols - 6); // outer paddingX(2) + footer paddingX(1)
|
|
2090
|
-
const runningBadgeWidth = compacting
|
|
2091
|
-
? " COMPACTING ".length
|
|
2092
|
-
: " RUNNING ".length;
|
|
2093
|
-
const cancelBadgeWidth = " ESC CANCEL ".length;
|
|
2094
|
-
const queuedBadgeWidth = state.queued.length > 0 ? ` ${state.queued.length} QUEUED `.length + 1 : 0;
|
|
2095
|
-
const scrollBadgeWidth = (maxOffset > offset ? ` ▲ ${maxOffset - offset} `.length + 1 : 0) +
|
|
2096
|
-
(offset > 0 ? ` ▼ ${offset} `.length + 1 : 0);
|
|
2097
|
-
const spinnerWidth = 3; // spinner plus following space
|
|
2098
|
-
const fixedRunningFooterWidth = runningBadgeWidth +
|
|
2099
|
-
1 +
|
|
2100
|
-
spinnerWidth +
|
|
2101
|
-
1 +
|
|
2102
|
-
cancelBadgeWidth +
|
|
2103
|
-
queuedBadgeWidth +
|
|
2104
|
-
scrollBadgeWidth;
|
|
2105
|
-
const runningActivityWidth = Math.max(12, footerInnerWidth - fixedRunningFooterWidth);
|
|
2106
|
-
const rawRunningActivity = `${compacting ? "compacting conversation" : state.status.activity || "working"}${state.status.step > 0 ? ` (step ${state.status.step})` : ""} · ${elapsed}s`;
|
|
2107
|
-
const runningActivity = truncateMiddle(rawRunningActivity.replace(/\s+/g, " ").trim(), runningActivityWidth);
|
|
2108
|
-
return (_jsxs(Box, { flexDirection: "column", width: cols, height: usableRows, paddingX: 2, children: [overlay.kind === "pager" ? (_jsx(Pager, { title: overlay.title, body: overlay.body, height: viewportH, onClose: closeOverlay })) : overlay.kind === "jobs" ? (_jsx(JobsPanel, { jobs: jobs, onClose: closeOverlay })) : overlay.kind === "picker" ? (_jsx(PickerPanel, { title: overlay.title, options: overlay.options, searchDescription: overlay.searchDescription, twoLine: overlay.twoLine, height: viewportH, onSelect: overlay.onSelect, onClose: closeOverlay })) : showPlanSidebar && livePlan ? (_jsx(Box, { flexDirection: "column", height: viewportH, width: cols - 6, overflow: "hidden", children: visible.map((line, i) => {
|
|
2109
|
-
const cleanLine = line.replace(/\r?\n/g, "");
|
|
2110
|
-
const left = padVisible(truncateAnsi(cleanLine, transcriptWidth), transcriptWidth);
|
|
2111
|
-
const right = planSidebarLines[i] ?? "";
|
|
2112
|
-
return (_jsx(Text, { wrap: "truncate-end", children: `${left} ${right}` }, i));
|
|
2113
|
-
}) })) : (_jsx(Box, { flexDirection: "row", height: viewportH, width: cols - 6, overflow: "hidden", children: _jsx(Box, { flexDirection: "column", width: transcriptWidth, height: viewportH, flexShrink: 0, overflow: "hidden", children: visible.map((line, i) => {
|
|
2114
|
-
const truncated = truncateAnsi(line, transcriptWidth);
|
|
2115
|
-
return (_jsx(Text, { wrap: "wrap", children: truncated === "" ? " " : truncated }, i));
|
|
2116
|
-
}) }) })), _jsx(Box, { height: gapH }), slashMenuOpen && !modalActive
|
|
2117
|
-
? visibleSlashSuggestions.map((cmd, i) => {
|
|
2118
|
-
const absoluteIndex = slashWindowStart + i;
|
|
2119
|
-
return (_jsxs(Text, { wrap: "truncate-end", backgroundColor: absoluteIndex === selected
|
|
2120
|
-
? "#2563EB"
|
|
2121
|
-
: absoluteIndex % 2 === 0
|
|
2122
|
-
? "#1E293B"
|
|
2123
|
-
: "#0F172A", children: [_jsxs(Text, { color: absoluteIndex === selected ? "#FFFFFF" : "#67E8F9", bold: true, children: [absoluteIndex === selected ? " ❯ " : " ", cmd.command.padEnd(14)] }), cmd.usage ? (_jsxs(Text, { color: absoluteIndex === selected ? "#FFFFFF" : "#CBD5E1", children: [cmd.usage, " "] })) : null, _jsxs(Text, { color: "#F8FAFC", children: [" ", cmd.description] }), i === visibleSlashSuggestions.length - 1 &&
|
|
2124
|
-
slashWindowStart + menuH < suggestions.length ? (_jsx(Text, { dimColor: true, children: ` · ${suggestions.length - slashWindowStart - menuH} more ↓` })) : null] }, cmd.command));
|
|
2125
|
-
})
|
|
2126
|
-
: null, fileMenuOpen && !modalActive
|
|
2127
|
-
? fileSuggestions.map((file, i) => (_jsxs(Text, { wrap: "truncate-end", backgroundColor: i === selected ? "#2563EB" : i % 2 === 0 ? "#1E293B" : "#0F172A", children: [_jsxs(Text, { color: i === selected
|
|
2128
|
-
? "#FFFFFF"
|
|
2129
|
-
: file.isDir
|
|
2130
|
-
? "#67E8F9"
|
|
2131
|
-
: "#F8FAFC", bold: i === selected, children: [i === selected ? "❯ " : " ", file.isDir ? "▸ " : "· ", file.value] }), _jsx(Text, { dimColor: true, children: file.isDir ? " directory" : " attach file" })] }, file.value)))
|
|
2132
|
-
: null, secretRequest ? (_jsx(SecretInputPanel, { title: secretRequest.title, prompt: secretRequest.prompt, onSubmit: (value) => answerSecret(value), onCancel: () => answerSecret(undefined) })) : state.pendingConfirm ? (_jsx(ConfirmModal, { confirm: state.pendingConfirm, onAnswer: answerConfirm, overlayOpen: overlayOpen, onViewPlan: state.pendingConfirm.kind === "plan"
|
|
2133
|
-
? () => {
|
|
2134
|
-
void (async () => {
|
|
2135
|
-
const plan = await loadPlan(runner.getSession().sessionId).catch(() => undefined);
|
|
2136
|
-
if (plan)
|
|
2137
|
-
setOverlay({
|
|
2138
|
-
kind: "pager",
|
|
2139
|
-
title: "Plan",
|
|
2140
|
-
body: renderPlanDocument(plan),
|
|
2141
|
-
});
|
|
2142
|
-
})();
|
|
2143
|
-
}
|
|
2144
|
-
: undefined })) : null, _jsxs(Box, { borderStyle: "round", borderColor: state.pendingConfirm || secretRequest
|
|
2145
|
-
? "yellow"
|
|
2146
|
-
: state.status.running
|
|
2147
|
-
? "yellow"
|
|
2148
|
-
: "cyan", paddingX: 1, children: [_jsx(Text, { color: state.status.running ? "yellow" : "cyan", bold: true, children: state.pendingConfirm || secretRequest ? "! " : "❯ " }), secretRequest ? (_jsx(Text, { bold: true, children: "Input locked \u00B7 complete the secure input above" })) : state.pendingConfirm ? (_jsx(Text, { bold: true, children: "Input locked \u00B7 answer the confirmation above with Y or N" })) : input.length === 0 ? (_jsx(Text, { dimColor: true, children: state.status.running
|
|
2149
|
-
? "type to queue a message…"
|
|
2150
|
-
: `ask anything · / for commands · @file to attach · ${newlineHint()} · esc to cancel` })) : ((() => {
|
|
2151
|
-
let cursorLineIdx = 0;
|
|
2152
|
-
for (let i = 0; i < wrappedInputLines.length; i++) {
|
|
2153
|
-
const line = wrappedInputLines[i];
|
|
2154
|
-
if (cursor >= line.startIdx && cursor <= line.endIdx) {
|
|
2155
|
-
cursorLineIdx = i;
|
|
2156
|
-
if (cursor === line.startIdx) {
|
|
2157
|
-
break;
|
|
2158
|
-
}
|
|
2159
|
-
}
|
|
2160
|
-
}
|
|
2161
|
-
let startLine = Math.max(0, cursorLineIdx - Math.floor(composerTextH / 2));
|
|
2162
|
-
let endLine = Math.min(wrappedInputLines.length - 1, startLine + composerTextH - 1);
|
|
2163
|
-
startLine = Math.max(0, endLine - composerTextH + 1);
|
|
2164
|
-
const startCharIdx = wrappedInputLines[startLine].startIdx;
|
|
2165
|
-
const endCharIdx = wrappedInputLines[endLine].endIdx;
|
|
2166
|
-
const slicedInput = input.slice(startCharIdx, endCharIdx);
|
|
2167
|
-
const slicedCursor = cursor - startCharIdx;
|
|
2168
|
-
const beforeSliced = slicedInput.slice(0, slicedCursor);
|
|
2169
|
-
const atSliced = slicedInput.slice(slicedCursor, slicedCursor + 1) || " ";
|
|
2170
|
-
const afterSliced = slicedInput.slice(slicedCursor + 1);
|
|
2171
|
-
return (_jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: "#F8FAFC", children: beforeSliced }), _jsx(Text, { backgroundColor: "#22D3EE", color: "#020617", bold: true, children: atSliced }), _jsx(Text, { color: "#F8FAFC", children: afterSliced })] }));
|
|
2172
|
-
})())] }), !secretRequest && !state.pendingConfirm ? (_jsxs(Box, { paddingX: 1, justifyContent: "center", children: [state.status.running || compacting ? (_jsxs(_Fragment, { children: [_jsxs(Text, { backgroundColor: "#B45309", color: "#FFFFFF", bold: true, children: [" ", compacting ? "COMPACTING" : "RUNNING", " "] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: "magenta", children: [spinner, " "] }), _jsx(Box, { width: runningActivityWidth, flexShrink: 1, children: _jsx(Text, { color: "yellow", wrap: "truncate-end", children: runningActivity }) }), _jsx(Text, { children: " " }), _jsxs(Text, { backgroundColor: "#334155", color: "#F8FAFC", children: [" ", "ESC CANCEL", " "] }), state.queued.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { backgroundColor: "#854D0E", color: "#FFFFFF", bold: true, children: ` ${state.queued.length} QUEUED ` })] })) : null] })) : (_jsxs(_Fragment, { children: [_jsxs(Text, { backgroundColor: "#166534", color: "#FFFFFF", bold: true, children: [" ", "READY", " "] }), state.queued.length > 0 ? (_jsx(Text, { backgroundColor: "#854D0E", color: "#FFFFFF", bold: true, children: ` ${state.queued.length} QUEUED ` })) : null, _jsx(Text, { children: " " }), _jsxs(Text, { backgroundColor: "#334155", color: "#F8FAFC", children: [" ", "/ COMMANDS", " "] }), _jsx(Text, { children: " " }), _jsxs(Text, { backgroundColor: "#334155", color: "#F8FAFC", children: [" ", "CTRL+T THINKING", " "] }), _jsx(Text, { children: " " }), _jsxs(Text, { backgroundColor: "#334155", color: "#F8FAFC", children: [" ", "CTRL+O OUTPUT", " "] }), livePlan ? (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { backgroundColor: planPaneVisible ? "#7E22CE" : "#334155", color: "#F8FAFC", children: ` CTRL+H PLAN ${planPaneVisible ? "ON" : "OFF"} ` })] })) : null, _jsx(Text, { children: " " }), _jsx(Text, { backgroundColor: mouseMode ? "#155E75" : "#334155", color: "#F8FAFC", children: ` MOUSE ${mouseMode ? "ON" : "OFF"} ` })] })), maxOffset > offset ? (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { backgroundColor: "#854D0E", color: "#FFFFFF", bold: true, children: ` ▲ ${maxOffset - offset} ` })] })) : null, offset > 0 ? (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { backgroundColor: "#854D0E", color: "#FFFFFF", bold: true, children: ` ▼ ${offset} ` })] })) : null] })) : null] }));
|
|
2173
|
-
}
|
|
2174
|
-
//# sourceMappingURL=App.js.map
|