@bubblebrain-ai/bubble 0.0.51 → 0.0.53
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 +4 -12
- package/dist/agent/routing-catalog.js +3 -2
- package/dist/agent/subagent/router.js +5 -0
- package/dist/agent/subagent/runtime.d.ts +1 -0
- package/dist/agent/subagent/runtime.js +13 -1
- package/dist/agent/subagent-control.d.ts +4 -0
- package/dist/agent/subagent-control.js +1 -0
- package/dist/agent/subagent-store.js +2 -0
- package/dist/agent/workflow/control.d.ts +2 -0
- package/dist/agent/workflow/runs.d.ts +1 -0
- package/dist/agent/workflow/runs.js +3 -0
- package/dist/agent.d.ts +11 -1
- package/dist/agent.js +158 -57
- package/dist/bin.js +2 -44
- package/dist/config.d.ts +14 -1
- package/dist/config.js +45 -4
- package/dist/context/budget.d.ts +7 -1
- package/dist/context/budget.js +25 -2
- package/dist/context/compact-llm.d.ts +1 -1
- package/dist/context/compact-llm.js +5 -2
- package/dist/context/overflow.js +2 -0
- package/dist/context/projector.d.ts +8 -0
- package/dist/context/projector.js +55 -6
- package/dist/context/tool-output-truncate.d.ts +17 -1
- package/dist/context/tool-output-truncate.js +171 -49
- package/dist/feishu/agent-host/run-driver.js +3 -0
- package/dist/feishu/serve.js +2 -0
- package/dist/goal/command.d.ts +1 -1
- package/dist/goal/command.js +7 -2
- package/dist/main.d.ts +1 -1
- package/dist/main.js +38 -8
- package/dist/memory/db.js +0 -7
- package/dist/model-catalog.js +25 -6
- package/dist/model-config.js +3 -2
- package/dist/model-pricing.d.ts +24 -5
- package/dist/model-pricing.js +0 -0
- package/dist/model-selection.js +13 -4
- package/dist/network/chatgpt-transport.js +0 -34
- package/dist/network/errors.d.ts +19 -0
- package/dist/network/errors.js +24 -0
- package/dist/network/provider-transport.d.ts +0 -4
- package/dist/network/provider-transport.js +3 -35
- package/dist/network/retry.d.ts +6 -1
- package/dist/network/retry.js +9 -0
- package/dist/provider-model-policy.d.ts +16 -0
- package/dist/provider-model-policy.js +33 -0
- package/dist/provider-registry.d.ts +1 -1
- package/dist/provider-registry.js +80 -11
- package/dist/provider-transform.js +32 -4
- package/dist/provider.d.ts +6 -2
- package/dist/provider.js +100 -11
- package/dist/question/controller.js +5 -4
- package/dist/question/normalize.d.ts +6 -0
- package/dist/question/normalize.js +11 -0
- package/dist/rewind.d.ts +20 -0
- package/dist/rewind.js +29 -0
- package/dist/sdk/index.d.ts +1 -1
- package/dist/sdk/index.js +11 -3
- package/dist/session.d.ts +7 -0
- package/dist/session.js +20 -0
- package/dist/skills/discovery.js +18 -0
- package/dist/skills/registry.d.ts +12 -2
- package/dist/skills/registry.js +40 -5
- package/dist/skills/types.d.ts +2 -0
- package/dist/slash-commands/commands.js +120 -81
- package/dist/slash-commands/registry.d.ts +2 -1
- package/dist/slash-commands/registry.js +3 -0
- package/dist/slash-commands/types.d.ts +22 -4
- package/dist/stats/usage.d.ts +3 -1
- package/dist/stats/usage.js +22 -13
- package/dist/tasks/manager.d.ts +4 -2
- package/dist/tasks/manager.js +34 -9
- package/dist/tasks/promotion.d.ts +2 -2
- package/dist/tasks/promotion.js +2 -2
- package/dist/tools/agent-lifecycle.js +4 -0
- package/dist/tools/bash.d.ts +3 -1
- package/dist/tools/bash.js +13 -2
- package/dist/tools/grep.d.ts +5 -1
- package/dist/tools/grep.js +318 -69
- package/dist/tools/index.d.ts +3 -1
- package/dist/tools/index.js +1 -0
- package/dist/tools/question.js +5 -4
- package/dist/tools/read.js +40 -21
- package/dist/tui/animation-clock.d.ts +14 -0
- package/dist/tui/animation-clock.js +34 -0
- package/dist/tui/app.d.ts +176 -0
- package/dist/tui/app.js +1647 -0
- package/dist/tui/components/approval-dialog.d.ts +43 -0
- package/dist/tui/components/approval-dialog.js +247 -0
- package/dist/tui/components/bottom-sheet.d.ts +9 -0
- package/dist/tui/components/bottom-sheet.js +32 -0
- package/dist/tui/components/context-info.d.ts +54 -0
- package/dist/tui/components/context-info.js +388 -0
- package/dist/tui/components/feedback-dialog.d.ts +56 -0
- package/dist/tui/components/feedback-dialog.js +262 -0
- package/dist/tui/components/feishu-setup-dialog.d.ts +65 -0
- package/dist/tui/components/feishu-setup-dialog.js +332 -0
- package/dist/tui/components/image-preview.d.ts +32 -0
- package/dist/tui/components/image-preview.js +130 -0
- package/dist/tui/components/plan-dialog.d.ts +34 -0
- package/dist/tui/components/plan-dialog.js +196 -0
- package/dist/tui/components/provider-key-input.d.ts +18 -0
- package/dist/tui/components/provider-key-input.js +49 -0
- package/dist/tui/components/question-dialog.d.ts +25 -0
- package/dist/tui/components/question-dialog.js +263 -0
- package/dist/tui/components/responsive-transcript.d.ts +31 -0
- package/dist/tui/components/responsive-transcript.js +77 -0
- package/dist/tui/components/rewind-picker.d.ts +62 -0
- package/dist/tui/components/rewind-picker.js +317 -0
- package/dist/tui/components/session-picker.d.ts +49 -0
- package/dist/tui/components/session-picker.js +303 -0
- package/dist/tui/components/skills-panel.d.ts +59 -0
- package/dist/tui/components/skills-panel.js +568 -0
- package/dist/tui/components/stats-panel.d.ts +40 -0
- package/dist/tui/components/stats-panel.js +301 -0
- package/dist/tui/components/streaming-message.d.ts +69 -0
- package/dist/tui/components/streaming-message.js +393 -0
- package/dist/tui/components/subagent-inspector.d.ts +28 -0
- package/dist/tui/components/subagent-inspector.js +127 -0
- package/dist/tui/components/task-inspector.d.ts +26 -0
- package/dist/tui/components/task-inspector.js +70 -0
- package/dist/tui/components/tasks-pane.d.ts +88 -0
- package/dist/tui/components/tasks-pane.js +416 -0
- package/dist/tui/components/transcript.d.ts +96 -0
- package/dist/tui/components/transcript.js +771 -0
- package/dist/tui/components/welcome.d.ts +20 -0
- package/dist/tui/components/welcome.js +133 -0
- package/dist/tui/components/workflow-inspector.d.ts +30 -0
- package/dist/tui/components/workflow-inspector.js +136 -0
- package/dist/tui/composer-autocomplete.d.ts +61 -0
- package/dist/tui/composer-autocomplete.js +383 -0
- package/dist/tui/composer-style.d.ts +7 -0
- package/dist/tui/composer-style.js +29 -0
- package/dist/tui/controller/agent-event-reducer.d.ts +75 -0
- package/dist/tui/controller/agent-event-reducer.js +238 -0
- package/dist/tui/controller/composer-controller.d.ts +95 -0
- package/dist/tui/controller/composer-controller.js +502 -0
- package/dist/tui/controller/controller.d.ts +194 -0
- package/dist/tui/controller/controller.js +1035 -0
- package/dist/tui/controller/effects.d.ts +71 -0
- package/dist/tui/controller/effects.js +1 -0
- package/dist/tui/controller/goal-runtime-controller.d.ts +47 -0
- package/dist/tui/controller/goal-runtime-controller.js +202 -0
- package/dist/tui/controller/input-queue-machine.d.ts +71 -0
- package/dist/tui/controller/input-queue-machine.js +121 -0
- package/dist/tui/controller/intents.d.ts +56 -0
- package/dist/tui/controller/intents.js +6 -0
- package/dist/tui/controller/overlay-controller.d.ts +64 -0
- package/dist/tui/controller/overlay-controller.js +129 -0
- package/dist/tui/controller/ports.d.ts +57 -0
- package/dist/tui/controller/ports.js +9 -0
- package/dist/tui/controller/session-transition.d.ts +54 -0
- package/dist/tui/controller/session-transition.js +68 -0
- package/dist/tui/controller/state.d.ts +14 -0
- package/dist/tui/controller/state.js +36 -0
- package/dist/tui/controller/task-runtime-controller.d.ts +66 -0
- package/dist/tui/controller/task-runtime-controller.js +158 -0
- package/dist/tui/footer.d.ts +37 -0
- package/dist/tui/footer.js +80 -0
- package/dist/{tui-ink → tui/formatting}/edit-diff.d.ts +1 -1
- package/dist/{tui-ink → tui/formatting}/edit-diff.js +1 -1
- package/dist/{tui-ink/app-helpers.d.ts → tui/formatting/summary.d.ts} +4 -4
- package/dist/{tui-ink/app-helpers.js → tui/formatting/summary.js} +2 -2
- package/dist/tui/fullscreen.d.ts +48 -0
- package/dist/tui/fullscreen.js +255 -0
- package/dist/tui/image-display.d.ts +4 -0
- package/dist/tui/image-display.js +30 -12
- package/dist/tui/markdown-style.d.ts +5 -0
- package/dist/tui/markdown-style.js +23 -0
- package/dist/{tui-ink → tui/model}/composer-buffer.js +1 -1
- package/dist/tui/model/composer-types.d.ts +17 -0
- package/dist/tui/model/composer-types.js +1 -0
- package/dist/{tui-ink → tui/model}/display-history.d.ts +7 -1
- package/dist/{tui-ink → tui/model}/display-reconstruct.d.ts +1 -1
- package/dist/{tui-ink → tui/model}/display-reconstruct.js +54 -6
- package/dist/{tui-ink → tui/model}/file-mentions.d.ts +2 -0
- package/dist/{tui-ink → tui/model}/file-mentions.js +4 -0
- package/dist/tui/model/image-attachment.d.ts +15 -0
- package/dist/tui/model/image-attachment.js +1 -0
- package/dist/{tui-ink → tui/model}/image-paste.d.ts +7 -1
- package/dist/{tui-ink → tui/model}/image-paste.js +46 -0
- package/dist/tui/model/input-history.d.ts +1 -0
- package/dist/tui/model/input-history.js +1 -0
- package/dist/{tui-ink → tui/model}/input-queue.d.ts +1 -1
- package/dist/{tui-ink → tui/model}/recent-activity.js +1 -1
- package/dist/{tui-ink → tui/model}/subagent-view.d.ts +17 -2
- package/dist/{tui-ink → tui/model}/subagent-view.js +125 -4
- package/dist/{tui-ink → tui/model}/submit-dedupe.d.ts +1 -1
- package/dist/tui/model/theme-style.d.ts +10 -0
- package/dist/tui/model/theme-style.js +79 -0
- package/dist/{tui-ink → tui/model}/theme.d.ts +11 -23
- package/dist/{tui-ink → tui/model}/theme.js +23 -38
- package/dist/{tui-ink → tui/model}/trace-groups.d.ts +2 -0
- package/dist/{tui-ink → tui/model}/trace-groups.js +91 -30
- package/dist/tui/model/trace-interaction.d.ts +63 -0
- package/dist/tui/model/trace-interaction.js +87 -0
- package/dist/tui/model-picker-data.d.ts +1 -2
- package/dist/tui/model-picker-data.js +8 -1
- package/dist/tui/model-switch.js +2 -0
- package/dist/{tui-ink → tui}/run-session-picker.d.ts +2 -3
- package/dist/tui/run-session-picker.js +64 -0
- package/dist/tui/run.d.ts +72 -0
- package/dist/tui/run.js +139 -0
- package/dist/tui/terminal/multiplexer.d.ts +5 -0
- package/dist/tui/terminal/multiplexer.js +14 -0
- package/dist/tui/testing/fake-agent.d.ts +24 -0
- package/dist/tui/testing/fake-agent.js +22 -0
- package/dist/tui/testing/spy-host.d.ts +21 -0
- package/dist/tui/testing/spy-host.js +55 -0
- package/dist/tui/trace-groups.js +27 -6
- package/dist/types.d.ts +7 -0
- package/node_modules/@bubblebrain-ai/pi-tui/LICENSE +21 -0
- package/node_modules/@bubblebrain-ai/pi-tui/README.md +854 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/alt-screen-search.d.ts +26 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/alt-screen-search.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/alt-screen-search.js +121 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/alt-screen-search.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/autocomplete.d.ts +86 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/autocomplete.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/autocomplete.js +649 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/autocomplete.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/alt-screen-flash.d.ts +13 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/alt-screen-flash.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/alt-screen-flash.js +36 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/alt-screen-flash.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/box.d.ts +22 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/box.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/box.js +99 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/box.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/cancellable-loader.d.ts +22 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/cancellable-loader.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/cancellable-loader.js +36 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/cancellable-loader.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/editor.d.ts +330 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/editor.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/editor.js +2344 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/editor.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/h-stack.d.ts +7 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/h-stack.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/h-stack.js +43 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/h-stack.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/image.d.ts +28 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/image.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/image.js +82 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/image.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/input.d.ts +48 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/input.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/input.js +390 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/input.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/loader.d.ts +31 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/loader.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/loader.js +67 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/loader.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/markdown.d.ts +103 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/markdown.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/markdown.js +797 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/markdown.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/scroll-view.d.ts +56 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/scroll-view.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/scroll-view.js +179 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/scroll-view.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/select-list.d.ts +53 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/select-list.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/select-list.js +175 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/select-list.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/settings-list.d.ts +50 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/settings-list.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/settings-list.js +174 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/settings-list.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/spacer.d.ts +12 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/spacer.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/spacer.js +22 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/spacer.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/stack.d.ts +32 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/stack.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/stack.js +109 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/stack.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/text.d.ts +19 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/text.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/text.js +81 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/text.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/truncated-text.d.ts +13 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/truncated-text.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/truncated-text.js +48 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/truncated-text.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/v-stack.d.ts +8 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/v-stack.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/v-stack.js +26 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/v-stack.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/editor-component.d.ts +39 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/editor-component.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/editor-component.js +2 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/editor-component.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/fuzzy.d.ts +16 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/fuzzy.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/fuzzy.js +110 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/fuzzy.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/index.d.ts +31 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/index.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/index.js +42 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/index.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/keybindings.d.ts +273 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/keybindings.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/keybindings.js +231 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/keybindings.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/keys.d.ts +184 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/keys.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/keys.js +1174 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/keys.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/kill-ring.d.ts +28 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/kill-ring.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/kill-ring.js +46 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/kill-ring.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/latex.d.ts +10 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/latex.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/latex.js +1268 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/latex.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/layout-node.d.ts +40 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/layout-node.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/layout-node.js +6 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/layout-node.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/layout.d.ts +42 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/layout.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/layout.js +335 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/layout.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/native-modifiers.d.ts +3 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/native-modifiers.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/native-modifiers.js +60 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/native-modifiers.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/stdin-buffer.d.ts +56 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/stdin-buffer.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/stdin-buffer.js +363 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/stdin-buffer.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/terminal-colors.d.ts +10 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/terminal-colors.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/terminal-colors.js +59 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/terminal-colors.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/terminal-image.d.ts +116 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/terminal-image.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/terminal-image.js +518 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/terminal-image.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/terminal.d.ts +142 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/terminal.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/terminal.js +544 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/terminal.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/testing.d.ts +11 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/testing.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/testing.js +11 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/testing.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.d.ts +140 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.js +1334 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-main-screen.d.ts +39 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-main-screen.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-main-screen.js +536 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-main-screen.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.d.ts +338 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.js +1016 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/undo-stack.d.ts +17 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/undo-stack.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/undo-stack.js +27 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/undo-stack.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/utils.d.ts +96 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/utils.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/utils.js +1193 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/utils.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/word-navigation.d.ts +25 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/word-navigation.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/word-navigation.js +96 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/word-navigation.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/test/virtual-terminal.d.ts +73 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/test/virtual-terminal.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/test/virtual-terminal.js +188 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/test/virtual-terminal.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/native/darwin/README.md +20 -0
- package/node_modules/@bubblebrain-ai/pi-tui/native/darwin/build.sh +62 -0
- package/node_modules/@bubblebrain-ai/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-modifiers.node +0 -0
- package/node_modules/@bubblebrain-ai/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-modifiers.node +0 -0
- package/node_modules/@bubblebrain-ai/pi-tui/native/darwin/src/darwin-modifiers.c +70 -0
- package/node_modules/@bubblebrain-ai/pi-tui/native/win32/README.md +22 -0
- package/node_modules/@bubblebrain-ai/pi-tui/native/win32/build.mjs +229 -0
- package/node_modules/@bubblebrain-ai/pi-tui/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
- package/node_modules/@bubblebrain-ai/pi-tui/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
- package/node_modules/@bubblebrain-ai/pi-tui/native/win32/src/win32-console-mode.c +119 -0
- package/node_modules/@bubblebrain-ai/pi-tui/package.json +66 -0
- package/node_modules/get-east-asian-width/index.d.ts +60 -0
- package/node_modules/get-east-asian-width/index.js +30 -0
- package/node_modules/get-east-asian-width/license +9 -0
- package/node_modules/get-east-asian-width/lookup-data.js +21 -0
- package/node_modules/get-east-asian-width/lookup.js +138 -0
- package/node_modules/get-east-asian-width/package.json +71 -0
- package/node_modules/get-east-asian-width/readme.md +65 -0
- package/node_modules/get-east-asian-width/utilities.js +24 -0
- package/package.json +23 -10
- package/packages/pi-tui/node_modules/marked/LICENSE +44 -0
- package/packages/pi-tui/node_modules/marked/README.md +115 -0
- package/packages/pi-tui/node_modules/marked/bin/main.js +284 -0
- package/packages/pi-tui/node_modules/marked/bin/marked.js +16 -0
- package/packages/pi-tui/node_modules/marked/lib/marked.d.ts +760 -0
- package/packages/pi-tui/node_modules/marked/lib/marked.esm.js +77 -0
- package/packages/pi-tui/node_modules/marked/lib/marked.esm.js.map +7 -0
- package/packages/pi-tui/node_modules/marked/lib/marked.umd.js +79 -0
- package/packages/pi-tui/node_modules/marked/lib/marked.umd.js.map +7 -0
- package/packages/pi-tui/node_modules/marked/man/marked.1 +113 -0
- package/packages/pi-tui/node_modules/marked/man/marked.1.md +93 -0
- package/packages/pi-tui/node_modules/marked/package.json +103 -0
- package/dist/tui-ink/app.d.ts +0 -76
- package/dist/tui-ink/app.js +0 -2377
- package/dist/tui-ink/approval/approval-dialog.d.ts +0 -13
- package/dist/tui-ink/approval/approval-dialog.js +0 -184
- package/dist/tui-ink/approval/diff-view.d.ts +0 -7
- package/dist/tui-ink/approval/diff-view.js +0 -44
- package/dist/tui-ink/approval/select.d.ts +0 -35
- package/dist/tui-ink/approval/select.js +0 -98
- package/dist/tui-ink/code-highlight.d.ts +0 -8
- package/dist/tui-ink/code-highlight.js +0 -121
- package/dist/tui-ink/command-palette.d.ts +0 -32
- package/dist/tui-ink/command-palette.js +0 -158
- package/dist/tui-ink/compaction-progress.d.ts +0 -19
- package/dist/tui-ink/compaction-progress.js +0 -74
- package/dist/tui-ink/detect-theme.d.ts +0 -1
- package/dist/tui-ink/detect-theme.js +0 -1
- package/dist/tui-ink/feedback-dialog.d.ts +0 -19
- package/dist/tui-ink/feedback-dialog.js +0 -133
- package/dist/tui-ink/feishu-setup-picker.d.ts +0 -5
- package/dist/tui-ink/feishu-setup-picker.js +0 -271
- package/dist/tui-ink/footer.d.ts +0 -26
- package/dist/tui-ink/footer.js +0 -51
- package/dist/tui-ink/input-box.d.ts +0 -189
- package/dist/tui-ink/input-box.js +0 -1279
- package/dist/tui-ink/input-history.d.ts +0 -1
- package/dist/tui-ink/input-history.js +0 -1
- package/dist/tui-ink/markdown.d.ts +0 -103
- package/dist/tui-ink/markdown.js +0 -747
- package/dist/tui-ink/message-list.d.ts +0 -49
- package/dist/tui-ink/message-list.js +0 -841
- package/dist/tui-ink/model-picker.d.ts +0 -143
- package/dist/tui-ink/model-picker.js +0 -966
- package/dist/tui-ink/plan-confirm.d.ts +0 -7
- package/dist/tui-ink/plan-confirm.js +0 -115
- package/dist/tui-ink/question-dialog.d.ts +0 -8
- package/dist/tui-ink/question-dialog.js +0 -135
- package/dist/tui-ink/rewind-picker.d.ts +0 -8
- package/dist/tui-ink/rewind-picker.js +0 -102
- package/dist/tui-ink/run-session-picker.js +0 -22
- package/dist/tui-ink/run.d.ts +0 -62
- package/dist/tui-ink/run.js +0 -103
- package/dist/tui-ink/session-picker.d.ts +0 -10
- package/dist/tui-ink/session-picker.js +0 -113
- package/dist/tui-ink/stats-panel.d.ts +0 -11
- package/dist/tui-ink/stats-panel.js +0 -65
- package/dist/tui-ink/subagent-inspector.d.ts +0 -21
- package/dist/tui-ink/subagent-inspector.js +0 -205
- package/dist/tui-ink/use-terminal-size.d.ts +0 -4
- package/dist/tui-ink/use-terminal-size.js +0 -48
- package/dist/tui-ink/waiting-indicator.d.ts +0 -12
- package/dist/tui-ink/waiting-indicator.js +0 -129
- package/dist/tui-ink/welcome.d.ts +0 -25
- package/dist/tui-ink/welcome.js +0 -88
- package/dist/tui-ink/wordmark.d.ts +0 -8
- package/dist/tui-ink/wordmark.js +0 -103
- /package/dist/{tui-ink → tui/model}/composer-buffer.d.ts +0 -0
- /package/dist/{tui-ink → tui/model}/display-history.js +0 -0
- /package/dist/{tui-ink → tui/model}/input-queue.js +0 -0
- /package/dist/{tui-ink → tui/model}/key-events.d.ts +0 -0
- /package/dist/{tui-ink → tui/model}/key-events.js +0 -0
- /package/dist/{tui-ink → tui/model}/paste-operation-tracker.d.ts +0 -0
- /package/dist/{tui-ink → tui/model}/paste-operation-tracker.js +0 -0
- /package/dist/{tui-ink → tui/model}/recent-activity.d.ts +0 -0
- /package/dist/{tui-ink → tui/model}/submit-dedupe.js +0 -0
- /package/dist/{tui-ink → tui/model}/sync-output.d.ts +0 -0
- /package/dist/{tui-ink → tui/model}/sync-output.js +0 -0
- /package/dist/{tui-ink → tui/model}/terminal-env.d.ts +0 -0
- /package/dist/{tui-ink → tui/model}/terminal-env.js +0 -0
- /package/dist/{tui-ink → tui/model}/terminal-mouse.d.ts +0 -0
- /package/dist/{tui-ink → tui/model}/terminal-mouse.js +0 -0
- /package/dist/{tui-ink → tui/model}/width.d.ts +0 -0
- /package/dist/{tui-ink → tui/model}/width.js +0 -0
package/dist/tui/app.js
ADDED
|
@@ -0,0 +1,1647 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bubble production TUI on the vendored pi-tui renderer.
|
|
3
|
+
*
|
|
4
|
+
* Owns Bubble's terminal product surface: transcript (user cards, assistant
|
|
5
|
+
* text, reasoning, tool traces), composer
|
|
6
|
+
* with native history, queue/steer while running, status footer, blocking
|
|
7
|
+
* overlays (plan approval, tool approval, questions), model/theme pickers,
|
|
8
|
+
* task wake notices, and clean terminal lifecycle on exit.
|
|
9
|
+
*
|
|
10
|
+
* All runtime logic flows through BubbleTuiController + OverlayRequestController;
|
|
11
|
+
* this file owns rendering and input routing only.
|
|
12
|
+
*/
|
|
13
|
+
import process from "node:process";
|
|
14
|
+
import { ProcessTerminal, TuiAltScreen, TuiMainScreen, VStack, Editor, Input, ScrollView, Markdown, matchesKey, isKeyRelease, isViewportTUI, } from "@bubblebrain-ai/pi-tui";
|
|
15
|
+
import { StreamingMessageComponent } from "./components/streaming-message.js";
|
|
16
|
+
import { ResponsiveTranscriptComponent } from "./components/responsive-transcript.js";
|
|
17
|
+
import { WelcomeBannerComponent } from "./components/welcome.js";
|
|
18
|
+
import { ApprovalDialogComponent } from "./components/approval-dialog.js";
|
|
19
|
+
import { PlanDialogComponent } from "./components/plan-dialog.js";
|
|
20
|
+
import { FeedbackDialogComponent } from "./components/feedback-dialog.js";
|
|
21
|
+
import { FeishuSetupDialogComponent } from "./components/feishu-setup-dialog.js";
|
|
22
|
+
import { QuestionDialogComponent } from "./components/question-dialog.js";
|
|
23
|
+
import { ProviderKeyInputComponent } from "./components/provider-key-input.js";
|
|
24
|
+
import { RewindPickerComponent, } from "./components/rewind-picker.js";
|
|
25
|
+
import { TaskStatusBarComponent, TasksPaneComponent, } from "./components/tasks-pane.js";
|
|
26
|
+
import { WorkflowInspectorComponent } from "./components/workflow-inspector.js";
|
|
27
|
+
import { SubagentInspectorComponent } from "./components/subagent-inspector.js";
|
|
28
|
+
import { TaskInspectorComponent } from "./components/task-inspector.js";
|
|
29
|
+
import { ContextInfoComponent } from "./components/context-info.js";
|
|
30
|
+
import { StatsPanelComponent } from "./components/stats-panel.js";
|
|
31
|
+
import { SkillsPanelComponent } from "./components/skills-panel.js";
|
|
32
|
+
import { SessionPickerComponent } from "./components/session-picker.js";
|
|
33
|
+
import { ComposerImagePreviewComponent, ImageViewerComponent } from "./components/image-preview.js";
|
|
34
|
+
import { registry as slashRegistry } from "../slash-commands/index.js";
|
|
35
|
+
import { collectUsageStatsBundle } from "../stats/usage.js";
|
|
36
|
+
import { ComposerController, } from "./controller/composer-controller.js";
|
|
37
|
+
import { OverlayRequestController } from "./controller/overlay-controller.js";
|
|
38
|
+
import { createTranscriptTheme, projectTranscript } from "./components/transcript.js";
|
|
39
|
+
import { friendlyCwd, sessionBasename } from "./formatting/summary.js";
|
|
40
|
+
import { formatExternalRuntimeFooterLabel, ResponsiveFooterComponent } from "./footer.js";
|
|
41
|
+
import { ComposerAutocompleteProvider } from "./composer-autocomplete.js";
|
|
42
|
+
import { COMPOSER_EDITOR_OPTIONS, createComposerEditorTheme } from "./composer-style.js";
|
|
43
|
+
import { SessionManager } from "../session.js";
|
|
44
|
+
import { collectFeedback } from "../feedback/collect.js";
|
|
45
|
+
import { TraceInteractionState } from "./model/trace-interaction.js";
|
|
46
|
+
import { parseSkillInvocation } from "../skills/invocation.js";
|
|
47
|
+
import { getNextPermissionMode } from "../permission/mode.js";
|
|
48
|
+
import { maskKey } from "../config.js";
|
|
49
|
+
import { copyToClipboard } from "../clipboard.js";
|
|
50
|
+
import { displayImagesFromPayload, formatImageUserDisplayText, nextImageDisplayLabelStart } from "./image-display.js";
|
|
51
|
+
import { createAssistantMarkdownTheme } from "./markdown-style.js";
|
|
52
|
+
import { resolveThemePalette, } from "./model/theme.js";
|
|
53
|
+
import { themeBackgroundCodes, themeDim, themeForeground } from "./model/theme-style.js";
|
|
54
|
+
import { TuiAnimationClock } from "./animation-clock.js";
|
|
55
|
+
export class PiTuiApp {
|
|
56
|
+
options;
|
|
57
|
+
tui;
|
|
58
|
+
editor;
|
|
59
|
+
composer;
|
|
60
|
+
composerPreview;
|
|
61
|
+
composerSlot = new VStack([]);
|
|
62
|
+
viewportScroll = null;
|
|
63
|
+
transcriptBox = new VStack([]);
|
|
64
|
+
streamingMessage;
|
|
65
|
+
markdown;
|
|
66
|
+
markdownRenderer = (text, width) => {
|
|
67
|
+
this.markdown.setText(text);
|
|
68
|
+
return this.markdown.render(width);
|
|
69
|
+
};
|
|
70
|
+
settledTranscript;
|
|
71
|
+
welcome;
|
|
72
|
+
footer;
|
|
73
|
+
tasksPane;
|
|
74
|
+
taskStatusBar;
|
|
75
|
+
animationClock;
|
|
76
|
+
traceInteraction = new TraceInteractionState();
|
|
77
|
+
overlays;
|
|
78
|
+
themeMode;
|
|
79
|
+
detectedTheme;
|
|
80
|
+
themeOverrides;
|
|
81
|
+
theme;
|
|
82
|
+
transcriptTheme;
|
|
83
|
+
gitBranch;
|
|
84
|
+
metadataManager = null;
|
|
85
|
+
metadataUnsubscribe = null;
|
|
86
|
+
currentUpdateNotice;
|
|
87
|
+
updateNoticeRefreshStarted = false;
|
|
88
|
+
showReasoning = false;
|
|
89
|
+
verboseTrace = false;
|
|
90
|
+
controllerUnsubscribe = null;
|
|
91
|
+
questionUnsubscribe = null;
|
|
92
|
+
activeQuestion = null;
|
|
93
|
+
questionQueue = [];
|
|
94
|
+
providerKeyPhase = null;
|
|
95
|
+
rewindPhase = null;
|
|
96
|
+
rewindPreviewMessageIndex;
|
|
97
|
+
feishuSetupPhase = null;
|
|
98
|
+
disposed = false;
|
|
99
|
+
/** Compatibility/debug view backed by the canonical persistent history. */
|
|
100
|
+
get history() {
|
|
101
|
+
return this.composer.historyTexts();
|
|
102
|
+
}
|
|
103
|
+
constructor(options) {
|
|
104
|
+
this.options = options;
|
|
105
|
+
const terminal = options.terminal ?? new ProcessTerminal();
|
|
106
|
+
this.currentUpdateNotice = options.updateNotice;
|
|
107
|
+
this.themeMode = options.themeMode ?? "auto";
|
|
108
|
+
this.detectedTheme = options.detectedTheme
|
|
109
|
+
?? (this.themeMode === "auto" ? "dark" : this.themeMode);
|
|
110
|
+
this.themeOverrides = options.themeOverrides;
|
|
111
|
+
this.theme = resolveThemePalette(this.themeMode, this.detectedTheme, this.themeOverrides).palette;
|
|
112
|
+
this.transcriptTheme = createTranscriptTheme(() => this.theme);
|
|
113
|
+
// Pick the renderer before start(). Entering fullscreen after the regular
|
|
114
|
+
// renderer has painted necessarily exposes a main-screen frame first.
|
|
115
|
+
// Constructing TuiAltScreen here makes its 1049h transition the first UI
|
|
116
|
+
// write and keeps the entire product surface on one application instance.
|
|
117
|
+
this.tui = (options.uiMode ?? "fullscreen") === "fullscreen"
|
|
118
|
+
? new TuiAltScreen(terminal, undefined, undefined, {
|
|
119
|
+
mouseMotion: "all",
|
|
120
|
+
screenBackground: () => themeBackgroundCodes(this.theme.background),
|
|
121
|
+
})
|
|
122
|
+
: new TuiMainScreen(terminal);
|
|
123
|
+
this.markdown = new Markdown("", 0, 0, createAssistantMarkdownTheme(() => this.theme));
|
|
124
|
+
this.streamingMessage = new StreamingMessageComponent(8, () => this.tui.requestRender(), (action) => this.handleTraceAction(action), () => this.theme);
|
|
125
|
+
this.editor = new Editor(this.tui, createComposerEditorTheme(() => this.theme), COMPOSER_EDITOR_OPTIONS);
|
|
126
|
+
this.composer = new ComposerController({
|
|
127
|
+
editor: this.editor,
|
|
128
|
+
scope: {
|
|
129
|
+
sessionFile: this.activeSessionManager().getSessionFile(),
|
|
130
|
+
cwd: process.cwd(),
|
|
131
|
+
},
|
|
132
|
+
nextImageLabelStart: nextImageDisplayLabelStart(options.controller.getTranscript()),
|
|
133
|
+
onSubmit: (payload) => this.handleSubmit(payload),
|
|
134
|
+
onNotice: (message) => this.pushNotice(message),
|
|
135
|
+
onStateChange: () => this.renderSnapshot(),
|
|
136
|
+
onOpenImage: (item) => this.openImageViewer(item),
|
|
137
|
+
allowImageAttachments: () => !this.activeSessionManager().getMetadata?.().externalRuntime,
|
|
138
|
+
readClipboardImage: options.readClipboardImage,
|
|
139
|
+
historyFilePath: options.inputHistoryFilePath,
|
|
140
|
+
getTheme: () => this.theme,
|
|
141
|
+
// Injected terminals are isolated hosts and must not mutate the user's
|
|
142
|
+
// real ~/.bubble history unless they explicitly provide a history file.
|
|
143
|
+
persistHistory: options.terminal === undefined || options.inputHistoryFilePath !== undefined,
|
|
144
|
+
});
|
|
145
|
+
this.composerPreview = new ComposerImagePreviewComponent(() => this.composer.previewAttachment(), () => this.theme);
|
|
146
|
+
this.editor.setAutocompleteProvider(new ComposerAutocompleteProvider({
|
|
147
|
+
cwd: process.cwd(),
|
|
148
|
+
commands: () => slashRegistry.list(),
|
|
149
|
+
skills: () => this.options.skillRegistry?.summaries() ?? [],
|
|
150
|
+
uiMode: () => this.tui.mode,
|
|
151
|
+
registry: this.options.registry,
|
|
152
|
+
thinkingLevel: () => this.options.agent.thinking,
|
|
153
|
+
providerId: () => this.options.agent.providerId,
|
|
154
|
+
themeMode: () => this.themeMode,
|
|
155
|
+
detectedTheme: () => this.detectedTheme,
|
|
156
|
+
onModelSuggestionsChanged: () => {
|
|
157
|
+
if (!this.disposed)
|
|
158
|
+
this.editor.refreshAutocomplete();
|
|
159
|
+
},
|
|
160
|
+
}));
|
|
161
|
+
this.settledTranscript = new ResponsiveTranscriptComponent(() => ({
|
|
162
|
+
messages: this.options.controller.getTranscript(),
|
|
163
|
+
options: this.transcriptRenderOptions(),
|
|
164
|
+
}), { onTraceAction: (action) => this.handleTraceAction(action) });
|
|
165
|
+
this.welcome = new WelcomeBannerComponent(() => {
|
|
166
|
+
const { agent } = this.options;
|
|
167
|
+
return {
|
|
168
|
+
cwd: friendlyCwd(process.cwd()),
|
|
169
|
+
session: sessionBasename(this.activeSessionManager().getSessionFile()),
|
|
170
|
+
model: agent.model,
|
|
171
|
+
provider: agent.providerId,
|
|
172
|
+
thinking: agent.thinking,
|
|
173
|
+
updateNotice: this.currentUpdateNotice,
|
|
174
|
+
theme: this.theme,
|
|
175
|
+
};
|
|
176
|
+
});
|
|
177
|
+
this.footer = new ResponsiveFooterComponent(() => {
|
|
178
|
+
const extra = [];
|
|
179
|
+
const steerCount = this.options.controller.pendingSteerCount();
|
|
180
|
+
const queuedCount = this.options.controller.queuedInputCount();
|
|
181
|
+
const pendingImages = this.composer.pendingImageCount();
|
|
182
|
+
if (steerCount)
|
|
183
|
+
extra.push(themeForeground(this.theme.warning, `steer ×${steerCount}`));
|
|
184
|
+
if (queuedCount)
|
|
185
|
+
extra.push(themeForeground(this.theme.warning, `queue ×${queuedCount}`));
|
|
186
|
+
if (pendingImages)
|
|
187
|
+
extra.push(themeDim(this.theme.dim, `reading image ×${pendingImages}`));
|
|
188
|
+
const session = this.activeSessionManager();
|
|
189
|
+
const metadata = typeof session?.getMetadata === "function" ? session.getMetadata() : undefined;
|
|
190
|
+
const runtimeLabel = formatExternalRuntimeFooterLabel(metadata?.externalRuntime);
|
|
191
|
+
return {
|
|
192
|
+
agent: this.options.agent,
|
|
193
|
+
cwd: friendlyCwd(process.cwd()),
|
|
194
|
+
branch: this.gitBranch,
|
|
195
|
+
sessionTitle: runtimeLabel ? undefined : metadata?.title,
|
|
196
|
+
runtimeLabel,
|
|
197
|
+
extra,
|
|
198
|
+
mode: this.options.agent.mode,
|
|
199
|
+
goalLine: runtimeLabel ? undefined : this.options.controller.getGoalIndicator?.(),
|
|
200
|
+
theme: this.theme,
|
|
201
|
+
// At two rows or fewer, preserve the focused editor body + border.
|
|
202
|
+
hidden: this.tui.terminal.rows <= 2,
|
|
203
|
+
};
|
|
204
|
+
});
|
|
205
|
+
this.tasksPane = new TasksPaneComponent(() => ({
|
|
206
|
+
groups: this.options.controller.getSubagentGroups?.() ?? [],
|
|
207
|
+
workflows: this.options.controller.getWorkflows?.() ?? [],
|
|
208
|
+
tasks: this.options.controller.getBackgroundTasks?.() ?? [],
|
|
209
|
+
}), () => this.tui.terminal.rows, {
|
|
210
|
+
// Pane navigation only changes local presentation state. Reprojecting
|
|
211
|
+
// the live trace/Markdown here is both unnecessary and expensive.
|
|
212
|
+
onRender: () => this.tui.requestRender(),
|
|
213
|
+
onOpenWorkflow: (item) => this.openWorkflowInspector(item),
|
|
214
|
+
onOpenSubagent: (item) => this.openSubagentInspector(item),
|
|
215
|
+
onOpenTask: (item) => this.openTaskInspector(item),
|
|
216
|
+
onStopWorkflow: (id) => this.options.controller.stopWorkflow(id),
|
|
217
|
+
onStopSubagent: (id) => this.options.controller.stopSubagent(id),
|
|
218
|
+
onStopTask: (id) => this.options.controller.stopBackgroundTask(id),
|
|
219
|
+
onCopyTaskOutput: (id) => {
|
|
220
|
+
const output = this.options.controller.getBackgroundTaskOutput(id);
|
|
221
|
+
if (!output)
|
|
222
|
+
return;
|
|
223
|
+
void copyToClipboard(output).then(() => this.pushNotice("Copied task output."));
|
|
224
|
+
},
|
|
225
|
+
onEscape: () => {
|
|
226
|
+
this.tasksPane.focused = false;
|
|
227
|
+
this.tui.setFocus(this.editor);
|
|
228
|
+
this.tui.requestRender();
|
|
229
|
+
},
|
|
230
|
+
}, () => this.theme);
|
|
231
|
+
this.animationClock = new TuiAnimationClock((elapsedMs) => {
|
|
232
|
+
const streamingChanged = this.streamingMessage.advanceAnimationFrame(elapsedMs);
|
|
233
|
+
const tasksChanged = this.tasksPane.advanceAnimationFrame();
|
|
234
|
+
if (streamingChanged || tasksChanged)
|
|
235
|
+
this.tui.requestRender();
|
|
236
|
+
});
|
|
237
|
+
this.taskStatusBar = new TaskStatusBarComponent(this.tasksPane);
|
|
238
|
+
this.overlays = new OverlayRequestController({ questionController: options.questionController });
|
|
239
|
+
this.syncMetadataSubscription();
|
|
240
|
+
this.installBlockingHandlers();
|
|
241
|
+
this.buildLayout();
|
|
242
|
+
this.wireInput();
|
|
243
|
+
this.renderSnapshot();
|
|
244
|
+
void options.resolveGitBranch?.(process.cwd()).then((branch) => {
|
|
245
|
+
if (this.disposed)
|
|
246
|
+
return;
|
|
247
|
+
this.gitBranch = branch?.trim() || undefined;
|
|
248
|
+
this.renderSnapshot();
|
|
249
|
+
}).catch(() => undefined);
|
|
250
|
+
}
|
|
251
|
+
activeSessionManager() {
|
|
252
|
+
return this.options.controller.getSessionManager?.() ?? this.options.sessionManager;
|
|
253
|
+
}
|
|
254
|
+
syncMetadataSubscription() {
|
|
255
|
+
const manager = this.activeSessionManager();
|
|
256
|
+
if (manager === this.metadataManager)
|
|
257
|
+
return;
|
|
258
|
+
this.metadataUnsubscribe?.();
|
|
259
|
+
this.metadataManager = manager;
|
|
260
|
+
this.metadataUnsubscribe = typeof manager?.subscribeMetadata === "function"
|
|
261
|
+
? manager.subscribeMetadata(() => this.renderSnapshot())
|
|
262
|
+
: null;
|
|
263
|
+
}
|
|
264
|
+
installBlockingHandlers() {
|
|
265
|
+
const { planHandlerRef, approvalHandlerRef, questionController } = this.options;
|
|
266
|
+
if (planHandlerRef) {
|
|
267
|
+
planHandlerRef.current = async (plan) => {
|
|
268
|
+
return this.planDialog(plan);
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
if (approvalHandlerRef) {
|
|
272
|
+
approvalHandlerRef.current = async (request) => {
|
|
273
|
+
const choice = await this.approvalDialog(request);
|
|
274
|
+
if (choice.kind === "approve_bash_prefix") {
|
|
275
|
+
if (request.type !== "bash" || !this.options.bashAllowlist) {
|
|
276
|
+
return {
|
|
277
|
+
action: "reject",
|
|
278
|
+
feedback: "This host cannot remember Bash approvals for the current session.",
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
// Session-scoped and command-scoped: PermissionAwareApprovalController
|
|
282
|
+
// consults this same instance before opening the next dialog.
|
|
283
|
+
this.options.bashAllowlist.add(choice.prefix);
|
|
284
|
+
return { action: "approve" };
|
|
285
|
+
}
|
|
286
|
+
return choice.kind === "approve_once"
|
|
287
|
+
? { action: "approve" }
|
|
288
|
+
: { action: "reject", feedback: "User denied the tool call." };
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
if (questionController) {
|
|
292
|
+
this.questionUnsubscribe = questionController.subscribe((event) => {
|
|
293
|
+
if (event.type === "asked") {
|
|
294
|
+
this.questionQueue.push(event.request);
|
|
295
|
+
this.openNextQuestion();
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
this.removeQueuedQuestion(event.request.id);
|
|
299
|
+
if (this.activeQuestion?.id === event.request.id) {
|
|
300
|
+
this.activeQuestion.close();
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
buildLayout() {
|
|
306
|
+
// Keep the live component permanently after the settled transcript. It
|
|
307
|
+
// renders zero rows while idle. Appending it per turn eventually placed
|
|
308
|
+
// the same component reference in the tree multiple times and let settled
|
|
309
|
+
// rows land after it, breaking spacing/order on later turns.
|
|
310
|
+
this.transcriptBox.addChild(this.settledTranscript);
|
|
311
|
+
this.transcriptBox.addChild(this.streamingMessage);
|
|
312
|
+
this.composerSlot.addChild(this.composerPreview);
|
|
313
|
+
this.composerSlot.addChild(this.editor);
|
|
314
|
+
if (isViewportTUI(this.tui)) {
|
|
315
|
+
// Fullscreen owns a bounded viewport. History scrolls while the composer
|
|
316
|
+
// and footer stay docked. The activity lane is a permanent one-row
|
|
317
|
+
// boundary: spinner text changes, its geometry never does.
|
|
318
|
+
const document = new VStack([this.welcome, this.transcriptBox]);
|
|
319
|
+
const scroll = new ScrollView(document, { follow: "end", primary: true });
|
|
320
|
+
this.viewportScroll = scroll;
|
|
321
|
+
this.tui.setLayoutRoot(new VStack([
|
|
322
|
+
{ component: scroll, basis: 0, grow: 1, minSize: 0 },
|
|
323
|
+
{ component: this.taskStatusBar, basis: "auto", shrink: 0 },
|
|
324
|
+
{ component: this.tasksPane, basis: "auto", shrink: 0 },
|
|
325
|
+
{ component: this.streamingMessage.activityLane, basis: "auto", shrink: 0 },
|
|
326
|
+
{ component: this.composerSlot, basis: "auto", shrink: 0 },
|
|
327
|
+
{ component: this.footer, basis: "auto", shrink: 0 },
|
|
328
|
+
]));
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
this.tui.addChild(this.welcome);
|
|
332
|
+
this.tui.addChild(this.transcriptBox);
|
|
333
|
+
this.tui.addChild(this.taskStatusBar);
|
|
334
|
+
this.tui.addChild(this.tasksPane);
|
|
335
|
+
this.tui.addChild(this.streamingMessage.activityLane);
|
|
336
|
+
this.tui.addChild(this.composerSlot);
|
|
337
|
+
this.tui.addChild(this.footer);
|
|
338
|
+
}
|
|
339
|
+
this.tui.setFocus(this.editor);
|
|
340
|
+
this.controllerUnsubscribe = this.options.controller.subscribe(() => {
|
|
341
|
+
this.syncMetadataSubscription();
|
|
342
|
+
this.composer.setScope({
|
|
343
|
+
sessionFile: this.activeSessionManager().getSessionFile(),
|
|
344
|
+
cwd: process.cwd(),
|
|
345
|
+
}, nextImageDisplayLabelStart(this.options.controller.getTranscript()));
|
|
346
|
+
this.renderSnapshot();
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
wireInput() {
|
|
350
|
+
let ctrlCArmed = false;
|
|
351
|
+
let rewindEscapeArmed = false;
|
|
352
|
+
this.tui.addInputListener((data) => {
|
|
353
|
+
// Global shortcuts act on key presses only. Kitty keyboard protocol also
|
|
354
|
+
// reports releases; allowing those through can trigger a shortcut twice.
|
|
355
|
+
if (isKeyRelease(data))
|
|
356
|
+
return { consume: true };
|
|
357
|
+
// Modal dialogs own their complete keyboard contract. In particular,
|
|
358
|
+
// an approval arrives while a run is active, so letting the global
|
|
359
|
+
// Escape/Ctrl+C handler run first would cancel the agent and strand the
|
|
360
|
+
// unresolved permission request instead of rejecting the dialog.
|
|
361
|
+
if (this.tui.hasOverlay())
|
|
362
|
+
return undefined;
|
|
363
|
+
// The inline credential phase owns all of its input. In particular,
|
|
364
|
+
// Escape returns to /provider instead of cancelling an unrelated run,
|
|
365
|
+
// and Shift+Tab must not change permissions while a key is being typed.
|
|
366
|
+
if (this.providerKeyPhase)
|
|
367
|
+
return undefined;
|
|
368
|
+
if (this.rewindPhase)
|
|
369
|
+
return undefined;
|
|
370
|
+
if (this.editor.focused
|
|
371
|
+
&& (matchesKey(data, "ctrl+v") || matchesKey(data, "super+v"))) {
|
|
372
|
+
// A terminal can only bracket text that it receives from the clipboard.
|
|
373
|
+
// Image-only Cmd+V is therefore silent in some terminals; Ctrl+V (and
|
|
374
|
+
// Kitty's super+v when available) gives the composer an explicit,
|
|
375
|
+
// terminal-independent signal to probe the native image clipboard.
|
|
376
|
+
this.composer.requestClipboardImagePaste();
|
|
377
|
+
return { consume: true };
|
|
378
|
+
}
|
|
379
|
+
if (matchesKey(data, "ctrl+g")) {
|
|
380
|
+
if (this.tasksPane.isOpen() && this.tasksPane.focused) {
|
|
381
|
+
this.tasksPane.close();
|
|
382
|
+
this.tasksPane.focused = false;
|
|
383
|
+
this.tui.setFocus(this.editor);
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
if (!this.tasksPane.isOpen())
|
|
387
|
+
this.tasksPane.toggle(true);
|
|
388
|
+
this.tasksPane.focused = true;
|
|
389
|
+
this.tui.setFocus(this.tasksPane);
|
|
390
|
+
}
|
|
391
|
+
this.renderSnapshot();
|
|
392
|
+
return { consume: true };
|
|
393
|
+
}
|
|
394
|
+
if (matchesKey(data, "ctrl+b") && this.options.controller.isRunning()) {
|
|
395
|
+
// Current Grok contract: Ctrl+B transfers a live Execute into Tasks;
|
|
396
|
+
// Ctrl+G remains dedicated to opening and closing the Tasks Pane.
|
|
397
|
+
this.options.controller.promoteActiveBash?.();
|
|
398
|
+
return { consume: true };
|
|
399
|
+
}
|
|
400
|
+
// A focused Tasks Pane owns Escape. It returns focus to the composer;
|
|
401
|
+
// it must never fall through and cancel the running parent Agent.
|
|
402
|
+
if (this.tasksPane.focused && matchesKey(data, "escape"))
|
|
403
|
+
return undefined;
|
|
404
|
+
if (matchesKey(data, "shift+tab")) {
|
|
405
|
+
this.options.agent.setMode(getNextPermissionMode(this.options.agent.mode));
|
|
406
|
+
this.renderSnapshot();
|
|
407
|
+
return { consume: true };
|
|
408
|
+
}
|
|
409
|
+
if (matchesKey(data, "escape") && this.options.controller.cancelActiveRun()) {
|
|
410
|
+
// Ink parity: Escape interrupts an active run. When idle, leave the
|
|
411
|
+
// key unconsumed so the editor/autocomplete layer can handle it.
|
|
412
|
+
return { consume: true };
|
|
413
|
+
}
|
|
414
|
+
if (matchesKey(data, "escape") && this.options.controller.isBusy?.()) {
|
|
415
|
+
// A second Escape while the provider is acknowledging Compact
|
|
416
|
+
// cancellation must not arm rewind or leak into the editor.
|
|
417
|
+
return { consume: true };
|
|
418
|
+
}
|
|
419
|
+
if (matchesKey(data, "escape") && !this.options.controller.isRunning() && !this.editor.getText()) {
|
|
420
|
+
if (rewindEscapeArmed) {
|
|
421
|
+
rewindEscapeArmed = false;
|
|
422
|
+
this.openRewindPicker();
|
|
423
|
+
return { consume: true };
|
|
424
|
+
}
|
|
425
|
+
rewindEscapeArmed = true;
|
|
426
|
+
setTimeout(() => { rewindEscapeArmed = false; }, 650);
|
|
427
|
+
return undefined;
|
|
428
|
+
}
|
|
429
|
+
rewindEscapeArmed = false;
|
|
430
|
+
if (matchesKey(data, "ctrl+c")) {
|
|
431
|
+
// Match Ink: the first Ctrl+C interrupts the active run. Requiring a
|
|
432
|
+
// second keypress here made cancellation feel like a quit gesture and
|
|
433
|
+
// left the tool trace running behind the composer.
|
|
434
|
+
if (this.options.controller.cancelActiveRun()) {
|
|
435
|
+
ctrlCArmed = false;
|
|
436
|
+
return { consume: true };
|
|
437
|
+
}
|
|
438
|
+
if (this.options.controller.isBusy?.())
|
|
439
|
+
return { consume: true };
|
|
440
|
+
if (ctrlCArmed) {
|
|
441
|
+
this.dispose();
|
|
442
|
+
return { consume: true };
|
|
443
|
+
}
|
|
444
|
+
ctrlCArmed = true;
|
|
445
|
+
setTimeout(() => {
|
|
446
|
+
ctrlCArmed = false;
|
|
447
|
+
}, 1_500);
|
|
448
|
+
return { consume: true };
|
|
449
|
+
}
|
|
450
|
+
if (data === "\x14") {
|
|
451
|
+
this.showReasoning = !this.showReasoning;
|
|
452
|
+
this.renderSnapshot();
|
|
453
|
+
return { consume: true };
|
|
454
|
+
}
|
|
455
|
+
if (data === "\x0f") {
|
|
456
|
+
this.verboseTrace = !this.verboseTrace;
|
|
457
|
+
this.renderSnapshot();
|
|
458
|
+
return { consume: true };
|
|
459
|
+
}
|
|
460
|
+
return undefined;
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
handleSubmit(input) {
|
|
464
|
+
const payload = typeof input === "string"
|
|
465
|
+
? { text: input, images: [] }
|
|
466
|
+
: input;
|
|
467
|
+
const trimmed = payload.text.trim();
|
|
468
|
+
if (!trimmed && payload.images.length === 0)
|
|
469
|
+
return;
|
|
470
|
+
// Passive output preserves a user's manual history position, but an
|
|
471
|
+
// explicit Composer submission changes their point of attention to the
|
|
472
|
+
// new turn. Re-enable follow-end before appending the user row so the same
|
|
473
|
+
// layout pass that renders it also brings it into view.
|
|
474
|
+
this.viewportScroll?.scrollToEnd();
|
|
475
|
+
if (trimmed.startsWith("/")) {
|
|
476
|
+
void this.handleCommand(trimmed);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
const controller = this.options.controller;
|
|
480
|
+
if (controller.isRunning()) {
|
|
481
|
+
// AgentRunInputQueue is intentionally text-only. Image payloads retain
|
|
482
|
+
// their bytes by waiting for the next provider turn instead of degrading
|
|
483
|
+
// into a text steer.
|
|
484
|
+
if (payload.images.length > 0)
|
|
485
|
+
controller.queueInput(payload);
|
|
486
|
+
else
|
|
487
|
+
controller.steer(trimmed);
|
|
488
|
+
this.renderSnapshot();
|
|
489
|
+
}
|
|
490
|
+
else if (controller.queueAfterCommand?.(payload)) {
|
|
491
|
+
this.renderSnapshot();
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
this.pushUserRow(payload);
|
|
495
|
+
const agentInput = payload.images.length > 0 ? payload : trimmed;
|
|
496
|
+
void controller.runTurn(agentInput, process.cwd()).finally(() => this.renderSnapshot());
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
async handleCommand(command) {
|
|
500
|
+
const [name, ...rest] = command.slice(1).split(/\s+/);
|
|
501
|
+
const args = rest.join(" ");
|
|
502
|
+
// pi-tui-local commands first (renderer-specific modes).
|
|
503
|
+
if (name === "fullscreen") {
|
|
504
|
+
if (this.fullscreen) {
|
|
505
|
+
this.exitFullscreen();
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
if (this.tui.mode === "fullscreen") {
|
|
509
|
+
this.pushNotice("Already in fullscreen mode");
|
|
510
|
+
this.renderSnapshot();
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
void this.enterFullscreen();
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
const skillInvocation = this.options.skillRegistry
|
|
517
|
+
? parseSkillInvocation(command, this.options.skillRegistry)
|
|
518
|
+
: undefined;
|
|
519
|
+
if (skillInvocation) {
|
|
520
|
+
this.pushUserRow({ text: command, images: [] });
|
|
521
|
+
const controller = this.options.controller;
|
|
522
|
+
if (controller.isRunning()) {
|
|
523
|
+
controller.steer(skillInvocation.actualPrompt);
|
|
524
|
+
this.renderSnapshot();
|
|
525
|
+
}
|
|
526
|
+
else {
|
|
527
|
+
void controller.runTurn(skillInvocation.actualPrompt, process.cwd()).finally(() => this.renderSnapshot());
|
|
528
|
+
}
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
// Everything else routes through the shared registry (21 builtin
|
|
532
|
+
// commands + MCP dynamic prompts + skill fallback) — the same surface
|
|
533
|
+
// the Ink TUI exposed.
|
|
534
|
+
const compactCommand = name === "compact";
|
|
535
|
+
let compactSignal;
|
|
536
|
+
try {
|
|
537
|
+
if (compactCommand)
|
|
538
|
+
compactSignal = this.options.controller.beginCommandActivity?.("compact");
|
|
539
|
+
const ctx = this.buildSlashContext(compactSignal);
|
|
540
|
+
const outcome = await slashRegistry.execute(`/${name}${args ? ` ${args}` : ""}`, ctx);
|
|
541
|
+
if (compactCommand) {
|
|
542
|
+
const text = outcome.result ?? (outcome.handled ? undefined : `Unknown command: /${name}`);
|
|
543
|
+
const compactionSummary = outcome.detail?.kind === "compaction-summary"
|
|
544
|
+
? outcome.detail.content.trim()
|
|
545
|
+
: "";
|
|
546
|
+
this.options.controller.finishCommandActivity?.("compact", text ? {
|
|
547
|
+
key: `compact-${Date.now()}`,
|
|
548
|
+
role: text.startsWith("Error:") ? "error" : "assistant",
|
|
549
|
+
content: text,
|
|
550
|
+
syntheticKind: text.startsWith("Error:")
|
|
551
|
+
? undefined
|
|
552
|
+
: compactionSummary
|
|
553
|
+
? "ui_compact_summary"
|
|
554
|
+
: "ui_notice",
|
|
555
|
+
compactionSummary: compactionSummary || undefined,
|
|
556
|
+
} : undefined);
|
|
557
|
+
void this.options.controller.drainQueuedInputs?.(process.cwd());
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
if (outcome.inject) {
|
|
561
|
+
// Command produced model-facing input (e.g. /rewind restore).
|
|
562
|
+
void this.handleSubmit(outcome.inject);
|
|
563
|
+
}
|
|
564
|
+
else if (outcome.result) {
|
|
565
|
+
this.pushNotice(outcome.result);
|
|
566
|
+
}
|
|
567
|
+
else if (!outcome.handled) {
|
|
568
|
+
this.pushNotice(`Unknown command: /${name}`);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
catch (error) {
|
|
572
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
573
|
+
if (compactCommand && compactSignal) {
|
|
574
|
+
this.options.controller.finishCommandActivity?.("compact", {
|
|
575
|
+
key: `compact-error-${Date.now()}`,
|
|
576
|
+
role: "error",
|
|
577
|
+
content: text,
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
this.pushNotice(text);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
finally {
|
|
585
|
+
if (compactSignal && this.options.controller.getCommandActivity?.()?.kind === "compact") {
|
|
586
|
+
this.options.controller.finishCommandActivity?.("compact");
|
|
587
|
+
}
|
|
588
|
+
this.renderSnapshot();
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
buildSlashContext(compactionAbortSignal) {
|
|
592
|
+
const options = this.options;
|
|
593
|
+
const agent = options.agent;
|
|
594
|
+
const addMessage = (role, content) => {
|
|
595
|
+
options.controller.appendDisplayMessage({
|
|
596
|
+
key: `cmd-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
|
|
597
|
+
role,
|
|
598
|
+
content,
|
|
599
|
+
syntheticKind: role === "assistant" ? "ui_notice" : undefined,
|
|
600
|
+
});
|
|
601
|
+
};
|
|
602
|
+
return {
|
|
603
|
+
agent,
|
|
604
|
+
addMessage,
|
|
605
|
+
clearMessages: () => options.controller.clearTranscript(),
|
|
606
|
+
cwd: process.cwd(),
|
|
607
|
+
exit: () => this.dispose(),
|
|
608
|
+
// Embedded/test hosts created before session switching may only expose
|
|
609
|
+
// the constructor manager; production controllers provide the live one.
|
|
610
|
+
sessionManager: (options.controller.getSessionManager?.() ?? options.sessionManager),
|
|
611
|
+
createProvider: options.createProvider,
|
|
612
|
+
openPicker: (mode, providerId) => {
|
|
613
|
+
if (mode === "model" || mode === "provider" || mode === "theme") {
|
|
614
|
+
// A bare picker command can still arrive from a fast Enter or a
|
|
615
|
+
// non-composer command host. Keep pi-tui on the shared inline
|
|
616
|
+
// command surface instead of falling back to a centered overlay.
|
|
617
|
+
this.composer.replaceDraft(mode === "model" ? "/model " : mode === "provider" ? "/provider " : "/theme ");
|
|
618
|
+
this.editor.refreshAutocomplete();
|
|
619
|
+
}
|
|
620
|
+
else if (mode === "key" && providerId) {
|
|
621
|
+
this.openProviderKeyPhase(providerId);
|
|
622
|
+
}
|
|
623
|
+
else if (mode === "skill") {
|
|
624
|
+
this.openSkills();
|
|
625
|
+
}
|
|
626
|
+
else if (mode === "feishu-setup") {
|
|
627
|
+
this.openFeishuSetup();
|
|
628
|
+
}
|
|
629
|
+
},
|
|
630
|
+
registry: options.registry,
|
|
631
|
+
skillRegistry: options.skillRegistry,
|
|
632
|
+
bashAllowlist: options.bashAllowlist,
|
|
633
|
+
settingsManager: options.settingsManager,
|
|
634
|
+
hookController: options.hookController,
|
|
635
|
+
mcpManager: options.mcpManager,
|
|
636
|
+
lspService: options.lspService,
|
|
637
|
+
flushMemory: options.flushMemory,
|
|
638
|
+
runMemoryCompaction: options.runMemoryCompaction,
|
|
639
|
+
runMemorySummary: options.runMemorySummary,
|
|
640
|
+
runMemoryRefresh: options.runMemoryRefresh,
|
|
641
|
+
getThemeMode: () => this.themeMode,
|
|
642
|
+
getResolvedTheme: () => this.resolvedTheme(),
|
|
643
|
+
setThemeMode: (mode) => this.applyThemeMode(mode),
|
|
644
|
+
openFeedback: (initialDescription) => this.openFeedback(initialDescription),
|
|
645
|
+
openStats: () => this.openStats(),
|
|
646
|
+
...(this.tui.mode === "fullscreen"
|
|
647
|
+
? { openContextInfo: (snapshot) => this.openContextInfo(snapshot) }
|
|
648
|
+
: {}),
|
|
649
|
+
handleGoalCommand: (input) => options.controller.handleGoalCommand(input, process.cwd()),
|
|
650
|
+
openRewindPicker: () => this.openRewindPicker(),
|
|
651
|
+
fillComposer: (text) => {
|
|
652
|
+
this.composer.replaceDraft(text);
|
|
653
|
+
this.tui.setFocus(this.editor);
|
|
654
|
+
},
|
|
655
|
+
rebuildTranscript: () => options.controller.rebuildTranscriptFromAgent(),
|
|
656
|
+
openSessionPicker: () => this.openSessionPicker(),
|
|
657
|
+
compactionProgress: () => { },
|
|
658
|
+
compactionAbortSignal,
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
pushUserRow(payload) {
|
|
662
|
+
this.appendTranscriptRow({
|
|
663
|
+
key: `user-${Date.now()}`,
|
|
664
|
+
role: "user",
|
|
665
|
+
content: formatImageUserDisplayText(payload.displayText ?? payload.text, payload.images.length, payload.imageDisplayStart),
|
|
666
|
+
images: displayImagesFromPayload(payload),
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
resolvedTheme() {
|
|
670
|
+
return this.themeMode === "auto" ? this.detectedTheme : this.themeMode;
|
|
671
|
+
}
|
|
672
|
+
applyThemeMode(mode) {
|
|
673
|
+
if (this.themeMode === mode) {
|
|
674
|
+
// Persist an explicit selection even when it resolves to the palette
|
|
675
|
+
// already on screen (e.g. auto-dark -> forced dark).
|
|
676
|
+
this.options.callbacks.onThemeModeChange?.(mode);
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
this.themeMode = mode;
|
|
680
|
+
// Preserve object identity so already-open panels that received this
|
|
681
|
+
// palette also repaint in place.
|
|
682
|
+
Object.assign(this.theme, resolveThemePalette(mode, this.detectedTheme, this.themeOverrides).palette);
|
|
683
|
+
this.options.callbacks.onThemeModeChange?.(mode);
|
|
684
|
+
this.tui.invalidate();
|
|
685
|
+
this.renderSnapshot();
|
|
686
|
+
}
|
|
687
|
+
pushNotice(text) {
|
|
688
|
+
this.appendTranscriptRow({
|
|
689
|
+
key: `notice-${Date.now()}`,
|
|
690
|
+
role: "assistant",
|
|
691
|
+
content: text,
|
|
692
|
+
syntheticKind: "ui_notice",
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
appendTranscriptRow(message) {
|
|
696
|
+
this.options.controller.appendDisplayMessage(message);
|
|
697
|
+
}
|
|
698
|
+
streamingMounted = false;
|
|
699
|
+
/** One display policy feeds both committed history and the live row pool. */
|
|
700
|
+
transcriptRenderOptions() {
|
|
701
|
+
return {
|
|
702
|
+
showReasoning: this.showReasoning,
|
|
703
|
+
verboseTrace: this.verboseTrace,
|
|
704
|
+
traceInteraction: this.traceInteraction,
|
|
705
|
+
dimFromMessageIndex: this.rewindPreviewMessageIndex,
|
|
706
|
+
theme: this.transcriptTheme,
|
|
707
|
+
// Stable identity lets the settled transcript cache survive composer-only
|
|
708
|
+
// frames. The stateful Markdown instance is still updated on cache misses.
|
|
709
|
+
markdownRenderer: this.markdownRenderer,
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
renderSnapshot() {
|
|
713
|
+
if (this.disposed)
|
|
714
|
+
return;
|
|
715
|
+
const columns = this.tui.terminal.columns || process.stdout.columns || 80;
|
|
716
|
+
this.updateStreamingRegion(columns);
|
|
717
|
+
this.tui.requestRender();
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* The streaming preview is a persistent row-pool component mounted as the
|
|
721
|
+
* transcript's LAST child (upstream AssistantMessageComponent pattern).
|
|
722
|
+
* Rows are only ever setText — re-creating components per frame is what
|
|
723
|
+
* leaked every intermediate prefix into scrollback (verified with a
|
|
724
|
+
* VirtualTerminal matrix). On turn end it collapses to zero rows in the
|
|
725
|
+
* same frame the settled message commits, so the full answer replaces the
|
|
726
|
+
* preview with no duplicate.
|
|
727
|
+
*/
|
|
728
|
+
updateStreamingRegion(columns) {
|
|
729
|
+
const controller = this.options.controller;
|
|
730
|
+
const tail = controller.isRunning() ? controller.getStreamingTail() : null;
|
|
731
|
+
const commandActivity = controller.getCommandActivity?.() ?? null;
|
|
732
|
+
if (tail) {
|
|
733
|
+
if (!this.streamingMounted) {
|
|
734
|
+
this.streamingMounted = true;
|
|
735
|
+
this.streamingMessage.startSpinner();
|
|
736
|
+
}
|
|
737
|
+
this.streamingMessage.noteWidth(columns);
|
|
738
|
+
this.streamingMessage.update(tail, columns, this.transcriptRenderOptions());
|
|
739
|
+
}
|
|
740
|
+
else if (commandActivity) {
|
|
741
|
+
if (!this.streamingMounted) {
|
|
742
|
+
this.streamingMounted = true;
|
|
743
|
+
this.streamingMessage.startSpinner();
|
|
744
|
+
}
|
|
745
|
+
this.streamingMessage.noteWidth(columns);
|
|
746
|
+
this.streamingMessage.updateCommandActivity(commandActivity.kind === "compact" ? "Compacting" : commandActivity.kind, commandActivity.status === "cancelling", columns);
|
|
747
|
+
}
|
|
748
|
+
else if (this.streamingMounted) {
|
|
749
|
+
this.streamingMounted = false;
|
|
750
|
+
this.streamingMessage.clearToNothing();
|
|
751
|
+
}
|
|
752
|
+
this.syncAnimationClock();
|
|
753
|
+
}
|
|
754
|
+
syncAnimationClock() {
|
|
755
|
+
this.animationClock.setActive(this.fullscreen === null
|
|
756
|
+
&& (this.streamingMessage.isAnimationActive() || this.tasksPane.isAnimationActive()));
|
|
757
|
+
}
|
|
758
|
+
// ---- Fullscreen mode ----------------------------------------------------
|
|
759
|
+
fullscreen = null;
|
|
760
|
+
/**
|
|
761
|
+
* Switch to the alternate-screen transcript view. The main screen stays
|
|
762
|
+
* mounted but paused (stopped renderer); both modes subscribe to the same
|
|
763
|
+
* controller so the transcript stays in sync. Escape / Ctrl+C / /fullscreen
|
|
764
|
+
* return here.
|
|
765
|
+
*/
|
|
766
|
+
async enterFullscreen() {
|
|
767
|
+
if (this.fullscreen)
|
|
768
|
+
return;
|
|
769
|
+
this.animationClock.setActive(false);
|
|
770
|
+
this.tui.stop();
|
|
771
|
+
const { FullscreenApp } = await import("./fullscreen.js");
|
|
772
|
+
this.fullscreen = new FullscreenApp({
|
|
773
|
+
controller: this.options.controller,
|
|
774
|
+
agent: this.options.agent,
|
|
775
|
+
onExit: () => this.exitFullscreen(),
|
|
776
|
+
onCommand: (command) => {
|
|
777
|
+
// Pickers and dialogs are owned by the main-screen TUI. Return to it
|
|
778
|
+
// before executing so every slash command uses the same registry and
|
|
779
|
+
// never leaks into the model as ordinary fullscreen input.
|
|
780
|
+
this.exitFullscreen();
|
|
781
|
+
void this.handleCommand(command);
|
|
782
|
+
},
|
|
783
|
+
terminal: this.options.terminal,
|
|
784
|
+
traceInteraction: this.traceInteraction,
|
|
785
|
+
composer: this.composer,
|
|
786
|
+
getTheme: () => this.theme,
|
|
787
|
+
});
|
|
788
|
+
this.fullscreen.start();
|
|
789
|
+
}
|
|
790
|
+
exitFullscreen() {
|
|
791
|
+
if (!this.fullscreen)
|
|
792
|
+
return;
|
|
793
|
+
this.fullscreen.dispose();
|
|
794
|
+
this.fullscreen = null;
|
|
795
|
+
this.composer.attachEditor(this.editor);
|
|
796
|
+
this.composer.setOpenImageHandler((item) => this.openImageViewer(item));
|
|
797
|
+
this.tui.start();
|
|
798
|
+
this.renderSnapshot();
|
|
799
|
+
}
|
|
800
|
+
// ---- Overlays -----------------------------------------------------------
|
|
801
|
+
openImageViewer(image) {
|
|
802
|
+
let handle;
|
|
803
|
+
const close = () => {
|
|
804
|
+
handle?.hide();
|
|
805
|
+
this.tui.setFocus(this.editor);
|
|
806
|
+
this.renderSnapshot();
|
|
807
|
+
};
|
|
808
|
+
const component = new ImageViewerComponent(image, close, () => this.tui.terminal.rows, () => this.theme);
|
|
809
|
+
handle = this.tui.showOverlay(component, {
|
|
810
|
+
anchor: "center",
|
|
811
|
+
width: "70%",
|
|
812
|
+
minWidth: 36,
|
|
813
|
+
maxWidth: 110,
|
|
814
|
+
maxHeight: "85%",
|
|
815
|
+
margin: { top: 2, right: 1, bottom: 2, left: 1 },
|
|
816
|
+
dismissOnOutsideClick: true,
|
|
817
|
+
});
|
|
818
|
+
this.tui.setFocus(component);
|
|
819
|
+
}
|
|
820
|
+
openStats() {
|
|
821
|
+
let handle;
|
|
822
|
+
const component = new StatsPanelComponent(collectUsageStatsBundle(), {
|
|
823
|
+
getTerminalRows: () => this.tui.terminal.rows,
|
|
824
|
+
onClose: () => handle?.hide(),
|
|
825
|
+
onRender: () => this.renderSnapshot(),
|
|
826
|
+
theme: this.theme,
|
|
827
|
+
});
|
|
828
|
+
handle = this.tui.showOverlay(component, {
|
|
829
|
+
anchor: "center",
|
|
830
|
+
width: "65%",
|
|
831
|
+
minWidth: 44,
|
|
832
|
+
maxWidth: 100,
|
|
833
|
+
maxHeight: 30,
|
|
834
|
+
margin: { top: 2, right: 0, bottom: 2, left: 0 },
|
|
835
|
+
});
|
|
836
|
+
this.tui.setFocus(component);
|
|
837
|
+
}
|
|
838
|
+
openSkills() {
|
|
839
|
+
const skillRegistry = this.options.skillRegistry;
|
|
840
|
+
if (!skillRegistry) {
|
|
841
|
+
this.pushNotice("No skill registry is attached to this session.");
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
let handle;
|
|
845
|
+
const component = new SkillsPanelComponent(skillRegistry, {
|
|
846
|
+
getTerminalRows: () => this.tui.terminal.rows,
|
|
847
|
+
onClose: () => handle?.hide(),
|
|
848
|
+
onRender: () => this.renderSnapshot(),
|
|
849
|
+
onSkillsChanged: () => {
|
|
850
|
+
this.options.agent.setSkillSummaries(skillRegistry.summaries());
|
|
851
|
+
this.editor.refreshAutocomplete();
|
|
852
|
+
this.renderSnapshot();
|
|
853
|
+
},
|
|
854
|
+
theme: this.theme,
|
|
855
|
+
});
|
|
856
|
+
handle = this.tui.showOverlay(component, {
|
|
857
|
+
anchor: "center",
|
|
858
|
+
width: "65%",
|
|
859
|
+
minWidth: 40,
|
|
860
|
+
maxWidth: 160,
|
|
861
|
+
maxHeight: 34,
|
|
862
|
+
margin: { top: 3, right: 0, bottom: 3, left: 0 },
|
|
863
|
+
dismissOnOutsideClick: true,
|
|
864
|
+
});
|
|
865
|
+
this.tui.setFocus(component);
|
|
866
|
+
}
|
|
867
|
+
openSessionPicker() {
|
|
868
|
+
if (this.options.controller.isBusy?.()) {
|
|
869
|
+
this.appendTranscriptRow({
|
|
870
|
+
key: `session-error-${Date.now()}`,
|
|
871
|
+
role: "error",
|
|
872
|
+
content: "Stop the current run before switching sessions.",
|
|
873
|
+
});
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
const activeFile = this.activeSessionManager().getSessionFile();
|
|
877
|
+
const currentSessions = SessionManager.summarizeSessionsForCwd(process.cwd());
|
|
878
|
+
const allSessions = SessionManager.listAllSessions();
|
|
879
|
+
let handle;
|
|
880
|
+
const close = () => {
|
|
881
|
+
handle?.hide();
|
|
882
|
+
this.tui.setFocus(this.editor);
|
|
883
|
+
this.renderSnapshot();
|
|
884
|
+
};
|
|
885
|
+
const fail = (prefix, error) => {
|
|
886
|
+
close();
|
|
887
|
+
this.appendTranscriptRow({
|
|
888
|
+
key: `session-error-${Date.now()}`,
|
|
889
|
+
role: "error",
|
|
890
|
+
content: `${prefix}: ${error || "unknown error"}`,
|
|
891
|
+
});
|
|
892
|
+
};
|
|
893
|
+
const component = new SessionPickerComponent({
|
|
894
|
+
currentCwd: process.cwd(),
|
|
895
|
+
currentSessions,
|
|
896
|
+
allSessions,
|
|
897
|
+
activeFile,
|
|
898
|
+
getTerminalRows: () => this.tui.terminal.rows,
|
|
899
|
+
onClose: close,
|
|
900
|
+
onNewSession: () => {
|
|
901
|
+
const outcome = this.options.controller.createFreshSession(process.cwd());
|
|
902
|
+
if (!outcome.ok) {
|
|
903
|
+
fail("Failed to start a new session", outcome.error);
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
this.composer.setScope({
|
|
907
|
+
sessionFile: this.activeSessionManager().getSessionFile(),
|
|
908
|
+
cwd: process.cwd(),
|
|
909
|
+
}, nextImageDisplayLabelStart(this.options.controller.getTranscript()));
|
|
910
|
+
this.viewportScroll?.scrollToEnd();
|
|
911
|
+
close();
|
|
912
|
+
},
|
|
913
|
+
onSelect: (file) => {
|
|
914
|
+
if (file === this.activeSessionManager().getSessionFile()) {
|
|
915
|
+
close();
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
const summary = allSessions.find((session) => session.file === file);
|
|
919
|
+
const displayName = summary?.title || summary?.preview || sessionBasename(file) || "Session";
|
|
920
|
+
const outcome = this.options.controller.switchSession({
|
|
921
|
+
targetFile: file,
|
|
922
|
+
notice: `⤷ Resumed session: ${displayName}`,
|
|
923
|
+
});
|
|
924
|
+
if (!outcome.ok) {
|
|
925
|
+
fail("Failed to switch session", outcome.error);
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
this.composer.setScope({
|
|
929
|
+
sessionFile: this.activeSessionManager().getSessionFile(),
|
|
930
|
+
cwd: process.cwd(),
|
|
931
|
+
}, nextImageDisplayLabelStart(this.options.controller.getTranscript()));
|
|
932
|
+
this.viewportScroll?.scrollToEnd();
|
|
933
|
+
close();
|
|
934
|
+
},
|
|
935
|
+
onRender: () => this.renderSnapshot(),
|
|
936
|
+
theme: this.theme,
|
|
937
|
+
});
|
|
938
|
+
handle = this.tui.showOverlay(component, {
|
|
939
|
+
anchor: "center",
|
|
940
|
+
width: "70%",
|
|
941
|
+
minWidth: 40,
|
|
942
|
+
maxWidth: 120,
|
|
943
|
+
maxHeight: 30,
|
|
944
|
+
margin: { top: 2, right: 0, bottom: 2, left: 0 },
|
|
945
|
+
dismissOnOutsideClick: true,
|
|
946
|
+
});
|
|
947
|
+
this.tui.setFocus(component);
|
|
948
|
+
}
|
|
949
|
+
openContextInfo(snapshot) {
|
|
950
|
+
let handle;
|
|
951
|
+
const messages = this.options.agent.messages;
|
|
952
|
+
const turnCount = messages.filter((message) => message.role === "user").length;
|
|
953
|
+
const toolCallCount = messages.reduce((count, message) => (count + (message.role === "assistant" ? message.toolCalls?.length ?? 0 : 0)), 0);
|
|
954
|
+
const compactionCount = this.options.agent.getCompactionStats().fired;
|
|
955
|
+
const mcpStates = this.options.mcpManager?.getStates?.() ?? [];
|
|
956
|
+
const sessionId = sessionBasename(this.activeSessionManager().getSessionFile());
|
|
957
|
+
const component = new ContextInfoComponent({
|
|
958
|
+
snapshot,
|
|
959
|
+
sessionId,
|
|
960
|
+
cwd: friendlyCwd(process.cwd()),
|
|
961
|
+
thinking: this.options.agent.thinking,
|
|
962
|
+
permissionMode: this.options.agent.mode,
|
|
963
|
+
turnCount,
|
|
964
|
+
toolCallCount,
|
|
965
|
+
compactionCount,
|
|
966
|
+
mcpServerCount: mcpStates.length,
|
|
967
|
+
}, {
|
|
968
|
+
getTerminalRows: () => this.tui.terminal.rows,
|
|
969
|
+
onClose: () => handle?.hide(),
|
|
970
|
+
onRender: () => this.renderSnapshot(),
|
|
971
|
+
...(sessionId ? { copySessionId: () => copyToClipboard(sessionId) } : {}),
|
|
972
|
+
theme: this.theme,
|
|
973
|
+
});
|
|
974
|
+
handle = this.tui.showOverlay(component, {
|
|
975
|
+
anchor: "center",
|
|
976
|
+
width: "65%",
|
|
977
|
+
minWidth: 44,
|
|
978
|
+
maxWidth: 100,
|
|
979
|
+
maxHeight: 30,
|
|
980
|
+
margin: { top: 2, right: 0, bottom: 2, left: 0 },
|
|
981
|
+
});
|
|
982
|
+
this.tui.setFocus(component);
|
|
983
|
+
}
|
|
984
|
+
openProviderKeyPhase(providerId) {
|
|
985
|
+
const registry = this.options.registry;
|
|
986
|
+
const provider = registry?.getConfigured().find((candidate) => candidate.id === providerId);
|
|
987
|
+
if (!registry || !provider) {
|
|
988
|
+
this.pushNotice(`Provider ${providerId} could not be prepared for API key setup.`);
|
|
989
|
+
this.renderSnapshot();
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
this.closeProviderKeyPhase(false);
|
|
993
|
+
const input = new Input({ prompt: "◆ ", mask: "•" });
|
|
994
|
+
const component = new ProviderKeyInputComponent(input, provider.name, () => this.theme);
|
|
995
|
+
const phase = { input, component, submitting: false };
|
|
996
|
+
this.providerKeyPhase = phase;
|
|
997
|
+
this.composerSlot.removeChild(this.editor);
|
|
998
|
+
this.composerSlot.addChild(component);
|
|
999
|
+
input.onEscape = () => this.closeProviderKeyPhase(true);
|
|
1000
|
+
input.onBackspaceAtStart = () => this.closeProviderKeyPhase(true);
|
|
1001
|
+
input.onSubmit = (rawKey) => {
|
|
1002
|
+
const key = rawKey.replace(/[\r\n\t]/g, "").trim();
|
|
1003
|
+
if (!key || phase.submitting || this.providerKeyPhase !== phase)
|
|
1004
|
+
return;
|
|
1005
|
+
phase.submitting = true;
|
|
1006
|
+
this.closeProviderKeyPhase(false);
|
|
1007
|
+
void this.saveProviderKey(providerId, key);
|
|
1008
|
+
};
|
|
1009
|
+
this.tui.setFocus(input);
|
|
1010
|
+
this.renderSnapshot();
|
|
1011
|
+
}
|
|
1012
|
+
closeProviderKeyPhase(returnToProviderPicker) {
|
|
1013
|
+
const phase = this.providerKeyPhase;
|
|
1014
|
+
if (!phase)
|
|
1015
|
+
return;
|
|
1016
|
+
// Erase the component-owned copy before releasing the phase. The caller
|
|
1017
|
+
// may retain the submitted value just long enough to persist it, but UI
|
|
1018
|
+
// state never keeps the raw credential after this point.
|
|
1019
|
+
phase.input.setValue("");
|
|
1020
|
+
phase.input.onSubmit = undefined;
|
|
1021
|
+
phase.input.onEscape = undefined;
|
|
1022
|
+
phase.input.onBackspaceAtStart = undefined;
|
|
1023
|
+
this.composerSlot.removeChild(phase.component);
|
|
1024
|
+
this.composerSlot.addChild(this.editor);
|
|
1025
|
+
this.providerKeyPhase = null;
|
|
1026
|
+
this.composer.replaceDraft(returnToProviderPicker ? "/provider " : "");
|
|
1027
|
+
if (returnToProviderPicker)
|
|
1028
|
+
this.editor.refreshAutocomplete();
|
|
1029
|
+
this.tui.setFocus(this.editor);
|
|
1030
|
+
this.renderSnapshot();
|
|
1031
|
+
}
|
|
1032
|
+
async saveProviderKey(providerId, key) {
|
|
1033
|
+
const registry = this.options.registry;
|
|
1034
|
+
if (!registry)
|
|
1035
|
+
return;
|
|
1036
|
+
try {
|
|
1037
|
+
registry.updateProviderKey(providerId, key);
|
|
1038
|
+
const provider = registry.getConfigured().find((candidate) => candidate.id === providerId);
|
|
1039
|
+
if (!provider)
|
|
1040
|
+
throw new Error("provider disappeared after saving credentials");
|
|
1041
|
+
// Saving credentials configures the provider; it must not partially
|
|
1042
|
+
// mutate the active route. Provider + model + thinking + prompt + session
|
|
1043
|
+
// move together only through the shared /model switch transaction.
|
|
1044
|
+
registry.setDefault(providerId);
|
|
1045
|
+
this.pushNotice(`API key updated for ${provider.name} to ${maskKey(key)}. Use /model to select a compatible model.`);
|
|
1046
|
+
}
|
|
1047
|
+
catch (error) {
|
|
1048
|
+
const message = (error instanceof Error ? error.message : String(error)).replaceAll(key, "[redacted]");
|
|
1049
|
+
this.pushNotice(`Failed to update API key for ${providerId}: ${message}`);
|
|
1050
|
+
}
|
|
1051
|
+
this.renderSnapshot();
|
|
1052
|
+
}
|
|
1053
|
+
openRewindPicker() {
|
|
1054
|
+
if (this.rewindPhase || this.providerKeyPhase || this.disposed)
|
|
1055
|
+
return;
|
|
1056
|
+
const controller = this.options.controller;
|
|
1057
|
+
const points = controller.isRunning() ? [] : this.collectRewindPoints();
|
|
1058
|
+
if (!controller.isRunning() && points.length === 0) {
|
|
1059
|
+
this.pushNotice("Nothing to rewind: no user messages in this session.");
|
|
1060
|
+
this.renderSnapshot();
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
const draft = this.composer.snapshotDraft();
|
|
1064
|
+
const component = new RewindPickerComponent(controller.isRunning() ? "cancel-offer" : "picker", points, {
|
|
1065
|
+
getTerminalRows: () => this.tui.terminal.rows,
|
|
1066
|
+
onPreview: (point, scope) => this.previewRewindPoint(point, scope),
|
|
1067
|
+
onScopeChange: (point, scope) => this.previewRewindPoint(point, scope),
|
|
1068
|
+
onCancel: () => this.closeRewindPicker(true),
|
|
1069
|
+
onCancelRun: () => void this.cancelRunForRewind(component),
|
|
1070
|
+
onConfirm: (point, scope) => void this.executeRewindFromPicker(component, point, scope),
|
|
1071
|
+
onRender: () => this.renderSnapshot(),
|
|
1072
|
+
theme: this.theme,
|
|
1073
|
+
});
|
|
1074
|
+
this.rewindPhase = {
|
|
1075
|
+
component,
|
|
1076
|
+
draft,
|
|
1077
|
+
previousScrollTop: this.viewportScroll?.scrollTop ?? 0,
|
|
1078
|
+
};
|
|
1079
|
+
this.composerSlot.removeChild(this.editor);
|
|
1080
|
+
this.composerSlot.addChild(component);
|
|
1081
|
+
this.tui.setFocus(component);
|
|
1082
|
+
const selected = component.getSelectedPoint();
|
|
1083
|
+
if (selected)
|
|
1084
|
+
this.previewRewindPoint(selected, "all");
|
|
1085
|
+
this.renderSnapshot();
|
|
1086
|
+
}
|
|
1087
|
+
collectRewindPoints() {
|
|
1088
|
+
const session = this.options.controller.getSessionManager();
|
|
1089
|
+
const turns = session.listUserTurns();
|
|
1090
|
+
const checkpoints = session.getCheckpoints();
|
|
1091
|
+
return turns.map((turn, turnIndex) => ({
|
|
1092
|
+
turn,
|
|
1093
|
+
turnIndex,
|
|
1094
|
+
fileCount: checkpoints.filesTouchedAt(turn.id).length,
|
|
1095
|
+
}));
|
|
1096
|
+
}
|
|
1097
|
+
async cancelRunForRewind(component) {
|
|
1098
|
+
if (this.rewindPhase?.component !== component || component.getPhase() !== "cancel-offer")
|
|
1099
|
+
return;
|
|
1100
|
+
component.showLoading();
|
|
1101
|
+
this.options.controller.cancelActiveRun("Cancelled before rewind");
|
|
1102
|
+
await this.waitForControllerIdle();
|
|
1103
|
+
if (this.rewindPhase?.component !== component || this.disposed)
|
|
1104
|
+
return;
|
|
1105
|
+
component.showPicker(this.collectRewindPoints());
|
|
1106
|
+
}
|
|
1107
|
+
waitForControllerIdle() {
|
|
1108
|
+
const controller = this.options.controller;
|
|
1109
|
+
if (!controller.isRunning())
|
|
1110
|
+
return Promise.resolve();
|
|
1111
|
+
return new Promise((resolve) => {
|
|
1112
|
+
const unsubscribe = controller.subscribe(() => {
|
|
1113
|
+
if (controller.isRunning())
|
|
1114
|
+
return;
|
|
1115
|
+
unsubscribe();
|
|
1116
|
+
resolve();
|
|
1117
|
+
});
|
|
1118
|
+
// The run can settle between the first check and subscription.
|
|
1119
|
+
if (!controller.isRunning()) {
|
|
1120
|
+
unsubscribe();
|
|
1121
|
+
resolve();
|
|
1122
|
+
}
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
async executeRewindFromPicker(component, point, scope) {
|
|
1126
|
+
if (this.rewindPhase?.component !== component || component.getPhase() === "executing")
|
|
1127
|
+
return;
|
|
1128
|
+
component.showExecuting();
|
|
1129
|
+
try {
|
|
1130
|
+
const result = await this.options.controller.rewindToTurn(point.turn.id, scope);
|
|
1131
|
+
const targetText = result.target.text;
|
|
1132
|
+
// Swap the panel, final composer value, transcript dim, and focus before
|
|
1133
|
+
// requesting the success paint. This prevents an empty-composer frame
|
|
1134
|
+
// between "Rewinding..." and the restored prompt.
|
|
1135
|
+
this.closeRewindPicker(scope === "code", scope === "code" ? undefined : targetText, false);
|
|
1136
|
+
this.viewportScroll?.scrollToEnd();
|
|
1137
|
+
const fileCount = result.files.restored.length + result.files.deleted.length;
|
|
1138
|
+
const failed = result.files.failed.length;
|
|
1139
|
+
if (scope === "code") {
|
|
1140
|
+
this.pushNotice(failed > 0
|
|
1141
|
+
? `Files restored with ${failed} failure${failed === 1 ? "" : "s"}.`
|
|
1142
|
+
: fileCount > 0
|
|
1143
|
+
? `Restored ${fileCount} file${fileCount === 1 ? "" : "s"}.`
|
|
1144
|
+
: "No tracked file edits to undo.");
|
|
1145
|
+
}
|
|
1146
|
+
else {
|
|
1147
|
+
const fileSuffix = scope === "all"
|
|
1148
|
+
? failed > 0
|
|
1149
|
+
? ` · ${failed} file restore failure${failed === 1 ? "" : "s"}`
|
|
1150
|
+
: fileCount > 0
|
|
1151
|
+
? ` · ${fileCount} file${fileCount === 1 ? "" : "s"} restored`
|
|
1152
|
+
: " · no tracked file edits"
|
|
1153
|
+
: "";
|
|
1154
|
+
this.pushNotice(`Reverted conversation${fileSuffix}`);
|
|
1155
|
+
}
|
|
1156
|
+
this.renderSnapshot();
|
|
1157
|
+
}
|
|
1158
|
+
catch (error) {
|
|
1159
|
+
if (this.rewindPhase?.component !== component)
|
|
1160
|
+
return;
|
|
1161
|
+
component.showError(error instanceof Error ? error.message : String(error));
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
closeRewindPicker(restoreDraft, replacementText, render = true) {
|
|
1165
|
+
const phase = this.rewindPhase;
|
|
1166
|
+
if (!phase)
|
|
1167
|
+
return;
|
|
1168
|
+
this.composerSlot.removeChild(phase.component);
|
|
1169
|
+
this.composerSlot.addChild(this.editor);
|
|
1170
|
+
this.rewindPhase = null;
|
|
1171
|
+
this.rewindPreviewMessageIndex = undefined;
|
|
1172
|
+
if (restoreDraft)
|
|
1173
|
+
this.composer.restoreDraft(phase.draft);
|
|
1174
|
+
else if (replacementText !== undefined)
|
|
1175
|
+
this.composer.replaceDraft(replacementText);
|
|
1176
|
+
if (restoreDraft) {
|
|
1177
|
+
this.viewportScroll?.scrollTo(phase.previousScrollTop, { disableFollow: true });
|
|
1178
|
+
}
|
|
1179
|
+
this.tui.setFocus(this.editor);
|
|
1180
|
+
if (render)
|
|
1181
|
+
this.renderSnapshot();
|
|
1182
|
+
}
|
|
1183
|
+
previewRewindPoint(point, scope) {
|
|
1184
|
+
if (!point) {
|
|
1185
|
+
this.rewindPreviewMessageIndex = undefined;
|
|
1186
|
+
this.renderSnapshot();
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
const transcript = this.options.controller.getTranscript();
|
|
1190
|
+
const userIndexes = transcript
|
|
1191
|
+
.map((message, index) => message.role === "user" ? index : -1)
|
|
1192
|
+
.filter((index) => index >= 0);
|
|
1193
|
+
const turnCount = this.options.controller.getSessionManager().listUserTurns().length;
|
|
1194
|
+
const relevantUsers = userIndexes.slice(Math.max(0, userIndexes.length - turnCount));
|
|
1195
|
+
const messageIndex = relevantUsers[point.turnIndex];
|
|
1196
|
+
this.rewindPreviewMessageIndex = scope === "code" ? undefined : messageIndex;
|
|
1197
|
+
if (messageIndex !== undefined && this.viewportScroll) {
|
|
1198
|
+
const columns = this.tui.terminal.columns || process.stdout.columns || 80;
|
|
1199
|
+
const rowsBefore = this.welcome.render(columns).length + projectTranscript(transcript.slice(0, messageIndex), { ...this.transcriptRenderOptions(), columns, trailingSpacer: false, dimFromMessageIndex: undefined }).rows.length;
|
|
1200
|
+
const centeredTop = rowsBefore - Math.floor(this.viewportScroll.viewportHeight / 2);
|
|
1201
|
+
this.viewportScroll.scrollTo(centeredTop, { disableFollow: true });
|
|
1202
|
+
}
|
|
1203
|
+
this.renderSnapshot();
|
|
1204
|
+
}
|
|
1205
|
+
planDialog(plan) {
|
|
1206
|
+
return new Promise((resolve) => {
|
|
1207
|
+
const dialog = new PlanDialogComponent(this.tui, plan, () => this.tui.terminal.rows, () => this.theme);
|
|
1208
|
+
const handle = this.tui.showOverlay(dialog, {
|
|
1209
|
+
anchor: "bottom-center",
|
|
1210
|
+
width: "100%",
|
|
1211
|
+
margin: { left: 1, right: 1 },
|
|
1212
|
+
});
|
|
1213
|
+
let settled = false;
|
|
1214
|
+
const finish = (decision) => {
|
|
1215
|
+
if (settled)
|
|
1216
|
+
return;
|
|
1217
|
+
settled = true;
|
|
1218
|
+
handle.hide();
|
|
1219
|
+
resolve(decision);
|
|
1220
|
+
};
|
|
1221
|
+
dialog.onApprove = (finalPlan) => finish({ action: "approve", plan: finalPlan });
|
|
1222
|
+
dialog.onReject = () => finish({ action: "reject", reason: "User rejected the plan." });
|
|
1223
|
+
this.tui.setFocus(dialog);
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
1226
|
+
openFeedback(initialDescription) {
|
|
1227
|
+
const collected = collectFeedback(this.options.agent, { description: "" });
|
|
1228
|
+
const { description: _description, ...base } = collected;
|
|
1229
|
+
let handle;
|
|
1230
|
+
const close = () => {
|
|
1231
|
+
handle?.hide();
|
|
1232
|
+
this.tui.setFocus(this.editor);
|
|
1233
|
+
this.renderSnapshot();
|
|
1234
|
+
};
|
|
1235
|
+
const recordResult = (result) => {
|
|
1236
|
+
if (result.kind === "success") {
|
|
1237
|
+
this.pushNotice(`Feedback submitted: ${result.url}`);
|
|
1238
|
+
}
|
|
1239
|
+
else if (result.kind === "error") {
|
|
1240
|
+
this.appendTranscriptRow({
|
|
1241
|
+
key: `feedback-error-${Date.now()}`,
|
|
1242
|
+
role: "error",
|
|
1243
|
+
content: `Feedback failed: ${result.message}`,
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
};
|
|
1247
|
+
const dialog = new FeedbackDialogComponent(this.tui, base, initialDescription, {
|
|
1248
|
+
getTerminalRows: () => this.tui.terminal.rows,
|
|
1249
|
+
getTheme: () => this.theme,
|
|
1250
|
+
submit: this.options.submitFeedback,
|
|
1251
|
+
onDismiss: close,
|
|
1252
|
+
onResult: recordResult,
|
|
1253
|
+
onRender: () => this.renderSnapshot(),
|
|
1254
|
+
});
|
|
1255
|
+
handle = this.tui.showOverlay(dialog, {
|
|
1256
|
+
anchor: "bottom-center",
|
|
1257
|
+
width: "100%",
|
|
1258
|
+
margin: { left: 1, right: 1 },
|
|
1259
|
+
});
|
|
1260
|
+
this.tui.setFocus(dialog);
|
|
1261
|
+
}
|
|
1262
|
+
openFeishuSetup() {
|
|
1263
|
+
if (this.feishuSetupPhase)
|
|
1264
|
+
return;
|
|
1265
|
+
let handle;
|
|
1266
|
+
let closed = false;
|
|
1267
|
+
const close = (result) => {
|
|
1268
|
+
if (closed)
|
|
1269
|
+
return;
|
|
1270
|
+
closed = true;
|
|
1271
|
+
handle?.hide();
|
|
1272
|
+
this.feishuSetupPhase = null;
|
|
1273
|
+
this.tui.setFocus(this.editor);
|
|
1274
|
+
if (result.kind === "completed") {
|
|
1275
|
+
this.appendTranscriptRow({
|
|
1276
|
+
key: `feishu-setup-${Date.now()}`,
|
|
1277
|
+
role: "assistant",
|
|
1278
|
+
content: result.summary,
|
|
1279
|
+
syntheticKind: "ui_notice",
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
else if (result.kind === "error") {
|
|
1283
|
+
this.appendTranscriptRow({
|
|
1284
|
+
key: `feishu-setup-error-${Date.now()}`,
|
|
1285
|
+
role: "error",
|
|
1286
|
+
content: `Feishu setup failed: ${result.message}`,
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
else {
|
|
1290
|
+
this.pushNotice("已取消 Feishu setup。");
|
|
1291
|
+
}
|
|
1292
|
+
this.renderSnapshot();
|
|
1293
|
+
};
|
|
1294
|
+
const dialog = new FeishuSetupDialogComponent({
|
|
1295
|
+
getTerminalRows: () => this.tui.terminal.rows,
|
|
1296
|
+
getTheme: () => this.theme,
|
|
1297
|
+
onResult: close,
|
|
1298
|
+
onRender: () => this.renderSnapshot(),
|
|
1299
|
+
});
|
|
1300
|
+
handle = this.tui.showOverlay(dialog, {
|
|
1301
|
+
anchor: "bottom-center",
|
|
1302
|
+
width: "100%",
|
|
1303
|
+
margin: { left: 1, right: 1 },
|
|
1304
|
+
});
|
|
1305
|
+
this.feishuSetupPhase = { component: dialog, handle };
|
|
1306
|
+
this.tui.setFocus(dialog);
|
|
1307
|
+
dialog.start();
|
|
1308
|
+
}
|
|
1309
|
+
openWorkflowInspector(item) {
|
|
1310
|
+
let handle;
|
|
1311
|
+
const getSnapshot = () => {
|
|
1312
|
+
const workflow = this.options.controller.getWorkflows().find((candidate) => candidate.runId === item.id);
|
|
1313
|
+
const group = this.options.controller.getSubagentGroups().find((candidate) => (candidate.kind === "workflow" && (candidate.runId === item.id || candidate.id === item.id)));
|
|
1314
|
+
const members = workflow?.snapshots.length
|
|
1315
|
+
? workflow.snapshots.map((snapshot) => ({
|
|
1316
|
+
subAgentId: snapshot.agentId,
|
|
1317
|
+
agentName: snapshot.agentName,
|
|
1318
|
+
nickname: snapshot.nickname,
|
|
1319
|
+
status: snapshot.status,
|
|
1320
|
+
category: snapshot.category,
|
|
1321
|
+
phase: snapshot.phase,
|
|
1322
|
+
route: snapshot.route,
|
|
1323
|
+
task: snapshot.task,
|
|
1324
|
+
summary: snapshot.summary,
|
|
1325
|
+
toolNotes: snapshot.toolNotes,
|
|
1326
|
+
error: snapshot.error,
|
|
1327
|
+
usage: snapshot.usage,
|
|
1328
|
+
createdAt: snapshot.createdAt,
|
|
1329
|
+
updatedAt: snapshot.updatedAt,
|
|
1330
|
+
}))
|
|
1331
|
+
: group?.members ?? item.members;
|
|
1332
|
+
return {
|
|
1333
|
+
id: item.id,
|
|
1334
|
+
title: workflow?.title ?? group?.label ?? item.title,
|
|
1335
|
+
status: workflow?.status ?? item.status,
|
|
1336
|
+
members,
|
|
1337
|
+
createdAt: workflow?.createdAt ?? item.createdAt,
|
|
1338
|
+
updatedAt: workflow?.updatedAt ?? item.updatedAt,
|
|
1339
|
+
};
|
|
1340
|
+
};
|
|
1341
|
+
const component = new WorkflowInspectorComponent({
|
|
1342
|
+
getSnapshot,
|
|
1343
|
+
getTerminalRows: () => this.tui.terminal.rows,
|
|
1344
|
+
theme: this.theme,
|
|
1345
|
+
onClose: () => handle?.hide(),
|
|
1346
|
+
onOpenAgent: (agentId) => {
|
|
1347
|
+
handle?.hide();
|
|
1348
|
+
const member = getSnapshot().members.find((candidate) => candidate.subAgentId === agentId);
|
|
1349
|
+
if (!member)
|
|
1350
|
+
return;
|
|
1351
|
+
this.openSubagentInspector({
|
|
1352
|
+
kind: "subagent",
|
|
1353
|
+
id: agentId,
|
|
1354
|
+
title: member.nickname ?? member.agentName ?? "subagent",
|
|
1355
|
+
status: member.status ?? "running",
|
|
1356
|
+
member,
|
|
1357
|
+
}, () => this.openWorkflowInspector(item));
|
|
1358
|
+
},
|
|
1359
|
+
onStop: (runId) => this.options.controller.stopWorkflow(runId),
|
|
1360
|
+
onRender: () => this.renderSnapshot(),
|
|
1361
|
+
});
|
|
1362
|
+
handle = this.tui.showOverlay(component, {
|
|
1363
|
+
width: "100%",
|
|
1364
|
+
maxHeight: "100%",
|
|
1365
|
+
margin: 1,
|
|
1366
|
+
});
|
|
1367
|
+
this.tui.setFocus(component);
|
|
1368
|
+
}
|
|
1369
|
+
openSubagentInspector(item, onClose) {
|
|
1370
|
+
let handle;
|
|
1371
|
+
const allAgentIds = () => this.options.controller.getSubagentGroups()
|
|
1372
|
+
.flatMap((group) => group.members)
|
|
1373
|
+
.map((member) => member.subAgentId)
|
|
1374
|
+
.filter((id) => !!id);
|
|
1375
|
+
const close = () => {
|
|
1376
|
+
handle?.hide();
|
|
1377
|
+
onClose?.();
|
|
1378
|
+
};
|
|
1379
|
+
const component = new SubagentInspectorComponent({
|
|
1380
|
+
agentId: item.id,
|
|
1381
|
+
controller: this.options.controller,
|
|
1382
|
+
theme: this.theme,
|
|
1383
|
+
getMember: () => this.options.controller.getSubagentGroups()
|
|
1384
|
+
.flatMap((group) => group.members)
|
|
1385
|
+
.find((member) => member.subAgentId === item.id) ?? item.member,
|
|
1386
|
+
getTerminalRows: () => this.tui.terminal.rows,
|
|
1387
|
+
renderOptions: () => this.transcriptRenderOptions(),
|
|
1388
|
+
onClose: close,
|
|
1389
|
+
onNavigate: (direction) => {
|
|
1390
|
+
const ids = allAgentIds();
|
|
1391
|
+
const current = ids.indexOf(item.id);
|
|
1392
|
+
if (current < 0 || ids.length < 2)
|
|
1393
|
+
return;
|
|
1394
|
+
const nextId = ids[(current + direction + ids.length) % ids.length];
|
|
1395
|
+
const member = this.options.controller.getSubagentGroups().flatMap((group) => group.members).find((candidate) => candidate.subAgentId === nextId);
|
|
1396
|
+
if (!member)
|
|
1397
|
+
return;
|
|
1398
|
+
handle?.hide();
|
|
1399
|
+
this.openSubagentInspector({
|
|
1400
|
+
kind: "subagent",
|
|
1401
|
+
id: nextId,
|
|
1402
|
+
title: member.nickname ?? member.agentName ?? "subagent",
|
|
1403
|
+
status: member.status ?? "running",
|
|
1404
|
+
member,
|
|
1405
|
+
}, onClose);
|
|
1406
|
+
},
|
|
1407
|
+
onRender: () => this.renderSnapshot(),
|
|
1408
|
+
});
|
|
1409
|
+
handle = this.tui.showOverlay(component, {
|
|
1410
|
+
width: "100%",
|
|
1411
|
+
maxHeight: "100%",
|
|
1412
|
+
margin: 1,
|
|
1413
|
+
});
|
|
1414
|
+
this.tui.setFocus(component);
|
|
1415
|
+
}
|
|
1416
|
+
handleTraceAction(action) {
|
|
1417
|
+
if (action.kind === "open-image") {
|
|
1418
|
+
const message = this.options.controller.getTranscript().find((candidate) => candidate.key === action.messageKey);
|
|
1419
|
+
const image = message?.images?.find((candidate) => candidate.label === action.imageLabel);
|
|
1420
|
+
if (image)
|
|
1421
|
+
this.openImageViewer(image);
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1424
|
+
if (action.kind === "open-task") {
|
|
1425
|
+
const task = this.options.controller.getBackgroundTasks()
|
|
1426
|
+
.find((candidate) => candidate.id === action.taskId);
|
|
1427
|
+
if (task) {
|
|
1428
|
+
this.openTaskInspector({
|
|
1429
|
+
kind: "task",
|
|
1430
|
+
id: task.id,
|
|
1431
|
+
title: task.description || task.command,
|
|
1432
|
+
status: task.status,
|
|
1433
|
+
task,
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
1436
|
+
else {
|
|
1437
|
+
const tool = this.options.controller.getTranscript()
|
|
1438
|
+
.flatMap((message) => message.toolCalls ?? [])
|
|
1439
|
+
.find((candidate) => candidate.metadata?.taskId === action.taskId
|
|
1440
|
+
&& candidate.metadata?.taskLifecycle !== undefined);
|
|
1441
|
+
if (tool) {
|
|
1442
|
+
const lifecycle = String(tool.metadata?.taskLifecycle ?? "completed");
|
|
1443
|
+
const command = String(tool.args.command ?? action.taskId);
|
|
1444
|
+
const description = typeof tool.args.description === "string" ? tool.args.description : undefined;
|
|
1445
|
+
const startedAt = tool.startedAt ?? Date.now();
|
|
1446
|
+
const endedAt = typeof tool.metadata?.endedAt === "number" ? tool.metadata.endedAt : startedAt;
|
|
1447
|
+
this.openTaskInspector({
|
|
1448
|
+
kind: "task",
|
|
1449
|
+
id: action.taskId,
|
|
1450
|
+
title: description || command,
|
|
1451
|
+
status: lifecycle,
|
|
1452
|
+
task: {
|
|
1453
|
+
kind: "task",
|
|
1454
|
+
id: action.taskId,
|
|
1455
|
+
command,
|
|
1456
|
+
description,
|
|
1457
|
+
cwd: process.cwd(),
|
|
1458
|
+
status: lifecycle === "failed" ? "failed" : lifecycle === "killed" ? "killed" : "completed",
|
|
1459
|
+
exitCode: typeof tool.metadata?.exitCode === "number" ? tool.metadata.exitCode : null,
|
|
1460
|
+
startedAt,
|
|
1461
|
+
endedAt,
|
|
1462
|
+
outputTruncated: false,
|
|
1463
|
+
outputLines: typeof tool.metadata?.outputLines === "number" ? tool.metadata.outputLines : 0,
|
|
1464
|
+
},
|
|
1465
|
+
}, tool.result ?? "");
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
if (action.kind !== "open-subagent")
|
|
1471
|
+
return;
|
|
1472
|
+
const member = this.options.controller.getSubagentGroups()
|
|
1473
|
+
.flatMap((group) => group.members)
|
|
1474
|
+
.find((candidate) => candidate.subAgentId === action.subAgentId);
|
|
1475
|
+
if (!member)
|
|
1476
|
+
return;
|
|
1477
|
+
this.openSubagentInspector({
|
|
1478
|
+
kind: "subagent",
|
|
1479
|
+
id: action.subAgentId,
|
|
1480
|
+
title: member.nickname ?? member.agentName ?? "subagent",
|
|
1481
|
+
status: member.status ?? "running",
|
|
1482
|
+
member,
|
|
1483
|
+
});
|
|
1484
|
+
}
|
|
1485
|
+
openTaskInspector(item, fallbackOutput = "") {
|
|
1486
|
+
let handle;
|
|
1487
|
+
const taskOutput = () => this.options.controller.getBackgroundTaskOutput(item.id) || fallbackOutput;
|
|
1488
|
+
const component = new TaskInspectorComponent({
|
|
1489
|
+
id: item.id,
|
|
1490
|
+
title: item.title,
|
|
1491
|
+
getStatus: () => this.options.controller.getBackgroundTasks().find((task) => task.id === item.id)?.status ?? item.status,
|
|
1492
|
+
getOutput: taskOutput,
|
|
1493
|
+
getTerminalRows: () => this.tui.terminal.rows,
|
|
1494
|
+
onClose: () => handle?.hide(),
|
|
1495
|
+
onStop: () => this.options.controller.stopBackgroundTask(item.id),
|
|
1496
|
+
onCopy: () => copyToClipboard(taskOutput()),
|
|
1497
|
+
onRender: () => this.renderSnapshot(),
|
|
1498
|
+
theme: this.theme,
|
|
1499
|
+
});
|
|
1500
|
+
handle = this.tui.showOverlay(component, {
|
|
1501
|
+
width: "100%",
|
|
1502
|
+
maxHeight: "100%",
|
|
1503
|
+
margin: 1,
|
|
1504
|
+
});
|
|
1505
|
+
this.tui.setFocus(component);
|
|
1506
|
+
}
|
|
1507
|
+
approvalDialog(request) {
|
|
1508
|
+
return new Promise((resolve) => {
|
|
1509
|
+
const dialog = new ApprovalDialogComponent(request, () => this.tui.terminal.rows, {
|
|
1510
|
+
allowBashPrefix: this.options.bashAllowlist !== undefined,
|
|
1511
|
+
getTheme: () => this.theme,
|
|
1512
|
+
});
|
|
1513
|
+
const handle = this.tui.showOverlay(dialog, {
|
|
1514
|
+
anchor: "bottom-center",
|
|
1515
|
+
width: "100%",
|
|
1516
|
+
margin: { left: 1, right: 1 },
|
|
1517
|
+
});
|
|
1518
|
+
const finish = (choice) => {
|
|
1519
|
+
handle.hide();
|
|
1520
|
+
resolve(choice);
|
|
1521
|
+
};
|
|
1522
|
+
dialog.onSelect = finish;
|
|
1523
|
+
dialog.onCancel = () => finish({ kind: "reject" });
|
|
1524
|
+
this.tui.setFocus(dialog);
|
|
1525
|
+
});
|
|
1526
|
+
}
|
|
1527
|
+
openNextQuestion() {
|
|
1528
|
+
if (this.disposed || this.activeQuestion)
|
|
1529
|
+
return;
|
|
1530
|
+
const request = this.questionQueue.shift();
|
|
1531
|
+
if (!request)
|
|
1532
|
+
return;
|
|
1533
|
+
const questionController = this.options.questionController;
|
|
1534
|
+
if (!questionController)
|
|
1535
|
+
return;
|
|
1536
|
+
const dialog = new QuestionDialogComponent(request, () => this.tui.terminal.rows, () => this.theme);
|
|
1537
|
+
const handle = this.tui.showOverlay(dialog, {
|
|
1538
|
+
anchor: "bottom-center",
|
|
1539
|
+
width: "100%",
|
|
1540
|
+
margin: { left: 1, right: 1 },
|
|
1541
|
+
});
|
|
1542
|
+
let closed = false;
|
|
1543
|
+
const close = () => {
|
|
1544
|
+
if (closed)
|
|
1545
|
+
return;
|
|
1546
|
+
closed = true;
|
|
1547
|
+
handle.hide();
|
|
1548
|
+
if (this.activeQuestion?.id === request.id)
|
|
1549
|
+
this.activeQuestion = null;
|
|
1550
|
+
this.openNextQuestion();
|
|
1551
|
+
};
|
|
1552
|
+
this.activeQuestion = { id: request.id, close };
|
|
1553
|
+
dialog.onSubmit = (answers) => {
|
|
1554
|
+
close();
|
|
1555
|
+
questionController.reply(request.id, answers);
|
|
1556
|
+
};
|
|
1557
|
+
dialog.onCancel = () => {
|
|
1558
|
+
close();
|
|
1559
|
+
questionController.reject(request.id);
|
|
1560
|
+
};
|
|
1561
|
+
this.tui.setFocus(dialog);
|
|
1562
|
+
}
|
|
1563
|
+
removeQueuedQuestion(id) {
|
|
1564
|
+
const index = this.questionQueue.findIndex((request) => request.id === id);
|
|
1565
|
+
if (index >= 0)
|
|
1566
|
+
this.questionQueue.splice(index, 1);
|
|
1567
|
+
}
|
|
1568
|
+
// ---- Lifecycle ----------------------------------------------------------
|
|
1569
|
+
start() {
|
|
1570
|
+
this.tui.start();
|
|
1571
|
+
this.renderSnapshot();
|
|
1572
|
+
this.watchUpdateNoticeRefresh();
|
|
1573
|
+
}
|
|
1574
|
+
watchUpdateNoticeRefresh() {
|
|
1575
|
+
if (this.updateNoticeRefreshStarted)
|
|
1576
|
+
return;
|
|
1577
|
+
this.updateNoticeRefreshStarted = true;
|
|
1578
|
+
const refresh = this.options.updateNoticeRefresh;
|
|
1579
|
+
if (!refresh)
|
|
1580
|
+
return;
|
|
1581
|
+
void refresh.then((notice) => {
|
|
1582
|
+
if (this.disposed || !notice || notice === this.currentUpdateNotice)
|
|
1583
|
+
return;
|
|
1584
|
+
this.currentUpdateNotice = notice;
|
|
1585
|
+
// The root fullscreen renderer includes the welcome card. The legacy
|
|
1586
|
+
// main-to-fullscreen transition does not, so it needs a transcript row
|
|
1587
|
+
// even before the first user turn.
|
|
1588
|
+
if (this.fullscreen || this.options.controller.getTranscript().length > 0) {
|
|
1589
|
+
this.pushNotice(notice);
|
|
1590
|
+
}
|
|
1591
|
+
else {
|
|
1592
|
+
this.renderSnapshot();
|
|
1593
|
+
}
|
|
1594
|
+
}).catch(() => {
|
|
1595
|
+
// Update checks are best-effort and must never disturb an active session.
|
|
1596
|
+
});
|
|
1597
|
+
}
|
|
1598
|
+
dispose() {
|
|
1599
|
+
if (this.disposed)
|
|
1600
|
+
return;
|
|
1601
|
+
this.disposed = true;
|
|
1602
|
+
this.controllerUnsubscribe?.();
|
|
1603
|
+
this.controllerUnsubscribe = null;
|
|
1604
|
+
this.metadataUnsubscribe?.();
|
|
1605
|
+
this.metadataUnsubscribe = null;
|
|
1606
|
+
this.metadataManager = null;
|
|
1607
|
+
this.questionUnsubscribe?.();
|
|
1608
|
+
this.questionUnsubscribe = null;
|
|
1609
|
+
this.activeQuestion?.close();
|
|
1610
|
+
this.activeQuestion = null;
|
|
1611
|
+
this.questionQueue.length = 0;
|
|
1612
|
+
this.feishuSetupPhase?.component.dispose();
|
|
1613
|
+
this.feishuSetupPhase?.handle.hide();
|
|
1614
|
+
this.feishuSetupPhase = null;
|
|
1615
|
+
if (this.providerKeyPhase) {
|
|
1616
|
+
this.providerKeyPhase.input.setValue("");
|
|
1617
|
+
this.providerKeyPhase = null;
|
|
1618
|
+
}
|
|
1619
|
+
this.options.questionController?.rejectAll();
|
|
1620
|
+
// Root lifecycle events (notably SIGTERM) can dispose the app while the
|
|
1621
|
+
// alternate screen is still open. Tear it down directly so its controller
|
|
1622
|
+
// listener, spinner timers, and terminal mode cannot outlive the root app.
|
|
1623
|
+
this.fullscreen?.dispose();
|
|
1624
|
+
this.fullscreen = null;
|
|
1625
|
+
this.animationClock.dispose();
|
|
1626
|
+
this.composer.dispose();
|
|
1627
|
+
this.streamingMessage.dispose();
|
|
1628
|
+
this.tasksPane.dispose();
|
|
1629
|
+
this.options.controller.shutdown("user-quit");
|
|
1630
|
+
this.overlays.dispose();
|
|
1631
|
+
this.options.callbacks.onExitRequest();
|
|
1632
|
+
// The fullscreen frame is transient application chrome. Reprinting it
|
|
1633
|
+
// into the restored main buffer leaves the docked composer above the
|
|
1634
|
+
// post-session summary. Exit the alternate screen without exporting that
|
|
1635
|
+
// frame; regular mode keeps its native scrollback behavior unchanged.
|
|
1636
|
+
this.tui.stop({ preserveScreen: this.tui.mode === "fullscreen" });
|
|
1637
|
+
}
|
|
1638
|
+
async waitUntilExit() {
|
|
1639
|
+
await new Promise((resolve) => {
|
|
1640
|
+
const original = this.dispose.bind(this);
|
|
1641
|
+
this.dispose = () => {
|
|
1642
|
+
original();
|
|
1643
|
+
resolve();
|
|
1644
|
+
};
|
|
1645
|
+
});
|
|
1646
|
+
}
|
|
1647
|
+
}
|