@prestyj/cli 4.3.209 → 4.3.237
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 +17 -9
- package/dist/cli/auth.d.ts +4 -0
- package/dist/cli/auth.d.ts.map +1 -0
- package/dist/cli/auth.js +344 -0
- package/dist/cli/auth.js.map +1 -0
- package/dist/cli/command-routing.d.ts +28 -0
- package/dist/cli/command-routing.d.ts.map +1 -0
- package/dist/cli/command-routing.js +53 -0
- package/dist/cli/command-routing.js.map +1 -0
- package/dist/cli/command-routing.test.d.ts +2 -0
- package/dist/cli/command-routing.test.d.ts.map +1 -0
- package/dist/cli/command-routing.test.js +60 -0
- package/dist/cli/command-routing.test.js.map +1 -0
- package/dist/cli/mcp.d.ts +2 -0
- package/dist/cli/mcp.d.ts.map +1 -0
- package/dist/cli/mcp.js +309 -0
- package/dist/cli/mcp.js.map +1 -0
- package/dist/cli/pixel.d.ts +27 -0
- package/dist/cli/pixel.d.ts.map +1 -0
- package/dist/cli/pixel.js +103 -0
- package/dist/cli/pixel.js.map +1 -0
- package/dist/cli/shared.d.ts +13 -0
- package/dist/cli/shared.d.ts.map +1 -0
- package/dist/cli/shared.js +82 -0
- package/dist/cli/shared.js.map +1 -0
- package/dist/cli.d.ts +4 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +174 -724
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +4 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +14 -1
- package/dist/config.js.map +1 -1
- package/dist/config.test.d.ts +2 -0
- package/dist/config.test.d.ts.map +1 -0
- package/dist/config.test.js +29 -0
- package/dist/config.test.js.map +1 -0
- package/dist/core/agent-session-compaction.test.d.ts +2 -0
- package/dist/core/agent-session-compaction.test.d.ts.map +1 -0
- package/dist/core/agent-session-compaction.test.js +114 -0
- package/dist/core/agent-session-compaction.test.js.map +1 -0
- package/dist/core/agent-session.d.ts +0 -23
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +6 -101
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/checkpoint-store.d.ts +93 -0
- package/dist/core/checkpoint-store.d.ts.map +1 -0
- package/dist/core/checkpoint-store.js +188 -0
- package/dist/core/checkpoint-store.js.map +1 -0
- package/dist/core/checkpoint-store.test.d.ts +2 -0
- package/dist/core/checkpoint-store.test.d.ts.map +1 -0
- package/dist/core/checkpoint-store.test.js +111 -0
- package/dist/core/checkpoint-store.test.js.map +1 -0
- package/dist/core/compaction/compactor.d.ts +4 -0
- package/dist/core/compaction/compactor.d.ts.map +1 -1
- package/dist/core/compaction/compactor.js +81 -28
- package/dist/core/compaction/compactor.js.map +1 -1
- package/dist/core/compaction/compactor.test.js +33 -9
- package/dist/core/compaction/compactor.test.js.map +1 -1
- package/dist/core/compaction/token-estimator.test.js +1 -1
- package/dist/core/continue-replay-inventory.test.d.ts +2 -0
- package/dist/core/continue-replay-inventory.test.d.ts.map +1 -0
- package/dist/core/continue-replay-inventory.test.js +42 -0
- package/dist/core/continue-replay-inventory.test.js.map +1 -0
- package/dist/core/goal-controller.d.ts +4 -0
- package/dist/core/goal-controller.d.ts.map +1 -1
- package/dist/core/goal-controller.js +223 -32
- package/dist/core/goal-controller.js.map +1 -1
- package/dist/core/goal-controller.test.js +387 -22
- package/dist/core/goal-controller.test.js.map +1 -1
- package/dist/core/goal-lifecycle-smoke.test.js +49 -8
- package/dist/core/goal-lifecycle-smoke.test.js.map +1 -1
- package/dist/core/goal-prerequisites.d.ts +23 -0
- package/dist/core/goal-prerequisites.d.ts.map +1 -0
- package/dist/core/goal-prerequisites.js +114 -0
- package/dist/core/goal-prerequisites.js.map +1 -0
- package/dist/core/goal-prerequisites.test.d.ts +2 -0
- package/dist/core/goal-prerequisites.test.d.ts.map +1 -0
- package/dist/core/goal-prerequisites.test.js +118 -0
- package/dist/core/goal-prerequisites.test.js.map +1 -0
- package/dist/core/goal-store.d.ts +12 -1
- package/dist/core/goal-store.d.ts.map +1 -1
- package/dist/core/goal-store.js +146 -40
- package/dist/core/goal-store.js.map +1 -1
- package/dist/core/goal-store.test.js +65 -8
- package/dist/core/goal-store.test.js.map +1 -1
- package/dist/core/goal-verifier.d.ts.map +1 -1
- package/dist/core/goal-verifier.js +4 -1
- package/dist/core/goal-verifier.js.map +1 -1
- package/dist/core/goal-verifier.test.js +43 -0
- package/dist/core/goal-verifier.test.js.map +1 -1
- package/dist/core/goal-worker.d.ts +2 -0
- package/dist/core/goal-worker.d.ts.map +1 -1
- package/dist/core/goal-worker.js +31 -7
- package/dist/core/goal-worker.js.map +1 -1
- package/dist/core/goal-worker.test.js +44 -0
- package/dist/core/goal-worker.test.js.map +1 -1
- package/dist/core/ideal-review.d.ts +20 -0
- package/dist/core/ideal-review.d.ts.map +1 -0
- package/dist/core/ideal-review.js +55 -0
- package/dist/core/ideal-review.js.map +1 -0
- package/dist/core/ideal-review.test.d.ts +2 -0
- package/dist/core/ideal-review.test.d.ts.map +1 -0
- package/dist/core/ideal-review.test.js +59 -0
- package/dist/core/ideal-review.test.js.map +1 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/loop-breaker.d.ts +46 -0
- package/dist/core/loop-breaker.d.ts.map +1 -0
- package/dist/core/loop-breaker.js +79 -0
- package/dist/core/loop-breaker.js.map +1 -0
- package/dist/core/loop-breaker.test.d.ts +2 -0
- package/dist/core/loop-breaker.test.d.ts.map +1 -0
- package/dist/core/loop-breaker.test.js +110 -0
- package/dist/core/loop-breaker.test.js.map +1 -0
- package/dist/core/mcp/client.d.ts +20 -0
- package/dist/core/mcp/client.d.ts.map +1 -1
- package/dist/core/mcp/client.js +55 -11
- package/dist/core/mcp/client.js.map +1 -1
- package/dist/core/mcp/defaults.d.ts +7 -0
- package/dist/core/mcp/defaults.d.ts.map +1 -1
- package/dist/core/mcp/defaults.js +14 -0
- package/dist/core/mcp/defaults.js.map +1 -1
- package/dist/core/mcp/index.d.ts +6 -1
- package/dist/core/mcp/index.d.ts.map +1 -1
- package/dist/core/mcp/index.js +3 -1
- package/dist/core/mcp/index.js.map +1 -1
- package/dist/core/mcp/parse-add-command.d.ts +25 -0
- package/dist/core/mcp/parse-add-command.d.ts.map +1 -0
- package/dist/core/mcp/parse-add-command.js +220 -0
- package/dist/core/mcp/parse-add-command.js.map +1 -0
- package/dist/core/mcp/parse-add-command.test.d.ts +2 -0
- package/dist/core/mcp/parse-add-command.test.d.ts.map +1 -0
- package/dist/core/mcp/parse-add-command.test.js +75 -0
- package/dist/core/mcp/parse-add-command.test.js.map +1 -0
- package/dist/core/mcp/store.d.ts +80 -0
- package/dist/core/mcp/store.d.ts.map +1 -0
- package/dist/core/mcp/store.js +157 -0
- package/dist/core/mcp/store.js.map +1 -0
- package/dist/core/mcp/store.test.d.ts +2 -0
- package/dist/core/mcp/store.test.d.ts.map +1 -0
- package/dist/core/mcp/store.test.js +101 -0
- package/dist/core/mcp/store.test.js.map +1 -0
- package/dist/core/model-registry.d.ts +2 -2
- package/dist/core/model-registry.js +4 -4
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/model-registry.test.js +1 -1
- package/dist/core/model-registry.test.js.map +1 -1
- package/dist/core/prompt-commands.d.ts.map +1 -1
- package/dist/core/prompt-commands.js +64 -894
- package/dist/core/prompt-commands.js.map +1 -1
- package/dist/core/prompt-commands.test.js +49 -41
- package/dist/core/prompt-commands.test.js.map +1 -1
- package/dist/core/regrounding.d.ts +23 -0
- package/dist/core/regrounding.d.ts.map +1 -0
- package/dist/core/regrounding.js +21 -0
- package/dist/core/regrounding.js.map +1 -0
- package/dist/core/regrounding.test.d.ts +2 -0
- package/dist/core/regrounding.test.d.ts.map +1 -0
- package/dist/core/regrounding.test.js +38 -0
- package/dist/core/regrounding.test.js.map +1 -0
- package/dist/core/runtime-mode.d.ts +23 -0
- package/dist/core/runtime-mode.d.ts.map +1 -0
- package/dist/core/runtime-mode.js +30 -0
- package/dist/core/runtime-mode.js.map +1 -0
- package/dist/core/session-manager.d.ts +4 -0
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +20 -0
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/session-restore-display.test.d.ts +2 -0
- package/dist/core/session-restore-display.test.d.ts.map +1 -0
- package/dist/core/session-restore-display.test.js +78 -0
- package/dist/core/session-restore-display.test.js.map +1 -0
- package/dist/core/settings-manager.d.ts +4 -2
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +3 -6
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/slash-commands.d.ts +0 -2
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +10 -15
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/style-packs/index.js +1 -1
- package/dist/core/style-packs/index.js.map +1 -1
- package/dist/core/task-store.d.ts +26 -0
- package/dist/core/task-store.d.ts.map +1 -0
- package/dist/core/task-store.js +147 -0
- package/dist/core/task-store.js.map +1 -0
- package/dist/core/tasks-store.d.ts +24 -0
- package/dist/core/tasks-store.d.ts.map +1 -0
- package/dist/core/tasks-store.js +81 -0
- package/dist/core/tasks-store.js.map +1 -0
- package/dist/core/thinking-level.d.ts +5 -0
- package/dist/core/thinking-level.d.ts.map +1 -0
- package/dist/core/thinking-level.js +59 -0
- package/dist/core/thinking-level.js.map +1 -0
- package/dist/core/thinking-level.test.d.ts +2 -0
- package/dist/core/thinking-level.test.d.ts.map +1 -0
- package/dist/core/thinking-level.test.js +38 -0
- package/dist/core/thinking-level.test.js.map +1 -0
- package/dist/core/verify-commands.js +4 -4
- package/dist/core/verify-commands.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/interactive.d.ts.map +1 -1
- package/dist/interactive.js +3 -1
- package/dist/interactive.js.map +1 -1
- package/dist/system-prompt.d.ts +5 -1
- package/dist/system-prompt.d.ts.map +1 -1
- package/dist/system-prompt.js +76 -48
- package/dist/system-prompt.js.map +1 -1
- package/dist/system-prompt.test.js +98 -46
- package/dist/system-prompt.test.js.map +1 -1
- package/dist/tools/bash.d.ts +6 -1
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +21 -3
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/checkpoint-hook.test.d.ts +2 -0
- package/dist/tools/checkpoint-hook.test.d.ts.map +1 -0
- package/dist/tools/checkpoint-hook.test.js +65 -0
- package/dist/tools/checkpoint-hook.test.js.map +1 -0
- package/dist/tools/edit-diff.d.ts +17 -4
- package/dist/tools/edit-diff.d.ts.map +1 -1
- package/dist/tools/edit-diff.js +78 -16
- package/dist/tools/edit-diff.js.map +1 -1
- package/dist/tools/edit-diff.test.js +31 -1
- package/dist/tools/edit-diff.test.js.map +1 -1
- package/dist/tools/edit-robustness.test.d.ts +2 -0
- package/dist/tools/edit-robustness.test.d.ts.map +1 -0
- package/dist/tools/edit-robustness.test.js +415 -0
- package/dist/tools/edit-robustness.test.js.map +1 -0
- package/dist/tools/edit.d.ts +8 -2
- package/dist/tools/edit.d.ts.map +1 -1
- package/dist/tools/edit.js +48 -20
- package/dist/tools/edit.js.map +1 -1
- package/dist/tools/edit.test.js +23 -22
- package/dist/tools/edit.test.js.map +1 -1
- package/dist/tools/enter-plan.d.ts +1 -1
- package/dist/tools/enter-plan.d.ts.map +1 -1
- package/dist/tools/enter-plan.js +8 -11
- package/dist/tools/enter-plan.js.map +1 -1
- package/dist/tools/exit-plan.d.ts.map +1 -1
- package/dist/tools/exit-plan.js +8 -9
- package/dist/tools/exit-plan.js.map +1 -1
- package/dist/tools/goal-mode.test.d.ts +2 -0
- package/dist/tools/goal-mode.test.d.ts.map +1 -0
- package/dist/tools/goal-mode.test.js +121 -0
- package/dist/tools/goal-mode.test.js.map +1 -0
- package/dist/tools/goals.d.ts +14 -2
- package/dist/tools/goals.d.ts.map +1 -1
- package/dist/tools/goals.js +257 -26
- package/dist/tools/goals.js.map +1 -1
- package/dist/tools/goals.test.js +365 -1
- package/dist/tools/goals.test.js.map +1 -1
- package/dist/tools/html-extract.d.ts +58 -0
- package/dist/tools/html-extract.d.ts.map +1 -0
- package/dist/tools/html-extract.js +130 -0
- package/dist/tools/html-extract.js.map +1 -0
- package/dist/tools/html-extract.test.d.ts +2 -0
- package/dist/tools/html-extract.test.d.ts.map +1 -0
- package/dist/tools/html-extract.test.js +60 -0
- package/dist/tools/html-extract.test.js.map +1 -0
- package/dist/tools/index.d.ts +16 -4
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +11 -7
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/pdf-extract.d.ts +18 -0
- package/dist/tools/pdf-extract.d.ts.map +1 -0
- package/dist/tools/pdf-extract.js +43 -0
- package/dist/tools/pdf-extract.js.map +1 -0
- package/dist/tools/pdf-extract.test.d.ts +2 -0
- package/dist/tools/pdf-extract.test.d.ts.map +1 -0
- package/dist/tools/pdf-extract.test.js +15 -0
- package/dist/tools/pdf-extract.test.js.map +1 -0
- package/dist/tools/plan-mode.test.js +82 -221
- package/dist/tools/plan-mode.test.js.map +1 -1
- package/dist/tools/prompt-hints.d.ts.map +1 -1
- package/dist/tools/prompt-hints.js +6 -9
- package/dist/tools/prompt-hints.js.map +1 -1
- package/dist/tools/read-only-bash.d.ts +13 -0
- package/dist/tools/read-only-bash.d.ts.map +1 -0
- package/dist/tools/read-only-bash.js +155 -0
- package/dist/tools/read-only-bash.js.map +1 -0
- package/dist/tools/read-only-bash.test.d.ts +2 -0
- package/dist/tools/read-only-bash.test.d.ts.map +1 -0
- package/dist/tools/read-only-bash.test.js +47 -0
- package/dist/tools/read-only-bash.test.js.map +1 -0
- package/dist/tools/read.d.ts.map +1 -1
- package/dist/tools/read.js +14 -1
- package/dist/tools/read.js.map +1 -1
- package/dist/tools/read.test.js +4 -0
- package/dist/tools/read.test.js.map +1 -1
- package/dist/tools/screenshot.d.ts +25 -0
- package/dist/tools/screenshot.d.ts.map +1 -0
- package/dist/tools/screenshot.js +165 -0
- package/dist/tools/screenshot.js.map +1 -0
- package/dist/tools/screenshot.test.d.ts +2 -0
- package/dist/tools/screenshot.test.d.ts.map +1 -0
- package/dist/tools/screenshot.test.js +145 -0
- package/dist/tools/screenshot.test.js.map +1 -0
- package/dist/tools/subagent.d.ts +7 -2
- package/dist/tools/subagent.d.ts.map +1 -1
- package/dist/tools/subagent.js +32 -11
- package/dist/tools/subagent.js.map +1 -1
- package/dist/tools/tasks.d.ts.map +1 -1
- package/dist/tools/tasks.js +22 -58
- package/dist/tools/tasks.js.map +1 -1
- package/dist/tools/web-fetch.d.ts +16 -1
- package/dist/tools/web-fetch.d.ts.map +1 -1
- package/dist/tools/web-fetch.js +357 -45
- package/dist/tools/web-fetch.js.map +1 -1
- package/dist/tools/web-fetch.test.js +263 -2
- package/dist/tools/web-fetch.test.js.map +1 -1
- package/dist/tools/web-search.d.ts +14 -0
- package/dist/tools/web-search.d.ts.map +1 -1
- package/dist/tools/web-search.js +321 -35
- package/dist/tools/web-search.js.map +1 -1
- package/dist/tools/web-search.test.js +144 -1
- package/dist/tools/web-search.test.js.map +1 -1
- package/dist/tools/write.d.ts +8 -2
- package/dist/tools/write.d.ts.map +1 -1
- package/dist/tools/write.js +36 -9
- package/dist/tools/write.js.map +1 -1
- package/dist/tools/write.test.js +0 -16
- package/dist/tools/write.test.js.map +1 -1
- package/dist/ui/App.d.ts +52 -272
- package/dist/ui/App.d.ts.map +1 -1
- package/dist/ui/App.js +1606 -2199
- package/dist/ui/App.js.map +1 -1
- package/dist/ui/activity-phrases.d.ts.map +1 -1
- package/dist/ui/activity-phrases.js +0 -2
- package/dist/ui/activity-phrases.js.map +1 -1
- package/dist/ui/app-items.d.ts +276 -0
- package/dist/ui/app-items.d.ts.map +1 -0
- package/dist/ui/app-items.js +52 -0
- package/dist/ui/app-items.js.map +1 -0
- package/dist/ui/app-items.test.d.ts +2 -0
- package/dist/ui/app-items.test.d.ts.map +1 -0
- package/dist/ui/app-items.test.js +38 -0
- package/dist/ui/app-items.test.js.map +1 -0
- package/dist/ui/app-state-persistence.test.js +45 -160
- package/dist/ui/app-state-persistence.test.js.map +1 -1
- package/dist/ui/assistant-stream-flush-parity.test.d.ts +2 -0
- package/dist/ui/assistant-stream-flush-parity.test.d.ts.map +1 -0
- package/dist/ui/assistant-stream-flush-parity.test.js +85 -0
- package/dist/ui/assistant-stream-flush-parity.test.js.map +1 -0
- package/dist/ui/chat-layout-pinning.test.d.ts +2 -0
- package/dist/ui/chat-layout-pinning.test.d.ts.map +1 -0
- package/dist/ui/chat-layout-pinning.test.js +525 -0
- package/dist/ui/chat-layout-pinning.test.js.map +1 -0
- package/dist/ui/components/ActivityIndicator.d.ts +1 -2
- package/dist/ui/components/ActivityIndicator.d.ts.map +1 -1
- package/dist/ui/components/ActivityIndicator.js +63 -94
- package/dist/ui/components/ActivityIndicator.js.map +1 -1
- package/dist/ui/components/AnimationContext.d.ts.map +1 -1
- package/dist/ui/components/AnimationContext.js +3 -5
- package/dist/ui/components/AnimationContext.js.map +1 -1
- package/dist/ui/components/AssistantMessage.d.ts +6 -2
- package/dist/ui/components/AssistantMessage.d.ts.map +1 -1
- package/dist/ui/components/AssistantMessage.js +9 -4
- package/dist/ui/components/AssistantMessage.js.map +1 -1
- package/dist/ui/components/AssistantMessage.test.d.ts +2 -0
- package/dist/ui/components/AssistantMessage.test.d.ts.map +1 -0
- package/dist/ui/components/AssistantMessage.test.js +387 -0
- package/dist/ui/components/AssistantMessage.test.js.map +1 -0
- package/dist/ui/components/BackgroundTasksBar.d.ts +1 -3
- package/dist/ui/components/BackgroundTasksBar.d.ts.map +1 -1
- package/dist/ui/components/BackgroundTasksBar.js +8 -10
- package/dist/ui/components/BackgroundTasksBar.js.map +1 -1
- package/dist/ui/components/Banner.d.ts +1 -2
- package/dist/ui/components/Banner.d.ts.map +1 -1
- package/dist/ui/components/Banner.js +9 -4
- package/dist/ui/components/Banner.js.map +1 -1
- package/dist/ui/components/ChatFooterPane.d.ts +25 -0
- package/dist/ui/components/ChatFooterPane.d.ts.map +1 -0
- package/dist/ui/components/ChatFooterPane.js +15 -0
- package/dist/ui/components/ChatFooterPane.js.map +1 -0
- package/dist/ui/components/ChatInputStack.d.ts +36 -0
- package/dist/ui/components/ChatInputStack.d.ts.map +1 -0
- package/dist/ui/components/ChatInputStack.js +10 -0
- package/dist/ui/components/ChatInputStack.js.map +1 -0
- package/dist/ui/components/ChatLayout.d.ts +37 -0
- package/dist/ui/components/ChatLayout.d.ts.map +1 -0
- package/dist/ui/components/ChatLayout.js +21 -0
- package/dist/ui/components/ChatLayout.js.map +1 -0
- package/dist/ui/components/ChatLivePane.d.ts +18 -0
- package/dist/ui/components/ChatLivePane.d.ts.map +1 -0
- package/dist/ui/components/ChatLivePane.js +22 -0
- package/dist/ui/components/ChatLivePane.js.map +1 -0
- package/dist/ui/components/ChatScreen.d.ts +136 -0
- package/dist/ui/components/ChatScreen.d.ts.map +1 -0
- package/dist/ui/components/ChatScreen.js +16 -0
- package/dist/ui/components/ChatScreen.js.map +1 -0
- package/dist/ui/components/ChatStatusRow.d.ts +34 -0
- package/dist/ui/components/ChatStatusRow.d.ts.map +1 -0
- package/dist/ui/components/ChatStatusRow.js +11 -0
- package/dist/ui/components/ChatStatusRow.js.map +1 -0
- package/dist/ui/components/CompactionNotice.d.ts +4 -2
- package/dist/ui/components/CompactionNotice.d.ts.map +1 -1
- package/dist/ui/components/CompactionNotice.js +4 -4
- package/dist/ui/components/CompactionNotice.js.map +1 -1
- package/dist/ui/components/Footer.d.ts +27 -6
- package/dist/ui/components/Footer.d.ts.map +1 -1
- package/dist/ui/components/Footer.js +75 -30
- package/dist/ui/components/Footer.js.map +1 -1
- package/dist/ui/components/FooterStatusRow.d.ts +20 -0
- package/dist/ui/components/FooterStatusRow.d.ts.map +1 -0
- package/dist/ui/components/FooterStatusRow.js +10 -0
- package/dist/ui/components/FooterStatusRow.js.map +1 -0
- package/dist/ui/components/FullScreenOverlayRouter.d.ts +19 -0
- package/dist/ui/components/FullScreenOverlayRouter.d.ts.map +1 -0
- package/dist/ui/components/FullScreenOverlayRouter.js +18 -0
- package/dist/ui/components/FullScreenOverlayRouter.js.map +1 -0
- package/dist/ui/components/GoalOverlay.d.ts +28 -20
- package/dist/ui/components/GoalOverlay.d.ts.map +1 -1
- package/dist/ui/components/GoalOverlay.js +283 -253
- package/dist/ui/components/GoalOverlay.js.map +1 -1
- package/dist/ui/components/GoalStatusBar.d.ts.map +1 -1
- package/dist/ui/components/GoalStatusBar.js +2 -1
- package/dist/ui/components/GoalStatusBar.js.map +1 -1
- package/dist/ui/components/IdealHookMessage.d.ts +14 -0
- package/dist/ui/components/IdealHookMessage.d.ts.map +1 -0
- package/dist/ui/components/IdealHookMessage.js +26 -0
- package/dist/ui/components/IdealHookMessage.js.map +1 -0
- package/dist/ui/components/InputArea.d.ts +31 -6
- package/dist/ui/components/InputArea.d.ts.map +1 -1
- package/dist/ui/components/InputArea.js +208 -50
- package/dist/ui/components/InputArea.js.map +1 -1
- package/dist/ui/components/InputArea.test.js +11 -1
- package/dist/ui/components/InputArea.test.js.map +1 -1
- package/dist/ui/components/LiveToolPanel.d.ts +30 -0
- package/dist/ui/components/LiveToolPanel.d.ts.map +1 -0
- package/dist/ui/components/LiveToolPanel.js +66 -0
- package/dist/ui/components/LiveToolPanel.js.map +1 -0
- package/dist/ui/components/Markdown.d.ts +11 -11
- package/dist/ui/components/Markdown.d.ts.map +1 -1
- package/dist/ui/components/Markdown.js +28 -198
- package/dist/ui/components/Markdown.js.map +1 -1
- package/dist/ui/components/ModelSelector.d.ts.map +1 -1
- package/dist/ui/components/ModelSelector.js +18 -18
- package/dist/ui/components/ModelSelector.js.map +1 -1
- package/dist/ui/components/PlanModeLogo.d.ts +3 -0
- package/dist/ui/components/PlanModeLogo.d.ts.map +1 -0
- package/dist/ui/components/PlanModeLogo.js +49 -0
- package/dist/ui/components/PlanModeLogo.js.map +1 -0
- package/dist/ui/components/PlanOverlay.d.ts +7 -0
- package/dist/ui/components/PlanOverlay.d.ts.map +1 -1
- package/dist/ui/components/PlanOverlay.js +17 -3
- package/dist/ui/components/PlanOverlay.js.map +1 -1
- package/dist/ui/components/PlanOverlay.test.d.ts +2 -0
- package/dist/ui/components/PlanOverlay.test.d.ts.map +1 -0
- package/dist/ui/components/PlanOverlay.test.js +24 -0
- package/dist/ui/components/PlanOverlay.test.js.map +1 -0
- package/dist/ui/components/QueueIndicator.d.ts +9 -0
- package/dist/ui/components/QueueIndicator.d.ts.map +1 -0
- package/dist/ui/components/QueueIndicator.js +9 -0
- package/dist/ui/components/QueueIndicator.js.map +1 -0
- package/dist/ui/components/RewindOverlay.d.ts +18 -0
- package/dist/ui/components/RewindOverlay.d.ts.map +1 -0
- package/dist/ui/components/RewindOverlay.js +52 -0
- package/dist/ui/components/RewindOverlay.js.map +1 -0
- package/dist/ui/components/SelectList.d.ts.map +1 -1
- package/dist/ui/components/SelectList.js +5 -0
- package/dist/ui/components/SelectList.js.map +1 -1
- package/dist/ui/components/ServerToolExecution.d.ts +2 -0
- package/dist/ui/components/ServerToolExecution.d.ts.map +1 -1
- package/dist/ui/components/ServerToolExecution.js +7 -23
- package/dist/ui/components/ServerToolExecution.js.map +1 -1
- package/dist/ui/components/SessionSummary.d.ts +5 -0
- package/dist/ui/components/SessionSummary.d.ts.map +1 -0
- package/dist/ui/components/SessionSummary.js +32 -0
- package/dist/ui/components/SessionSummary.js.map +1 -0
- package/dist/ui/components/SlashCommandMenu.d.ts +4 -3
- package/dist/ui/components/SlashCommandMenu.d.ts.map +1 -1
- package/dist/ui/components/SlashCommandMenu.js +38 -26
- package/dist/ui/components/SlashCommandMenu.js.map +1 -1
- package/dist/ui/components/SlashStyledSelectList.d.ts +21 -0
- package/dist/ui/components/SlashStyledSelectList.d.ts.map +1 -0
- package/dist/ui/components/SlashStyledSelectList.js +71 -0
- package/dist/ui/components/SlashStyledSelectList.js.map +1 -0
- package/dist/ui/components/StreamingArea.d.ts +11 -2
- package/dist/ui/components/StreamingArea.d.ts.map +1 -1
- package/dist/ui/components/StreamingArea.js +20 -23
- package/dist/ui/components/StreamingArea.js.map +1 -1
- package/dist/ui/components/StreamingArea.test.d.ts +2 -0
- package/dist/ui/components/StreamingArea.test.d.ts.map +1 -0
- package/dist/ui/components/StreamingArea.test.js +18 -0
- package/dist/ui/components/StreamingArea.test.js.map +1 -0
- package/dist/ui/components/SubAgentPanel.d.ts +2 -1
- package/dist/ui/components/SubAgentPanel.d.ts.map +1 -1
- package/dist/ui/components/SubAgentPanel.js +4 -3
- package/dist/ui/components/SubAgentPanel.js.map +1 -1
- package/dist/ui/components/TaskOverlay.d.ts.map +1 -1
- package/dist/ui/components/TaskOverlay.js +53 -82
- package/dist/ui/components/TaskOverlay.js.map +1 -1
- package/dist/ui/components/TaskPickerMenu.d.ts +9 -0
- package/dist/ui/components/TaskPickerMenu.d.ts.map +1 -0
- package/dist/ui/components/TaskPickerMenu.js +33 -0
- package/dist/ui/components/TaskPickerMenu.js.map +1 -0
- package/dist/ui/components/ThemeSelector.js +2 -2
- package/dist/ui/components/ThemeSelector.js.map +1 -1
- package/dist/ui/components/ToolExecution.d.ts +2 -0
- package/dist/ui/components/ToolExecution.d.ts.map +1 -1
- package/dist/ui/components/ToolExecution.js +18 -52
- package/dist/ui/components/ToolExecution.js.map +1 -1
- package/dist/ui/components/ToolGroupExecution.d.ts +6 -2
- package/dist/ui/components/ToolGroupExecution.d.ts.map +1 -1
- package/dist/ui/components/ToolGroupExecution.js +15 -147
- package/dist/ui/components/ToolGroupExecution.js.map +1 -1
- package/dist/ui/components/ToolUseLoader.d.ts +3 -1
- package/dist/ui/components/ToolUseLoader.d.ts.map +1 -1
- package/dist/ui/components/ToolUseLoader.js +2 -2
- package/dist/ui/components/ToolUseLoader.js.map +1 -1
- package/dist/ui/components/TranscriptViewport.d.ts +34 -0
- package/dist/ui/components/TranscriptViewport.d.ts.map +1 -0
- package/dist/ui/components/TranscriptViewport.js +45 -0
- package/dist/ui/components/TranscriptViewport.js.map +1 -0
- package/dist/ui/components/UserMessage.d.ts.map +1 -1
- package/dist/ui/components/UserMessage.js +15 -10
- package/dist/ui/components/UserMessage.js.map +1 -1
- package/dist/ui/components/UserMessage.test.d.ts +2 -0
- package/dist/ui/components/UserMessage.test.d.ts.map +1 -0
- package/dist/ui/components/UserMessage.test.js +39 -0
- package/dist/ui/components/UserMessage.test.js.map +1 -0
- package/dist/ui/components/index.d.ts +5 -0
- package/dist/ui/components/index.d.ts.map +1 -1
- package/dist/ui/components/index.js +5 -0
- package/dist/ui/components/index.js.map +1 -1
- package/dist/ui/duration-format.d.ts +2 -0
- package/dist/ui/duration-format.d.ts.map +1 -0
- package/dist/ui/duration-format.js +9 -0
- package/dist/ui/duration-format.js.map +1 -0
- package/dist/ui/duration-summary.d.ts +2 -0
- package/dist/ui/duration-summary.d.ts.map +1 -0
- package/dist/ui/duration-summary.js +66 -0
- package/dist/ui/duration-summary.js.map +1 -0
- package/dist/ui/error-item.d.ts +8 -0
- package/dist/ui/error-item.d.ts.map +1 -0
- package/dist/ui/error-item.js +32 -0
- package/dist/ui/error-item.js.map +1 -0
- package/dist/ui/footer-jump-regression.test.d.ts +2 -0
- package/dist/ui/footer-jump-regression.test.d.ts.map +1 -0
- package/dist/ui/footer-jump-regression.test.js +177 -0
- package/dist/ui/footer-jump-regression.test.js.map +1 -0
- package/dist/ui/footer-status-layout.test.js +22 -7
- package/dist/ui/footer-status-layout.test.js.map +1 -1
- package/dist/ui/goal-events.d.ts.map +1 -1
- package/dist/ui/goal-events.js +14 -6
- package/dist/ui/goal-events.js.map +1 -1
- package/dist/ui/goal-events.test.js +40 -2
- package/dist/ui/goal-events.test.js.map +1 -1
- package/dist/ui/goal-lifecycle-orchestration.test.d.ts +2 -0
- package/dist/ui/goal-lifecycle-orchestration.test.d.ts.map +1 -0
- package/dist/ui/goal-lifecycle-orchestration.test.js +512 -0
- package/dist/ui/goal-lifecycle-orchestration.test.js.map +1 -0
- package/dist/ui/goal-overlay.test.js +122 -44
- package/dist/ui/goal-overlay.test.js.map +1 -1
- package/dist/ui/goal-status-bar.test.js +29 -0
- package/dist/ui/goal-status-bar.test.js.map +1 -1
- package/dist/ui/goal-summary.d.ts +14 -0
- package/dist/ui/goal-summary.d.ts.map +1 -0
- package/dist/ui/goal-summary.js +194 -0
- package/dist/ui/goal-summary.js.map +1 -0
- package/dist/ui/hooks/useAgentLoop.d.ts +25 -4
- package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
- package/dist/ui/hooks/useAgentLoop.js +197 -22
- package/dist/ui/hooks/useAgentLoop.js.map +1 -1
- package/dist/ui/hooks/useAgentLoop.test.d.ts +2 -0
- package/dist/ui/hooks/useAgentLoop.test.d.ts.map +1 -0
- package/dist/ui/hooks/useAgentLoop.test.js +8 -0
- package/dist/ui/hooks/useAgentLoop.test.js.map +1 -0
- package/dist/ui/hooks/useChatLayoutMeasurements.d.ts +45 -0
- package/dist/ui/hooks/useChatLayoutMeasurements.d.ts.map +1 -0
- package/dist/ui/hooks/useChatLayoutMeasurements.js +97 -0
- package/dist/ui/hooks/useChatLayoutMeasurements.js.map +1 -0
- package/dist/ui/hooks/useContextCompaction.d.ts +41 -0
- package/dist/ui/hooks/useContextCompaction.d.ts.map +1 -0
- package/dist/ui/hooks/useContextCompaction.js +149 -0
- package/dist/ui/hooks/useContextCompaction.js.map +1 -0
- package/dist/ui/hooks/useModeState.d.ts +56 -0
- package/dist/ui/hooks/useModeState.d.ts.map +1 -0
- package/dist/ui/hooks/useModeState.js +65 -0
- package/dist/ui/hooks/useModeState.js.map +1 -0
- package/dist/ui/hooks/usePixelFixFlow.d.ts +57 -0
- package/dist/ui/hooks/usePixelFixFlow.d.ts.map +1 -0
- package/dist/ui/hooks/usePixelFixFlow.js +102 -0
- package/dist/ui/hooks/usePixelFixFlow.js.map +1 -0
- package/dist/ui/hooks/useSessionPersistence.d.ts +34 -0
- package/dist/ui/hooks/useSessionPersistence.d.ts.map +1 -0
- package/dist/ui/hooks/useSessionPersistence.js +67 -0
- package/dist/ui/hooks/useSessionPersistence.js.map +1 -0
- package/dist/ui/hooks/useTaskPickerController.d.ts +19 -0
- package/dist/ui/hooks/useTaskPickerController.d.ts.map +1 -0
- package/dist/ui/hooks/useTaskPickerController.js +41 -0
- package/dist/ui/hooks/useTaskPickerController.js.map +1 -0
- package/dist/ui/hooks/useTerminalSize.d.ts +13 -10
- package/dist/ui/hooks/useTerminalSize.d.ts.map +1 -1
- package/dist/ui/hooks/useTerminalSize.js +21 -16
- package/dist/ui/hooks/useTerminalSize.js.map +1 -1
- package/dist/ui/hooks/useTranscriptHistory.d.ts +48 -0
- package/dist/ui/hooks/useTranscriptHistory.d.ts.map +1 -0
- package/dist/ui/hooks/useTranscriptHistory.js +113 -0
- package/dist/ui/hooks/useTranscriptHistory.js.map +1 -0
- package/dist/ui/hooks/useTranscriptHistory.test.d.ts +2 -0
- package/dist/ui/hooks/useTranscriptHistory.test.d.ts.map +1 -0
- package/dist/ui/hooks/useTranscriptHistory.test.js +61 -0
- package/dist/ui/hooks/useTranscriptHistory.test.js.map +1 -0
- package/dist/ui/hooks/useTranscriptScroll.d.ts +30 -0
- package/dist/ui/hooks/useTranscriptScroll.d.ts.map +1 -0
- package/dist/ui/hooks/useTranscriptScroll.js +44 -0
- package/dist/ui/hooks/useTranscriptScroll.js.map +1 -0
- package/dist/ui/hooks/useTranscriptScroll.test.d.ts +2 -0
- package/dist/ui/hooks/useTranscriptScroll.test.d.ts.map +1 -0
- package/dist/ui/hooks/useTranscriptScroll.test.js +119 -0
- package/dist/ui/hooks/useTranscriptScroll.test.js.map +1 -0
- package/dist/ui/item-helpers.d.ts +26 -0
- package/dist/ui/item-helpers.d.ts.map +1 -0
- package/dist/ui/item-helpers.js +114 -0
- package/dist/ui/item-helpers.js.map +1 -0
- package/dist/ui/layout-decisions.d.ts +81 -0
- package/dist/ui/layout-decisions.d.ts.map +1 -0
- package/dist/ui/layout-decisions.js +89 -0
- package/dist/ui/layout-decisions.js.map +1 -0
- package/dist/ui/live-area-clamp.test.d.ts +2 -0
- package/dist/ui/live-area-clamp.test.d.ts.map +1 -0
- package/dist/ui/live-area-clamp.test.js +88 -0
- package/dist/ui/live-area-clamp.test.js.map +1 -0
- package/dist/ui/live-area-height.d.ts +35 -0
- package/dist/ui/live-area-height.d.ts.map +1 -0
- package/dist/ui/live-area-height.js +80 -0
- package/dist/ui/live-area-height.js.map +1 -0
- package/dist/ui/live-area-height.test.d.ts +2 -0
- package/dist/ui/live-area-height.test.d.ts.map +1 -0
- package/dist/ui/live-area-height.test.js +67 -0
- package/dist/ui/live-area-height.test.js.map +1 -0
- package/dist/ui/live-frame-height.test.d.ts +2 -0
- package/dist/ui/live-frame-height.test.d.ts.map +1 -0
- package/dist/ui/live-frame-height.test.js +116 -0
- package/dist/ui/live-frame-height.test.js.map +1 -0
- package/dist/ui/live-item-flush.d.ts +2 -2
- package/dist/ui/live-item-flush.d.ts.map +1 -1
- package/dist/ui/live-item-flush.js +8 -4
- package/dist/ui/live-item-flush.js.map +1 -1
- package/dist/ui/login.js +1 -1
- package/dist/ui/long-prompt-regression-harness.test.d.ts +2 -0
- package/dist/ui/long-prompt-regression-harness.test.d.ts.map +1 -0
- package/dist/ui/long-prompt-regression-harness.test.js +195 -0
- package/dist/ui/long-prompt-regression-harness.test.js.map +1 -0
- package/dist/ui/mcp.d.ts +62 -0
- package/dist/ui/mcp.d.ts.map +1 -0
- package/dist/ui/mcp.js +306 -0
- package/dist/ui/mcp.js.map +1 -0
- package/dist/ui/plan-overlay.test.js +7 -29
- package/dist/ui/plan-overlay.test.js.map +1 -1
- package/dist/ui/prompt-routing.d.ts +11 -0
- package/dist/ui/prompt-routing.d.ts.map +1 -0
- package/dist/ui/prompt-routing.js +61 -0
- package/dist/ui/prompt-routing.js.map +1 -0
- package/dist/ui/queued-message.test.d.ts +2 -0
- package/dist/ui/queued-message.test.d.ts.map +1 -0
- package/dist/ui/queued-message.test.js +162 -0
- package/dist/ui/queued-message.test.js.map +1 -0
- package/dist/ui/render.d.ts +38 -34
- package/dist/ui/render.d.ts.map +1 -1
- package/dist/ui/render.js +128 -41
- package/dist/ui/render.js.map +1 -1
- package/dist/ui/render.test.d.ts +2 -0
- package/dist/ui/render.test.d.ts.map +1 -0
- package/dist/ui/render.test.js +16 -0
- package/dist/ui/render.test.js.map +1 -0
- package/dist/ui/scroll-stabilization.test.js +1 -7
- package/dist/ui/scroll-stabilization.test.js.map +1 -1
- package/dist/ui/session-summary.d.ts +63 -0
- package/dist/ui/session-summary.d.ts.map +1 -0
- package/dist/ui/session-summary.js +81 -0
- package/dist/ui/session-summary.js.map +1 -0
- package/dist/ui/slash-command-images.test.js +12 -11
- package/dist/ui/slash-command-images.test.js.map +1 -1
- package/dist/ui/stores/transcript-scroll-store.d.ts +27 -0
- package/dist/ui/stores/transcript-scroll-store.d.ts.map +1 -0
- package/dist/ui/stores/transcript-scroll-store.js +73 -0
- package/dist/ui/stores/transcript-scroll-store.js.map +1 -0
- package/dist/ui/stores/transcript-scroll-store.test.d.ts +2 -0
- package/dist/ui/stores/transcript-scroll-store.test.d.ts.map +1 -0
- package/dist/ui/stores/transcript-scroll-store.test.js +73 -0
- package/dist/ui/stores/transcript-scroll-store.test.js.map +1 -0
- package/dist/ui/streaming-flush-bounce.test.d.ts +2 -0
- package/dist/ui/streaming-flush-bounce.test.d.ts.map +1 -0
- package/dist/ui/streaming-flush-bounce.test.js +156 -0
- package/dist/ui/streaming-flush-bounce.test.js.map +1 -0
- package/dist/ui/submit-prompt-command.d.ts +49 -0
- package/dist/ui/submit-prompt-command.d.ts.map +1 -0
- package/dist/ui/submit-prompt-command.js +107 -0
- package/dist/ui/submit-prompt-command.js.map +1 -0
- package/dist/ui/submit-slash-commands.d.ts +12 -0
- package/dist/ui/submit-slash-commands.d.ts.map +1 -0
- package/dist/ui/submit-slash-commands.js +32 -0
- package/dist/ui/submit-slash-commands.js.map +1 -0
- package/dist/ui/terminal-history-format.d.ts +43 -0
- package/dist/ui/terminal-history-format.d.ts.map +1 -0
- package/dist/ui/terminal-history-format.js +133 -0
- package/dist/ui/terminal-history-format.js.map +1 -0
- package/dist/ui/terminal-history-retry-dedup.test.d.ts +2 -0
- package/dist/ui/terminal-history-retry-dedup.test.d.ts.map +1 -0
- package/dist/ui/terminal-history-retry-dedup.test.js +61 -0
- package/dist/ui/terminal-history-retry-dedup.test.js.map +1 -0
- package/dist/ui/terminal-history-spacing.d.ts +2 -0
- package/dist/ui/terminal-history-spacing.d.ts.map +1 -0
- package/dist/ui/terminal-history-spacing.js +2 -0
- package/dist/ui/terminal-history-spacing.js.map +1 -0
- package/dist/ui/terminal-history-status-renderers.d.ts +15 -0
- package/dist/ui/terminal-history-status-renderers.d.ts.map +1 -0
- package/dist/ui/terminal-history-status-renderers.js +99 -0
- package/dist/ui/terminal-history-status-renderers.js.map +1 -0
- package/dist/ui/terminal-history.d.ts +26 -0
- package/dist/ui/terminal-history.d.ts.map +1 -0
- package/dist/ui/terminal-history.js +916 -0
- package/dist/ui/terminal-history.js.map +1 -0
- package/dist/ui/terminal-history.test.d.ts +2 -0
- package/dist/ui/terminal-history.test.d.ts.map +1 -0
- package/dist/ui/terminal-history.test.js +550 -0
- package/dist/ui/terminal-history.test.js.map +1 -0
- package/dist/ui/testing/screen-recorder.d.ts +29 -0
- package/dist/ui/testing/screen-recorder.d.ts.map +1 -0
- package/dist/ui/testing/screen-recorder.js +179 -0
- package/dist/ui/testing/screen-recorder.js.map +1 -0
- package/dist/ui/thinking-level-cycle.test.d.ts +2 -0
- package/dist/ui/thinking-level-cycle.test.d.ts.map +1 -0
- package/dist/ui/thinking-level-cycle.test.js +45 -0
- package/dist/ui/thinking-level-cycle.test.js.map +1 -0
- package/dist/ui/thinking-level.d.ts +2 -0
- package/dist/ui/thinking-level.d.ts.map +1 -0
- package/dist/ui/thinking-level.js +2 -0
- package/dist/ui/thinking-level.js.map +1 -0
- package/dist/ui/tool-group-summary.d.ts +24 -0
- package/dist/ui/tool-group-summary.d.ts.map +1 -0
- package/dist/ui/tool-group-summary.js +233 -0
- package/dist/ui/tool-group-summary.js.map +1 -0
- package/dist/ui/tool-group-summary.test.d.ts +2 -0
- package/dist/ui/tool-group-summary.test.d.ts.map +1 -0
- package/dist/ui/tool-group-summary.test.js +79 -0
- package/dist/ui/tool-group-summary.test.js.map +1 -0
- package/dist/ui/tool-line-summary.d.ts +29 -0
- package/dist/ui/tool-line-summary.d.ts.map +1 -0
- package/dist/ui/tool-line-summary.js +153 -0
- package/dist/ui/tool-line-summary.js.map +1 -0
- package/dist/ui/transcript/MiscRows.d.ts +26 -0
- package/dist/ui/transcript/MiscRows.d.ts.map +1 -0
- package/dist/ui/transcript/MiscRows.js +68 -0
- package/dist/ui/transcript/MiscRows.js.map +1 -0
- package/dist/ui/transcript/StatusRow.d.ts +14 -0
- package/dist/ui/transcript/StatusRow.d.ts.map +1 -0
- package/dist/ui/transcript/StatusRow.js +14 -0
- package/dist/ui/transcript/StatusRow.js.map +1 -0
- package/dist/ui/transcript/ToolRows.d.ts +20 -0
- package/dist/ui/transcript/ToolRows.d.ts.map +1 -0
- package/dist/ui/transcript/ToolRows.js +25 -0
- package/dist/ui/transcript/ToolRows.js.map +1 -0
- package/dist/ui/transcript/TranscriptItemFrame.d.ts +8 -0
- package/dist/ui/transcript/TranscriptItemFrame.d.ts.map +1 -0
- package/dist/ui/transcript/TranscriptItemFrame.js +9 -0
- package/dist/ui/transcript/TranscriptItemFrame.js.map +1 -0
- package/dist/ui/transcript/TranscriptRenderer.d.ts +22 -0
- package/dist/ui/transcript/TranscriptRenderer.d.ts.map +1 -0
- package/dist/ui/transcript/TranscriptRenderer.js +111 -0
- package/dist/ui/transcript/TranscriptRenderer.js.map +1 -0
- package/dist/ui/transcript/presentation.d.ts +61 -0
- package/dist/ui/transcript/presentation.d.ts.map +1 -0
- package/dist/ui/transcript/presentation.js +86 -0
- package/dist/ui/transcript/presentation.js.map +1 -0
- package/dist/ui/transcript/spacing.d.ts +56 -0
- package/dist/ui/transcript/spacing.d.ts.map +1 -0
- package/dist/ui/transcript/spacing.js +129 -0
- package/dist/ui/transcript/spacing.js.map +1 -0
- package/dist/ui/transcript/spacing.test.d.ts +2 -0
- package/dist/ui/transcript/spacing.test.d.ts.map +1 -0
- package/dist/ui/transcript/spacing.test.js +193 -0
- package/dist/ui/transcript/spacing.test.js.map +1 -0
- package/dist/ui/transcript/tool-presentation.d.ts +12 -0
- package/dist/ui/transcript/tool-presentation.d.ts.map +1 -0
- package/dist/ui/transcript/tool-presentation.js +55 -0
- package/dist/ui/transcript/tool-presentation.js.map +1 -0
- package/dist/ui/transcript/transcript-lines.d.ts +32 -0
- package/dist/ui/transcript/transcript-lines.d.ts.map +1 -0
- package/dist/ui/transcript/transcript-lines.js +86 -0
- package/dist/ui/transcript/transcript-lines.js.map +1 -0
- package/dist/ui/transcript/transcript-lines.test.d.ts +2 -0
- package/dist/ui/transcript/transcript-lines.test.d.ts.map +1 -0
- package/dist/ui/transcript/transcript-lines.test.js +76 -0
- package/dist/ui/transcript/transcript-lines.test.js.map +1 -0
- package/dist/ui/transcript-viewport-pinning.test.d.ts +2 -0
- package/dist/ui/transcript-viewport-pinning.test.d.ts.map +1 -0
- package/dist/ui/transcript-viewport-pinning.test.js +148 -0
- package/dist/ui/transcript-viewport-pinning.test.js.map +1 -0
- package/dist/ui/tui-history-parity.test.d.ts +2 -0
- package/dist/ui/tui-history-parity.test.d.ts.map +1 -0
- package/dist/ui/tui-history-parity.test.js +381 -0
- package/dist/ui/tui-history-parity.test.js.map +1 -0
- package/dist/ui/tui-simulation.test.d.ts +2 -0
- package/dist/ui/tui-simulation.test.d.ts.map +1 -0
- package/dist/ui/tui-simulation.test.js +139 -0
- package/dist/ui/tui-simulation.test.js.map +1 -0
- package/dist/ui/tui-terminal-recorder.test.d.ts +2 -0
- package/dist/ui/tui-terminal-recorder.test.d.ts.map +1 -0
- package/dist/ui/tui-terminal-recorder.test.js +134 -0
- package/dist/ui/tui-terminal-recorder.test.js.map +1 -0
- package/dist/ui/utils/assistant-stream-split.d.ts +23 -0
- package/dist/ui/utils/assistant-stream-split.d.ts.map +1 -0
- package/dist/ui/utils/assistant-stream-split.js +41 -0
- package/dist/ui/utils/assistant-stream-split.js.map +1 -0
- package/dist/ui/utils/assistant-stream-split.test.d.ts +2 -0
- package/dist/ui/utils/assistant-stream-split.test.d.ts.map +1 -0
- package/dist/ui/utils/assistant-stream-split.test.js +40 -0
- package/dist/ui/utils/assistant-stream-split.test.js.map +1 -0
- package/dist/ui/utils/latex-to-unicode.d.ts +22 -0
- package/dist/ui/utils/latex-to-unicode.d.ts.map +1 -0
- package/dist/ui/utils/latex-to-unicode.js +538 -0
- package/dist/ui/utils/latex-to-unicode.js.map +1 -0
- package/dist/ui/utils/markdown-renderer.d.ts +20 -0
- package/dist/ui/utils/markdown-renderer.d.ts.map +1 -0
- package/dist/ui/utils/markdown-renderer.js +327 -0
- package/dist/ui/utils/markdown-renderer.js.map +1 -0
- package/dist/ui/utils/markdown-table.d.ts +9 -0
- package/dist/ui/utils/markdown-table.d.ts.map +1 -0
- package/dist/ui/utils/markdown-table.js +95 -0
- package/dist/ui/utils/markdown-table.js.map +1 -0
- package/dist/ui/utils/terminal-graphics.d.ts +16 -0
- package/dist/ui/utils/terminal-graphics.d.ts.map +1 -0
- package/dist/ui/utils/terminal-graphics.js +68 -0
- package/dist/ui/utils/terminal-graphics.js.map +1 -0
- package/dist/ui/utils/terminal-graphics.test.d.ts +2 -0
- package/dist/ui/utils/terminal-graphics.test.d.ts.map +1 -0
- package/dist/ui/utils/terminal-graphics.test.js +61 -0
- package/dist/ui/utils/terminal-graphics.test.js.map +1 -0
- package/dist/ui/utils/terminal-input.d.ts +3 -0
- package/dist/ui/utils/terminal-input.d.ts.map +1 -0
- package/dist/ui/utils/terminal-input.js +28 -0
- package/dist/ui/utils/terminal-input.js.map +1 -0
- package/dist/ui/utils/terminal-input.test.d.ts +2 -0
- package/dist/ui/utils/terminal-input.test.d.ts.map +1 -0
- package/dist/ui/utils/terminal-input.test.js +15 -0
- package/dist/ui/utils/terminal-input.test.js.map +1 -0
- package/dist/ui/utils/text-utils.d.ts +8 -0
- package/dist/ui/utils/text-utils.d.ts.map +1 -0
- package/dist/ui/utils/text-utils.js +16 -0
- package/dist/ui/utils/text-utils.js.map +1 -0
- package/dist/ui/utils/token-to-ansi.js +19 -9
- package/dist/ui/utils/token-to-ansi.js.map +1 -1
- package/dist/ui/utils/user-message-display.d.ts +7 -0
- package/dist/ui/utils/user-message-display.d.ts.map +1 -0
- package/dist/ui/utils/user-message-display.js +26 -0
- package/dist/ui/utils/user-message-display.js.map +1 -0
- package/dist/utils/format.js +7 -9
- package/dist/utils/format.js.map +1 -1
- package/dist/utils/image.d.ts +9 -0
- package/dist/utils/image.d.ts.map +1 -1
- package/dist/utils/image.js +25 -0
- package/dist/utils/image.js.map +1 -1
- package/dist/utils/plan-steps.d.ts.map +1 -1
- package/dist/utils/plan-steps.js +5 -1
- package/dist/utils/plan-steps.js.map +1 -1
- package/dist/utils/plan-steps.test.d.ts +2 -0
- package/dist/utils/plan-steps.test.d.ts.map +1 -0
- package/dist/utils/plan-steps.test.js +16 -0
- package/dist/utils/plan-steps.test.js.map +1 -0
- package/package.json +50 -10
- package/dist/core/repomap-context.d.ts +0 -11
- package/dist/core/repomap-context.d.ts.map +0 -1
- package/dist/core/repomap-context.js +0 -68
- package/dist/core/repomap-context.js.map +0 -1
- package/dist/core/repomap-context.test.d.ts +0 -2
- package/dist/core/repomap-context.test.d.ts.map +0 -1
- package/dist/core/repomap-context.test.js +0 -47
- package/dist/core/repomap-context.test.js.map +0 -1
- package/dist/core/repomap.d.ts +0 -74
- package/dist/core/repomap.d.ts.map +0 -1
- package/dist/core/repomap.js +0 -906
- package/dist/core/repomap.js.map +0 -1
- package/dist/core/repomap.test.d.ts +0 -2
- package/dist/core/repomap.test.d.ts.map +0 -1
- package/dist/core/repomap.test.js +0 -494
- package/dist/core/repomap.test.js.map +0 -1
- package/dist/ui/components/EyesOverlay.d.ts +0 -10
- package/dist/ui/components/EyesOverlay.d.ts.map +0 -1
- package/dist/ui/components/EyesOverlay.js +0 -220
- package/dist/ui/components/EyesOverlay.js.map +0 -1
package/dist/ui/App.js
CHANGED
|
@@ -1,254 +1,149 @@
|
|
|
1
|
-
import { jsx as _jsx
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import React, { useState, useRef, useCallback, useEffect, useMemo } from "react";
|
|
3
|
-
import { Box,
|
|
3
|
+
import { Box, useStdout } from "ink";
|
|
4
4
|
import { useTerminalSize } from "./hooks/useTerminalSize.js";
|
|
5
|
+
import { useChatLayoutMeasurements } from "./hooks/useChatLayoutMeasurements.js";
|
|
6
|
+
import { useTaskPickerController } from "./hooks/useTaskPickerController.js";
|
|
7
|
+
import { useModeState } from "./hooks/useModeState.js";
|
|
8
|
+
import { useSessionPersistence } from "./hooks/useSessionPersistence.js";
|
|
9
|
+
import { useContextCompaction } from "./hooks/useContextCompaction.js";
|
|
10
|
+
import { usePixelFixFlow } from "./hooks/usePixelFixFlow.js";
|
|
5
11
|
import { useDoublePress } from "./hooks/useDoublePress.js";
|
|
6
12
|
import { useTaskBarStore, useTaskBarPolling, focusTaskBar, exitTaskBar, expandTaskBar, collapseTaskBar, navigateTaskBar, killTask, } from "./stores/taskbar-store.js";
|
|
7
|
-
import { createHash } from "node:crypto";
|
|
8
|
-
import { readFileSync, writeFileSync } from "node:fs";
|
|
9
|
-
import { homedir } from "node:os";
|
|
10
|
-
import { join } from "node:path";
|
|
11
13
|
import { playNotificationSound } from "../utils/sound.js";
|
|
12
|
-
import {
|
|
13
|
-
import { extractImagePaths } from "../utils/image.js";
|
|
14
|
+
import {} from "@prestyj/ai";
|
|
15
|
+
import { downscaleForPreview, extractImagePaths } from "../utils/image.js";
|
|
14
16
|
import { useAgentLoop } from "./hooks/useAgentLoop.js";
|
|
15
|
-
import {
|
|
16
|
-
import { UserMessage } from "./components/UserMessage.js";
|
|
17
|
-
import { AssistantMessage } from "./components/AssistantMessage.js";
|
|
18
|
-
import { ToolExecution } from "./components/ToolExecution.js";
|
|
19
|
-
import { ToolGroupExecution } from "./components/ToolGroupExecution.js";
|
|
20
|
-
import { ServerToolExecution } from "./components/ServerToolExecution.js";
|
|
21
|
-
import { SubAgentPanel } from "./components/SubAgentPanel.js";
|
|
22
|
-
import { CompactionSpinner, CompactionDone } from "./components/CompactionNotice.js";
|
|
17
|
+
import { useTranscriptHistory } from "./hooks/useTranscriptHistory.js";
|
|
23
18
|
import { createWebSearchTool } from "../tools/web-search.js";
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import { GoalStatusBar, reconcileGoalStatusEntriesWithRuns, removeGoalStatusEntry, syncGoalStatusEntries, } from "./components/GoalStatusBar.js";
|
|
29
|
-
import { Banner } from "./components/Banner.js";
|
|
30
|
-
import { PlanOverlay } from "./components/PlanOverlay.js";
|
|
31
|
-
import { ModelSelector } from "./components/ModelSelector.js";
|
|
32
|
-
import { TaskOverlay } from "./components/TaskOverlay.js";
|
|
33
|
-
import { GoalOverlay } from "./components/GoalOverlay.js";
|
|
34
|
-
import { PixelOverlay } from "./components/PixelOverlay.js";
|
|
35
|
-
import { SkillsOverlay } from "./components/SkillsOverlay.js";
|
|
36
|
-
import { EyesOverlay } from "./components/EyesOverlay.js";
|
|
37
|
-
import { ThemeSelector } from "./components/ThemeSelector.js";
|
|
38
|
-
import { BackgroundTasksBar, getFooterStatusLayoutDecision, } from "./components/BackgroundTasksBar.js";
|
|
19
|
+
import { ChatScreen } from "./components/ChatScreen.js";
|
|
20
|
+
import { LIVE_TOOL_PANEL_ROWS } from "./components/LiveToolPanel.js";
|
|
21
|
+
import { FullScreenOverlayRouter } from "./components/FullScreenOverlayRouter.js";
|
|
22
|
+
import { SessionSummaryDisplay } from "./components/SessionSummary.js";
|
|
39
23
|
import { useTheme, useSetTheme } from "./theme/theme.js";
|
|
40
24
|
import { useTerminalTitle } from "./hooks/useTerminalTitle.js";
|
|
41
25
|
import { getGitBranch } from "../utils/git.js";
|
|
42
|
-
import { getModel
|
|
26
|
+
import { getModel } from "../core/model-registry.js";
|
|
43
27
|
import { SessionManager } from "../core/session-manager.js";
|
|
44
|
-
import { appendMessagesToSession as appendSessionMessages, createCompactedSessionCheckpoint, } from "../core/session-compaction.js";
|
|
45
28
|
import { log } from "../core/logger.js";
|
|
46
29
|
import { getPendingUpdate, startPeriodicUpdateCheck, stopPeriodicUpdateCheck, } from "../core/auto-update.js";
|
|
47
30
|
import { generateSessionTitle } from "../utils/session-title.js";
|
|
48
31
|
import { SettingsManager } from "../core/settings-manager.js";
|
|
49
|
-
import { shouldCompact, compact, getCompactionReserveTokens, } from "../core/compaction/compactor.js";
|
|
50
|
-
import { estimateConversationTokens } from "../core/compaction/token-estimator.js";
|
|
51
32
|
import { PROMPT_COMMANDS, getPromptCommand } from "../core/prompt-commands.js";
|
|
52
33
|
import { isFirstTimeSetup, markSetupAudited, getAnnouncedLanguages, markLanguagesAnnounced, } from "../core/setup-history.js";
|
|
53
34
|
import { loadCustomCommands } from "../core/custom-commands.js";
|
|
54
|
-
import {
|
|
55
|
-
import { detectLanguages, LANGUAGE_DISPLAY_NAMES, } from "../core/language-detector.js";
|
|
35
|
+
import { detectLanguages } from "../core/language-detector.js";
|
|
56
36
|
import { detectVerifyCommands } from "../core/verify-commands.js";
|
|
57
|
-
import {
|
|
58
|
-
import {
|
|
37
|
+
import { RewindOverlay } from "./components/RewindOverlay.js";
|
|
38
|
+
import { reconcileGoalStatusEntriesWithRuns, removeGoalStatusEntry, syncGoalStatusEntries, } from "./components/GoalStatusBar.js";
|
|
59
39
|
import { extractPlanSteps, findCompletedMarkers, markStepsCompleted, segmentDisplayText, stripDoneMarkers, } from "../utils/plan-steps.js";
|
|
60
40
|
import { getMCPServers } from "../core/mcp/index.js";
|
|
61
41
|
import { trimFlushedItems, flushOnTurnText, flushOnTurnEnd, flushOverflow, } from "./live-item-flush.js";
|
|
62
|
-
import {
|
|
63
|
-
import {
|
|
42
|
+
import { splitAssistantStreamingText } from "./utils/assistant-stream-split.js";
|
|
43
|
+
import { getNextPendingTask, markTaskInProgress } from "../core/tasks-store.js";
|
|
44
|
+
import { buildUserContentWithAttachments } from "./prompt-routing.js";
|
|
45
|
+
import { submitPromptCommand } from "./submit-prompt-command.js";
|
|
46
|
+
import { handleUiSlashCommand } from "./submit-slash-commands.js";
|
|
47
|
+
import { buildIdealReviewMessage, evaluateIdealReview } from "../core/ideal-review.js";
|
|
48
|
+
import { buildLoopBreakMessage, evaluateLoopBreak } from "../core/loop-breaker.js";
|
|
49
|
+
import { buildRegroundingMessage } from "../core/regrounding.js";
|
|
50
|
+
import { getNextThinkingLevel, isThinkingLevelSupported } from "./thinking-level.js";
|
|
51
|
+
import { getDoneFlushDecision, shouldTopSpaceAfterPrintedAgentBoundary, shouldTopSpaceStreamingAssistant, } from "./layout-decisions.js";
|
|
52
|
+
import { isTranscriptSpacingItem } from "./transcript/spacing.js";
|
|
53
|
+
import { buildTranscriptLines } from "./transcript/transcript-lines.js";
|
|
54
|
+
import { useTranscriptScroll } from "./hooks/useTranscriptScroll.js";
|
|
55
|
+
import { scrollTranscriptByLines } from "./stores/transcript-scroll-store.js";
|
|
56
|
+
import { renderTranscriptItem } from "./transcript/TranscriptRenderer.js";
|
|
57
|
+
import { formatDuration } from "./duration-format.js";
|
|
58
|
+
import { pickDurationVerb } from "./duration-summary.js";
|
|
59
|
+
import { toErrorItem } from "./error-item.js";
|
|
60
|
+
import { appendGoalDecision, appendGoalEvidence, createGoalTask, formatGoalBlockingPrerequisites, goalHasBlockingPrerequisites, loadGoalRuns, reconcileActiveGoalRuns, updateGoalTask, upsertGoalRun, } from "../core/goal-store.js";
|
|
61
|
+
import { canCompleteGoalRun, decideGoalNextAction, } from "../core/goal-controller.js";
|
|
62
|
+
import { runGoalPrerequisiteChecks } from "../core/goal-prerequisites.js";
|
|
64
63
|
import { runGoalVerifierCommand } from "../core/goal-verifier.js";
|
|
65
64
|
import { listGoalWorkers, startGoalWorker, stopGoalWorker, subscribeGoalWorkerCompletions, } from "../core/goal-worker.js";
|
|
66
|
-
import { formatGoalVerifierCompletionEvent, formatGoalWorkerCompletionEvent,
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
source: f.source,
|
|
86
|
-
message: f.message,
|
|
87
|
-
...(f.provider ? { provider: f.provider } : {}),
|
|
88
|
-
...(f.statusCode != null ? { statusCode: String(f.statusCode) } : {}),
|
|
89
|
-
...(f.requestId ? { requestId: f.requestId } : {}),
|
|
90
|
-
...(err instanceof Error && err.stack ? { stack: err.stack } : {}),
|
|
91
|
-
});
|
|
92
|
-
return {
|
|
93
|
-
kind: "error",
|
|
94
|
-
headline,
|
|
95
|
-
message: f.message,
|
|
96
|
-
guidance,
|
|
97
|
-
id,
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
export function routePromptCommandInput(input, promptCommands = PROMPT_COMMANDS, customCommands = []) {
|
|
101
|
-
const trimmed = input.trim();
|
|
102
|
-
if (!trimmed.startsWith("/"))
|
|
103
|
-
return null;
|
|
104
|
-
const parts = trimmed.slice(1).split(" ");
|
|
105
|
-
const cmdName = parts[0];
|
|
106
|
-
const cmdArgs = parts.slice(1).join(" ").trim();
|
|
107
|
-
const builtinCmd = promptCommands.find((c) => c.name === cmdName || c.aliases.includes(cmdName));
|
|
108
|
-
const customCmd = !builtinCmd ? customCommands.find((c) => c.name === cmdName) : undefined;
|
|
109
|
-
const promptText = builtinCmd?.prompt ?? customCmd?.prompt;
|
|
110
|
-
if (!promptText)
|
|
111
|
-
return null;
|
|
112
|
-
return {
|
|
113
|
-
cmdName,
|
|
114
|
-
cmdArgs,
|
|
115
|
-
promptText,
|
|
116
|
-
fullPrompt: cmdArgs ? `${promptText}\n\n## User Instructions\n\n${cmdArgs}` : promptText,
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
export function buildUserContentWithAttachments(text, inputImages, modelSupportsImages) {
|
|
120
|
-
if (inputImages.length === 0)
|
|
121
|
-
return text;
|
|
122
|
-
const parts = [];
|
|
123
|
-
if (text) {
|
|
124
|
-
parts.push({ type: "text", text });
|
|
125
|
-
}
|
|
126
|
-
for (const img of inputImages) {
|
|
127
|
-
if (img.kind === "text") {
|
|
128
|
-
parts.push({
|
|
129
|
-
type: "text",
|
|
130
|
-
text: `<file name="${img.fileName}">\n${img.data}\n</file>`,
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
else if (modelSupportsImages) {
|
|
134
|
-
parts.push({ type: "image", mediaType: img.mediaType, data: img.data });
|
|
135
|
-
}
|
|
136
|
-
else {
|
|
137
|
-
// GLM models: save image to temp file and instruct model to use vision MCP tool
|
|
138
|
-
const ext = img.mediaType.split("/")[1] ?? "png";
|
|
139
|
-
const tmpPath = `/tmp/ezcoder-img-${Date.now()}.${ext}`;
|
|
140
|
-
try {
|
|
141
|
-
writeFileSync(tmpPath, Buffer.from(img.data, "base64"));
|
|
142
|
-
parts.push({
|
|
143
|
-
type: "text",
|
|
144
|
-
text: `[User attached an image saved at: ${tmpPath} — use the image_analysis tool to view and analyze it]`,
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
catch {
|
|
148
|
-
parts.push({
|
|
149
|
-
type: "text",
|
|
150
|
-
text: `[User attached an image but it could not be saved for analysis]`,
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
// If only text parts remain after stripping images, simplify to plain string
|
|
156
|
-
return parts.length === 1 && parts[0].type === "text" ? parts[0].text : parts;
|
|
157
|
-
}
|
|
158
|
-
/** Tools that get aggregated into a single compact group when concurrent. */
|
|
159
|
-
const AGGREGATABLE_TOOLS = new Set(["read", "grep", "find", "ls"]);
|
|
65
|
+
import { formatGoalVerifierCompletionEvent, formatGoalWorkerCompletionEvent, } from "./goal-events.js";
|
|
66
|
+
import { buildGoalFinalSummarySections, buildGoalSummaryRows, goalPassedDetail, } from "./goal-summary.js";
|
|
67
|
+
import { addLinesChanged, buildSessionSummary, createSessionStats, recordServerToolCall, recordToolEnd, recordTurnEnd, } from "./session-summary.js";
|
|
68
|
+
import { compactHistory, getNextGeneratedItemId, isActiveItem, isSameAssistantText, normalizeAssistantText, partitionCompleted, pinStreamingTextBeforeToolBoundary, removeItemsWithIds, uniqueItemsById, } from "./item-helpers.js";
|
|
69
|
+
import { IDEAL_HOOK_NOTICE_TEXT, LOOP_BREAK_NOTICE_TEXT, REGROUNDING_NOTICE_TEXT, lastVisibleTranscriptItem, } from "./app-items.js";
|
|
70
|
+
export { buildUserContentWithAttachments, routePromptCommandInput } from "./prompt-routing.js";
|
|
71
|
+
export { getNextThinkingLevel } from "./thinking-level.js";
|
|
72
|
+
export { getChatControlsLayoutDecision, getDoneFlushDecision, getScrollStabilizationDecision, getStaticHistoryKey, hasParagraphBreakLiveUserMessage, isTallLiveUserMessage, shouldHideHistoryForOverlayView, shouldHideStaticItemsForOverlayView, shouldStabilizeOverlayPaneRerender, shouldTopSpaceAfterPrintedAgentBoundary, shouldTopSpaceAssistantAfterToolBoundary, shouldTopSpaceStreamingAssistant, } from "./layout-decisions.js";
|
|
73
|
+
export { getNextGeneratedItemId, isActiveItem, partitionCompleted, pinStreamingTextBeforeToolBoundary, } from "./item-helpers.js";
|
|
74
|
+
/** Tools that get aggregated into a single compact group when possible. */
|
|
75
|
+
const AGGREGATABLE_TOOLS = new Set([
|
|
76
|
+
"read",
|
|
77
|
+
"grep",
|
|
78
|
+
"find",
|
|
79
|
+
"ls",
|
|
80
|
+
"mcp__kencode-search__searchCode",
|
|
81
|
+
"mcp__kencode-search__referenceSources",
|
|
82
|
+
"mcp__kencode-search__discoverRepos",
|
|
83
|
+
]);
|
|
160
84
|
const RUNNING_INDICATOR_ANIMATION_MS = 1_200;
|
|
161
|
-
/**
|
|
162
|
-
* Cap memory by replacing old items with tiny tombstones. Ink's <Static>
|
|
163
|
-
* tracks rendered items by array length — the array must never shrink, but
|
|
164
|
-
* we can swap out heavy objects for lightweight `{ kind: "tombstone", id }`
|
|
165
|
-
* entries so GC can reclaim the original data.
|
|
166
|
-
*/
|
|
167
|
-
const MAX_LIVE_HISTORY = 200;
|
|
168
|
-
function compactHistory(items) {
|
|
169
|
-
if (items.length <= MAX_LIVE_HISTORY)
|
|
170
|
-
return items;
|
|
171
|
-
const cutoff = items.length - MAX_LIVE_HISTORY;
|
|
172
|
-
const compacted = new Array(items.length);
|
|
173
|
-
for (let i = 0; i < cutoff; i++) {
|
|
174
|
-
const it = items[i];
|
|
175
|
-
compacted[i] = it.kind === "tombstone" ? it : { kind: "tombstone", id: it.id };
|
|
176
|
-
}
|
|
177
|
-
for (let i = cutoff; i < items.length; i++) {
|
|
178
|
-
compacted[i] = items[i];
|
|
179
|
-
}
|
|
180
|
-
return compacted;
|
|
181
|
-
}
|
|
182
85
|
function summarizeGoalCompletion(summary) {
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const statusLine = lines.find((line) => /^Status:/i.test(line));
|
|
188
|
-
const changedLine = lines.find((line) => /^(Changed|Implemented|Fixed|Added|Key findings|Full verifier)/i.test(line));
|
|
189
|
-
const verificationLine = lines.find((line) => /^(Verification|Verified|Result):/i.test(line));
|
|
190
|
-
return statusLine ?? changedLine ?? verificationLine ?? lines[0];
|
|
86
|
+
const trimmed = summary.trim();
|
|
87
|
+
if (!trimmed)
|
|
88
|
+
return undefined;
|
|
89
|
+
return trimmed.length > 400 ? `${trimmed.slice(0, 397)}...` : trimmed;
|
|
191
90
|
}
|
|
192
91
|
function formatGoalWorkerFinishedTitle(taskTitle, status) {
|
|
193
|
-
return status === "done" ? `
|
|
92
|
+
return status === "done" ? `Worker finished: ${taskTitle}` : `Worker failed: ${taskTitle}`;
|
|
194
93
|
}
|
|
195
|
-
function
|
|
196
|
-
return
|
|
94
|
+
function goalCompletionReason(check) {
|
|
95
|
+
return check.reason;
|
|
96
|
+
}
|
|
97
|
+
function getGoalContinuationChoiceKey({ runId, decision, }) {
|
|
98
|
+
switch (decision.kind) {
|
|
99
|
+
case "create_task":
|
|
100
|
+
return `${runId}:create_task:${decision.title}`;
|
|
101
|
+
case "start_worker":
|
|
102
|
+
case "pause":
|
|
103
|
+
return `${runId}:${decision.kind}:${decision.task.id}:${decision.attempts}`;
|
|
104
|
+
case "run_verifier":
|
|
105
|
+
return `${runId}:run_verifier:${decision.command}`;
|
|
106
|
+
case "terminal":
|
|
107
|
+
return `${runId}:terminal:${decision.status}`;
|
|
108
|
+
case "blocked":
|
|
109
|
+
case "wait":
|
|
110
|
+
case "complete":
|
|
111
|
+
return `${runId}:${decision.kind}:${decision.reason}`;
|
|
112
|
+
}
|
|
197
113
|
}
|
|
198
|
-
function
|
|
199
|
-
|
|
114
|
+
function goalTerminalRunIdFromItem(item) {
|
|
115
|
+
if (item.kind !== "goal_progress" || item.phase !== "terminal")
|
|
116
|
+
return undefined;
|
|
117
|
+
if (!item.id.startsWith("goal-terminal-"))
|
|
118
|
+
return undefined;
|
|
119
|
+
return item.id.slice("goal-terminal-".length);
|
|
200
120
|
}
|
|
201
|
-
function
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
121
|
+
function goalProgressMatchesDraft(item, draft) {
|
|
122
|
+
return (item.title === draft.title &&
|
|
123
|
+
item.detail === draft.detail &&
|
|
124
|
+
item.status === draft.status &&
|
|
125
|
+
JSON.stringify(item.summaryRows ?? []) === JSON.stringify(draft.summaryRows ?? []) &&
|
|
126
|
+
JSON.stringify(item.summarySections ?? []) === JSON.stringify(draft.summarySections ?? []));
|
|
206
127
|
}
|
|
207
|
-
export function
|
|
208
|
-
const
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
label: "Verifier",
|
|
225
|
-
value: verifierResult?.status ?? (run.verifier?.command ? "ready" : "missing"),
|
|
226
|
-
...(verifierDetail ? { detail: truncateGoalSummary(verifierDetail) } : {}),
|
|
227
|
-
});
|
|
228
|
-
const latestEvidence = run.evidence.at(-1);
|
|
229
|
-
rows.push({
|
|
230
|
-
label: "Evidence",
|
|
231
|
-
value: `${run.evidence.length} recorded`,
|
|
232
|
-
...(latestEvidence
|
|
233
|
-
? { detail: truncateGoalSummary(latestEvidence.path ?? latestEvidence.label) }
|
|
234
|
-
: {}),
|
|
128
|
+
export function completedItemsWithDurableGoalTerminalProgress(items, runs) {
|
|
129
|
+
const runIds = new Set(runs.map((run) => run.id));
|
|
130
|
+
const terminalByRun = new Map(runs
|
|
131
|
+
.map((run) => [run.id, formatGoalTerminalProgress(run)])
|
|
132
|
+
.filter((entry) => entry[1] !== null));
|
|
133
|
+
if (runIds.size === 0)
|
|
134
|
+
return items;
|
|
135
|
+
let changed = false;
|
|
136
|
+
const reconciled = items.map((item, index) => {
|
|
137
|
+
const runId = goalTerminalRunIdFromItem(item);
|
|
138
|
+
if (!runId || !runIds.has(runId))
|
|
139
|
+
return item;
|
|
140
|
+
const draft = terminalByRun.get(runId);
|
|
141
|
+
if (item.kind === "goal_progress" && draft && goalProgressMatchesDraft(item, draft))
|
|
142
|
+
return item;
|
|
143
|
+
changed = true;
|
|
144
|
+
return { kind: "tombstone", id: `tombstone-${item.id}-${index}` };
|
|
235
145
|
});
|
|
236
|
-
|
|
237
|
-
rows.push({
|
|
238
|
-
label: run.status === "paused" ? "Paused on" : "Blocked on",
|
|
239
|
-
value: truncateGoalSummary(goalHasBlockingPrerequisites(run)
|
|
240
|
-
? formatGoalBlockingPrerequisites(run)
|
|
241
|
-
: (run.blockers[0] ?? "manual review"), 110),
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
else if (run.successCriteria.length > 0) {
|
|
245
|
-
rows.push({
|
|
246
|
-
label: "Criteria",
|
|
247
|
-
value: `${run.successCriteria.length} checked`,
|
|
248
|
-
detail: truncateGoalSummary(run.successCriteria[0] ?? "", 80),
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
return rows.slice(0, 4);
|
|
146
|
+
return changed ? reconciled : items;
|
|
252
147
|
}
|
|
253
148
|
export function formatGoalTerminalProgress(run) {
|
|
254
149
|
switch (run.status) {
|
|
@@ -257,8 +152,9 @@ export function formatGoalTerminalProgress(run) {
|
|
|
257
152
|
kind: "goal_progress",
|
|
258
153
|
phase: "terminal",
|
|
259
154
|
title: `Goal passed: ${run.title}`,
|
|
260
|
-
detail:
|
|
155
|
+
detail: goalPassedDetail(run),
|
|
261
156
|
summaryRows: buildGoalSummaryRows(run),
|
|
157
|
+
summarySections: buildGoalFinalSummarySections(run),
|
|
262
158
|
status: run.status,
|
|
263
159
|
};
|
|
264
160
|
case "failed":
|
|
@@ -297,221 +193,49 @@ export function formatGoalTerminalProgress(run) {
|
|
|
297
193
|
return null;
|
|
298
194
|
}
|
|
299
195
|
}
|
|
300
|
-
export function
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
export function getScrollStabilizationDecision({ isUserScrolled, hasNewOutput, hasTallLiveUserMessage = false, }) {
|
|
313
|
-
const shouldStabilize = isUserScrolled || hasTallLiveUserMessage;
|
|
314
|
-
return {
|
|
315
|
-
preserveStatic: shouldStabilize && hasNewOutput,
|
|
316
|
-
autoFollow: !shouldStabilize,
|
|
317
|
-
};
|
|
318
|
-
}
|
|
319
|
-
export function isTallLiveUserMessage(text, rows) {
|
|
320
|
-
return text.split("\n").length > Math.max(8, Math.floor(rows * 0.6));
|
|
321
|
-
}
|
|
322
|
-
export function getStaticHistoryKey({ resizeKey }) {
|
|
323
|
-
return `${resizeKey}`;
|
|
324
|
-
}
|
|
325
|
-
// flushOnTurnText, flushOnTurnEnd are imported from ./live-item-flush.ts
|
|
326
|
-
/** Check whether an item is still active (running spinner, pending result). */
|
|
327
|
-
function isActiveItem(item) {
|
|
328
|
-
switch (item.kind) {
|
|
329
|
-
case "tool_start":
|
|
330
|
-
case "server_tool_start":
|
|
331
|
-
case "compacting":
|
|
332
|
-
return true;
|
|
333
|
-
case "tool_group":
|
|
334
|
-
return item.tools.some((t) => t.status === "running");
|
|
335
|
-
case "subagent_group":
|
|
336
|
-
return item.agents.some((a) => a.status === "running");
|
|
337
|
-
default:
|
|
338
|
-
return false;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
/**
|
|
342
|
-
* Partition live items into completed (flushable to Static) and still-active.
|
|
343
|
-
* Completed items precede active ones — we flush the longest contiguous prefix
|
|
344
|
-
* of completed items to keep ordering stable.
|
|
345
|
-
*/
|
|
346
|
-
function partitionCompleted(items) {
|
|
347
|
-
// Find the first active item — everything before it is safe to flush
|
|
348
|
-
const firstActiveIdx = items.findIndex(isActiveItem);
|
|
349
|
-
if (firstActiveIdx === -1) {
|
|
350
|
-
// All items are completed
|
|
351
|
-
return { flushed: items, remaining: [] };
|
|
352
|
-
}
|
|
353
|
-
if (firstActiveIdx === 0) {
|
|
354
|
-
return { flushed: [], remaining: items };
|
|
355
|
-
}
|
|
356
|
-
return {
|
|
357
|
-
flushed: items.slice(0, firstActiveIdx),
|
|
358
|
-
remaining: items.slice(firstActiveIdx),
|
|
359
|
-
};
|
|
360
|
-
}
|
|
361
|
-
// ── Duration summary ─────────────────────────────────────
|
|
362
|
-
function formatDuration(ms) {
|
|
363
|
-
const totalSec = Math.round(ms / 1000);
|
|
364
|
-
if (totalSec < 60)
|
|
365
|
-
return `${totalSec}s`;
|
|
366
|
-
const min = Math.floor(totalSec / 60);
|
|
367
|
-
const sec = totalSec % 60;
|
|
368
|
-
return sec > 0 ? `${min}m ${sec}s` : `${min}m`;
|
|
369
|
-
}
|
|
370
|
-
function pickDurationVerb(toolsUsed) {
|
|
371
|
-
const has = (name) => toolsUsed.includes(name);
|
|
372
|
-
const hasAny = (...names) => names.some(has);
|
|
373
|
-
const writing = has("edit") || has("write");
|
|
374
|
-
const reading = has("read") || has("grep") || has("find") || has("ls");
|
|
375
|
-
// Multi-tool combos (most specific first)
|
|
376
|
-
if (has("subagent") && writing)
|
|
377
|
-
return "Orchestrated changes for";
|
|
378
|
-
if (has("subagent"))
|
|
379
|
-
return "Delegated work for";
|
|
380
|
-
if (has("web-fetch") && writing)
|
|
381
|
-
return "Researched & coded for";
|
|
382
|
-
if (has("web-fetch") && reading)
|
|
383
|
-
return "Researched for";
|
|
384
|
-
if (has("web-fetch"))
|
|
385
|
-
return "Fetched the web for";
|
|
386
|
-
if (has("bash") && writing)
|
|
387
|
-
return "Built & ran for";
|
|
388
|
-
if (has("edit") && has("write"))
|
|
389
|
-
return "Crafted code for";
|
|
390
|
-
if (has("edit") && has("bash"))
|
|
391
|
-
return "Refactored & tested for";
|
|
392
|
-
if (has("edit") && reading)
|
|
393
|
-
return "Refactored for";
|
|
394
|
-
if (has("edit"))
|
|
395
|
-
return "Refactored for";
|
|
396
|
-
if (has("write") && has("bash"))
|
|
397
|
-
return "Wrote & ran for";
|
|
398
|
-
if (has("write") && reading)
|
|
399
|
-
return "Wrote code for";
|
|
400
|
-
if (has("write"))
|
|
401
|
-
return "Wrote code for";
|
|
402
|
-
if (has("bash") && has("grep"))
|
|
403
|
-
return "Hacked away for";
|
|
404
|
-
if (has("bash") && reading)
|
|
405
|
-
return "Ran & investigated for";
|
|
406
|
-
if (has("bash"))
|
|
407
|
-
return "Executed commands for";
|
|
408
|
-
if (hasAny("tasks", "task-output", "task-stop"))
|
|
409
|
-
return "Managed tasks for";
|
|
410
|
-
if (has("grep") && has("read"))
|
|
411
|
-
return "Investigated for";
|
|
412
|
-
if (has("grep") && has("find"))
|
|
413
|
-
return "Scoured the codebase for";
|
|
414
|
-
if (has("grep"))
|
|
415
|
-
return "Searched for";
|
|
416
|
-
if (has("read") && has("find"))
|
|
417
|
-
return "Explored for";
|
|
418
|
-
if (has("read"))
|
|
419
|
-
return "Studied the code for";
|
|
420
|
-
if (has("find") || has("ls"))
|
|
421
|
-
return "Browsed files for";
|
|
422
|
-
// No tools used — pure text response
|
|
423
|
-
const phrases = [
|
|
424
|
-
"Pondered for",
|
|
425
|
-
"Thought for",
|
|
426
|
-
"Reasoned for",
|
|
427
|
-
"Mulled it over for",
|
|
428
|
-
"Noodled on it for",
|
|
429
|
-
"Brewed up a response in",
|
|
430
|
-
"Cooked up an answer in",
|
|
431
|
-
"Worked out a reply in",
|
|
432
|
-
"Channeled wisdom for",
|
|
433
|
-
"Conjured a response in",
|
|
434
|
-
];
|
|
435
|
-
return phrases[Math.floor(Math.random() * phrases.length)];
|
|
436
|
-
}
|
|
437
|
-
// ── Animated thinking border ────────────────────────────────
|
|
438
|
-
const THINKING_BORDER_COLORS = ["#60a5fa", "#818cf8", "#a78bfa", "#818cf8", "#60a5fa"];
|
|
439
|
-
// ── Task count helper ───────────────────────────────────────
|
|
440
|
-
function getTaskCount(cwd) {
|
|
441
|
-
try {
|
|
442
|
-
const hash = createHash("sha256").update(cwd).digest("hex").slice(0, 16);
|
|
443
|
-
const data = readFileSync(join(homedir(), ".ezcoder-tasks", "projects", hash, "tasks.json"), "utf-8");
|
|
444
|
-
const tasks = JSON.parse(data);
|
|
445
|
-
return tasks.filter((t) => t.status !== "done").length;
|
|
446
|
-
}
|
|
447
|
-
catch {
|
|
448
|
-
return 0;
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
function getNextPendingTask(cwd) {
|
|
452
|
-
try {
|
|
453
|
-
const hash = createHash("sha256").update(cwd).digest("hex").slice(0, 16);
|
|
454
|
-
const data = readFileSync(join(homedir(), ".ezcoder-tasks", "projects", hash, "tasks.json"), "utf-8");
|
|
455
|
-
const tasks = JSON.parse(data);
|
|
456
|
-
const pending = tasks.find((t) => t.status === "pending");
|
|
457
|
-
if (!pending)
|
|
458
|
-
return null;
|
|
459
|
-
return {
|
|
460
|
-
id: pending.id,
|
|
461
|
-
title: pending.title,
|
|
462
|
-
prompt: pending.prompt || pending.text || pending.title,
|
|
463
|
-
};
|
|
464
|
-
}
|
|
465
|
-
catch {
|
|
466
|
-
return null;
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
function markTaskInProgress(cwd, taskId) {
|
|
470
|
-
try {
|
|
471
|
-
const hash = createHash("sha256").update(cwd).digest("hex").slice(0, 16);
|
|
472
|
-
const filePath = join(homedir(), ".ezcoder-tasks", "projects", hash, "tasks.json");
|
|
473
|
-
const data = readFileSync(filePath, "utf-8");
|
|
474
|
-
const tasks = JSON.parse(data);
|
|
475
|
-
const updated = tasks.map((t) => (t.id === taskId ? { ...t, status: "in-progress" } : t));
|
|
476
|
-
writeFileSync(filePath, JSON.stringify(updated, null, 2) + "\n", "utf-8");
|
|
477
|
-
}
|
|
478
|
-
catch {
|
|
479
|
-
// ignore
|
|
480
|
-
}
|
|
196
|
+
export function nextGoalModeAfterAgentDone({ currentMode, runningGoalIds, queuedSyntheticEvents, activeContinuationFlights = 0, wasGoalSetupTurn, }) {
|
|
197
|
+
if (wasGoalSetupTurn)
|
|
198
|
+
return "off";
|
|
199
|
+
if (currentMode === "planner" || currentMode === "setup")
|
|
200
|
+
return currentMode;
|
|
201
|
+
if (queuedSyntheticEvents > 0)
|
|
202
|
+
return "coordinator";
|
|
203
|
+
if (activeContinuationFlights > 0)
|
|
204
|
+
return "coordinator";
|
|
205
|
+
if (currentMode === "coordinator" && runningGoalIds > 0)
|
|
206
|
+
return "coordinator";
|
|
207
|
+
return "off";
|
|
481
208
|
}
|
|
209
|
+
// ── App Component ──────────────────────────────────────────
|
|
482
210
|
/**
|
|
483
|
-
*
|
|
484
|
-
*
|
|
485
|
-
*
|
|
486
|
-
* the id is stale, the next auto-advance step still proceeds correctly.
|
|
211
|
+
* Extract inline image previews carried on a tool result's `details` payload.
|
|
212
|
+
* Image-producing tools (e.g. screenshot) attach `{ imagePreviews: [...] }` so
|
|
213
|
+
* the terminal-history printer can render the captured pixels inline.
|
|
487
214
|
*/
|
|
488
|
-
function
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
}
|
|
215
|
+
function extractToolImagePreviews(details) {
|
|
216
|
+
if (!details || typeof details !== "object")
|
|
217
|
+
return undefined;
|
|
218
|
+
const previews = details.imagePreviews;
|
|
219
|
+
if (!Array.isArray(previews))
|
|
220
|
+
return undefined;
|
|
221
|
+
const valid = previews.filter((p) => typeof p === "object" &&
|
|
222
|
+
p !== null &&
|
|
223
|
+
typeof p.base64 === "string" &&
|
|
224
|
+
typeof p.mediaType === "string");
|
|
225
|
+
return valid.length > 0 ? valid : undefined;
|
|
500
226
|
}
|
|
501
|
-
// ── App Component ──────────────────────────────────────────
|
|
502
227
|
export function App(props) {
|
|
503
228
|
const theme = useTheme();
|
|
504
229
|
const switchTheme = useSetTheme();
|
|
505
|
-
const {
|
|
230
|
+
const { write: writeStdout } = useStdout();
|
|
231
|
+
const { columns, rows } = useTerminalSize();
|
|
506
232
|
// Hoisted before terminal title hook so it can reference them
|
|
507
233
|
const [lastUserMessage, setLastUserMessage] = useState("");
|
|
234
|
+
// Bumped on every prompt submit; the fullscreen transcript scroll controller
|
|
235
|
+
// watches this to snap back to the bottom so the newest output is visible.
|
|
236
|
+
const [scrollResetToken, setScrollResetToken] = useState(0);
|
|
508
237
|
const [exitPending, setExitPending] = useState(false);
|
|
509
|
-
|
|
510
|
-
// mode survives /clear's unmount/remount, matching the prior behavior
|
|
511
|
-
// where /clear didn't toggle plan mode off.
|
|
512
|
-
const [planMode, setPlanMode] = useState(props.planModeRef?.current ?? false);
|
|
513
|
-
const planModeLocalRef = useRef(false);
|
|
514
|
-
planModeLocalRef.current = planMode;
|
|
238
|
+
const [quittingSummary, setQuittingSummary] = useState(null);
|
|
515
239
|
// Terminal title — updated later after agentLoop is created
|
|
516
240
|
// (hoisted here so the hook is always called in the same order)
|
|
517
241
|
const [titleRunning, setTitleRunning] = useState(false);
|
|
@@ -521,14 +245,11 @@ export function App(props) {
|
|
|
521
245
|
isRunning: titleRunning,
|
|
522
246
|
sessionTitle,
|
|
523
247
|
});
|
|
524
|
-
//
|
|
525
|
-
//
|
|
526
|
-
//
|
|
527
|
-
//
|
|
528
|
-
// so slice(0) returns the full array regardless of length.
|
|
248
|
+
// Completed transcript rows are kept as durable session data but are no longer
|
|
249
|
+
// rendered through Ink history. They are serialized once into real terminal
|
|
250
|
+
// scrollback via terminalHistoryPrinter, while Ink owns only live rows and
|
|
251
|
+
// controls. This avoids Static/log-update replay drift on resize/remount.
|
|
529
252
|
const [history, setHistory] = useState(() => {
|
|
530
|
-
// sessionStore wins (lives across remount). Falls back to initialHistory
|
|
531
|
-
// (loaded from a session file at startup), then a fresh banner-only list.
|
|
532
253
|
const stored = props.sessionStore?.history;
|
|
533
254
|
if (stored && stored.length > 0)
|
|
534
255
|
return stored;
|
|
@@ -538,57 +259,68 @@ export function App(props) {
|
|
|
538
259
|
return [{ kind: "banner", id: "banner" }];
|
|
539
260
|
});
|
|
540
261
|
// Items from the current/last turn — rendered in the live area so they stay visible.
|
|
541
|
-
// Seed from sessionStore so
|
|
542
|
-
//
|
|
543
|
-
const [liveItems, setLiveItems] = useState(() =>
|
|
262
|
+
// Seed from sessionStore so live output
|
|
263
|
+
// survives pane/overlay/resize remounts before it is finalized.
|
|
264
|
+
const [liveItems, setLiveItems] = useState(() => {
|
|
265
|
+
const restoredLiveItems = uniqueItemsById(props.sessionStore?.liveItems ?? []);
|
|
266
|
+
const restoredHistoryIds = new Set(history.map((item) => item.id));
|
|
267
|
+
return removeItemsWithIds(restoredLiveItems, restoredHistoryIds);
|
|
268
|
+
});
|
|
269
|
+
// Rolling feed of recent tool actions for the pinned LiveToolPanel. Kept
|
|
270
|
+
// separate from `liveItems` (the scrollback record) so tool calls mutate in
|
|
271
|
+
// place above the activity bar instead of spamming the transcript.
|
|
272
|
+
const [liveToolFeed, setLiveToolFeed] = useState([]);
|
|
544
273
|
// overlay seeded from sessionStore (lives across remount). Falls back to
|
|
545
274
|
// props.initialOverlay (CLI launched with one), then null.
|
|
546
275
|
const [overlay, setOverlay] = useState(props.sessionStore?.overlay ?? props.initialOverlay ?? null);
|
|
547
|
-
const [taskCount, setTaskCount] = useState(() => getTaskCount(props.cwd));
|
|
548
|
-
const [goalCount, setGoalCount] = useState(0);
|
|
549
|
-
const [goalStatusEntries, setGoalStatusEntries] = useState(props.sessionStore?.goalStatusEntries ?? []);
|
|
550
|
-
const [eyesCount, setEyesCount] = useState(() => isEyesActive(props.cwd) ? journalCount({ status: "open" }, props.cwd) : undefined);
|
|
551
276
|
const [updatePending, setUpdatePending] = useState(() => getPendingUpdate(props.version) !== null);
|
|
552
|
-
//
|
|
553
|
-
//
|
|
277
|
+
// Signal that pushes text into the InputArea composer (e.g. restoring queued
|
|
278
|
+
// messages after an interrupt). Bumping `nonce` triggers the injection even
|
|
279
|
+
// when the text is identical to a prior restore.
|
|
280
|
+
const [composerInject, setComposerInject] = useState(null);
|
|
281
|
+
const agentRunningRef = useRef(false);
|
|
554
282
|
const [runAllTasks, setRunAllTasks] = useState(props.sessionStore?.runAllTasks ?? false);
|
|
555
283
|
const runAllTasksRef = useRef(props.sessionStore?.runAllTasks ?? false);
|
|
556
|
-
// Task id of the run-all task currently driving the agent. Tracked so the
|
|
557
|
-
// run-all `onDone` auto-advance can mark the just-finished task as done
|
|
558
|
-
// even when the agent forgets to call the tasks tool. Mirrored through
|
|
559
|
-
// sessionStore so the value survives the resetUI() remount between tasks.
|
|
560
|
-
const currentTaskIdRef = useRef(props.sessionStore?.currentTaskId ?? null);
|
|
561
284
|
const startTaskRef = useRef(() => { });
|
|
562
|
-
const agentRunningRef = useRef(false);
|
|
563
|
-
const runningGoalIdsRef = useRef(new Set());
|
|
564
|
-
const activeVerifierRunIdsRef = useRef(new Set());
|
|
565
|
-
const startGoalRunRef = useRef(() => { });
|
|
566
285
|
const runAllPixelRef = useRef(props.sessionStore?.runAllPixel ?? false);
|
|
567
286
|
const currentPixelFixRef = useRef(null);
|
|
568
287
|
const startPixelFixRef = useRef(() => { });
|
|
569
288
|
const cwdRef = useRef(props.cwd);
|
|
570
289
|
const [displayedCwd, setDisplayedCwd] = useState(props.cwd);
|
|
290
|
+
// /rewind overlay: holds the checkpoint list while the picker is open.
|
|
291
|
+
const [rewindCheckpoints, setRewindCheckpoints] = useState(null);
|
|
292
|
+
// Monotonic user-turn counter keying per-turn checkpoints.
|
|
293
|
+
const rewindTurnRef = useRef(0);
|
|
294
|
+
const taskPicker = useTaskPickerController({
|
|
295
|
+
displayedCwd,
|
|
296
|
+
onStartTask: (title, prompt, taskId) => startTaskRef.current(title, prompt, taskId),
|
|
297
|
+
onRunAllTasksChange: setRunAllTasks,
|
|
298
|
+
});
|
|
571
299
|
const [doneStatus, setDoneStatus] = useState(props.sessionStore?.doneStatus ?? null);
|
|
572
300
|
// Suppress "done" status when a plan overlay is about to open
|
|
573
301
|
const planOverlayPendingRef = useRef(false);
|
|
574
302
|
const [gitBranch, setGitBranch] = useState(null);
|
|
575
303
|
const [currentModel, setCurrentModel] = useState(props.model);
|
|
304
|
+
const currentModelRef = useRef(props.model);
|
|
576
305
|
const [currentProvider, setCurrentProvider] = useState(props.provider);
|
|
306
|
+
const currentProviderRef = useRef(props.provider);
|
|
577
307
|
const [currentTools, setCurrentTools] = useState(props.tools);
|
|
578
308
|
const currentToolsRef = useRef(props.tools);
|
|
579
|
-
const [
|
|
309
|
+
const [thinkingLevel, setThinkingLevel] = useState(props.thinking);
|
|
310
|
+
const [renderMarkdown, setRenderMarkdown] = useState(true);
|
|
580
311
|
const messagesRef = useRef(props.sessionStore?.messages ?? props.messages);
|
|
581
|
-
const repoMapInjectionEnabledRef = useRef(true);
|
|
582
|
-
const repoMapDirtyRef = useRef(true);
|
|
583
|
-
const repoMapMarkdownRef = useRef("");
|
|
584
|
-
const repoMapSnapshotRef = useRef(undefined);
|
|
585
|
-
const repoMapChangedCountRef = useRef(0);
|
|
586
|
-
const repoMapCacheRef = useRef(createRepoMapCache());
|
|
587
312
|
const [planAutoExpand, setPlanAutoExpand] = useState(props.sessionStore?.planAutoExpand ?? false);
|
|
313
|
+
const [goalAutoExpand, setGoalAutoExpand] = useState(props.sessionStore?.goalAutoExpand ?? false);
|
|
314
|
+
const goalAutoExpandRef = useRef(props.sessionStore?.goalAutoExpand ?? false);
|
|
315
|
+
const [goalStatusEntries, setGoalStatusEntries] = useState(props.sessionStore?.goalStatusEntries ?? []);
|
|
316
|
+
const runningGoalIdsRef = useRef(new Set());
|
|
317
|
+
const activeVerifierRunIdsRef = useRef(new Set());
|
|
318
|
+
const goalContinuationFlightsRef = useRef(new Set());
|
|
319
|
+
const goalContinuationRecentChoicesRef = useRef(new Map());
|
|
320
|
+
const startGoalRunRef = useRef(() => { });
|
|
588
321
|
const approvedPlanPathRef = useRef(props.sessionStore?.approvedPlanPath);
|
|
589
322
|
const planStepsRef = useRef(props.sessionStore?.planSteps ?? []);
|
|
590
323
|
const [planSteps, setPlanSteps] = useState(props.sessionStore?.planSteps ?? []);
|
|
591
|
-
const planModeStateRef = useRef(planMode);
|
|
592
324
|
// Stuck-guard for the plan-continuation follow-up nudge. Tracks how many
|
|
593
325
|
// times we've nudged the agent to continue the same step. Reset whenever a
|
|
594
326
|
// new [DONE:n] marker advances progress (see onTurnText). Caps at 2 nudges
|
|
@@ -596,32 +328,25 @@ export function App(props) {
|
|
|
596
328
|
const followUpNudgesRef = useRef({ step: 0, count: 0 });
|
|
597
329
|
// Seed the per-item ID counter so it doesn't collide with IDs already in
|
|
598
330
|
// sessionStore.history (which survives remount). Without this, a remount
|
|
599
|
-
// (resize, overlay toggle, etc.) starts the counter at 0
|
|
600
|
-
// generate ids "0", "1", "2"… that collide with
|
|
601
|
-
// previous mount, triggering React's duplicate-key
|
|
602
|
-
// duplicate/omitted renders.
|
|
603
|
-
const nextIdRef = useRef((
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
];
|
|
608
|
-
let max = -1;
|
|
609
|
-
for (const item of items) {
|
|
610
|
-
const n = Number(item.id);
|
|
611
|
-
if (Number.isFinite(n) && n > max)
|
|
612
|
-
max = n;
|
|
613
|
-
}
|
|
614
|
-
return max + 1;
|
|
615
|
-
})());
|
|
331
|
+
// (resize, overlay toggle, task pane open, etc.) starts the counter at 0
|
|
332
|
+
// and new items generate ids "ui-0", "ui-1", "ui-2"… that collide with
|
|
333
|
+
// the same ids from the previous mount, triggering React's duplicate-key
|
|
334
|
+
// warning and causing duplicate/omitted renders.
|
|
335
|
+
const nextIdRef = useRef(getNextGeneratedItemId([
|
|
336
|
+
...(props.sessionStore?.history ?? props.initialHistory ?? []),
|
|
337
|
+
...(props.sessionStore?.liveItems ?? []),
|
|
338
|
+
]));
|
|
616
339
|
const sessionManagerRef = useRef(props.sessionsDir ? new SessionManager(props.sessionsDir) : null);
|
|
617
340
|
const sessionPathRef = useRef(props.sessionStore?.sessionPath ?? props.sessionPath);
|
|
618
341
|
const persistedIndexRef = useRef(messagesRef.current.length);
|
|
342
|
+
const sessionStatsRef = useRef(props.sessionStore?.sessionStats ??
|
|
343
|
+
createSessionStats({ sessionId: props.sessionStore?.sessionId ?? props.sessionId }));
|
|
344
|
+
const [idealReviewEnabled, setIdealReviewEnabled] = useState(props.sessionStore?.idealReviewEnabled ?? props.idealReviewEnabled ?? true);
|
|
345
|
+
const idealReviewEnabledRef = useRef(idealReviewEnabled);
|
|
619
346
|
/** Last actual API-reported input token count (from turn_end). */
|
|
620
347
|
const lastActualTokensRef = useRef(0);
|
|
621
348
|
/** Timestamp (ms) when lastActualTokensRef was last updated by turn_end. */
|
|
622
349
|
const lastActualTokensTimestampRef = useRef(0);
|
|
623
|
-
/** Timestamp of last compaction — used for time-based cooldown and staleness detection. */
|
|
624
|
-
const lastCompactionTimeRef = useRef(0);
|
|
625
350
|
/**
|
|
626
351
|
* Languages whose style packs are currently injected into the system prompt.
|
|
627
352
|
* Grown by `maybeInjectLanguagePacks` after `write`/`bash` tool results when
|
|
@@ -643,6 +368,29 @@ export function App(props) {
|
|
|
643
368
|
*/
|
|
644
369
|
const triggerAutoSetupRef = useRef(async () => { });
|
|
645
370
|
const getId = () => `ui-${nextIdRef.current++}`;
|
|
371
|
+
useEffect(() => {
|
|
372
|
+
idealReviewEnabledRef.current = idealReviewEnabled;
|
|
373
|
+
if (props.sessionStore)
|
|
374
|
+
props.sessionStore.idealReviewEnabled = idealReviewEnabled;
|
|
375
|
+
}, [idealReviewEnabled, props.sessionStore]);
|
|
376
|
+
const sessionStore = props.sessionStore;
|
|
377
|
+
const { planMode, goalMode, rebuildSystemPrompt, replaceSystemPrompt, setPlanModeAndPrompt, setGoalModeAndPrompt, } = useModeState({
|
|
378
|
+
initialPlanMode: props.sessionStore?.planMode ?? props.planModeRef?.current ?? false,
|
|
379
|
+
initialGoalMode: props.sessionStore?.goalMode ?? props.goalModeRef?.current ?? "off",
|
|
380
|
+
skills: props.skills,
|
|
381
|
+
planModeRef: props.planModeRef,
|
|
382
|
+
goalModeRef: props.goalModeRef,
|
|
383
|
+
sessionStore: props.sessionStore,
|
|
384
|
+
cwdRef,
|
|
385
|
+
currentToolsRef,
|
|
386
|
+
providerRef: currentProviderRef,
|
|
387
|
+
approvedPlanPathRef,
|
|
388
|
+
injectedLanguagesRef,
|
|
389
|
+
messagesRef,
|
|
390
|
+
});
|
|
391
|
+
const appendGoalAgentTransition = useCallback((text) => {
|
|
392
|
+
setLiveItems((prev) => [...prev, { kind: "goal_agent_transition", text, id: getId() }]);
|
|
393
|
+
}, []);
|
|
646
394
|
const appendGoalProgress = useCallback((item) => {
|
|
647
395
|
setLiveItems((prev) => [...prev, { ...item, id: getId() }]);
|
|
648
396
|
}, []);
|
|
@@ -663,47 +411,64 @@ export function App(props) {
|
|
|
663
411
|
return next;
|
|
664
412
|
});
|
|
665
413
|
}, [props.sessionStore]);
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
414
|
+
const { pendingHistoryFlushRef, streamedAssistantFlushRef, queueFlush, finalizeSubmittedUserItem, clearPendingHistory, } = useTranscriptHistory({
|
|
415
|
+
// In fullscreen alt-screen mode the transcript renders inside Ink (the
|
|
416
|
+
// viewport), so we must NOT write completed rows to native scrollback —
|
|
417
|
+
// doing so would scroll the live frame. Dropping the printer keeps history
|
|
418
|
+
// accumulating in React state (still flushed + persisted) without any
|
|
419
|
+
// stdout writes. The legacy scrollback path keeps the printer.
|
|
420
|
+
terminalHistoryPrinter: props.fullscreen ? undefined : props.terminalHistoryPrinter,
|
|
421
|
+
terminalHistoryContext: {
|
|
422
|
+
theme,
|
|
423
|
+
columns,
|
|
424
|
+
version: props.version,
|
|
425
|
+
model: currentModel,
|
|
426
|
+
provider: currentProvider,
|
|
427
|
+
cwd: displayedCwd,
|
|
428
|
+
},
|
|
429
|
+
writeStdout,
|
|
430
|
+
sessionPathRef,
|
|
431
|
+
sessionManagerRef,
|
|
432
|
+
sessionStore,
|
|
433
|
+
history,
|
|
434
|
+
setHistory,
|
|
435
|
+
setLiveItems,
|
|
436
|
+
});
|
|
681
437
|
// Mirror runtime state choices (model/provider/thinking) into renderApp's
|
|
682
438
|
// closure so unmount/remount preserves them.
|
|
683
439
|
const onRuntimeStateChange = props.onRuntimeStateChange;
|
|
684
440
|
useEffect(() => {
|
|
441
|
+
currentModelRef.current = currentModel;
|
|
685
442
|
onRuntimeStateChange?.({ model: currentModel });
|
|
686
443
|
}, [currentModel, onRuntimeStateChange]);
|
|
687
444
|
useEffect(() => {
|
|
688
445
|
onRuntimeStateChange?.({ provider: currentProvider });
|
|
689
446
|
}, [currentProvider, onRuntimeStateChange]);
|
|
447
|
+
useEffect(() => {
|
|
448
|
+
if (thinkingLevel && !isThinkingLevelSupported(currentProvider, currentModel, thinkingLevel)) {
|
|
449
|
+
setThinkingLevel(getNextThinkingLevel(currentProvider, currentModel, undefined));
|
|
450
|
+
}
|
|
451
|
+
}, [currentProvider, currentModel, thinkingLevel]);
|
|
690
452
|
useEffect(() => {
|
|
691
453
|
onRuntimeStateChange?.({
|
|
692
|
-
thinking:
|
|
454
|
+
thinking: thinkingLevel,
|
|
693
455
|
});
|
|
694
|
-
}, [
|
|
456
|
+
}, [thinkingLevel, onRuntimeStateChange]);
|
|
695
457
|
// Mirror session state into renderApp's closure so resetUI() can re-seed
|
|
696
458
|
// the conversation on remount. Each panel that previously did a bare ANSI
|
|
697
|
-
// screen clear (overlay open/close, plan accept/reject, /clear
|
|
459
|
+
// screen clear (overlay open/close, plan accept/reject, /clear)
|
|
698
460
|
// now goes through resetUI; without these mirrors, the chat would vanish.
|
|
699
|
-
const
|
|
461
|
+
const historyRef = useRef(history);
|
|
700
462
|
useEffect(() => {
|
|
463
|
+
historyRef.current = history;
|
|
701
464
|
if (sessionStore)
|
|
702
465
|
sessionStore.history = history;
|
|
703
466
|
}, [history, sessionStore]);
|
|
704
467
|
useEffect(() => {
|
|
705
|
-
if (sessionStore)
|
|
706
|
-
|
|
468
|
+
if (!sessionStore)
|
|
469
|
+
return;
|
|
470
|
+
const historyIds = new Set(historyRef.current.map((item) => item.id));
|
|
471
|
+
sessionStore.liveItems = removeItemsWithIds(uniqueItemsById(liveItems), historyIds);
|
|
707
472
|
}, [liveItems, sessionStore]);
|
|
708
473
|
useEffect(() => {
|
|
709
474
|
if (sessionStore)
|
|
@@ -721,10 +486,27 @@ export function App(props) {
|
|
|
721
486
|
if (sessionStore)
|
|
722
487
|
sessionStore.overlay = overlay;
|
|
723
488
|
}, [overlay, sessionStore]);
|
|
489
|
+
useEffect(() => {
|
|
490
|
+
goalAutoExpandRef.current = goalAutoExpand;
|
|
491
|
+
if (sessionStore)
|
|
492
|
+
sessionStore.goalAutoExpand = goalAutoExpand;
|
|
493
|
+
}, [goalAutoExpand, sessionStore]);
|
|
724
494
|
useEffect(() => {
|
|
725
495
|
if (sessionStore)
|
|
726
496
|
sessionStore.goalStatusEntries = goalStatusEntries;
|
|
727
497
|
}, [goalStatusEntries, sessionStore]);
|
|
498
|
+
useEffect(() => {
|
|
499
|
+
if (sessionStore)
|
|
500
|
+
sessionStore.planMode = planMode;
|
|
501
|
+
}, [planMode, sessionStore]);
|
|
502
|
+
useEffect(() => {
|
|
503
|
+
if (sessionStore)
|
|
504
|
+
sessionStore.goalMode = goalMode;
|
|
505
|
+
}, [goalMode, sessionStore]);
|
|
506
|
+
useEffect(() => {
|
|
507
|
+
if (sessionStore)
|
|
508
|
+
sessionStore.sessionStats = sessionStatsRef.current;
|
|
509
|
+
}, [sessionStore]);
|
|
728
510
|
// pendingAction is consumed via a useEffect AFTER agentLoop is created
|
|
729
511
|
// — see below where useAgentLoop is set up.
|
|
730
512
|
const pendingActionConsumedRef = useRef(false);
|
|
@@ -742,14 +524,13 @@ export function App(props) {
|
|
|
742
524
|
}, [displayedCwd]);
|
|
743
525
|
useEffect(() => {
|
|
744
526
|
let cancelled = false;
|
|
745
|
-
const
|
|
527
|
+
const refreshGoalState = () => {
|
|
746
528
|
void reconcileActiveGoalRuns(props.cwd, {
|
|
747
529
|
isWorkerActive: (workerId) => listGoalWorkers(props.cwd).some((worker) => worker.id === workerId && worker.status === "running"),
|
|
748
530
|
}).then(({ runs }) => {
|
|
749
|
-
const counts = summarizeGoalCountsFromRuns(runs);
|
|
750
531
|
if (cancelled)
|
|
751
532
|
return;
|
|
752
|
-
|
|
533
|
+
setHistory((prev) => completedItemsWithDurableGoalTerminalProgress(prev, runs));
|
|
753
534
|
setGoalStatusEntries((prev) => {
|
|
754
535
|
const next = reconcileGoalStatusEntriesWithRuns(prev, runs, {
|
|
755
536
|
isWorkerActive: (workerId, run) => listGoalWorkers(props.cwd).some((worker) => worker.id === workerId &&
|
|
@@ -763,13 +544,13 @@ export function App(props) {
|
|
|
763
544
|
});
|
|
764
545
|
});
|
|
765
546
|
};
|
|
766
|
-
|
|
767
|
-
const interval = setInterval(
|
|
547
|
+
refreshGoalState();
|
|
548
|
+
const interval = setInterval(refreshGoalState, 1_000);
|
|
768
549
|
return () => {
|
|
769
550
|
cancelled = true;
|
|
770
551
|
clearInterval(interval);
|
|
771
552
|
};
|
|
772
|
-
}, [props.cwd]);
|
|
553
|
+
}, [props.cwd, props.sessionStore]);
|
|
773
554
|
// Periodic update check during long sessions
|
|
774
555
|
useEffect(() => {
|
|
775
556
|
startPeriodicUpdateCheck(props.version, (msg) => {
|
|
@@ -789,27 +570,29 @@ export function App(props) {
|
|
|
789
570
|
useEffect(() => {
|
|
790
571
|
currentToolsRef.current = currentTools;
|
|
791
572
|
}, [currentTools]);
|
|
792
|
-
// ── Plan mode wiring ─────────────────────────────────────
|
|
793
|
-
// Sync planModeRef with React state
|
|
794
573
|
useEffect(() => {
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
574
|
+
if (!props.connectInitialMcpTools)
|
|
575
|
+
return;
|
|
576
|
+
let cancelled = false;
|
|
577
|
+
void props
|
|
578
|
+
.connectInitialMcpTools()
|
|
579
|
+
.then((mcpTools) => {
|
|
580
|
+
if (cancelled || mcpTools.length === 0)
|
|
581
|
+
return;
|
|
582
|
+
setCurrentTools((prev) => {
|
|
583
|
+
const next = [...prev.filter((tool) => !tool.name.startsWith("mcp__")), ...mcpTools];
|
|
584
|
+
currentToolsRef.current = next;
|
|
585
|
+
void replaceSystemPrompt({ tools: next });
|
|
586
|
+
return next;
|
|
587
|
+
});
|
|
588
|
+
})
|
|
589
|
+
.catch((err) => {
|
|
590
|
+
log("WARN", "mcp", `MCP initialization failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
591
|
+
});
|
|
592
|
+
return () => {
|
|
593
|
+
cancelled = true;
|
|
594
|
+
};
|
|
595
|
+
}, [props.connectInitialMcpTools, replaceSystemPrompt]);
|
|
813
596
|
/**
|
|
814
597
|
* Unified "apply detection result" pipeline. Called from three sites:
|
|
815
598
|
* 1. Initial mount (existing project at startup).
|
|
@@ -894,89 +677,17 @@ export function App(props) {
|
|
|
894
677
|
useEffect(() => {
|
|
895
678
|
void applyLanguageDetectionRef.current("initial");
|
|
896
679
|
}, []);
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
...prev,
|
|
909
|
-
{ kind: "plan_transition", text: msg, active: true, id: getId() },
|
|
910
|
-
]);
|
|
911
|
-
};
|
|
912
|
-
}
|
|
913
|
-
}, [props.onEnterPlanRef]);
|
|
914
|
-
// Wire onExitPlan callback ref
|
|
915
|
-
useEffect(() => {
|
|
916
|
-
if (props.onExitPlanRef) {
|
|
917
|
-
props.onExitPlanRef.current = async (planPath) => {
|
|
918
|
-
// Deactivate plan mode, store approved plan path, open pane
|
|
919
|
-
planModeStateRef.current = false;
|
|
920
|
-
setPlanMode(false);
|
|
921
|
-
approvedPlanPathRef.current = planPath;
|
|
922
|
-
await replaceSystemPrompt({ planMode: false, approvedPlanPath: planPath });
|
|
923
|
-
// Use setTimeout to open pane after the current tool execution completes,
|
|
924
|
-
// so the turn can finish and the UI transitions cleanly
|
|
925
|
-
// Flag that the plan overlay is about to open — suppresses the
|
|
926
|
-
// premature "done" status that fires when the agent loop finishes
|
|
927
|
-
planOverlayPendingRef.current = true;
|
|
928
|
-
setTimeout(() => {
|
|
929
|
-
setPlanAutoExpand(true);
|
|
930
|
-
setOverlay("plan");
|
|
931
|
-
// Don't clear planOverlayPendingRef here — keep it true until
|
|
932
|
-
// the user actually approves/rejects the plan. Clearing it on a
|
|
933
|
-
// timer causes a race where agent_done fires after the 300ms
|
|
934
|
-
// timeout but before the user interacts, triggering a premature
|
|
935
|
-
// completion sound.
|
|
936
|
-
}, 300);
|
|
937
|
-
return ("Plan submitted. Exiting plan mode.\n" +
|
|
938
|
-
"The plan pane is opening for user review.\n" +
|
|
939
|
-
"Plan saved at: " +
|
|
940
|
-
planPath);
|
|
941
|
-
};
|
|
942
|
-
}
|
|
943
|
-
}, [props.onExitPlanRef, replaceSystemPrompt]);
|
|
944
|
-
const appendMessagesToSession = useCallback(async (sessionPath, messages, startIndex) => {
|
|
945
|
-
const sm = sessionManagerRef.current;
|
|
946
|
-
if (!sm)
|
|
947
|
-
return;
|
|
948
|
-
await appendSessionMessages(sm, sessionPath, messages, startIndex);
|
|
949
|
-
}, []);
|
|
950
|
-
const persistCompactedSession = useCallback(async (compactedMessages) => {
|
|
951
|
-
const sm = sessionManagerRef.current;
|
|
952
|
-
if (!sm)
|
|
953
|
-
return;
|
|
954
|
-
const session = await createCompactedSessionCheckpoint(sm, {
|
|
955
|
-
cwd: cwdRef.current,
|
|
956
|
-
provider: currentProvider,
|
|
957
|
-
model: currentModel,
|
|
958
|
-
messages: compactedMessages,
|
|
959
|
-
});
|
|
960
|
-
sessionPathRef.current = session.path;
|
|
961
|
-
persistedIndexRef.current = compactedMessages.length;
|
|
962
|
-
if (sessionStore) {
|
|
963
|
-
sessionStore.sessionPath = session.path;
|
|
964
|
-
sessionStore.messages = [...compactedMessages];
|
|
965
|
-
}
|
|
966
|
-
log("INFO", "compaction", "Persisted compacted session checkpoint", { path: session.path });
|
|
967
|
-
}, [currentModel, currentProvider, sessionStore]);
|
|
968
|
-
const persistNewMessages = useCallback(async () => {
|
|
969
|
-
const sp = sessionPathRef.current;
|
|
970
|
-
if (!sp)
|
|
971
|
-
return;
|
|
972
|
-
const allMsgs = messagesRef.current;
|
|
973
|
-
await appendMessagesToSession(sp, allMsgs, persistedIndexRef.current);
|
|
974
|
-
persistedIndexRef.current = allMsgs.length;
|
|
975
|
-
if (sessionStore) {
|
|
976
|
-
sessionStore.messages = [...allMsgs];
|
|
977
|
-
sessionStore.sessionPath = sp;
|
|
978
|
-
}
|
|
979
|
-
}, [appendMessagesToSession, sessionStore]);
|
|
680
|
+
const { persistCompactedSession, persistNewMessages } = useSessionPersistence({
|
|
681
|
+
sessionManagerRef,
|
|
682
|
+
sessionPathRef,
|
|
683
|
+
sessionStatsRef,
|
|
684
|
+
persistedIndexRef,
|
|
685
|
+
messagesRef,
|
|
686
|
+
cwdRef,
|
|
687
|
+
currentProvider,
|
|
688
|
+
currentModel,
|
|
689
|
+
sessionStore,
|
|
690
|
+
});
|
|
980
691
|
/**
|
|
981
692
|
* Run the language detector against the current cwd. If the detected set is a
|
|
982
693
|
* strict superset of what's already injected, rebuild the system prompt with
|
|
@@ -987,7 +698,7 @@ export function App(props) {
|
|
|
987
698
|
* Other tool kinds skip detection entirely to avoid wasted filesystem stats.
|
|
988
699
|
*
|
|
989
700
|
* No restart required: the system prompt is mutated in place, same mechanism
|
|
990
|
-
*
|
|
701
|
+
* used for pixel-fix chdir.
|
|
991
702
|
*
|
|
992
703
|
* Stored in a ref so `onToolEnd` (whose useCallback dep array is intentionally
|
|
993
704
|
* empty to keep agent-loop options stable) can call the freshest version.
|
|
@@ -1011,189 +722,25 @@ export function App(props) {
|
|
|
1011
722
|
});
|
|
1012
723
|
}
|
|
1013
724
|
}, [props.settingsFile]);
|
|
1014
|
-
const compactionAbortRef =
|
|
1015
|
-
const compactConversation = useCallback(async (messages, signal) => {
|
|
1016
|
-
const contextWindow = getContextWindow(currentModel, contextWindowOptions);
|
|
1017
|
-
const tokensBefore = estimateConversationTokens(messages);
|
|
1018
|
-
const spinId = getId();
|
|
1019
|
-
log("INFO", "compaction", `Running compaction`, {
|
|
1020
|
-
messages: String(messages.length),
|
|
1021
|
-
estimatedTokens: String(tokensBefore),
|
|
1022
|
-
contextWindow: String(contextWindow),
|
|
1023
|
-
});
|
|
1024
|
-
// Show animated spinner
|
|
1025
|
-
setLiveItems((prev) => [...prev, { kind: "compacting", id: spinId }]);
|
|
1026
|
-
const ownedAbort = signal ? null : new AbortController();
|
|
1027
|
-
const compactionSignal = signal ?? ownedAbort?.signal;
|
|
1028
|
-
if (ownedAbort)
|
|
1029
|
-
compactionAbortRef.current = ownedAbort;
|
|
1030
|
-
try {
|
|
1031
|
-
// Resolve fresh credentials for compaction too
|
|
1032
|
-
let compactApiKey = activeApiKey;
|
|
1033
|
-
let compactAccountId = activeAccountId;
|
|
1034
|
-
let compactProjectId = activeProjectId;
|
|
1035
|
-
let compactBaseUrl = activeBaseUrl;
|
|
1036
|
-
if (props.authStorage) {
|
|
1037
|
-
const creds = await props.authStorage.resolveCredentials(currentProvider);
|
|
1038
|
-
compactApiKey = creds.accessToken;
|
|
1039
|
-
compactAccountId = creds.accountId;
|
|
1040
|
-
compactProjectId = creds.projectId;
|
|
1041
|
-
compactBaseUrl = creds.baseUrl ?? compactBaseUrl;
|
|
1042
|
-
}
|
|
1043
|
-
const result = await compact(messages, {
|
|
1044
|
-
provider: currentProvider,
|
|
1045
|
-
model: currentModel,
|
|
1046
|
-
apiKey: compactApiKey,
|
|
1047
|
-
accountId: compactAccountId,
|
|
1048
|
-
projectId: compactProjectId,
|
|
1049
|
-
baseUrl: compactBaseUrl,
|
|
1050
|
-
contextWindow,
|
|
1051
|
-
signal: compactionSignal,
|
|
1052
|
-
approvedPlanPath: approvedPlanPathRef.current,
|
|
1053
|
-
});
|
|
1054
|
-
if (result.result.compacted) {
|
|
1055
|
-
// Replace spinner with completed notice
|
|
1056
|
-
setLiveItems((prev) => prev.map((item) => item.id === spinId
|
|
1057
|
-
? {
|
|
1058
|
-
kind: "compacted",
|
|
1059
|
-
originalCount: result.result.originalCount,
|
|
1060
|
-
newCount: result.result.newCount,
|
|
1061
|
-
tokensBefore: result.result.tokensBeforeEstimate,
|
|
1062
|
-
tokensAfter: result.result.tokensAfterEstimate,
|
|
1063
|
-
id: spinId,
|
|
1064
|
-
}
|
|
1065
|
-
: item));
|
|
1066
|
-
}
|
|
1067
|
-
else {
|
|
1068
|
-
// Nothing was actually compacted — remove spinner silently
|
|
1069
|
-
log("INFO", "compaction", `Compaction skipped: ${result.result.reason ?? "unknown"}`);
|
|
1070
|
-
setLiveItems((prev) => prev.filter((item) => item.id !== spinId));
|
|
1071
|
-
}
|
|
1072
|
-
return result.messages;
|
|
1073
|
-
}
|
|
1074
|
-
catch (err) {
|
|
1075
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1076
|
-
const isAbort = compactionSignal?.aborted || msg.includes("aborted") || msg.includes("abort");
|
|
1077
|
-
log(isAbort ? "WARN" : "ERROR", "compaction", isAbort ? "Compaction aborted" : `Compaction failed: ${msg}`);
|
|
1078
|
-
setLiveItems((prev) => isAbort
|
|
1079
|
-
? prev.filter((item) => item.id !== spinId)
|
|
1080
|
-
: prev.map((item) => item.id === spinId ? toErrorItem(err, spinId, "Compaction failed") : item));
|
|
1081
|
-
return messages; // Return unchanged on failure/abort
|
|
1082
|
-
}
|
|
1083
|
-
finally {
|
|
1084
|
-
if (ownedAbort && compactionAbortRef.current === ownedAbort)
|
|
1085
|
-
compactionAbortRef.current = null;
|
|
1086
|
-
}
|
|
1087
|
-
}, [
|
|
725
|
+
const { compactionAbortRef, compactConversation, transformContext } = useContextCompaction({
|
|
1088
726
|
currentModel,
|
|
1089
727
|
currentProvider,
|
|
728
|
+
maxTokens: props.maxTokens,
|
|
729
|
+
authStorage: props.authStorage,
|
|
730
|
+
contextWindowOptions,
|
|
1090
731
|
activeApiKey,
|
|
1091
732
|
activeAccountId,
|
|
1092
733
|
activeProjectId,
|
|
1093
734
|
activeBaseUrl,
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
const getRepoMapBudget = useCallback(() => {
|
|
1102
|
-
const userTurns = messagesRef.current.filter((message) => message.role === "user").length;
|
|
1103
|
-
const readCount = props.repoMapReadFilesRef?.current.size ?? 0;
|
|
1104
|
-
if (userTurns <= 1 && readCount === 0)
|
|
1105
|
-
return FIRST_TURN_REPO_MAP_MAX_CHARS;
|
|
1106
|
-
if (readCount > 0)
|
|
1107
|
-
return FOCUSED_REPO_MAP_MAX_CHARS;
|
|
1108
|
-
return FOCUSED_REPO_MAP_MAX_CHARS + 1000;
|
|
1109
|
-
}, [props.repoMapReadFilesRef]);
|
|
1110
|
-
const refreshRepoMap = useCallback(async (latestUserPrompt) => {
|
|
1111
|
-
const rendered = await buildRepoMap({
|
|
1112
|
-
cwd: cwdRef.current,
|
|
1113
|
-
maxChars: getRepoMapBudget(),
|
|
1114
|
-
changedFiles: [...(props.repoMapChangedFilesRef?.current ?? new Set())],
|
|
1115
|
-
readFiles: [...(props.repoMapReadFilesRef?.current ?? new Set())],
|
|
1116
|
-
focusTerms: latestUserPrompt ? [latestUserPrompt] : [],
|
|
1117
|
-
cache: repoMapCacheRef.current,
|
|
1118
|
-
});
|
|
1119
|
-
repoMapMarkdownRef.current = rendered.markdown;
|
|
1120
|
-
repoMapSnapshotRef.current = rendered.snapshot;
|
|
1121
|
-
repoMapChangedCountRef.current = getRepoMapSignalCount();
|
|
1122
|
-
repoMapDirtyRef.current = false;
|
|
1123
|
-
return rendered.markdown;
|
|
1124
|
-
}, [
|
|
1125
|
-
getRepoMapBudget,
|
|
1126
|
-
getRepoMapSignalCount,
|
|
1127
|
-
props.repoMapChangedFilesRef,
|
|
1128
|
-
props.repoMapReadFilesRef,
|
|
1129
|
-
]);
|
|
1130
|
-
const stripRepoMapMessages = useCallback((messages) => {
|
|
1131
|
-
return stripRepoMapContextMessages(messages);
|
|
1132
|
-
}, []);
|
|
1133
|
-
const injectRepoMapContext = useCallback(async (messages) => {
|
|
1134
|
-
if (!repoMapInjectionEnabledRef.current)
|
|
1135
|
-
return stripRepoMapMessages(messages);
|
|
1136
|
-
const stripped = stripRepoMapMessages(messages);
|
|
1137
|
-
const latestUserPrompt = getLatestUserText(stripped);
|
|
1138
|
-
const signalCount = getRepoMapSignalCount();
|
|
1139
|
-
if (signalCount !== repoMapChangedCountRef.current)
|
|
1140
|
-
repoMapDirtyRef.current = true;
|
|
1141
|
-
if (repoMapDirtyRef.current || !repoMapMarkdownRef.current) {
|
|
1142
|
-
await refreshRepoMap(latestUserPrompt);
|
|
1143
|
-
}
|
|
1144
|
-
if (!repoMapMarkdownRef.current)
|
|
1145
|
-
return stripped;
|
|
1146
|
-
return injectRepoMapContextMessages(stripped, repoMapMarkdownRef.current);
|
|
1147
|
-
}, [props.repoMapChangedFilesRef, props.repoMapReadFilesRef, refreshRepoMap, stripRepoMapMessages]);
|
|
1148
|
-
/**
|
|
1149
|
-
* transformContext callback for the agent loop.
|
|
1150
|
-
* Called before each LLM call and on context overflow.
|
|
1151
|
-
* Compacts persistent chat only, then injects the dynamic repo map transiently.
|
|
1152
|
-
*/
|
|
1153
|
-
const transformContext = useCallback(async (messages, options) => {
|
|
1154
|
-
const stripped = stripRepoMapMessages(messages);
|
|
1155
|
-
const settings = settingsRef.current;
|
|
1156
|
-
const autoCompact = settings?.get("autoCompact") ?? true;
|
|
1157
|
-
const threshold = settings?.get("compactThreshold") ?? 0.8;
|
|
1158
|
-
// Force-compact on context overflow regardless of settings
|
|
1159
|
-
if (options?.force) {
|
|
1160
|
-
const result = await compactConversation(stripped);
|
|
1161
|
-
if (result !== stripped) {
|
|
1162
|
-
messagesRef.current = result;
|
|
1163
|
-
await persistCompactedSession(result);
|
|
1164
|
-
}
|
|
1165
|
-
lastCompactionTimeRef.current = Date.now();
|
|
1166
|
-
return injectRepoMapContext(result);
|
|
1167
|
-
}
|
|
1168
|
-
if (!autoCompact)
|
|
1169
|
-
return injectRepoMapContext(stripped);
|
|
1170
|
-
// Time-based cooldown: skip if compaction ran within the last 30 seconds
|
|
1171
|
-
if (Date.now() - lastCompactionTimeRef.current < 30_000) {
|
|
1172
|
-
log("INFO", "compaction", `Skipping compaction — cooldown active`);
|
|
1173
|
-
return injectRepoMapContext(stripped);
|
|
1174
|
-
}
|
|
1175
|
-
const contextWindow = getContextWindow(currentModel, contextWindowOptions);
|
|
1176
|
-
const reserveTokens = getCompactionReserveTokens(props.maxTokens);
|
|
1177
|
-
const tokensFresh = lastActualTokensTimestampRef.current > lastCompactionTimeRef.current;
|
|
1178
|
-
const actualTokens = lastActualTokensRef.current > 0 && tokensFresh ? lastActualTokensRef.current : undefined;
|
|
1179
|
-
if (shouldCompact(stripped, contextWindow, threshold, actualTokens, reserveTokens)) {
|
|
1180
|
-
const result = await compactConversation(stripped);
|
|
1181
|
-
if (result !== stripped) {
|
|
1182
|
-
messagesRef.current = result;
|
|
1183
|
-
await persistCompactedSession(result);
|
|
1184
|
-
}
|
|
1185
|
-
lastCompactionTimeRef.current = Date.now();
|
|
1186
|
-
return injectRepoMapContext(result);
|
|
1187
|
-
}
|
|
1188
|
-
return injectRepoMapContext(stripped);
|
|
1189
|
-
}, [
|
|
1190
|
-
currentModel,
|
|
1191
|
-
compactConversation,
|
|
1192
|
-
contextWindowOptions,
|
|
1193
|
-
injectRepoMapContext,
|
|
735
|
+
setLiveItems,
|
|
736
|
+
getId,
|
|
737
|
+
approvedPlanPathRef,
|
|
738
|
+
settingsRef,
|
|
739
|
+
messagesRef,
|
|
740
|
+
lastActualTokensRef,
|
|
741
|
+
lastActualTokensTimestampRef,
|
|
1194
742
|
persistCompactedSession,
|
|
1195
|
-
|
|
1196
|
-
]);
|
|
743
|
+
});
|
|
1197
744
|
// ── Background task bar state (external store) ──────────
|
|
1198
745
|
const { bgTasks, focused: taskBarFocused, expanded: taskBarExpanded, selectedIndex: selectedTaskIndex, } = useTaskBarStore();
|
|
1199
746
|
useTaskBarPolling(props.processManager);
|
|
@@ -1225,16 +772,56 @@ export function App(props) {
|
|
|
1225
772
|
tools: currentTools,
|
|
1226
773
|
webSearch: props.webSearch,
|
|
1227
774
|
maxTokens: props.maxTokens,
|
|
1228
|
-
thinking:
|
|
775
|
+
thinking: thinkingLevel,
|
|
1229
776
|
apiKey: activeApiKey,
|
|
1230
777
|
baseUrl: activeBaseUrl,
|
|
1231
778
|
accountId: activeAccountId,
|
|
1232
779
|
projectId: activeProjectId,
|
|
1233
780
|
resolveCredentials,
|
|
1234
781
|
transformContext,
|
|
782
|
+
getIdealReviewMessage: (stats) => {
|
|
783
|
+
if (!idealReviewEnabledRef.current)
|
|
784
|
+
return null;
|
|
785
|
+
const decision = evaluateIdealReview(stats);
|
|
786
|
+
if (!decision.shouldReview)
|
|
787
|
+
return null;
|
|
788
|
+
log("INFO", "ideal", "Injecting ideal review before final response", {
|
|
789
|
+
score: String(decision.score),
|
|
790
|
+
reasons: decision.reasons.join(", "),
|
|
791
|
+
});
|
|
792
|
+
setLiveItems((prev) => [
|
|
793
|
+
...prev,
|
|
794
|
+
{ kind: "ideal_hook", text: IDEAL_HOOK_NOTICE_TEXT, tone: "review", id: getId() },
|
|
795
|
+
]);
|
|
796
|
+
return buildIdealReviewMessage(decision.reasons);
|
|
797
|
+
},
|
|
798
|
+
getLoopBreakMessage: (stats) => {
|
|
799
|
+
if (!idealReviewEnabledRef.current)
|
|
800
|
+
return null;
|
|
801
|
+
const decision = evaluateLoopBreak(stats);
|
|
802
|
+
if (!decision.shouldBreak)
|
|
803
|
+
return null;
|
|
804
|
+
log("INFO", "loop-break", "Injecting loop-break nudge", {
|
|
805
|
+
reasons: decision.reasons.join(", "),
|
|
806
|
+
});
|
|
807
|
+
setLiveItems((prev) => [
|
|
808
|
+
...prev,
|
|
809
|
+
{ kind: "ideal_hook", text: LOOP_BREAK_NOTICE_TEXT, tone: "warning", id: getId() },
|
|
810
|
+
]);
|
|
811
|
+
return buildLoopBreakMessage(decision.reasons);
|
|
812
|
+
},
|
|
813
|
+
getRegroundingMessage: (originalRequest) => {
|
|
814
|
+
if (!idealReviewEnabledRef.current)
|
|
815
|
+
return null;
|
|
816
|
+
log("INFO", "reground", "Injecting re-grounding after compaction", {});
|
|
817
|
+
setLiveItems((prev) => [
|
|
818
|
+
...prev,
|
|
819
|
+
{ kind: "ideal_hook", text: REGROUNDING_NOTICE_TEXT, tone: "info", id: getId() },
|
|
820
|
+
]);
|
|
821
|
+
return buildRegroundingMessage(originalRequest);
|
|
822
|
+
},
|
|
1235
823
|
}, {
|
|
1236
824
|
onComplete: useCallback(() => {
|
|
1237
|
-
messagesRef.current = stripRepoMapMessages(messagesRef.current);
|
|
1238
825
|
persistNewMessages();
|
|
1239
826
|
// Auto-clear plan progress and approved plan when all steps are completed
|
|
1240
827
|
const steps = planStepsRef.current;
|
|
@@ -1287,8 +874,6 @@ export function App(props) {
|
|
|
1287
874
|
}
|
|
1288
875
|
}, [
|
|
1289
876
|
persistNewMessages,
|
|
1290
|
-
stripRepoMapMessages,
|
|
1291
|
-
planMode,
|
|
1292
877
|
props.cwd,
|
|
1293
878
|
props.skills,
|
|
1294
879
|
currentProvider,
|
|
@@ -1298,6 +883,8 @@ export function App(props) {
|
|
|
1298
883
|
resolveCredentials,
|
|
1299
884
|
]),
|
|
1300
885
|
onTurnText: useCallback((text, thinking, thinkingMs) => {
|
|
886
|
+
const hadStreamedAssistantFlush = streamedAssistantFlushRef.current.flushedChars > 0;
|
|
887
|
+
const unflushedAssistantText = text.slice(streamedAssistantFlushRef.current.flushedChars);
|
|
1301
888
|
// Track [DONE:n] markers for plan step progress
|
|
1302
889
|
if (planStepsRef.current.length > 0) {
|
|
1303
890
|
const completed = findCompletedMarkers(text);
|
|
@@ -1312,15 +899,9 @@ export function App(props) {
|
|
|
1312
899
|
followUpNudgesRef.current = { step: 0, count: 0 };
|
|
1313
900
|
}
|
|
1314
901
|
}
|
|
1315
|
-
// Flush
|
|
1316
|
-
//
|
|
1317
|
-
//
|
|
1318
|
-
//
|
|
1319
|
-
// Items are queued in pendingFlushRef (not sent to setHistory directly)
|
|
1320
|
-
// so the Static write happens in a SEPARATE render cycle from the
|
|
1321
|
-
// live-area change — avoiding both Ink cursor-math clipping and the
|
|
1322
|
-
// brief duplicate that occurred when setHistory was nested inside the
|
|
1323
|
-
// setLiveItems updater.
|
|
902
|
+
// Flush completed rows from the previous turn to finalized terminal
|
|
903
|
+
// history. Ink keeps only the active turn, preventing live-area growth
|
|
904
|
+
// and avoiding Static/log-update replay during resize/remount churn.
|
|
1324
905
|
setLiveItems((prev) => {
|
|
1325
906
|
const flushed = flushOnTurnText(prev);
|
|
1326
907
|
if (flushed.length > 0) {
|
|
@@ -1329,10 +910,7 @@ export function App(props) {
|
|
|
1329
910
|
// Split text on [DONE:N] markers so each marker renders inline as
|
|
1330
911
|
// a styled "✓ Step N: <description>" item at the position the
|
|
1331
912
|
// agent emitted it, instead of vanishing into stripped whitespace.
|
|
1332
|
-
|
|
1333
|
-
// marker-stripped text when there are no markers (keeps the
|
|
1334
|
-
// common case zero-cost).
|
|
1335
|
-
const segments = segmentDisplayText(text, planStepsRef.current);
|
|
913
|
+
const segments = segmentDisplayText(unflushedAssistantText, planStepsRef.current);
|
|
1336
914
|
const items = [];
|
|
1337
915
|
let thinkingAttached = false;
|
|
1338
916
|
for (const seg of segments) {
|
|
@@ -1345,7 +923,7 @@ export function App(props) {
|
|
|
1345
923
|
// contains multiple text chunks split by markers.
|
|
1346
924
|
thinking: thinkingAttached ? undefined : thinking,
|
|
1347
925
|
thinkingMs: thinkingAttached ? undefined : thinkingMs,
|
|
1348
|
-
|
|
926
|
+
continuation: hadStreamedAssistantFlush,
|
|
1349
927
|
id: getId(),
|
|
1350
928
|
});
|
|
1351
929
|
thinkingAttached = true;
|
|
@@ -1360,36 +938,54 @@ export function App(props) {
|
|
|
1360
938
|
}
|
|
1361
939
|
}
|
|
1362
940
|
// No segments at all (text was empty/whitespace, no markers).
|
|
1363
|
-
// Still
|
|
1364
|
-
// there was thinking content for this turn.
|
|
941
|
+
// Still persist an assistant item so a thinking block renders in
|
|
942
|
+
// terminal history if there was thinking content for this turn.
|
|
1365
943
|
if (items.length === 0) {
|
|
1366
944
|
items.push({
|
|
1367
945
|
kind: "assistant",
|
|
1368
946
|
text: "",
|
|
1369
947
|
thinking,
|
|
1370
948
|
thinkingMs,
|
|
1371
|
-
planMode: planModeLocalRef.current,
|
|
1372
949
|
id: getId(),
|
|
1373
950
|
});
|
|
1374
951
|
}
|
|
1375
|
-
|
|
952
|
+
const assistantItems = prev.filter((item) => item.kind === "assistant");
|
|
953
|
+
const newAssistantText = normalizeAssistantText(unflushedAssistantText);
|
|
954
|
+
const duplicatePinnedText = newAssistantText.length > 0 &&
|
|
955
|
+
[...assistantItems, ...pendingHistoryFlushRef.current, ...historyRef.current].some((item) => isSameAssistantText(item, newAssistantText));
|
|
956
|
+
const nextItems = duplicatePinnedText
|
|
957
|
+
? items.filter((item) => !isSameAssistantText(item, newAssistantText))
|
|
958
|
+
: items;
|
|
959
|
+
const flushablePrev = prev.filter((item) => item.kind !== "assistant");
|
|
960
|
+
if (flushablePrev.length > 0)
|
|
961
|
+
queueFlush(flushablePrev);
|
|
962
|
+
streamedAssistantFlushRef.current = { flushedChars: 0, text: "" };
|
|
963
|
+
return [...assistantItems, ...nextItems];
|
|
1376
964
|
});
|
|
1377
|
-
}, []),
|
|
1378
|
-
onToolStart: useCallback((toolCallId, name, args) => {
|
|
965
|
+
}, [queueFlush]),
|
|
966
|
+
onToolStart: useCallback((toolCallId, name, args, stream) => {
|
|
1379
967
|
log("INFO", "tool", `Tool call started: ${name}`, { id: toolCallId });
|
|
1380
968
|
const startedAt = Date.now();
|
|
1381
969
|
const animateUntil = startedAt + RUNNING_INDICATOR_ANIMATION_MS;
|
|
1382
|
-
//
|
|
1383
|
-
//
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
const
|
|
970
|
+
// Feed the pinned LiveToolPanel. Keep a small tail (panel shows the
|
|
971
|
+
// last few rows) so memory stays bounded across long sessions.
|
|
972
|
+
setLiveToolFeed((prev) => [...prev, { id: toolCallId, name, args, status: "running" }].slice(-(LIVE_TOOL_PANEL_ROWS * 2)));
|
|
973
|
+
const appendToolStart = (prev) => {
|
|
974
|
+
const visible = pinStreamingTextBeforeToolBoundary({
|
|
975
|
+
items: prev,
|
|
976
|
+
visibleStreamingText: stream.text,
|
|
977
|
+
thinking: stream.thinking,
|
|
978
|
+
thinkingMs: stream.thinkingMs,
|
|
979
|
+
makeId: getId,
|
|
980
|
+
});
|
|
981
|
+
const { flushed, remaining } = partitionCompleted(visible);
|
|
1387
982
|
if (flushed.length > 0) {
|
|
1388
983
|
queueFlush(flushed);
|
|
1389
984
|
}
|
|
1390
985
|
return remaining;
|
|
1391
|
-
}
|
|
986
|
+
};
|
|
1392
987
|
if (name === "subagent") {
|
|
988
|
+
setLiveItems(appendToolStart);
|
|
1393
989
|
// Create or update the sub-agent group item
|
|
1394
990
|
const newAgent = {
|
|
1395
991
|
toolCallId,
|
|
@@ -1414,25 +1010,32 @@ export function App(props) {
|
|
|
1414
1010
|
});
|
|
1415
1011
|
}
|
|
1416
1012
|
else if (AGGREGATABLE_TOOLS.has(name)) {
|
|
1417
|
-
// Group concurrent read-only tools into a single compact item
|
|
1418
1013
|
setLiveItems((prev) => {
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
if (
|
|
1423
|
-
const
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1014
|
+
const reusableGroupIdx = prev.findIndex((item) => item.kind === "tool_group" &&
|
|
1015
|
+
item.tools.every((tool) => tool.name === name && !tool.isError));
|
|
1016
|
+
const prior = reusableGroupIdx === -1 ? [] : prev.slice(0, reusableGroupIdx);
|
|
1017
|
+
if (reusableGroupIdx !== -1 && prior.every((item) => !isActiveItem(item))) {
|
|
1018
|
+
const flushablePrior = prior.filter((item) => item.kind !== "assistant");
|
|
1019
|
+
if (flushablePrior.length > 0)
|
|
1020
|
+
queueFlush(flushablePrior);
|
|
1021
|
+
const pinnedPrior = prior.filter((item) => item.kind === "assistant");
|
|
1022
|
+
const candidates = prev.slice(reusableGroupIdx);
|
|
1023
|
+
const group = candidates[0];
|
|
1024
|
+
return [
|
|
1025
|
+
...pinnedPrior,
|
|
1026
|
+
{
|
|
1027
|
+
...group,
|
|
1028
|
+
tools: [
|
|
1029
|
+
...group.tools,
|
|
1030
|
+
{ toolCallId, name, args, status: "running", animateUntil },
|
|
1031
|
+
],
|
|
1032
|
+
},
|
|
1033
|
+
...candidates.slice(1),
|
|
1034
|
+
];
|
|
1433
1035
|
}
|
|
1036
|
+
const remaining = appendToolStart(prev);
|
|
1434
1037
|
return [
|
|
1435
|
-
...
|
|
1038
|
+
...remaining,
|
|
1436
1039
|
{
|
|
1437
1040
|
kind: "tool_group",
|
|
1438
1041
|
tools: [{ toolCallId, name, args, status: "running", animateUntil }],
|
|
@@ -1443,11 +1046,11 @@ export function App(props) {
|
|
|
1443
1046
|
}
|
|
1444
1047
|
else {
|
|
1445
1048
|
setLiveItems((prev) => [
|
|
1446
|
-
...prev,
|
|
1049
|
+
...appendToolStart(prev),
|
|
1447
1050
|
{ kind: "tool_start", toolCallId, name, args, id: getId(), startedAt, animateUntil },
|
|
1448
1051
|
]);
|
|
1449
1052
|
}
|
|
1450
|
-
}, []),
|
|
1053
|
+
}, [queueFlush]),
|
|
1451
1054
|
onToolUpdate: useCallback((toolCallId, update) => {
|
|
1452
1055
|
const u = update;
|
|
1453
1056
|
// Bash progress streaming — append output to tool_start item
|
|
@@ -1489,6 +1092,17 @@ export function App(props) {
|
|
|
1489
1092
|
});
|
|
1490
1093
|
}, []),
|
|
1491
1094
|
onToolEnd: useCallback((toolCallId, name, result, isError, durationMs, details) => {
|
|
1095
|
+
recordToolEnd(sessionStatsRef.current, name, isError, durationMs);
|
|
1096
|
+
setLiveToolFeed((prev) => prev.map((entry) => entry.id === toolCallId
|
|
1097
|
+
? { ...entry, status: "done", isError, result, details }
|
|
1098
|
+
: entry));
|
|
1099
|
+
if (name === "edit" && !isError) {
|
|
1100
|
+
const diff = details?.diff ?? result;
|
|
1101
|
+
addLinesChanged(sessionStatsRef.current, {
|
|
1102
|
+
added: (diff.match(/^\+[^+]/gm) ?? []).length,
|
|
1103
|
+
removed: (diff.match(/^-[^-]/gm) ?? []).length,
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1492
1106
|
// Language-pack detection — gated on `write`/`bash` inside the
|
|
1493
1107
|
// helper; cheap to call unconditionally. Fire-and-forget; the next
|
|
1494
1108
|
// LLM turn picks up the swapped system prompt automatically.
|
|
@@ -1520,7 +1134,7 @@ export function App(props) {
|
|
|
1520
1134
|
};
|
|
1521
1135
|
const next = [...prev];
|
|
1522
1136
|
next[groupIdx] = { ...group, agents: updatedAgents };
|
|
1523
|
-
// Flush completed items to
|
|
1137
|
+
// Flush completed items to finalized history to keep the live area small
|
|
1524
1138
|
const { flushed, remaining } = partitionCompleted(next);
|
|
1525
1139
|
if (flushed.length > 0) {
|
|
1526
1140
|
queueFlush(flushed);
|
|
@@ -1530,6 +1144,19 @@ export function App(props) {
|
|
|
1530
1144
|
}
|
|
1531
1145
|
else {
|
|
1532
1146
|
setLiveItems((prev) => {
|
|
1147
|
+
if (name === "enter_plan") {
|
|
1148
|
+
const updated = prev
|
|
1149
|
+
.filter((item) => !(item.kind === "tool_start" && item.toolCallId === toolCallId))
|
|
1150
|
+
.map((item) => item.kind === "plan_transition" && item.active
|
|
1151
|
+
? { ...item, active: false }
|
|
1152
|
+
: item);
|
|
1153
|
+
const { flushed, remaining } = partitionCompleted(updated);
|
|
1154
|
+
if (flushed.length > 0) {
|
|
1155
|
+
queueFlush(flushed);
|
|
1156
|
+
return remaining;
|
|
1157
|
+
}
|
|
1158
|
+
return updated;
|
|
1159
|
+
}
|
|
1533
1160
|
// Check if this tool is in a tool_group
|
|
1534
1161
|
const groupIdx = prev.findIndex((item) => item.kind === "tool_group" &&
|
|
1535
1162
|
item.tools.some((t) => t.toolCallId === toolCallId));
|
|
@@ -1557,6 +1184,7 @@ export function App(props) {
|
|
|
1557
1184
|
isError,
|
|
1558
1185
|
durationMs,
|
|
1559
1186
|
details,
|
|
1187
|
+
imagePreviews: extractToolImagePreviews(details),
|
|
1560
1188
|
id: startItem.id,
|
|
1561
1189
|
};
|
|
1562
1190
|
updated = [...prev];
|
|
@@ -1574,12 +1202,13 @@ export function App(props) {
|
|
|
1574
1202
|
isError,
|
|
1575
1203
|
durationMs,
|
|
1576
1204
|
details,
|
|
1205
|
+
imagePreviews: extractToolImagePreviews(details),
|
|
1577
1206
|
id: getId(),
|
|
1578
1207
|
},
|
|
1579
1208
|
];
|
|
1580
1209
|
}
|
|
1581
1210
|
}
|
|
1582
|
-
// Flush completed items to
|
|
1211
|
+
// Flush completed items to finalized history to keep the live area small
|
|
1583
1212
|
const { flushed, remaining } = partitionCompleted(updated);
|
|
1584
1213
|
if (flushed.length > 0) {
|
|
1585
1214
|
queueFlush(flushed);
|
|
@@ -1595,14 +1224,22 @@ export function App(props) {
|
|
|
1595
1224
|
});
|
|
1596
1225
|
}
|
|
1597
1226
|
}, []),
|
|
1598
|
-
onServerToolCall: useCallback((id, name, input) => {
|
|
1227
|
+
onServerToolCall: useCallback((id, name, input, stream) => {
|
|
1228
|
+
recordServerToolCall(sessionStatsRef.current);
|
|
1599
1229
|
log("INFO", "server_tool", `Server tool call: ${name}`, { id });
|
|
1600
1230
|
const startedAt = Date.now();
|
|
1601
1231
|
const animateUntil = startedAt + RUNNING_INDICATOR_ANIMATION_MS;
|
|
1602
|
-
// Flush completed items (including assistant text)
|
|
1603
|
-
//
|
|
1232
|
+
// Flush completed items (including assistant text) before adding server
|
|
1233
|
+
// tool UI — same rationale as onToolStart.
|
|
1604
1234
|
setLiveItems((prev) => {
|
|
1605
|
-
const
|
|
1235
|
+
const visible = pinStreamingTextBeforeToolBoundary({
|
|
1236
|
+
items: prev,
|
|
1237
|
+
visibleStreamingText: stream.text,
|
|
1238
|
+
thinking: stream.thinking,
|
|
1239
|
+
thinkingMs: stream.thinkingMs,
|
|
1240
|
+
makeId: getId,
|
|
1241
|
+
});
|
|
1242
|
+
const { flushed, remaining } = partitionCompleted(visible);
|
|
1606
1243
|
if (flushed.length > 0) {
|
|
1607
1244
|
queueFlush(flushed);
|
|
1608
1245
|
}
|
|
@@ -1619,7 +1256,7 @@ export function App(props) {
|
|
|
1619
1256
|
},
|
|
1620
1257
|
];
|
|
1621
1258
|
});
|
|
1622
|
-
}, []),
|
|
1259
|
+
}, [queueFlush]),
|
|
1623
1260
|
onServerToolResult: useCallback((toolUseId, resultType, data) => {
|
|
1624
1261
|
log("INFO", "server_tool", `Server tool result`, { toolUseId, resultType });
|
|
1625
1262
|
setLiveItems((prev) => {
|
|
@@ -1653,15 +1290,16 @@ export function App(props) {
|
|
|
1653
1290
|
},
|
|
1654
1291
|
];
|
|
1655
1292
|
}
|
|
1656
|
-
// Flush completed items to
|
|
1293
|
+
// Flush completed items to finalized history
|
|
1657
1294
|
const { flushed, remaining } = partitionCompleted(updated);
|
|
1658
1295
|
if (flushed.length > 0) {
|
|
1659
1296
|
queueFlush(flushed);
|
|
1660
1297
|
}
|
|
1661
1298
|
return remaining;
|
|
1662
1299
|
});
|
|
1663
|
-
}, []),
|
|
1300
|
+
}, [queueFlush]),
|
|
1664
1301
|
onTurnEnd: useCallback((turn, stopReason, usage) => {
|
|
1302
|
+
recordTurnEnd(sessionStatsRef.current, usage);
|
|
1665
1303
|
log("INFO", "turn", `Turn ${turn} ended`, {
|
|
1666
1304
|
stopReason,
|
|
1667
1305
|
inputTokens: String(usage.inputTokens),
|
|
@@ -1676,8 +1314,8 @@ export function App(props) {
|
|
|
1676
1314
|
lastActualTokensRef.current =
|
|
1677
1315
|
currentProvider === "anthropic" ? inputContext : inputContext + usage.outputTokens;
|
|
1678
1316
|
lastActualTokensTimestampRef.current = Date.now();
|
|
1679
|
-
// For tool-only turns (no text), flush completed items to
|
|
1680
|
-
// liveItems doesn't grow unbounded across consecutive
|
|
1317
|
+
// For tool-only turns (no text), flush completed items to finalized
|
|
1318
|
+
// history so liveItems doesn't grow unbounded across consecutive turns.
|
|
1681
1319
|
setLiveItems((prev) => {
|
|
1682
1320
|
const { flushed, remaining } = flushOnTurnEnd(prev, stopReason);
|
|
1683
1321
|
if (flushed.length > 0) {
|
|
@@ -1685,46 +1323,41 @@ export function App(props) {
|
|
|
1685
1323
|
}
|
|
1686
1324
|
return remaining;
|
|
1687
1325
|
});
|
|
1688
|
-
}, []),
|
|
1689
|
-
onDone: useCallback((durationMs, toolsUsed
|
|
1326
|
+
}, [queueFlush]),
|
|
1327
|
+
onDone: useCallback((durationMs, toolsUsed) => {
|
|
1690
1328
|
log("INFO", "agent", `Agent done`, {
|
|
1691
1329
|
duration: `${durationMs}ms`,
|
|
1692
1330
|
toolsUsed: toolsUsed.join(",") || "none",
|
|
1693
1331
|
});
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
if (planOverlayPendingRef.current)
|
|
1697
|
-
return;
|
|
1698
|
-
setDoneStatus({ durationMs, toolsUsed, verb: pickDurationVerb(toolsUsed) });
|
|
1699
|
-
playNotificationSound();
|
|
1700
|
-
// Two-phase flush to avoid Ink text clipping.
|
|
1701
|
-
// Phase 1 (here): clear the live area so Ink commits a render with
|
|
1702
|
-
// the smaller output and updates its internal line counter.
|
|
1703
|
-
// Phase 2 (useEffect below): push items to Static history in a
|
|
1704
|
-
// separate render cycle so the Static write never coincides with
|
|
1705
|
-
// a live-area height change in the same frame.
|
|
1706
|
-
setLiveItems((prev) => {
|
|
1707
|
-
if (prev.length > 0)
|
|
1708
|
-
queueFlush(prev);
|
|
1709
|
-
return [];
|
|
1332
|
+
const doneDecision = getDoneFlushDecision({
|
|
1333
|
+
planOverlayPending: planOverlayPendingRef.current,
|
|
1710
1334
|
});
|
|
1711
|
-
//
|
|
1712
|
-
//
|
|
1335
|
+
// Don't show "done" status when the plan review pane is about to open —
|
|
1336
|
+
// the agent loop finished but we're waiting for user approval/review.
|
|
1337
|
+
// Still flush live transcript rows before the pane remounts; otherwise
|
|
1338
|
+
// setup output remains in ephemeral liveItems and appears to vanish.
|
|
1339
|
+
if (doneDecision.showDoneStatus) {
|
|
1340
|
+
setDoneStatus({ durationMs, toolsUsed, verb: pickDurationVerb(toolsUsed) });
|
|
1341
|
+
playNotificationSound();
|
|
1342
|
+
}
|
|
1343
|
+
// Keep the final assistant response mounted in the live frame after a
|
|
1344
|
+
// normal chat turn finishes. Moving a large final response to terminal
|
|
1345
|
+
// history at this moment writes many scrollback rows while the footer is
|
|
1346
|
+
// still mounted, which visibly pushes the input/footer upward. The final
|
|
1347
|
+
// response is flushed on the next submit before the new prompt is shown.
|
|
1348
|
+
// Non-chat overlay transitions still flush so setup/plan output
|
|
1349
|
+
// does not vanish during remounts.
|
|
1350
|
+
if (doneDecision.flushLiveItems && !doneDecision.showDoneStatus) {
|
|
1351
|
+
setLiveItems((prev) => {
|
|
1352
|
+
if (prev.length > 0)
|
|
1353
|
+
queueFlush(prev);
|
|
1354
|
+
return prev;
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
// Run-all: auto-start next pending task after a short delay.
|
|
1713
1358
|
if (runAllTasksRef.current) {
|
|
1714
1359
|
setTimeout(() => {
|
|
1715
1360
|
const cwd = cwdRef.current;
|
|
1716
|
-
// Clean turn: mark the in-flight task done so the row flips to ✓.
|
|
1717
|
-
// Error path: leave it at `in-progress` so the failed task is
|
|
1718
|
-
// visibly distinct from completed ones. The chain still advances
|
|
1719
|
-
// either way — matching how errors were handled before this fix.
|
|
1720
|
-
const finishedId = currentTaskIdRef.current;
|
|
1721
|
-
if (finishedId) {
|
|
1722
|
-
if (!errored)
|
|
1723
|
-
markTaskDone(cwd, finishedId);
|
|
1724
|
-
currentTaskIdRef.current = null;
|
|
1725
|
-
if (props.sessionStore)
|
|
1726
|
-
props.sessionStore.currentTaskId = null;
|
|
1727
|
-
}
|
|
1728
1361
|
const next = getNextPendingTask(cwd);
|
|
1729
1362
|
if (next) {
|
|
1730
1363
|
markTaskInProgress(cwd, next.id);
|
|
@@ -1736,11 +1369,6 @@ export function App(props) {
|
|
|
1736
1369
|
}
|
|
1737
1370
|
}, 500);
|
|
1738
1371
|
}
|
|
1739
|
-
// Goal loop: after the orchestrator handles a worker/verifier event,
|
|
1740
|
-
// continue the same Goal automatically until it reaches a terminal state.
|
|
1741
|
-
for (const runId of [...runningGoalIdsRef.current]) {
|
|
1742
|
-
setTimeout(() => continueGoalRun(runId), 500);
|
|
1743
|
-
}
|
|
1744
1372
|
// Pixel fix: observe branch + commits, patch status, optionally pick
|
|
1745
1373
|
// up the next open error if run-all is active.
|
|
1746
1374
|
const pendingFix = currentPixelFixRef.current;
|
|
@@ -1782,12 +1410,6 @@ export function App(props) {
|
|
|
1782
1410
|
setRunAllTasks(false);
|
|
1783
1411
|
setRunAllPixel(false);
|
|
1784
1412
|
currentPixelFixRef.current = null;
|
|
1785
|
-
// Clear the run-all task id so a subsequent run-all does not
|
|
1786
|
-
// mistakenly mark this aborted task as done. The on-disk row stays
|
|
1787
|
-
// at `in-progress`, which is the correct truth for an aborted task.
|
|
1788
|
-
currentTaskIdRef.current = null;
|
|
1789
|
-
if (props.sessionStore)
|
|
1790
|
-
props.sessionStore.currentTaskId = null;
|
|
1791
1413
|
setDoneStatus(null);
|
|
1792
1414
|
setLiveItems((prev) => {
|
|
1793
1415
|
const next = prev.map((item) => {
|
|
@@ -1833,42 +1455,16 @@ export function App(props) {
|
|
|
1833
1455
|
}, []),
|
|
1834
1456
|
onQueuedStart: useCallback((content) => {
|
|
1835
1457
|
// When a queued message starts processing, show it as a UserItem
|
|
1836
|
-
// and flush prior items to history.
|
|
1837
|
-
// from the transcript but still routed through the main agent context.
|
|
1458
|
+
// and flush prior items to history.
|
|
1838
1459
|
const displayText = typeof content === "string"
|
|
1839
1460
|
? content
|
|
1840
1461
|
: content
|
|
1841
1462
|
.filter((c) => c.type === "text")
|
|
1842
1463
|
.map((c) => c.text)
|
|
1843
1464
|
.join("\n");
|
|
1844
|
-
if (isGoalSyntheticEvent(displayText)) {
|
|
1845
|
-
const eventInfo = parseGoalSyntheticEvent(displayText);
|
|
1846
|
-
setLiveItems((prev) => {
|
|
1847
|
-
if (prev.length > 0)
|
|
1848
|
-
queueFlush(prev);
|
|
1849
|
-
return [];
|
|
1850
|
-
});
|
|
1851
|
-
setDoneStatus(null);
|
|
1852
|
-
appendGoalProgress({
|
|
1853
|
-
kind: "goal_progress",
|
|
1854
|
-
phase: "orchestrator_reviewing",
|
|
1855
|
-
title: "Orchestrator reviewing Goal update",
|
|
1856
|
-
detail: eventInfo?.kind === "worker"
|
|
1857
|
-
? `Worker ${eventInfo.worker ?? "finished"} reported back${eventInfo.task ? ` on ${eventInfo.task}` : ""}. Inspecting Goal state.`
|
|
1858
|
-
: `Verifier reported ${eventInfo?.status ?? "status"}. Inspecting evidence and next action.`,
|
|
1859
|
-
workerId: eventInfo?.worker,
|
|
1860
|
-
status: eventInfo?.status,
|
|
1861
|
-
});
|
|
1862
|
-
return;
|
|
1863
|
-
}
|
|
1864
1465
|
const imageCount = typeof content === "string"
|
|
1865
1466
|
? undefined
|
|
1866
1467
|
: content.filter((c) => c.type === "image").length || undefined;
|
|
1867
|
-
setLiveItems((prev) => {
|
|
1868
|
-
if (prev.length > 0)
|
|
1869
|
-
queueFlush(prev);
|
|
1870
|
-
return [];
|
|
1871
|
-
});
|
|
1872
1468
|
const userItem = {
|
|
1873
1469
|
kind: "user",
|
|
1874
1470
|
text: displayText,
|
|
@@ -1877,8 +1473,8 @@ export function App(props) {
|
|
|
1877
1473
|
};
|
|
1878
1474
|
setLastUserMessage(displayText);
|
|
1879
1475
|
setDoneStatus(null);
|
|
1880
|
-
|
|
1881
|
-
}, []),
|
|
1476
|
+
finalizeSubmittedUserItem(userItem);
|
|
1477
|
+
}, [finalizeSubmittedUserItem]),
|
|
1882
1478
|
// Inject a "continue with the next step" follow-up when the agent
|
|
1883
1479
|
// would otherwise stop mid-plan. The prompt-only instruction wasn't
|
|
1884
1480
|
// enough — some models (notably Opus) treat each [DONE:n] as a
|
|
@@ -1910,11 +1506,282 @@ export function App(props) {
|
|
|
1910
1506
|
];
|
|
1911
1507
|
}, []),
|
|
1912
1508
|
});
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1509
|
+
const continueGoalRun = useCallback((run) => {
|
|
1510
|
+
const cwd = cwdRef.current;
|
|
1511
|
+
const decision = decideGoalNextAction(run);
|
|
1512
|
+
const choiceKey = getGoalContinuationChoiceKey({ runId: run.id, decision });
|
|
1513
|
+
const now = Date.now();
|
|
1514
|
+
for (const [key, timestamp] of goalContinuationRecentChoicesRef.current.entries()) {
|
|
1515
|
+
if (now - timestamp > 5_000)
|
|
1516
|
+
goalContinuationRecentChoicesRef.current.delete(key);
|
|
1517
|
+
}
|
|
1518
|
+
if (goalContinuationFlightsRef.current.has(choiceKey))
|
|
1519
|
+
return;
|
|
1520
|
+
const lastChosenAt = goalContinuationRecentChoicesRef.current.get(choiceKey);
|
|
1521
|
+
if (lastChosenAt && now - lastChosenAt < 1_500)
|
|
1522
|
+
return;
|
|
1523
|
+
goalContinuationFlightsRef.current.add(choiceKey);
|
|
1524
|
+
goalContinuationRecentChoicesRef.current.set(choiceKey, now);
|
|
1525
|
+
const releaseChoice = () => {
|
|
1526
|
+
setTimeout(() => goalContinuationFlightsRef.current.delete(choiceKey), 250);
|
|
1527
|
+
};
|
|
1528
|
+
void (async () => {
|
|
1529
|
+
try {
|
|
1530
|
+
if (decision.kind === "create_task") {
|
|
1531
|
+
const nextTask = createGoalTask({ title: decision.title, prompt: decision.prompt });
|
|
1532
|
+
const updated = await upsertGoalRun(cwd, {
|
|
1533
|
+
...run,
|
|
1534
|
+
status: "ready",
|
|
1535
|
+
tasks: [...run.tasks, nextTask],
|
|
1536
|
+
});
|
|
1537
|
+
await appendGoalDecision(cwd, run.id, decision);
|
|
1538
|
+
appendGoalProgress({
|
|
1539
|
+
kind: "goal_progress",
|
|
1540
|
+
phase: "orchestrator_working",
|
|
1541
|
+
title: `Goal ${goalNumberForRun(run.id)} task queued`,
|
|
1542
|
+
detail: decision.reason,
|
|
1543
|
+
status: updated.status,
|
|
1544
|
+
});
|
|
1545
|
+
continueGoalRun(updated);
|
|
1546
|
+
return;
|
|
1547
|
+
}
|
|
1548
|
+
if (decision.kind === "start_worker") {
|
|
1549
|
+
const updatedRun = await updateGoalTask(cwd, run.id, decision.task.id, {
|
|
1550
|
+
status: "running",
|
|
1551
|
+
attempts: decision.attempts,
|
|
1552
|
+
lastSummary: `Worker attempt ${decision.attempts} running.`,
|
|
1553
|
+
});
|
|
1554
|
+
const taskForPrompt = updatedRun?.tasks.find((task) => task.id === decision.task.id) ?? {
|
|
1555
|
+
...decision.task,
|
|
1556
|
+
attempts: decision.attempts,
|
|
1557
|
+
};
|
|
1558
|
+
const worker = await startGoalWorker({
|
|
1559
|
+
cwd,
|
|
1560
|
+
goalRunId: run.id,
|
|
1561
|
+
goalTaskId: taskForPrompt.id,
|
|
1562
|
+
taskTitle: taskForPrompt.title,
|
|
1563
|
+
provider: currentProviderRef.current,
|
|
1564
|
+
model: currentModelRef.current,
|
|
1565
|
+
prompt: taskForPrompt.prompt,
|
|
1566
|
+
});
|
|
1567
|
+
upsertGoalStatusEntry({
|
|
1568
|
+
runId: run.id,
|
|
1569
|
+
label: run.title,
|
|
1570
|
+
phase: "worker",
|
|
1571
|
+
detail: `Worker: ${decision.task.title}`,
|
|
1572
|
+
workerId: worker.id,
|
|
1573
|
+
startedAt: Date.now(),
|
|
1574
|
+
});
|
|
1575
|
+
appendGoalProgress({
|
|
1576
|
+
kind: "goal_progress",
|
|
1577
|
+
phase: "worker_started",
|
|
1578
|
+
title: `Goal ${goalNumberForRun(run.id)} worker started`,
|
|
1579
|
+
detail: decision.task.title,
|
|
1580
|
+
workerId: worker.id,
|
|
1581
|
+
status: "running",
|
|
1582
|
+
});
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
if (decision.kind === "run_verifier") {
|
|
1586
|
+
if (activeVerifierRunIdsRef.current.has(run.id))
|
|
1587
|
+
return;
|
|
1588
|
+
activeVerifierRunIdsRef.current.add(run.id);
|
|
1589
|
+
upsertGoalStatusEntry({
|
|
1590
|
+
runId: run.id,
|
|
1591
|
+
label: run.title,
|
|
1592
|
+
phase: "verifier",
|
|
1593
|
+
detail: "Verifier running",
|
|
1594
|
+
startedAt: Date.now(),
|
|
1595
|
+
});
|
|
1596
|
+
appendGoalProgress({
|
|
1597
|
+
kind: "goal_progress",
|
|
1598
|
+
phase: "verifier_started",
|
|
1599
|
+
title: `Goal ${goalNumberForRun(run.id)} verifier started`,
|
|
1600
|
+
detail: decision.command,
|
|
1601
|
+
status: "running",
|
|
1602
|
+
});
|
|
1603
|
+
try {
|
|
1604
|
+
const completed = await runGoalVerifierCommand({
|
|
1605
|
+
cwd,
|
|
1606
|
+
runId: run.id,
|
|
1607
|
+
command: decision.command,
|
|
1608
|
+
});
|
|
1609
|
+
const verification = completed.verification;
|
|
1610
|
+
const verificationStatus = verification.status === "pass" ? "pass" : "fail";
|
|
1611
|
+
const latestBeforeVerifier = (await loadGoalRuns(cwd)).find((candidate) => candidate.id === run.id) ?? run;
|
|
1612
|
+
const latest = await upsertGoalRun(cwd, {
|
|
1613
|
+
...latestBeforeVerifier,
|
|
1614
|
+
status: verificationStatus === "pass" ? "passed" : "ready",
|
|
1615
|
+
verifier: {
|
|
1616
|
+
...(latestBeforeVerifier.verifier ?? { description: "Goal verifier" }),
|
|
1617
|
+
command: verification.command ?? decision.command,
|
|
1618
|
+
lastResult: verification,
|
|
1619
|
+
},
|
|
1620
|
+
});
|
|
1621
|
+
await appendGoalEvidence(cwd, run.id, {
|
|
1622
|
+
kind: "log",
|
|
1623
|
+
label: "Verifier result",
|
|
1624
|
+
content: formatGoalVerifierCompletionEvent(latest, verificationStatus, verification.command ?? decision.command, verification.exitCode ?? (verificationStatus === "pass" ? 0 : 1), verification.summary),
|
|
1625
|
+
...(verification.outputPath ? { path: verification.outputPath } : {}),
|
|
1626
|
+
});
|
|
1627
|
+
appendGoalProgress({
|
|
1628
|
+
kind: "goal_progress",
|
|
1629
|
+
phase: "verifier_finished",
|
|
1630
|
+
title: `Goal ${goalNumberForRun(run.id)} verifier ${verificationStatus}`,
|
|
1631
|
+
detail: verification.summary,
|
|
1632
|
+
status: verificationStatus,
|
|
1633
|
+
});
|
|
1634
|
+
upsertGoalStatusEntry({
|
|
1635
|
+
runId: run.id,
|
|
1636
|
+
label: latest.title,
|
|
1637
|
+
phase: verificationStatus === "pass" ? "reviewing" : "failed",
|
|
1638
|
+
detail: verificationStatus === "pass" ? "Verifier passed" : "Verifier failed",
|
|
1639
|
+
startedAt: Date.now(),
|
|
1640
|
+
});
|
|
1641
|
+
continueGoalRun(latest);
|
|
1642
|
+
}
|
|
1643
|
+
catch (error) {
|
|
1644
|
+
appendGoalProgress({
|
|
1645
|
+
kind: "goal_progress",
|
|
1646
|
+
phase: "verifier_finished",
|
|
1647
|
+
title: `Goal ${goalNumberForRun(run.id)} verifier failed`,
|
|
1648
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
1649
|
+
status: "failed",
|
|
1650
|
+
});
|
|
1651
|
+
}
|
|
1652
|
+
finally {
|
|
1653
|
+
activeVerifierRunIdsRef.current.delete(run.id);
|
|
1654
|
+
}
|
|
1655
|
+
return;
|
|
1656
|
+
}
|
|
1657
|
+
if (decision.kind === "complete") {
|
|
1658
|
+
const completion = canCompleteGoalRun(run);
|
|
1659
|
+
const updated = await upsertGoalRun(cwd, { ...run, status: "passed" });
|
|
1660
|
+
const summary = completion.ok
|
|
1661
|
+
? "Verifier evidence proves the Goal success criteria."
|
|
1662
|
+
: goalCompletionReason(completion);
|
|
1663
|
+
await appendGoalDecision(cwd, run.id, {
|
|
1664
|
+
kind: "summary",
|
|
1665
|
+
content: summary,
|
|
1666
|
+
});
|
|
1667
|
+
appendGoalProgress({
|
|
1668
|
+
kind: "goal_progress",
|
|
1669
|
+
phase: "terminal",
|
|
1670
|
+
title: `Goal passed: ${updated.title}`,
|
|
1671
|
+
detail: summary,
|
|
1672
|
+
status: updated.status,
|
|
1673
|
+
});
|
|
1674
|
+
clearGoalStatusEntry(run.id);
|
|
1675
|
+
return;
|
|
1676
|
+
}
|
|
1677
|
+
if (decision.kind === "pause") {
|
|
1678
|
+
const updated = await upsertGoalRun(cwd, {
|
|
1679
|
+
...run,
|
|
1680
|
+
status: "paused",
|
|
1681
|
+
blockers: [...run.blockers, decision.reason],
|
|
1682
|
+
});
|
|
1683
|
+
await appendGoalDecision(cwd, run.id, decision);
|
|
1684
|
+
upsertGoalStatusEntry({
|
|
1685
|
+
runId: updated.id,
|
|
1686
|
+
label: updated.title,
|
|
1687
|
+
phase: "failed",
|
|
1688
|
+
detail: decision.reason,
|
|
1689
|
+
startedAt: Date.now(),
|
|
1690
|
+
});
|
|
1691
|
+
appendGoalProgress({
|
|
1692
|
+
kind: "goal_progress",
|
|
1693
|
+
phase: "terminal",
|
|
1694
|
+
title: `Goal paused: ${updated.title}`,
|
|
1695
|
+
detail: decision.reason,
|
|
1696
|
+
summaryRows: buildGoalSummaryRows(updated),
|
|
1697
|
+
status: "paused",
|
|
1698
|
+
});
|
|
1699
|
+
return;
|
|
1700
|
+
}
|
|
1701
|
+
if (decision.kind === "terminal") {
|
|
1702
|
+
const updated = await upsertGoalRun(cwd, { ...run, status: decision.status });
|
|
1703
|
+
if (decision.status === "passed")
|
|
1704
|
+
clearGoalStatusEntry(run.id);
|
|
1705
|
+
appendGoalProgress({
|
|
1706
|
+
kind: "goal_progress",
|
|
1707
|
+
phase: "terminal",
|
|
1708
|
+
title: `Goal ${decision.status}: ${updated.title}`,
|
|
1709
|
+
detail: decision.reason,
|
|
1710
|
+
summaryRows: buildGoalSummaryRows(updated),
|
|
1711
|
+
status: decision.status,
|
|
1712
|
+
});
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1715
|
+
if (decision.kind === "blocked") {
|
|
1716
|
+
const updated = await upsertGoalRun(cwd, {
|
|
1717
|
+
...run,
|
|
1718
|
+
status: "blocked",
|
|
1719
|
+
blockers: [...run.blockers, decision.reason],
|
|
1720
|
+
});
|
|
1721
|
+
upsertGoalStatusEntry({
|
|
1722
|
+
runId: updated.id,
|
|
1723
|
+
label: updated.title,
|
|
1724
|
+
phase: "failed",
|
|
1725
|
+
detail: decision.reason,
|
|
1726
|
+
startedAt: Date.now(),
|
|
1727
|
+
});
|
|
1728
|
+
appendGoalProgress({
|
|
1729
|
+
kind: "goal_progress",
|
|
1730
|
+
phase: "terminal",
|
|
1731
|
+
title: `Goal ${goalNumberForRun(run.id)} blocked`,
|
|
1732
|
+
detail: decision.reason,
|
|
1733
|
+
status: "blocked",
|
|
1734
|
+
});
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
finally {
|
|
1738
|
+
releaseChoice();
|
|
1739
|
+
}
|
|
1740
|
+
})();
|
|
1741
|
+
}, [appendGoalProgress, clearGoalStatusEntry, goalNumberForRun, upsertGoalStatusEntry]);
|
|
1742
|
+
const handleGoalWorkerCompletion = useCallback((completion) => {
|
|
1743
|
+
const cwd = cwdRef.current;
|
|
1744
|
+
runningGoalIdsRef.current.delete(completion.worker.goalRunId);
|
|
1745
|
+
void (async () => {
|
|
1746
|
+
const run = (await loadGoalRuns(cwd)).find((candidate) => candidate.id === completion.worker.goalRunId);
|
|
1747
|
+
const taskTitle = run?.tasks.find((task) => task.id === completion.worker.goalTaskId)?.title ??
|
|
1748
|
+
completion.worker.goalTaskId;
|
|
1749
|
+
await appendGoalEvidence(cwd, completion.worker.goalRunId, {
|
|
1750
|
+
kind: "log",
|
|
1751
|
+
label: formatGoalWorkerFinishedTitle(taskTitle, completion.status),
|
|
1752
|
+
content: run
|
|
1753
|
+
? formatGoalWorkerCompletionEvent(run, taskTitle, completion)
|
|
1754
|
+
: completion.summary,
|
|
1755
|
+
path: completion.worker.logFile,
|
|
1756
|
+
});
|
|
1757
|
+
appendGoalProgress({
|
|
1758
|
+
kind: "goal_progress",
|
|
1759
|
+
phase: "worker_finished",
|
|
1760
|
+
status: completion.status === "done" ? "done" : "failed",
|
|
1761
|
+
title: formatGoalWorkerFinishedTitle(taskTitle, completion.status),
|
|
1762
|
+
detail: summarizeGoalCompletion(completion.summary),
|
|
1763
|
+
workerId: completion.worker.id,
|
|
1764
|
+
});
|
|
1765
|
+
if (!run) {
|
|
1766
|
+
clearGoalStatusEntry(completion.worker.goalRunId);
|
|
1767
|
+
return;
|
|
1768
|
+
}
|
|
1769
|
+
upsertGoalStatusEntry({
|
|
1770
|
+
runId: run.id,
|
|
1771
|
+
label: run.title,
|
|
1772
|
+
phase: completion.status === "done" ? "orchestrating" : "failed",
|
|
1773
|
+
detail: completion.status === "done" ? "Worker finished" : "Worker failed",
|
|
1774
|
+
startedAt: Date.now(),
|
|
1775
|
+
});
|
|
1776
|
+
continueGoalRun(run);
|
|
1777
|
+
})();
|
|
1778
|
+
}, [appendGoalProgress, clearGoalStatusEntry, continueGoalRun, upsertGoalStatusEntry]);
|
|
1779
|
+
useEffect(() => subscribeGoalWorkerCompletions(handleGoalWorkerCompletion), [handleGoalWorkerCompletion]);
|
|
1780
|
+
// First-time-per-project auto-run of /setup. Bound after `agentLoop` is in
|
|
1781
|
+
// scope so the ref closure can dispatch to it. Called from the initial
|
|
1782
|
+
// language-detection path when `isFirstTimeSetup(cwd)` is true. Pushes a
|
|
1783
|
+
// notice item explaining what's happening, then runs the audit prompt.
|
|
1784
|
+
triggerAutoSetupRef.current = async () => {
|
|
1918
1785
|
const setupCmd = getPromptCommand("setup");
|
|
1919
1786
|
if (!setupCmd) {
|
|
1920
1787
|
log("WARN", "setup", "Auto-setup skipped — /setup command not found in registry.");
|
|
@@ -1948,27 +1815,6 @@ export function App(props) {
|
|
|
1948
1815
|
]);
|
|
1949
1816
|
}
|
|
1950
1817
|
};
|
|
1951
|
-
// Phase 2 of the two-phase flush: after onDone clears liveItems (phase 1)
|
|
1952
|
-
// and Ink renders the smaller live area (updating its internal line
|
|
1953
|
-
// counter), this effect pushes the stashed items into Static history.
|
|
1954
|
-
// Because the Static write happens in a SEPARATE render cycle from the
|
|
1955
|
-
// live-area shrink, Ink's log-update never needs to erase the old tall
|
|
1956
|
-
// live area AND write Static content in the same frame — avoiding the
|
|
1957
|
-
// cursor-math mismatch that caused text clipping.
|
|
1958
|
-
useEffect(() => {
|
|
1959
|
-
if (pendingFlushRef.current.length > 0) {
|
|
1960
|
-
const items = pendingFlushRef.current;
|
|
1961
|
-
pendingFlushRef.current = [];
|
|
1962
|
-
setHistory((h) => {
|
|
1963
|
-
const next = compactHistory([...h, ...trimFlushedItems(items)]);
|
|
1964
|
-
if (sessionStore)
|
|
1965
|
-
sessionStore.history = next;
|
|
1966
|
-
return next;
|
|
1967
|
-
});
|
|
1968
|
-
if (sessionStore)
|
|
1969
|
-
sessionStore.liveItems = liveItems;
|
|
1970
|
-
}
|
|
1971
|
-
}, [flushGeneration]);
|
|
1972
1818
|
// Sync terminal title with agent loop state
|
|
1973
1819
|
useEffect(() => {
|
|
1974
1820
|
setTitleRunning(agentLoop.isRunning);
|
|
@@ -1996,10 +1842,28 @@ export function App(props) {
|
|
|
1996
1842
|
return () => clearTimeout(timer);
|
|
1997
1843
|
}
|
|
1998
1844
|
}, [agentLoop.isRunning, sessionStore, props.resetUI]);
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
1845
|
+
const showSessionSummaryAndExit = useCallback(() => {
|
|
1846
|
+
const summary = buildSessionSummary({
|
|
1847
|
+
stats: sessionStatsRef.current,
|
|
1848
|
+
provider: currentProvider,
|
|
1849
|
+
model: currentModel,
|
|
1850
|
+
cwd: displayedCwd,
|
|
1851
|
+
footer: sessionStatsRef.current.sessionId
|
|
1852
|
+
? `To resume this session: ezcoder --resume ${sessionStatsRef.current.sessionId}`
|
|
1853
|
+
: undefined,
|
|
1854
|
+
});
|
|
1855
|
+
setDoneStatus(null);
|
|
1856
|
+
setExitPending(false);
|
|
1857
|
+
setOverlay(null);
|
|
1858
|
+
setLiveItems([]);
|
|
1859
|
+
setQuittingSummary(summary);
|
|
1860
|
+
writeStdout("\x1b[2J\x1b[3J\x1b[H");
|
|
1861
|
+
setTimeout(() => process.exit(0), 150);
|
|
1862
|
+
}, [currentModel, currentProvider, displayedCwd, writeStdout]);
|
|
1863
|
+
// Consume pending post-remount work once on mount. Set by resetUI options
|
|
1864
|
+
// for paths that remount AND immediately drive work (plan accept/reject,
|
|
1865
|
+
// pixel fix, plan accept/reject). The work survives the unmount because
|
|
1866
|
+
// it lives in renderApp's closure (sessionStore), not React state.
|
|
2003
1867
|
useEffect(() => {
|
|
2004
1868
|
if (pendingActionConsumedRef.current)
|
|
2005
1869
|
return;
|
|
@@ -2007,8 +1871,10 @@ export function App(props) {
|
|
|
2007
1871
|
if (!action)
|
|
2008
1872
|
return;
|
|
2009
1873
|
pendingActionConsumedRef.current = true;
|
|
2010
|
-
if (sessionStore)
|
|
1874
|
+
if (sessionStore) {
|
|
2011
1875
|
sessionStore.pendingAction = undefined;
|
|
1876
|
+
}
|
|
1877
|
+
setDoneStatus(null);
|
|
2012
1878
|
if (action.planEvent) {
|
|
2013
1879
|
const ev = action.planEvent;
|
|
2014
1880
|
setLiveItems((prev) => [
|
|
@@ -2022,7 +1888,6 @@ export function App(props) {
|
|
|
2022
1888
|
{ kind: "info", text: action.infoText, id: getId() },
|
|
2023
1889
|
]);
|
|
2024
1890
|
}
|
|
2025
|
-
setDoneStatus(null);
|
|
2026
1891
|
void agentLoop.run(action.prompt).catch((err) => {
|
|
2027
1892
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
2028
1893
|
log("ERROR", "error", errMsg);
|
|
@@ -2030,14 +1895,6 @@ export function App(props) {
|
|
|
2030
1895
|
});
|
|
2031
1896
|
// Intentional one-shot: run once on mount, never re-fire on re-render.
|
|
2032
1897
|
}, []);
|
|
2033
|
-
// Refresh eyes badge count when the agent settles (end of a turn) — a turn
|
|
2034
|
-
// may have logged new rough/wish/blocked signals. Also covers the case where
|
|
2035
|
-
// /eyes was run for the first time (manifest now exists).
|
|
2036
|
-
useEffect(() => {
|
|
2037
|
-
if (!agentLoop.isRunning) {
|
|
2038
|
-
setEyesCount(isEyesActive(props.cwd) ? journalCount({ status: "open" }, props.cwd) : undefined);
|
|
2039
|
-
}
|
|
2040
|
-
}, [agentLoop.isRunning, props.cwd]);
|
|
2041
1898
|
const handleSubmit = useCallback(async (input, inputImages = [], pasteInfo) => {
|
|
2042
1899
|
const trimmed = input.trim();
|
|
2043
1900
|
if (trimmed.startsWith("/")) {
|
|
@@ -2052,241 +1909,141 @@ export function App(props) {
|
|
|
2052
1909
|
// has not grown.
|
|
2053
1910
|
await applyLanguageDetectionRef.current("input");
|
|
2054
1911
|
}
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
const ac = new AbortController();
|
|
2063
|
-
compactionAbortRef.current = ac;
|
|
2064
|
-
const compacted = await compactConversation(messagesRef.current, ac.signal);
|
|
2065
|
-
if (!ac.signal.aborted && compacted !== messagesRef.current) {
|
|
2066
|
-
messagesRef.current = compacted;
|
|
2067
|
-
await persistCompactedSession(compacted);
|
|
2068
|
-
}
|
|
2069
|
-
if (compactionAbortRef.current === ac)
|
|
2070
|
-
compactionAbortRef.current = null;
|
|
2071
|
-
return;
|
|
2072
|
-
}
|
|
2073
|
-
// Handle /quit — exit the agent
|
|
2074
|
-
if (trimmed === "/quit" || trimmed === "/q" || trimmed === "/exit") {
|
|
2075
|
-
process.exit(0);
|
|
2076
|
-
}
|
|
2077
|
-
// Handle /clear — tear down the entire Ink instance and rebuild fresh.
|
|
2078
|
-
// Avoid direct ANSI terminal clears here; they can erase scrollback.
|
|
2079
|
-
// Runtime state (model, provider, thinking) survives via renderApp's
|
|
2080
|
-
// closure-held `runtimeState`, mirrored from React state via the
|
|
2081
|
-
// useEffects above.
|
|
2082
|
-
if (trimmed === "/clear") {
|
|
2083
|
-
if (props.resetUI) {
|
|
2084
|
-
void (async () => {
|
|
2085
|
-
const newPrompt = await rebuildSystemPrompt({ clearApprovedPlan: true });
|
|
2086
|
-
props.resetUI?.({
|
|
2087
|
-
wipeSession: true,
|
|
2088
|
-
messages: [{ role: "system", content: newPrompt }],
|
|
2089
|
-
});
|
|
2090
|
-
})();
|
|
2091
|
-
return;
|
|
2092
|
-
}
|
|
2093
|
-
// Fallback path (resetUI not wired — e.g. tests). Best-effort: clear
|
|
2094
|
-
// React state in place without touching terminal scrollback.
|
|
2095
|
-
pendingFlushRef.current = [];
|
|
2096
|
-
setHistory([{ kind: "banner", id: "banner" }]);
|
|
2097
|
-
setLiveItems([]);
|
|
2098
|
-
setDoneStatus(null);
|
|
2099
|
-
approvedPlanPathRef.current = undefined;
|
|
2100
|
-
planStepsRef.current = [];
|
|
2101
|
-
setPlanSteps([]);
|
|
2102
|
-
void (async () => {
|
|
2103
|
-
const newPrompt = await rebuildSystemPrompt({ clearApprovedPlan: true });
|
|
2104
|
-
messagesRef.current = [{ role: "system", content: newPrompt }];
|
|
2105
|
-
persistedIndexRef.current = messagesRef.current.length;
|
|
2106
|
-
})();
|
|
2107
|
-
agentLoop.reset();
|
|
2108
|
-
setSessionTitle(undefined);
|
|
2109
|
-
sessionTitleGeneratedRef.current = false;
|
|
2110
|
-
setLiveItems([{ kind: "info", text: "Session cleared.", id: getId() }]);
|
|
2111
|
-
return;
|
|
2112
|
-
}
|
|
2113
|
-
// Handle /theme — open theme selector overlay
|
|
2114
|
-
if (trimmed === "/theme" || trimmed === "/t") {
|
|
2115
|
-
setOverlay("theme");
|
|
2116
|
-
return;
|
|
2117
|
-
}
|
|
2118
|
-
// Open the Eyes pane — read-only review of installed probes + open signals.
|
|
2119
|
-
// Gated by the ezcoder-eyes manifest: in projects without /eyes set up,
|
|
2120
|
-
// there's nothing useful to show.
|
|
2121
|
-
if (trimmed === "/eyes-view" || trimmed === "/ev") {
|
|
2122
|
-
if (!isEyesActive(props.cwd)) {
|
|
2123
|
-
setLiveItems((prev) => [
|
|
2124
|
-
...prev,
|
|
2125
|
-
{
|
|
2126
|
-
kind: "info",
|
|
2127
|
-
text: "Eyes not set up in this project. Run /setup-eyes to get started.",
|
|
2128
|
-
id: getId(),
|
|
2129
|
-
},
|
|
2130
|
-
]);
|
|
2131
|
-
return;
|
|
1912
|
+
if (trimmed === "/ideal-on" || trimmed === "/ideal-off") {
|
|
1913
|
+
const next = trimmed === "/ideal-on";
|
|
1914
|
+
setIdealReviewEnabled(next);
|
|
1915
|
+
if (props.settingsFile) {
|
|
1916
|
+
const sm = new SettingsManager(props.settingsFile);
|
|
1917
|
+
await sm.load();
|
|
1918
|
+
await sm.set("idealReviewEnabled", next);
|
|
2132
1919
|
}
|
|
2133
|
-
setOverlay("eyes");
|
|
2134
|
-
return;
|
|
2135
|
-
}
|
|
2136
|
-
// Handle /plan — toggle plan mode
|
|
2137
|
-
if (trimmed === "/plan" || trimmed === "/plan on") {
|
|
2138
|
-
setPlanMode(true);
|
|
2139
|
-
setLiveItems((prev) => [
|
|
2140
|
-
...prev,
|
|
2141
|
-
{ kind: "plan_transition", text: "Plan Mode Activated", active: true, id: getId() },
|
|
2142
|
-
]);
|
|
2143
|
-
return;
|
|
2144
|
-
}
|
|
2145
|
-
if (trimmed === "/plan off") {
|
|
2146
|
-
setPlanMode(false);
|
|
2147
1920
|
setLiveItems((prev) => [
|
|
2148
1921
|
...prev,
|
|
2149
1922
|
{
|
|
2150
|
-
kind: "
|
|
2151
|
-
text:
|
|
2152
|
-
|
|
1923
|
+
kind: "info",
|
|
1924
|
+
text: next
|
|
1925
|
+
? "Ideal review enabled. Use /ideal-off to disable it."
|
|
1926
|
+
: "Ideal review disabled. Use /ideal-on to enable it.",
|
|
2153
1927
|
id: getId(),
|
|
2154
1928
|
},
|
|
2155
1929
|
]);
|
|
2156
1930
|
return;
|
|
2157
1931
|
}
|
|
2158
|
-
//
|
|
2159
|
-
if (trimmed === "/
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
setPlanSteps([]);
|
|
2163
|
-
// Rebuild system prompt without the plan
|
|
2164
|
-
void replaceSystemPrompt({ clearApprovedPlan: true });
|
|
2165
|
-
setLiveItems([{ kind: "plan_event", event: "dismissed", id: getId() }]);
|
|
2166
|
-
return;
|
|
2167
|
-
}
|
|
2168
|
-
// Handle /map — show, refresh, or toggle dynamic repo map injection
|
|
2169
|
-
if (trimmed === "/map" ||
|
|
2170
|
-
trimmed === "/map refresh" ||
|
|
2171
|
-
trimmed === "/map on" ||
|
|
2172
|
-
trimmed === "/map off") {
|
|
2173
|
-
const action = trimmed.slice("/map".length).trim();
|
|
2174
|
-
if (action === "on") {
|
|
2175
|
-
repoMapInjectionEnabledRef.current = true;
|
|
2176
|
-
repoMapDirtyRef.current = true;
|
|
1932
|
+
// /rewind — open the checkpoint picker (needs React state + the store).
|
|
1933
|
+
if (trimmed === "/rewind") {
|
|
1934
|
+
const store = props.checkpointStore;
|
|
1935
|
+
if (!store) {
|
|
2177
1936
|
setLiveItems((prev) => [
|
|
2178
1937
|
...prev,
|
|
2179
|
-
{ kind: "info", text: "
|
|
1938
|
+
{ kind: "info", text: "Checkpoints are not available in this session.", id: getId() },
|
|
2180
1939
|
]);
|
|
2181
1940
|
return;
|
|
2182
1941
|
}
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
messagesRef.current = stripRepoMapMessages(messagesRef.current);
|
|
1942
|
+
const cps = await store.listCheckpoints();
|
|
1943
|
+
if (cps.length === 0) {
|
|
2186
1944
|
setLiveItems((prev) => [
|
|
2187
1945
|
...prev,
|
|
2188
1946
|
{
|
|
2189
1947
|
kind: "info",
|
|
2190
|
-
text: "
|
|
1948
|
+
text: "No checkpoints yet \u2014 edit a file through ezcoder first.",
|
|
2191
1949
|
id: getId(),
|
|
2192
1950
|
},
|
|
2193
1951
|
]);
|
|
2194
1952
|
return;
|
|
2195
1953
|
}
|
|
2196
|
-
|
|
2197
|
-
repoMapDirtyRef.current = true;
|
|
2198
|
-
const markdown = await refreshRepoMap(getLatestUserText(messagesRef.current));
|
|
2199
|
-
setLiveItems((prev) => [
|
|
2200
|
-
...prev,
|
|
2201
|
-
{
|
|
2202
|
-
kind: "info",
|
|
2203
|
-
text: formatRepoMapCommandOutput(repoMapInjectionEnabledRef.current, markdown, action === "refresh"),
|
|
2204
|
-
id: getId(),
|
|
2205
|
-
},
|
|
2206
|
-
]);
|
|
1954
|
+
setRewindCheckpoints(cps);
|
|
2207
1955
|
return;
|
|
2208
1956
|
}
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
props.sessionStore.overlay = "goal";
|
|
2219
|
-
props.sessionStore.planAutoExpand = false;
|
|
2220
|
-
if (agentLoop.isRunning)
|
|
2221
|
-
props.sessionStore.pendingResetUI = true;
|
|
1957
|
+
if (await handleUiSlashCommand(trimmed, {
|
|
1958
|
+
openModelSelector: () => setOverlay("model"),
|
|
1959
|
+
compactConversation: async () => {
|
|
1960
|
+
const ac = new AbortController();
|
|
1961
|
+
compactionAbortRef.current = ac;
|
|
1962
|
+
const compacted = await compactConversation(messagesRef.current, ac.signal);
|
|
1963
|
+
if (!ac.signal.aborted && compacted !== messagesRef.current) {
|
|
1964
|
+
messagesRef.current = compacted;
|
|
1965
|
+
await persistCompactedSession(compacted);
|
|
2222
1966
|
}
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
}
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
props.sessionStore.overlay = "plan";
|
|
2238
|
-
props.sessionStore.planAutoExpand = false;
|
|
2239
|
-
if (agentLoop.isRunning)
|
|
2240
|
-
props.sessionStore.pendingResetUI = true;
|
|
1967
|
+
if (compactionAbortRef.current === ac)
|
|
1968
|
+
compactionAbortRef.current = null;
|
|
1969
|
+
},
|
|
1970
|
+
quit: showSessionSummaryAndExit,
|
|
1971
|
+
clearSession: () => {
|
|
1972
|
+
if (props.resetUI) {
|
|
1973
|
+
void (async () => {
|
|
1974
|
+
const newPrompt = await rebuildSystemPrompt({ clearApprovedPlan: true });
|
|
1975
|
+
props.resetUI?.({
|
|
1976
|
+
wipeSession: true,
|
|
1977
|
+
messages: [{ role: "system", content: newPrompt }],
|
|
1978
|
+
});
|
|
1979
|
+
})();
|
|
1980
|
+
return;
|
|
2241
1981
|
}
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
1982
|
+
clearPendingHistory();
|
|
1983
|
+
setHistory([{ kind: "banner", id: "banner" }]);
|
|
1984
|
+
setLiveItems([]);
|
|
1985
|
+
setDoneStatus(null);
|
|
1986
|
+
approvedPlanPathRef.current = undefined;
|
|
1987
|
+
planStepsRef.current = [];
|
|
1988
|
+
setPlanSteps([]);
|
|
1989
|
+
void (async () => {
|
|
1990
|
+
const newPrompt = await rebuildSystemPrompt({ clearApprovedPlan: true });
|
|
1991
|
+
messagesRef.current = [{ role: "system", content: newPrompt }];
|
|
1992
|
+
persistedIndexRef.current = messagesRef.current.length;
|
|
1993
|
+
})();
|
|
1994
|
+
agentLoop.reset();
|
|
1995
|
+
setSessionTitle(undefined);
|
|
1996
|
+
sessionTitleGeneratedRef.current = false;
|
|
1997
|
+
setLiveItems([{ kind: "info", text: "Session cleared.", id: getId() }]);
|
|
1998
|
+
},
|
|
1999
|
+
openThemeSelector: () => setOverlay("theme"),
|
|
2000
|
+
toggleMarkdown: () => {
|
|
2001
|
+
setRenderMarkdown((prev) => {
|
|
2002
|
+
const next = !prev;
|
|
2003
|
+
setLiveItems([
|
|
2004
|
+
{
|
|
2005
|
+
kind: "info",
|
|
2006
|
+
text: next ? "Rendered markdown mode." : "Raw markdown mode.",
|
|
2007
|
+
id: getId(),
|
|
2008
|
+
},
|
|
2009
|
+
]);
|
|
2010
|
+
return next;
|
|
2011
|
+
});
|
|
2012
|
+
},
|
|
2013
|
+
clearApprovedPlan: () => {
|
|
2014
|
+
approvedPlanPathRef.current = undefined;
|
|
2015
|
+
planStepsRef.current = [];
|
|
2016
|
+
setPlanSteps([]);
|
|
2017
|
+
void replaceSystemPrompt({ clearApprovedPlan: true });
|
|
2018
|
+
setLiveItems([{ kind: "plan_event", event: "dismissed", id: getId() }]);
|
|
2019
|
+
},
|
|
2020
|
+
})) {
|
|
2245
2021
|
return;
|
|
2246
2022
|
}
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2023
|
+
if (await submitPromptCommand({
|
|
2024
|
+
trimmed,
|
|
2025
|
+
inputImages,
|
|
2026
|
+
currentModel,
|
|
2027
|
+
customCommands,
|
|
2028
|
+
setLastUserMessage,
|
|
2029
|
+
setDoneStatus,
|
|
2030
|
+
finalizeSubmittedUserItem,
|
|
2031
|
+
runAgent: (content) => agentLoop.run(content),
|
|
2032
|
+
setLiveItems,
|
|
2033
|
+
getId,
|
|
2034
|
+
reloadCustomCommands,
|
|
2035
|
+
messagesRef,
|
|
2036
|
+
setGoalModeAndPrompt,
|
|
2037
|
+
onGoalStage: appendGoalAgentTransition,
|
|
2038
|
+
onGoalSetupComplete: () => {
|
|
2039
|
+
setGoalAutoExpand(true);
|
|
2040
|
+
if (props.sessionStore) {
|
|
2041
|
+
props.sessionStore.overlay = "goal";
|
|
2042
|
+
props.sessionStore.goalAutoExpand = true;
|
|
2256
2043
|
}
|
|
2257
|
-
|
|
2258
|
-
}
|
|
2259
|
-
|
|
2260
|
-
const modelInfo = getModel(currentModel);
|
|
2261
|
-
const modelSupportsImages = modelInfo?.supportsImages ?? true;
|
|
2262
|
-
const userContent = buildUserContentWithAttachments(fullPrompt, inputImages, modelSupportsImages);
|
|
2263
|
-
// Show the typed command as the user message
|
|
2264
|
-
const userItem = {
|
|
2265
|
-
kind: "user",
|
|
2266
|
-
text: trimmed,
|
|
2267
|
-
imageCount: hasImages ? inputImages.length : undefined,
|
|
2268
|
-
id: getId(),
|
|
2269
|
-
};
|
|
2270
|
-
setLastUserMessage(trimmed);
|
|
2271
|
-
setDoneStatus(null);
|
|
2272
|
-
setLiveItems([userItem]);
|
|
2273
|
-
// Send the full prompt to the agent, with user args appended if provided
|
|
2274
|
-
try {
|
|
2275
|
-
await agentLoop.run(userContent);
|
|
2276
|
-
}
|
|
2277
|
-
catch (err) {
|
|
2278
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
2279
|
-
log("ERROR", "error", msg);
|
|
2280
|
-
const isAbort = msg.includes("aborted") || msg.includes("abort");
|
|
2281
|
-
setLiveItems((prev) => [
|
|
2282
|
-
...prev,
|
|
2283
|
-
isAbort
|
|
2284
|
-
? { kind: "stopped", text: "Request was stopped.", id: getId() }
|
|
2285
|
-
: toErrorItem(err, getId()),
|
|
2286
|
-
]);
|
|
2287
|
-
}
|
|
2288
|
-
// Reload custom commands in case a setup command created new ones
|
|
2289
|
-
reloadCustomCommands();
|
|
2044
|
+
setOverlay("goal");
|
|
2045
|
+
},
|
|
2046
|
+
})) {
|
|
2290
2047
|
return;
|
|
2291
2048
|
}
|
|
2292
2049
|
// Check slash commands
|
|
@@ -2305,7 +2062,7 @@ export function App(props) {
|
|
|
2305
2062
|
// ── Queue message if agent is already running ──
|
|
2306
2063
|
if (agentLoop.isRunning) {
|
|
2307
2064
|
log("INFO", "queue", `Queued message: ${trimmed.length > 80 ? trimmed.slice(0, 80) + "..." : trimmed}`);
|
|
2308
|
-
agentLoop.queueMessage(userContent);
|
|
2065
|
+
agentLoop.queueMessage(userContent, input);
|
|
2309
2066
|
let displayText = input;
|
|
2310
2067
|
if (hasImages) {
|
|
2311
2068
|
const { cleanText } = await extractImagePaths(input, props.cwd);
|
|
@@ -2320,31 +2077,32 @@ export function App(props) {
|
|
|
2320
2077
|
setLiveItems((prev) => [...prev, queuedItem]);
|
|
2321
2078
|
return;
|
|
2322
2079
|
}
|
|
2323
|
-
// Move any remaining live items into history (Static) before starting a
|
|
2324
|
-
// new turn. Must go through queueFlush so flushGeneration bumps and the
|
|
2325
|
-
// drain effect actually runs — mutating pendingFlushRef directly here
|
|
2326
|
-
// stashed items that nothing was signalled to pick up, so they sat in
|
|
2327
|
-
// limbo until some unrelated later code path happened to call queueFlush.
|
|
2328
|
-
setLiveItems((prev) => {
|
|
2329
|
-
if (prev.length > 0) {
|
|
2330
|
-
queueFlush(prev);
|
|
2331
|
-
}
|
|
2332
|
-
return [];
|
|
2333
|
-
});
|
|
2334
2080
|
// Build display text — strip image paths, show badges instead
|
|
2335
2081
|
let displayText = input;
|
|
2336
2082
|
if (hasImages) {
|
|
2337
2083
|
const { cleanText } = await extractImagePaths(input, props.cwd);
|
|
2338
2084
|
displayText = cleanText;
|
|
2339
2085
|
}
|
|
2086
|
+
let imagePreviews;
|
|
2087
|
+
if (hasImages) {
|
|
2088
|
+
const built = await Promise.all(inputImages
|
|
2089
|
+
.filter((img) => img.kind === "image")
|
|
2090
|
+
.map(async (img) => {
|
|
2091
|
+
const downscaled = await downscaleForPreview(Buffer.from(img.data, "base64"));
|
|
2092
|
+
return { base64: downscaled.toString("base64"), mediaType: img.mediaType };
|
|
2093
|
+
}));
|
|
2094
|
+
imagePreviews = built.length > 0 ? built : undefined;
|
|
2095
|
+
}
|
|
2340
2096
|
const userItem = {
|
|
2341
2097
|
kind: "user",
|
|
2342
2098
|
text: displayText,
|
|
2343
2099
|
imageCount: hasImages ? inputImages.length : undefined,
|
|
2100
|
+
imagePreviews,
|
|
2344
2101
|
pasteInfo,
|
|
2345
2102
|
id: getId(),
|
|
2346
2103
|
};
|
|
2347
2104
|
setLastUserMessage(input);
|
|
2105
|
+
setScrollResetToken((token) => token + 1);
|
|
2348
2106
|
setDoneStatus(null);
|
|
2349
2107
|
// Clear stale plan progress if there's no active approved plan
|
|
2350
2108
|
// (avoids lingering progress from a completed or abandoned plan run)
|
|
@@ -2352,7 +2110,18 @@ export function App(props) {
|
|
|
2352
2110
|
planStepsRef.current = [];
|
|
2353
2111
|
setPlanSteps([]);
|
|
2354
2112
|
}
|
|
2355
|
-
|
|
2113
|
+
finalizeSubmittedUserItem(userItem, liveItems);
|
|
2114
|
+
// Open a per-turn checkpoint capturing the conversation position right
|
|
2115
|
+
// before this exchange, so /rewind can restore pre-turn code/chat state.
|
|
2116
|
+
if (props.checkpointStore) {
|
|
2117
|
+
rewindTurnRef.current += 1;
|
|
2118
|
+
await props.checkpointStore
|
|
2119
|
+
.openCheckpoint({
|
|
2120
|
+
turnIndex: rewindTurnRef.current,
|
|
2121
|
+
messageIndex: messagesRef.current.length,
|
|
2122
|
+
})
|
|
2123
|
+
.catch(() => { });
|
|
2124
|
+
}
|
|
2356
2125
|
// Run agent
|
|
2357
2126
|
try {
|
|
2358
2127
|
await agentLoop.run(userContent);
|
|
@@ -2370,17 +2139,29 @@ export function App(props) {
|
|
|
2370
2139
|
}
|
|
2371
2140
|
}, [
|
|
2372
2141
|
agentLoop,
|
|
2373
|
-
props.onSlashCommand,
|
|
2374
2142
|
compactConversation,
|
|
2143
|
+
currentModel,
|
|
2144
|
+
finalizeSubmittedUserItem,
|
|
2145
|
+
liveItems,
|
|
2146
|
+
props.cwd,
|
|
2147
|
+
props.onSlashCommand,
|
|
2148
|
+
props.resetUI,
|
|
2149
|
+
props.sessionStore,
|
|
2375
2150
|
rebuildSystemPrompt,
|
|
2151
|
+
showSessionSummaryAndExit,
|
|
2152
|
+
reloadCustomCommands,
|
|
2376
2153
|
replaceSystemPrompt,
|
|
2377
|
-
refreshRepoMap,
|
|
2378
|
-
stripRepoMapMessages,
|
|
2379
2154
|
]);
|
|
2380
|
-
const handleDoubleExit = useDoublePress(setExitPending,
|
|
2155
|
+
const handleDoubleExit = useDoublePress(setExitPending, showSessionSummaryAndExit);
|
|
2381
2156
|
const handleAbort = useCallback(() => {
|
|
2382
2157
|
if (agentLoop.isRunning) {
|
|
2383
|
-
|
|
2158
|
+
// Restore any unsent queued messages to the composer instead of dropping
|
|
2159
|
+
// them, so an interrupt never silently discards what the user typed.
|
|
2160
|
+
const queuedText = agentLoop.drainQueuedText();
|
|
2161
|
+
if (queuedText) {
|
|
2162
|
+
setLiveItems((prev) => prev.filter((item) => item.kind !== "queued"));
|
|
2163
|
+
setComposerInject({ text: queuedText, nonce: nextIdRef.current++ });
|
|
2164
|
+
}
|
|
2384
2165
|
agentLoop.abort();
|
|
2385
2166
|
}
|
|
2386
2167
|
else if (compactionAbortRef.current) {
|
|
@@ -2389,22 +2170,22 @@ export function App(props) {
|
|
|
2389
2170
|
else {
|
|
2390
2171
|
handleDoubleExit();
|
|
2391
2172
|
}
|
|
2392
|
-
}, [agentLoop, handleDoubleExit]);
|
|
2173
|
+
}, [agentLoop, handleDoubleExit, setLiveItems]);
|
|
2393
2174
|
const handleToggleThinking = useCallback(() => {
|
|
2394
|
-
|
|
2395
|
-
const next =
|
|
2396
|
-
log("INFO", "thinking", `Thinking ${next
|
|
2397
|
-
setLiveItems((items) => [
|
|
2398
|
-
...items,
|
|
2399
|
-
{ kind: "thinking_transition", active: next, id: getId() },
|
|
2400
|
-
]);
|
|
2175
|
+
setThinkingLevel((prev) => {
|
|
2176
|
+
const next = getNextThinkingLevel(currentProvider, currentModel, prev);
|
|
2177
|
+
log("INFO", "thinking", next ? `Thinking ${next}` : "Thinking disabled");
|
|
2401
2178
|
if (props.settingsFile) {
|
|
2402
2179
|
const sm = new SettingsManager(props.settingsFile);
|
|
2403
|
-
sm.load().then(() =>
|
|
2180
|
+
void sm.load().then(async () => {
|
|
2181
|
+
await sm.set("thinkingEnabled", !!next);
|
|
2182
|
+
if (next)
|
|
2183
|
+
await sm.set("thinkingLevel", next);
|
|
2184
|
+
});
|
|
2404
2185
|
}
|
|
2405
2186
|
return next;
|
|
2406
2187
|
});
|
|
2407
|
-
}, [props.settingsFile]);
|
|
2188
|
+
}, [currentProvider, currentModel, props.settingsFile]);
|
|
2408
2189
|
const handleModelSelect = useCallback((value) => {
|
|
2409
2190
|
setOverlay(null);
|
|
2410
2191
|
const colonIdx = value.indexOf(":");
|
|
@@ -2413,6 +2194,9 @@ export function App(props) {
|
|
|
2413
2194
|
const newProvider = value.slice(0, colonIdx);
|
|
2414
2195
|
const newModelId = value.slice(colonIdx + 1);
|
|
2415
2196
|
log("INFO", "model", `Model changed`, { provider: newProvider, model: newModelId });
|
|
2197
|
+
// Keep the ref in sync before any prompt rebuild so the identity (Claude
|
|
2198
|
+
// Code vs EZ Coder) reflects the newly selected provider immediately.
|
|
2199
|
+
currentProviderRef.current = newProvider;
|
|
2416
2200
|
const rebuildPromptWithTools = (tools) => {
|
|
2417
2201
|
currentToolsRef.current = tools;
|
|
2418
2202
|
void replaceSystemPrompt({ tools });
|
|
@@ -2512,198 +2296,179 @@ export function App(props) {
|
|
|
2512
2296
|
const promptByName = new Map(PROMPT_COMMANDS.map((c) => [c.name, c]));
|
|
2513
2297
|
const fromPrompt = (name) => {
|
|
2514
2298
|
const c = promptByName.get(name);
|
|
2515
|
-
return c
|
|
2299
|
+
return c
|
|
2300
|
+
? {
|
|
2301
|
+
name: c.name,
|
|
2302
|
+
aliases: c.aliases,
|
|
2303
|
+
description: c.description,
|
|
2304
|
+
sectionTitle: "workflows",
|
|
2305
|
+
}
|
|
2306
|
+
: null;
|
|
2516
2307
|
};
|
|
2517
2308
|
const promptOrder = [
|
|
2518
2309
|
// Project audits / one-shot analysis
|
|
2519
|
-
"goal",
|
|
2520
2310
|
"init",
|
|
2521
|
-
"research",
|
|
2522
|
-
"scan",
|
|
2523
|
-
"verify",
|
|
2524
2311
|
"expand",
|
|
2525
2312
|
"bullet-proof",
|
|
2526
|
-
"simplify",
|
|
2527
2313
|
"compare",
|
|
2528
|
-
"batch",
|
|
2529
2314
|
// Setup / installers
|
|
2530
|
-
"setup-lint",
|
|
2531
|
-
"setup-tests",
|
|
2532
2315
|
"setup-commit",
|
|
2533
|
-
"setup-update",
|
|
2534
|
-
"setup-eyes",
|
|
2535
|
-
"eyes-improve",
|
|
2536
2316
|
"setup-skills",
|
|
2537
2317
|
];
|
|
2538
2318
|
const orderedPromptCommands = promptOrder
|
|
2539
2319
|
.map(fromPrompt)
|
|
2540
2320
|
.filter((c) => c !== null);
|
|
2541
2321
|
const knownPromptNames = new Set(promptOrder);
|
|
2542
|
-
const remainingPromptCommands = PROMPT_COMMANDS.filter((c) => !knownPromptNames.has(c.name)).map((c) => ({
|
|
2322
|
+
const remainingPromptCommands = PROMPT_COMMANDS.filter((c) => !knownPromptNames.has(c.name)).map((c) => ({
|
|
2323
|
+
name: c.name,
|
|
2324
|
+
aliases: c.aliases,
|
|
2325
|
+
description: c.description,
|
|
2326
|
+
sectionTitle: "workflows",
|
|
2327
|
+
}));
|
|
2543
2328
|
return [
|
|
2544
2329
|
// Session actions (most frequent)
|
|
2545
|
-
{ name: "model", aliases: ["m"], description: "Switch model" },
|
|
2546
|
-
{ name: "compact", aliases: ["c"], description: "Compact
|
|
2547
|
-
{ name: "clear", aliases: [], description: "Clear session
|
|
2548
|
-
{ name: "theme", aliases: ["t"], description: "Switch theme" },
|
|
2549
|
-
{
|
|
2330
|
+
{ name: "model", aliases: ["m"], description: "Switch model", sectionTitle: "built-in" },
|
|
2331
|
+
{ name: "compact", aliases: ["c"], description: "Compact context", sectionTitle: "built-in" },
|
|
2332
|
+
{ name: "clear", aliases: [], description: "Clear session", sectionTitle: "built-in" },
|
|
2333
|
+
{ name: "theme", aliases: ["t"], description: "Switch theme", sectionTitle: "built-in" },
|
|
2334
|
+
{
|
|
2335
|
+
name: idealReviewEnabled ? "ideal-off" : "ideal-on",
|
|
2336
|
+
aliases: [],
|
|
2337
|
+
description: idealReviewEnabled
|
|
2338
|
+
? "Disable pre-final ideal review"
|
|
2339
|
+
: "Enable pre-final ideal review",
|
|
2340
|
+
sectionTitle: "built-in",
|
|
2341
|
+
},
|
|
2342
|
+
{
|
|
2343
|
+
name: "rewind",
|
|
2344
|
+
aliases: [],
|
|
2345
|
+
description: "Restore files/conversation to a checkpoint",
|
|
2346
|
+
sectionTitle: "built-in",
|
|
2347
|
+
},
|
|
2550
2348
|
...orderedPromptCommands,
|
|
2551
2349
|
...remainingPromptCommands,
|
|
2552
2350
|
...customCommands.map((cmd) => ({
|
|
2553
2351
|
name: cmd.name,
|
|
2554
2352
|
aliases: [],
|
|
2555
2353
|
description: cmd.description,
|
|
2354
|
+
sectionTitle: "custom",
|
|
2556
2355
|
})),
|
|
2557
|
-
{
|
|
2356
|
+
{
|
|
2357
|
+
name: "quit",
|
|
2358
|
+
aliases: ["q", "exit"],
|
|
2359
|
+
description: "Exit ezcoder",
|
|
2360
|
+
sectionTitle: "built-in",
|
|
2361
|
+
},
|
|
2558
2362
|
];
|
|
2559
|
-
}, [customCommands]);
|
|
2560
|
-
const renderItem = (item) => {
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
? theme.warning
|
|
2584
|
-
: item.phase === "verifier_started"
|
|
2585
|
-
? theme.accent
|
|
2586
|
-
: item.phase === "worker_started"
|
|
2587
|
-
? theme.primary
|
|
2588
|
-
: item.phase === "terminal"
|
|
2589
|
-
? theme.success
|
|
2590
|
-
: theme.primary;
|
|
2591
|
-
const glyph = item.phase === "worker_finished" || item.phase === "verifier_finished"
|
|
2592
|
-
? "✓ "
|
|
2593
|
-
: item.phase === "terminal"
|
|
2594
|
-
? item.status === "passed"
|
|
2595
|
-
? "◆ "
|
|
2596
|
-
: "! "
|
|
2597
|
-
: "↻ ";
|
|
2598
|
-
return (_jsxs(Box, { marginTop: 1, flexDirection: "column", flexShrink: 1, children: [_jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: color, bold: true, children: glyph }), _jsx(Text, { color: color, bold: true, children: item.title }), item.workerId ? _jsxs(Text, { color: theme.textDim, children: [" \u00B7 worker ", item.workerId] }) : null] }), item.detail ? (_jsx(Text, { color: theme.textDim, wrap: "wrap", children: ` ${item.detail}` })) : null, item.summaryRows && item.summaryRows.length > 0 ? (_jsx(Box, { flexDirection: "column", marginTop: 1, marginLeft: 2, flexShrink: 1, children: item.summaryRows.map((row) => (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: theme.textDim, children: row.label.padEnd(10) }), _jsx(Text, { color: theme.text, children: row.value }), row.detail ? _jsxs(Text, { color: theme.textDim, children: [" \u00B7 ", row.detail] }) : null] }, row.label))) })) : null] }, item.id));
|
|
2599
|
-
}
|
|
2600
|
-
case "style_pack": {
|
|
2601
|
-
const names = item.added.map((id) => LANGUAGE_DISPLAY_NAMES[id]);
|
|
2602
|
-
const headerLabel = item.added.length > 1 ? "STYLE PACKS ACTIVE" : "STYLE PACK ACTIVE";
|
|
2603
|
-
return (_jsxs(Box, { marginTop: 1, flexShrink: 1, flexDirection: "column", borderStyle: "round", borderColor: theme.language, paddingX: 1, children: [_jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: theme.language, bold: true, children: "◆ " }), _jsx(Text, { color: theme.language, bold: true, children: headerLabel })] }), _jsx(Text, { color: theme.text, bold: true, wrap: "wrap", children: names.join(", ") }), item.showSetupHint && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: theme.textMuted, children: "Tip: run " }), _jsx(Text, { color: theme.language, bold: true, children: "/setup" }), _jsx(Text, { color: theme.textMuted, children: " to audit this project against the active pack(s)" })] }) }))] }, item.id));
|
|
2604
|
-
}
|
|
2605
|
-
case "setup_hint":
|
|
2606
|
-
return (_jsxs(Box, { marginTop: 1, flexShrink: 1, flexDirection: "column", borderStyle: "round", borderColor: theme.language, paddingX: 1, children: [_jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: theme.language, bold: true, children: "◆ " }), _jsx(Text, { color: theme.language, bold: true, children: "NO STYLE PACKS DETECTED" })] }), _jsx(Text, { color: theme.textMuted, wrap: "wrap", children: "This directory has no recognized language manifest at its root." }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: theme.textMuted, children: "Tip: run " }), _jsx(Text, { color: theme.language, bold: true, children: "/setup" }), _jsx(Text, { color: theme.textMuted, children: " to audit project hygiene or bootstrap a new project from scratch" })] }) })] }, item.id));
|
|
2607
|
-
case "assistant":
|
|
2608
|
-
return (_jsx(AssistantMessage, { text: item.text, thinking: item.thinking, thinkingMs: item.thinkingMs, planMode: item.planMode }, item.id));
|
|
2609
|
-
case "tool_start":
|
|
2610
|
-
return (_jsx(ToolExecution, { status: "running", name: item.name, args: item.args, progressOutput: item.progressOutput, animateUntil: item.animateUntil }, item.id));
|
|
2611
|
-
case "tool_done":
|
|
2612
|
-
return (_jsx(ToolExecution, { status: "done", name: item.name, args: item.args, result: item.result, isError: item.isError, details: item.details }, item.id));
|
|
2613
|
-
case "tool_group":
|
|
2614
|
-
return _jsx(ToolGroupExecution, { tools: item.tools }, item.id);
|
|
2615
|
-
case "server_tool_start":
|
|
2616
|
-
return (_jsx(ServerToolExecution, { status: "running", name: item.name, input: item.input, startedAt: item.startedAt, animateUntil: item.animateUntil }, item.id));
|
|
2617
|
-
case "server_tool_done":
|
|
2618
|
-
return (_jsx(ServerToolExecution, { status: "done", name: item.name, input: item.input, durationMs: item.durationMs, resultType: item.resultType }, item.id));
|
|
2619
|
-
case "error": {
|
|
2620
|
-
const showMessage = item.message && item.message !== item.headline;
|
|
2621
|
-
return (_jsxs(Box, { marginTop: 1, flexDirection: "column", flexShrink: 1, children: [_jsxs(Text, { color: theme.error, wrap: "wrap", children: ["✗ ", item.headline] }), showMessage && (_jsx(Text, { color: theme.textDim, wrap: "wrap", children: ` ${item.message}` })), _jsx(Text, { color: theme.textDim, wrap: "wrap", children: ` → ${item.guidance}` })] }, item.id));
|
|
2622
|
-
}
|
|
2623
|
-
case "info":
|
|
2624
|
-
return (_jsx(Box, { marginTop: 1, flexShrink: 1, children: _jsx(Text, { color: theme.textDim, wrap: "wrap", children: item.text }) }, item.id));
|
|
2625
|
-
case "update_notice":
|
|
2626
|
-
return (_jsx(Box, { marginTop: 1, flexShrink: 1, borderStyle: "round", borderColor: theme.success, paddingX: 1, children: _jsxs(Text, { color: theme.success, bold: true, wrap: "wrap", children: ["✨ ", item.text] }) }, item.id));
|
|
2627
|
-
case "plan_transition":
|
|
2628
|
-
return (_jsx(Box, { marginTop: 1, flexShrink: 1, children: _jsxs(Text, { color: theme.planPrimary, bold: true, wrap: "wrap", children: [item.active ? "● " : "● ", item.text] }) }, item.id));
|
|
2629
|
-
case "thinking_transition": {
|
|
2630
|
-
const glyphColor = item.active ? THINKING_BORDER_COLORS[0] : theme.textDim;
|
|
2631
|
-
return (_jsxs(Box, { marginTop: 1, flexShrink: 1, children: [_jsx(Text, { color: glyphColor, bold: true, children: "✻ " }), _jsx(Text, { color: item.active ? theme.accent : theme.textDim, bold: true, children: item.active ? "Thinking ON" : "Thinking OFF" })] }, item.id));
|
|
2632
|
-
}
|
|
2633
|
-
case "model_transition": {
|
|
2634
|
-
const glyphColor = THINKING_BORDER_COLORS[0];
|
|
2635
|
-
return (_jsxs(Box, { marginTop: 1, flexShrink: 1, children: [_jsx(Text, { color: glyphColor, bold: true, children: "▸ " }), _jsx(Text, { color: theme.textDim, children: "Switched to " }), _jsx(Text, { color: theme.primary, bold: true, children: item.modelName })] }, item.id));
|
|
2636
|
-
}
|
|
2637
|
-
case "theme_transition": {
|
|
2638
|
-
const glyphColor = THINKING_BORDER_COLORS[0];
|
|
2639
|
-
return (_jsxs(Box, { marginTop: 1, flexShrink: 1, children: [_jsx(Text, { color: glyphColor, bold: true, children: "◐ " }), _jsx(Text, { color: theme.textDim, children: "Theme switched to " }), _jsx(Text, { color: theme.primary, bold: true, children: item.themeName })] }, item.id));
|
|
2640
|
-
}
|
|
2641
|
-
case "plan_event": {
|
|
2642
|
-
// Plan-domain status changes (approve / reject / dismiss). Uses
|
|
2643
|
-
// theme.planPrimary to match the existing plan_transition family,
|
|
2644
|
-
// distinct from the model/thinking gradient.
|
|
2645
|
-
const label = item.event === "approved"
|
|
2646
|
-
? "Plan approved"
|
|
2647
|
-
: item.event === "rejected"
|
|
2648
|
-
? "Plan rejected"
|
|
2649
|
-
: "Plan dismissed";
|
|
2650
|
-
return (_jsxs(Box, { marginTop: 1, flexShrink: 1, children: [_jsxs(Text, { color: theme.planPrimary, bold: true, children: ["○ ", label] }), item.detail ? _jsx(Text, { color: theme.textDim, children: ` — "${item.detail}"` }) : null] }, item.id));
|
|
2651
|
-
}
|
|
2652
|
-
case "stopped":
|
|
2653
|
-
// Cancellation / abort acknowledgement (ESC, auto-setup cancel, etc.).
|
|
2654
|
-
// Muted dim treatment — this is an ack, not a state change worth a
|
|
2655
|
-
// gradient. Glyph `⊘` reads as "stop" without being alarming.
|
|
2656
|
-
return (_jsx(Box, { marginTop: 1, flexShrink: 1, children: _jsxs(Text, { color: theme.textDim, bold: true, children: ["⊘ ", item.text] }) }, item.id));
|
|
2657
|
-
case "step_done":
|
|
2658
|
-
return (_jsx(Box, { marginTop: 1, flexShrink: 1, children: _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: theme.success, bold: true, children: "✓ " }), _jsx(Text, { color: theme.success, bold: true, children: `Step ${item.stepNum} done` }), item.description ? (_jsx(Text, { color: theme.textDim, children: ` — ${item.description}` })) : null] }) }, item.id));
|
|
2659
|
-
case "queued":
|
|
2660
|
-
return (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.accent, bold: true, children: "⏳ Queued: " }), _jsxs(Text, { color: theme.text, wrap: "wrap", children: [item.text, item.imageCount
|
|
2661
|
-
? ` (+${item.imageCount} image${item.imageCount > 1 ? "s" : ""})`
|
|
2662
|
-
: ""] })] }, item.id));
|
|
2663
|
-
case "compacting":
|
|
2664
|
-
return _jsx(CompactionSpinner, { staticDisplay: true }, item.id);
|
|
2665
|
-
case "compacted":
|
|
2666
|
-
return (_jsx(CompactionDone, { originalCount: item.originalCount, newCount: item.newCount, tokensBefore: item.tokensBefore, tokensAfter: item.tokensAfter }, item.id));
|
|
2667
|
-
case "duration":
|
|
2668
|
-
return (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.textDim, children: ["✻ ", item.verb, " ", formatDuration(item.durationMs)] }) }, item.id));
|
|
2669
|
-
case "subagent_group":
|
|
2670
|
-
return _jsx(SubAgentPanel, { agents: item.agents, aborted: item.aborted }, item.id);
|
|
2363
|
+
}, [customCommands, idealReviewEnabled]);
|
|
2364
|
+
const renderItem = (item, index, items) => renderTranscriptItem({
|
|
2365
|
+
item,
|
|
2366
|
+
index,
|
|
2367
|
+
items,
|
|
2368
|
+
pendingHistoryFlushLastItem: index === 0 ? lastVisibleTranscriptItem(pendingHistoryFlushRef.current) : undefined,
|
|
2369
|
+
historyLastItem: index === 0 ? lastVisibleTranscriptItem(history) : undefined,
|
|
2370
|
+
version: props.version,
|
|
2371
|
+
currentModel,
|
|
2372
|
+
currentProvider,
|
|
2373
|
+
displayedCwd,
|
|
2374
|
+
columns,
|
|
2375
|
+
theme,
|
|
2376
|
+
renderMarkdown,
|
|
2377
|
+
measuredLiveAreaRows,
|
|
2378
|
+
});
|
|
2379
|
+
const openOverlay = useCallback((kind) => {
|
|
2380
|
+
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
2381
|
+
props.sessionStore.overlay = kind;
|
|
2382
|
+
if (kind !== "plan")
|
|
2383
|
+
props.sessionStore.planAutoExpand = false;
|
|
2384
|
+
if (kind !== "goal")
|
|
2385
|
+
props.sessionStore.goalAutoExpand = false;
|
|
2386
|
+
props.resetUI();
|
|
2671
2387
|
}
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2388
|
+
else {
|
|
2389
|
+
if (props.sessionStore) {
|
|
2390
|
+
props.sessionStore.overlay = kind;
|
|
2391
|
+
if (kind !== "plan")
|
|
2392
|
+
props.sessionStore.planAutoExpand = false;
|
|
2393
|
+
if (kind !== "goal")
|
|
2394
|
+
props.sessionStore.goalAutoExpand = false;
|
|
2395
|
+
if (agentLoop.isRunning && kind !== "plan") {
|
|
2396
|
+
props.sessionStore.pendingResetUI = true;
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
if (kind !== "plan")
|
|
2400
|
+
setPlanAutoExpand(false);
|
|
2401
|
+
if (kind !== "goal")
|
|
2402
|
+
setGoalAutoExpand(false);
|
|
2403
|
+
setOverlay(kind);
|
|
2404
|
+
}
|
|
2405
|
+
}, [agentLoop.isRunning, props]);
|
|
2406
|
+
useEffect(() => {
|
|
2407
|
+
runAllTasksRef.current = runAllTasks;
|
|
2408
|
+
if (props.sessionStore)
|
|
2409
|
+
props.sessionStore.runAllTasks = runAllTasks;
|
|
2410
|
+
}, [runAllTasks, props.sessionStore]);
|
|
2411
|
+
useEffect(() => {
|
|
2412
|
+
agentRunningRef.current = agentLoop.isRunning;
|
|
2413
|
+
}, [agentLoop.isRunning]);
|
|
2414
|
+
const { startPixelFix, setRunAllPixel } = usePixelFixFlow({
|
|
2415
|
+
agentLoop,
|
|
2416
|
+
cwd: props.cwd,
|
|
2417
|
+
currentProvider,
|
|
2418
|
+
currentModel,
|
|
2419
|
+
rebuildToolsForCwd: props.rebuildToolsForCwd,
|
|
2420
|
+
sessionStore: props.sessionStore,
|
|
2421
|
+
currentPixelFixRef,
|
|
2422
|
+
runAllPixelRef,
|
|
2423
|
+
startPixelFixRef,
|
|
2424
|
+
cwdRef,
|
|
2425
|
+
currentToolsRef,
|
|
2426
|
+
injectedLanguagesRef,
|
|
2427
|
+
setupHintShownRef,
|
|
2428
|
+
messagesRef,
|
|
2429
|
+
persistedIndexRef,
|
|
2430
|
+
sessionManagerRef,
|
|
2431
|
+
sessionPathRef,
|
|
2432
|
+
setDisplayedCwd,
|
|
2433
|
+
setCurrentTools,
|
|
2434
|
+
setHistory,
|
|
2435
|
+
setLiveItems,
|
|
2436
|
+
setLastUserMessage,
|
|
2437
|
+
setDoneStatus,
|
|
2438
|
+
rebuildSystemPrompt,
|
|
2439
|
+
clearPendingHistory,
|
|
2440
|
+
getId,
|
|
2441
|
+
initialRunAllPixel: props.sessionStore?.runAllPixel ?? false,
|
|
2442
|
+
});
|
|
2443
|
+
// Starts a single task: opens a fresh session + chat and runs the task
|
|
2444
|
+
// prompt through the agent loop. Wired into startTaskRef so both the task
|
|
2445
|
+
// picker (Enter = start one, r = run all) and the run-all auto-advance in
|
|
2446
|
+
// onDone can invoke it from stale closures.
|
|
2447
|
+
const startTask = useCallback((title, prompt, taskId) => {
|
|
2448
|
+
const taskCwd = cwdRef.current;
|
|
2449
|
+
const shortId = taskId.slice(0, 8);
|
|
2450
|
+
const completionHint = `\n\n---\nWhen you have fully completed this task, call the tasks tool to mark it done:\n` +
|
|
2451
|
+
`tasks({ action: "done", id: "${shortId}" })`;
|
|
2452
|
+
const fullPrompt = prompt + completionHint;
|
|
2453
|
+
if (props.resetUI && props.sessionStore) {
|
|
2683
2454
|
const sysMsg = messagesRef.current[0];
|
|
2684
2455
|
const newMessages = sysMsg && sysMsg.role === "system" ? [sysMsg] : messagesRef.current.slice(0, 1);
|
|
2685
|
-
const taskItem = { kind: "task", title, id:
|
|
2456
|
+
const taskItem = { kind: "task", title, id: getId() };
|
|
2686
2457
|
const sm = sessionManagerRef.current;
|
|
2687
2458
|
void (async () => {
|
|
2688
2459
|
let newSessionPath;
|
|
2689
2460
|
if (sm) {
|
|
2690
2461
|
try {
|
|
2691
|
-
const
|
|
2692
|
-
newSessionPath =
|
|
2693
|
-
log("INFO", "tasks", "New session for task", { path:
|
|
2462
|
+
const session = await sm.create(taskCwd, currentProvider, currentModel);
|
|
2463
|
+
newSessionPath = session.path;
|
|
2464
|
+
log("INFO", "tasks", "New session for task", { path: session.path });
|
|
2694
2465
|
}
|
|
2695
2466
|
catch {
|
|
2696
|
-
//
|
|
2467
|
+
// Session creation is best-effort.
|
|
2697
2468
|
}
|
|
2698
2469
|
}
|
|
2699
|
-
if (props.sessionStore)
|
|
2470
|
+
if (props.sessionStore)
|
|
2700
2471
|
props.sessionStore.overlay = null;
|
|
2701
|
-
// Mirror through sessionStore so the post-resetUI remount can
|
|
2702
|
-
// re-seed the ref. markTaskDone() in the run-all onDone branch
|
|
2703
|
-
// reads currentTaskIdRef.current, which is seeded from this.
|
|
2704
|
-
props.sessionStore.currentTaskId = taskId;
|
|
2705
|
-
}
|
|
2706
|
-
currentTaskIdRef.current = taskId;
|
|
2707
2472
|
props.resetUI?.({
|
|
2708
2473
|
wipeSession: true,
|
|
2709
2474
|
messages: newMessages,
|
|
@@ -2714,10 +2479,7 @@ export function App(props) {
|
|
|
2714
2479
|
})();
|
|
2715
2480
|
return;
|
|
2716
2481
|
}
|
|
2717
|
-
|
|
2718
|
-
currentTaskIdRef.current = taskId;
|
|
2719
|
-
if (props.sessionStore)
|
|
2720
|
-
props.sessionStore.currentTaskId = taskId;
|
|
2482
|
+
clearPendingHistory();
|
|
2721
2483
|
setHistory([{ kind: "banner", id: "banner" }]);
|
|
2722
2484
|
setLiveItems([]);
|
|
2723
2485
|
messagesRef.current = messagesRef.current.slice(0, 1);
|
|
@@ -2725,940 +2487,585 @@ export function App(props) {
|
|
|
2725
2487
|
persistedIndexRef.current = messagesRef.current.length;
|
|
2726
2488
|
const sm = sessionManagerRef.current;
|
|
2727
2489
|
if (sm) {
|
|
2728
|
-
void sm.create(
|
|
2729
|
-
sessionPathRef.current =
|
|
2730
|
-
log("INFO", "tasks", "New session for task", { path:
|
|
2490
|
+
void sm.create(taskCwd, currentProvider, currentModel).then((session) => {
|
|
2491
|
+
sessionPathRef.current = session.path;
|
|
2492
|
+
log("INFO", "tasks", "New session for task", { path: session.path });
|
|
2731
2493
|
});
|
|
2732
2494
|
}
|
|
2733
2495
|
const taskItem = { kind: "task", title, id: getId() };
|
|
2734
2496
|
setLastUserMessage(title);
|
|
2735
2497
|
setDoneStatus(null);
|
|
2736
2498
|
setLiveItems([taskItem]);
|
|
2499
|
+
void agentLoop.run(fullPrompt).catch((err) => {
|
|
2500
|
+
setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
|
|
2501
|
+
});
|
|
2502
|
+
}, [agentLoop, currentModel, currentProvider, props]);
|
|
2503
|
+
// Keep the ref in sync so stale closures (task picker, onDone run-all) can
|
|
2504
|
+
// start a task without being recreated each render.
|
|
2505
|
+
startTaskRef.current = startTask;
|
|
2506
|
+
// Reset the live tool feed at the start of each run so the pinned panel only
|
|
2507
|
+
// ever reflects the current turn's activity, not the previous one's.
|
|
2508
|
+
const wasRunningRef = useRef(false);
|
|
2509
|
+
useEffect(() => {
|
|
2510
|
+
if (agentLoop.isRunning && !wasRunningRef.current) {
|
|
2511
|
+
setLiveToolFeed([]);
|
|
2512
|
+
}
|
|
2513
|
+
wasRunningRef.current = agentLoop.isRunning;
|
|
2514
|
+
}, [agentLoop.isRunning]);
|
|
2515
|
+
const isSkillsView = overlay === "skills";
|
|
2516
|
+
const isPlanView = overlay === "plan";
|
|
2517
|
+
const { footerStatusLayout, activityVisible, stallStatusVisible, statusSlotVisible, mainControlsRef, measuredLiveAreaRows, viewportRows, } = useChatLayoutMeasurements({
|
|
2518
|
+
rows,
|
|
2519
|
+
columns,
|
|
2520
|
+
backgroundTaskCount: bgTasks.length,
|
|
2521
|
+
updatePending,
|
|
2522
|
+
agentRunning: agentLoop.isRunning,
|
|
2523
|
+
activityPhase: agentLoop.activityPhase,
|
|
2524
|
+
stallError: agentLoop.stallError,
|
|
2525
|
+
doneStatus,
|
|
2526
|
+
currentModel,
|
|
2527
|
+
contextUsed: agentLoop.contextUsed,
|
|
2528
|
+
contextWindowOptions,
|
|
2529
|
+
displayedCwd,
|
|
2530
|
+
gitBranch,
|
|
2531
|
+
thinkingLevel,
|
|
2532
|
+
exitPending,
|
|
2533
|
+
taskBarExpanded,
|
|
2534
|
+
liveToolFeedCount: liveToolFeed.length,
|
|
2535
|
+
});
|
|
2536
|
+
const isPixelView = overlay === "pixel";
|
|
2537
|
+
const hasLiveAssistantItem = liveItems.some((item) => item.kind === "assistant");
|
|
2538
|
+
const rawVisibleStreamingText = hasLiveAssistantItem ? "" : agentLoop.streamingText;
|
|
2539
|
+
// Compute the prospective paragraph flush DURING render so the live frame
|
|
2540
|
+
// immediately drops the prefix that is about to be written to scrollback.
|
|
2541
|
+
// The queueFlush below runs in an effect (after paint), so if the live text
|
|
2542
|
+
// were sliced only by the already-committed `flushedChars`, the just-flushed
|
|
2543
|
+
// paragraph would render BOTH in scrollback and live for one frame — that
|
|
2544
|
+
// transient extra height is what shoves the footer up and then back down on
|
|
2545
|
+
// every chunk boundary. Slicing by the prospective flush here keeps the live
|
|
2546
|
+
// frame height monotonic, so the footer never bounces.
|
|
2547
|
+
const alreadyFlushedChars = streamedAssistantFlushRef.current.flushedChars;
|
|
2548
|
+
const pendingFlushChars = rawVisibleStreamingText
|
|
2549
|
+
? splitAssistantStreamingText(rawVisibleStreamingText.slice(alreadyFlushedChars)).flushedText
|
|
2550
|
+
.length
|
|
2551
|
+
: 0;
|
|
2552
|
+
useEffect(() => {
|
|
2553
|
+
if (!rawVisibleStreamingText) {
|
|
2554
|
+
streamedAssistantFlushRef.current = { flushedChars: 0, text: "" };
|
|
2555
|
+
return;
|
|
2556
|
+
}
|
|
2557
|
+
if (rawVisibleStreamingText === streamedAssistantFlushRef.current.text)
|
|
2558
|
+
return;
|
|
2559
|
+
const alreadyFlushed = streamedAssistantFlushRef.current.flushedChars;
|
|
2560
|
+
const unflushedText = rawVisibleStreamingText.slice(alreadyFlushed);
|
|
2561
|
+
const split = splitAssistantStreamingText(unflushedText);
|
|
2562
|
+
if (split.flushedText.length > 0) {
|
|
2563
|
+
queueFlush([
|
|
2564
|
+
{
|
|
2565
|
+
kind: "assistant",
|
|
2566
|
+
text: stripDoneMarkers(split.flushedText),
|
|
2567
|
+
continuation: streamedAssistantFlushRef.current.flushedChars > 0,
|
|
2568
|
+
id: getId(),
|
|
2569
|
+
},
|
|
2570
|
+
]);
|
|
2571
|
+
streamedAssistantFlushRef.current = {
|
|
2572
|
+
flushedChars: alreadyFlushed + split.flushedText.length,
|
|
2573
|
+
text: rawVisibleStreamingText,
|
|
2574
|
+
};
|
|
2575
|
+
return;
|
|
2576
|
+
}
|
|
2577
|
+
streamedAssistantFlushRef.current = {
|
|
2578
|
+
...streamedAssistantFlushRef.current,
|
|
2579
|
+
text: rawVisibleStreamingText,
|
|
2580
|
+
};
|
|
2581
|
+
}, [rawVisibleStreamingText, queueFlush]);
|
|
2582
|
+
const visibleStreamingText = stripDoneMarkers(rawVisibleStreamingText.slice(alreadyFlushedChars + pendingFlushChars));
|
|
2583
|
+
const lastLiveItem = liveItems.at(-1);
|
|
2584
|
+
// For spacing decisions, the previous row is the last item that actually
|
|
2585
|
+
// RENDERS. Panel-replaced tool items (now shown only in the LiveToolPanel)
|
|
2586
|
+
// render null, so counting them as the boundary inserts a blank separator
|
|
2587
|
+
// above the streamed response with nothing visible above it.
|
|
2588
|
+
const lastVisibleLiveItem = lastVisibleTranscriptItem(liveItems);
|
|
2589
|
+
const lastPendingHistoryItem = pendingHistoryFlushRef.current.at(-1);
|
|
2590
|
+
const lastHistoryItem = history.at(-1);
|
|
2591
|
+
// Spacing variants: flushed tool rows render null (LiveToolPanel owns them), so
|
|
2592
|
+
// the streamed/first-live boundary must look past them to the last row that
|
|
2593
|
+
// actually printed — otherwise a tool→assistant separator leaves a phantom gap.
|
|
2594
|
+
const lastVisiblePendingHistoryItem = lastVisibleTranscriptItem(pendingHistoryFlushRef.current);
|
|
2595
|
+
const lastVisibleHistoryItem = lastVisibleTranscriptItem(history);
|
|
2596
|
+
const previousTranscriptItem = lastPendingHistoryItem ?? lastHistoryItem;
|
|
2597
|
+
const isAwaitingAssistantAfterUser = agentLoop.isRunning &&
|
|
2598
|
+
!hasLiveAssistantItem &&
|
|
2599
|
+
visibleStreamingText.trim().length === 0 &&
|
|
2600
|
+
(lastLiveItem?.kind === "user" || (!lastLiveItem && previousTranscriptItem?.kind === "user"));
|
|
2601
|
+
const shouldReserveStreamingSpacing = agentLoop.isRunning &&
|
|
2602
|
+
!hasLiveAssistantItem &&
|
|
2603
|
+
(visibleStreamingText.trim().length > 0 ||
|
|
2604
|
+
liveItems.some(isTranscriptSpacingItem) ||
|
|
2605
|
+
isAwaitingAssistantAfterUser);
|
|
2606
|
+
const shouldTopSpaceStreamingText = shouldTopSpaceStreamingAssistant({
|
|
2607
|
+
visibleStreamingText,
|
|
2608
|
+
lastLiveItem: lastVisibleLiveItem,
|
|
2609
|
+
lastPendingHistoryItem: lastVisiblePendingHistoryItem,
|
|
2610
|
+
lastHistoryItem: lastVisibleHistoryItem,
|
|
2611
|
+
});
|
|
2612
|
+
// When earlier paragraphs of THIS response were already flushed to scrollback
|
|
2613
|
+
// mid-stream, the live remainder is the next paragraph — re-insert the blank
|
|
2614
|
+
// line that separated them so the live tail lines up with the flushed history.
|
|
2615
|
+
const streamingContinuesFlushed = alreadyFlushedChars + pendingFlushChars > 0;
|
|
2616
|
+
// ── Fullscreen alt-screen transcript ───────────────────
|
|
2617
|
+
// Flatten history + live items + in-flight streaming into the flat ANSI line
|
|
2618
|
+
// buffer the viewport renders. Reuses the same serializer the legacy
|
|
2619
|
+
// scrollback printer used, so the transcript looks identical. Only computed
|
|
2620
|
+
// when fullscreen is active (the legacy path renders items through Ink).
|
|
2621
|
+
const transcriptContext = useMemo(() => ({
|
|
2622
|
+
theme,
|
|
2623
|
+
columns,
|
|
2624
|
+
version: props.version,
|
|
2625
|
+
model: currentModel,
|
|
2626
|
+
provider: currentProvider,
|
|
2627
|
+
cwd: displayedCwd,
|
|
2628
|
+
}), [theme, columns, props.version, currentModel, currentProvider, displayedCwd]);
|
|
2629
|
+
const transcriptLines = useMemo(() => {
|
|
2630
|
+
if (!props.fullscreen)
|
|
2631
|
+
return [];
|
|
2632
|
+
const items = [...history, ...uniqueItemsById(liveItems)];
|
|
2633
|
+
const hasStreaming = visibleStreamingText.length > 0 || agentLoop.streamingThinking.length > 0;
|
|
2634
|
+
if (hasStreaming) {
|
|
2635
|
+
items.push({
|
|
2636
|
+
kind: "assistant",
|
|
2637
|
+
text: visibleStreamingText,
|
|
2638
|
+
thinking: agentLoop.streamingThinking,
|
|
2639
|
+
thinkingMs: agentLoop.thinkingMs,
|
|
2640
|
+
continuation: streamingContinuesFlushed,
|
|
2641
|
+
id: "__streaming__",
|
|
2642
|
+
});
|
|
2643
|
+
}
|
|
2644
|
+
return buildTranscriptLines(items, transcriptContext);
|
|
2645
|
+
}, [
|
|
2646
|
+
props.fullscreen,
|
|
2647
|
+
history,
|
|
2648
|
+
liveItems,
|
|
2649
|
+
visibleStreamingText,
|
|
2650
|
+
agentLoop.streamingThinking,
|
|
2651
|
+
agentLoop.thinkingMs,
|
|
2652
|
+
streamingContinuesFlushed,
|
|
2653
|
+
transcriptContext,
|
|
2654
|
+
]);
|
|
2655
|
+
// Keyboard + bounds controller. The offset itself lives in the external
|
|
2656
|
+
// transcript-scroll store; the viewport subscribes to it directly so scroll
|
|
2657
|
+
// re-renders only the viewport, not this whole component.
|
|
2658
|
+
useTranscriptScroll({
|
|
2659
|
+
totalLines: transcriptLines.length,
|
|
2660
|
+
viewportRows,
|
|
2661
|
+
active: !!props.fullscreen && !overlay && !taskBarFocused,
|
|
2662
|
+
resetToken: scrollResetToken,
|
|
2663
|
+
});
|
|
2664
|
+
const visibleQueuedCount = liveItems.filter((item) => item.kind === "queued").length;
|
|
2665
|
+
const hiddenQueuedCount = Math.max(0, agentLoop.queuedCount - visibleQueuedCount);
|
|
2666
|
+
const shouldTopSpaceQueueIndicator = hiddenQueuedCount > 0 &&
|
|
2667
|
+
shouldTopSpaceAfterPrintedAgentBoundary({
|
|
2668
|
+
currentKind: "queued",
|
|
2669
|
+
previousLiveItem: lastLiveItem,
|
|
2670
|
+
lastPendingHistoryItem,
|
|
2671
|
+
lastHistoryItem,
|
|
2672
|
+
});
|
|
2673
|
+
const handleRewindCancel = useCallback(() => setRewindCheckpoints(null), []);
|
|
2674
|
+
const handleRewindRestore = useCallback((id, mode) => {
|
|
2675
|
+
const store = props.checkpointStore;
|
|
2676
|
+
setRewindCheckpoints(null);
|
|
2677
|
+
if (!store)
|
|
2678
|
+
return;
|
|
2737
2679
|
void (async () => {
|
|
2738
2680
|
try {
|
|
2739
|
-
await
|
|
2681
|
+
const result = await store.restore(id, mode);
|
|
2682
|
+
const turnNum = id.replace(/^cp-0*/, "") || id;
|
|
2683
|
+
const detailParts = [];
|
|
2684
|
+
if (mode === "code" || mode === "both") {
|
|
2685
|
+
detailParts.push(`${result.filesRestored} file${result.filesRestored === 1 ? "" : "s"} restored`);
|
|
2686
|
+
}
|
|
2687
|
+
if (mode === "conversation" || mode === "both")
|
|
2688
|
+
detailParts.push("conversation rewound");
|
|
2689
|
+
const infoText = `Rewound to checkpoint #${turnNum} (${detailParts.join(", ")}).`;
|
|
2690
|
+
if (mode === "conversation" || mode === "both") {
|
|
2691
|
+
const truncated = messagesRef.current.slice(0, Math.max(1, result.messageIndex));
|
|
2692
|
+
messagesRef.current = truncated;
|
|
2693
|
+
persistedIndexRef.current = truncated.length;
|
|
2694
|
+
agentLoop.reset();
|
|
2695
|
+
// Remount in lockstep so the banner + confirmation re-render cleanly
|
|
2696
|
+
// after the conversation context is truncated (CLAUDE.md pattern).
|
|
2697
|
+
if (props.resetUI) {
|
|
2698
|
+
props.resetUI({
|
|
2699
|
+
wipeSession: true,
|
|
2700
|
+
messages: truncated,
|
|
2701
|
+
history: [
|
|
2702
|
+
{ kind: "banner", id: "banner" },
|
|
2703
|
+
{ kind: "info", text: infoText, id: getId() },
|
|
2704
|
+
],
|
|
2705
|
+
});
|
|
2706
|
+
return;
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
setLiveItems((prev) => [...prev, { kind: "info", text: infoText, id: getId() }]);
|
|
2740
2710
|
}
|
|
2741
2711
|
catch (err) {
|
|
2742
2712
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2743
|
-
log("ERROR", "error", msg);
|
|
2744
|
-
const isAbort = msg.includes("aborted") || msg.includes("abort");
|
|
2745
2713
|
setLiveItems((prev) => [
|
|
2746
2714
|
...prev,
|
|
2747
|
-
|
|
2748
|
-
? { kind: "stopped", text: "Request was stopped.", id: getId() }
|
|
2749
|
-
: toErrorItem(err, getId()),
|
|
2715
|
+
{ kind: "info", text: `Rewind failed: ${msg}`, id: getId() },
|
|
2750
2716
|
]);
|
|
2751
|
-
setRunAllTasks(false);
|
|
2752
2717
|
}
|
|
2753
2718
|
})();
|
|
2754
|
-
}, [props.
|
|
2755
|
-
const
|
|
2719
|
+
}, [props.checkpointStore, props.resetUI, agentLoop, messagesRef, persistedIndexRef]);
|
|
2720
|
+
const handleCloseRemountableOverlay = () => {
|
|
2756
2721
|
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
2757
|
-
props.sessionStore.overlay =
|
|
2758
|
-
if (kind !== "plan")
|
|
2759
|
-
props.sessionStore.planAutoExpand = false;
|
|
2722
|
+
props.sessionStore.overlay = null;
|
|
2760
2723
|
props.resetUI();
|
|
2724
|
+
return;
|
|
2761
2725
|
}
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
props.sessionStore.planAutoExpand = false;
|
|
2767
|
-
if (agentLoop.isRunning && kind !== "goal" && kind !== "plan") {
|
|
2768
|
-
props.sessionStore.pendingResetUI = true;
|
|
2769
|
-
}
|
|
2770
|
-
}
|
|
2771
|
-
if (kind !== "plan")
|
|
2772
|
-
setPlanAutoExpand(false);
|
|
2773
|
-
setOverlay(kind);
|
|
2726
|
+
if (props.sessionStore) {
|
|
2727
|
+
props.sessionStore.overlay = null;
|
|
2728
|
+
if (agentLoop.isRunning)
|
|
2729
|
+
props.sessionStore.pendingResetUI = true;
|
|
2774
2730
|
}
|
|
2775
|
-
|
|
2776
|
-
|
|
2731
|
+
setOverlay(null);
|
|
2732
|
+
};
|
|
2733
|
+
const handleEnterPlanMode = useCallback(async (reason) => {
|
|
2734
|
+
await setPlanModeAndPrompt(true);
|
|
2735
|
+
setLiveItems((prev) => [
|
|
2736
|
+
...prev,
|
|
2737
|
+
{ kind: "plan_transition", text: reason ?? "", id: getId(), active: true },
|
|
2738
|
+
]);
|
|
2739
|
+
}, [setPlanModeAndPrompt]);
|
|
2740
|
+
const handleExitPlanMode = useCallback(async (_planPath) => {
|
|
2741
|
+
await setPlanModeAndPrompt(false);
|
|
2742
|
+
planOverlayPendingRef.current = true;
|
|
2743
|
+
setPlanAutoExpand(true);
|
|
2744
|
+
if (props.sessionStore) {
|
|
2745
|
+
props.sessionStore.overlay = "plan";
|
|
2746
|
+
props.sessionStore.planAutoExpand = true;
|
|
2747
|
+
}
|
|
2748
|
+
setOverlay("plan");
|
|
2749
|
+
return "Plan submitted for user review. Wait for the user to approve, reject, or dismiss it before implementing.";
|
|
2750
|
+
}, [props.sessionStore, setPlanModeAndPrompt]);
|
|
2751
|
+
useEffect(() => {
|
|
2752
|
+
if (!props.planCallbacks)
|
|
2753
|
+
return;
|
|
2754
|
+
props.planCallbacks.onEnterPlan = handleEnterPlanMode;
|
|
2755
|
+
props.planCallbacks.onExitPlan = handleExitPlanMode;
|
|
2756
|
+
}, [handleEnterPlanMode, handleExitPlanMode, props.planCallbacks]);
|
|
2757
|
+
const handleCloseGoalOverlay = () => {
|
|
2777
2758
|
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
2778
2759
|
props.sessionStore.overlay = null;
|
|
2760
|
+
props.sessionStore.goalAutoExpand = false;
|
|
2779
2761
|
props.resetUI();
|
|
2762
|
+
return;
|
|
2780
2763
|
}
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2764
|
+
if (props.sessionStore) {
|
|
2765
|
+
props.sessionStore.overlay = null;
|
|
2766
|
+
props.sessionStore.goalAutoExpand = false;
|
|
2767
|
+
if (agentLoop.isRunning)
|
|
2768
|
+
props.sessionStore.pendingResetUI = true;
|
|
2786
2769
|
}
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2770
|
+
setGoalAutoExpand(false);
|
|
2771
|
+
setOverlay(null);
|
|
2772
|
+
};
|
|
2773
|
+
const handleRunGoal = (run) => {
|
|
2774
|
+
if (goalHasBlockingPrerequisites(run)) {
|
|
2775
|
+
upsertGoalStatusEntry({
|
|
2776
|
+
runId: run.id,
|
|
2777
|
+
label: run.title,
|
|
2778
|
+
phase: "failed",
|
|
2779
|
+
detail: formatGoalBlockingPrerequisites(run),
|
|
2780
|
+
startedAt: Date.now(),
|
|
2781
|
+
});
|
|
2794
2782
|
appendGoalProgress({
|
|
2795
2783
|
kind: "goal_progress",
|
|
2796
|
-
phase: "
|
|
2797
|
-
title:
|
|
2798
|
-
detail:
|
|
2799
|
-
|
|
2800
|
-
status:
|
|
2784
|
+
phase: "terminal",
|
|
2785
|
+
title: `Goal blocked: ${run.title}`,
|
|
2786
|
+
detail: formatGoalBlockingPrerequisites(run),
|
|
2787
|
+
summaryRows: buildGoalSummaryRows(run),
|
|
2788
|
+
status: "blocked",
|
|
2801
2789
|
});
|
|
2802
|
-
agentLoop.queueMessage(eventText);
|
|
2803
2790
|
return;
|
|
2804
2791
|
}
|
|
2805
|
-
appendGoalProgress({
|
|
2806
|
-
kind: "goal_progress",
|
|
2807
|
-
phase: "orchestrator_reviewing",
|
|
2808
|
-
title: "Orchestrator reviewing Goal update",
|
|
2809
|
-
detail,
|
|
2810
|
-
workerId: eventInfo?.worker,
|
|
2811
|
-
status: eventInfo?.status,
|
|
2812
|
-
});
|
|
2813
|
-
setLastUserMessage("");
|
|
2814
|
-
setDoneStatus(null);
|
|
2815
|
-
void agentLoop.run(eventText).catch((err) => {
|
|
2816
|
-
log("ERROR", "goal", err instanceof Error ? err.message : String(err));
|
|
2817
|
-
setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
|
|
2818
|
-
});
|
|
2819
|
-
}, [agentLoop, appendGoalProgress]);
|
|
2820
|
-
const continueGoalRun = useCallback((runId) => {
|
|
2821
2792
|
void (async () => {
|
|
2822
|
-
const
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
if (!latestRun) {
|
|
2826
|
-
runningGoalIdsRef.current.delete(runId);
|
|
2827
|
-
clearGoalStatusEntry(runId);
|
|
2828
|
-
return;
|
|
2829
|
-
}
|
|
2830
|
-
const decision = decideGoalNextAction(latestRun);
|
|
2831
|
-
if (decision.kind === "wait")
|
|
2832
|
-
return;
|
|
2833
|
-
if (decision.kind === "terminal" ||
|
|
2834
|
-
decision.kind === "blocked" ||
|
|
2835
|
-
decision.kind === "pause") {
|
|
2836
|
-
const status = decision.kind === "terminal"
|
|
2837
|
-
? decision.status
|
|
2838
|
-
: decision.kind === "blocked"
|
|
2839
|
-
? "blocked"
|
|
2840
|
-
: "paused";
|
|
2841
|
-
const nextRun = {
|
|
2842
|
-
...latestRun,
|
|
2843
|
-
status,
|
|
2844
|
-
continueRequestedAt: undefined,
|
|
2845
|
-
blockers: decision.kind === "blocked" || decision.kind === "pause"
|
|
2846
|
-
? Array.from(new Set([...latestRun.blockers, decision.reason]))
|
|
2847
|
-
: latestRun.blockers,
|
|
2848
|
-
};
|
|
2849
|
-
await upsertGoalRun(props.cwd, nextRun);
|
|
2850
|
-
await appendGoalDecision(props.cwd, latestRun.id, {
|
|
2851
|
-
kind: "continuation_stopped",
|
|
2852
|
-
reason: decision.reason,
|
|
2853
|
-
content: `terminal=${status}`,
|
|
2854
|
-
});
|
|
2855
|
-
const terminalProgress = formatGoalTerminalProgress(nextRun);
|
|
2856
|
-
if (terminalProgress)
|
|
2857
|
-
appendGoalProgress(terminalProgress);
|
|
2858
|
-
runningGoalIdsRef.current.delete(runId);
|
|
2859
|
-
clearGoalStatusEntry(runId);
|
|
2860
|
-
return;
|
|
2861
|
-
}
|
|
2862
|
-
let runForNextAction = latestRun;
|
|
2863
|
-
if (latestRun.continueRequestedAt &&
|
|
2864
|
-
!listGoalWorkers(props.cwd).some((worker) => worker.status === "running") &&
|
|
2865
|
-
activeVerifierRunIdsRef.current.size === 0) {
|
|
2866
|
-
await appendGoalDecision(props.cwd, latestRun.id, {
|
|
2867
|
-
kind: "continuation_consumed",
|
|
2868
|
-
reason: `Continuation request consumed by ${decision.kind}.`,
|
|
2869
|
-
});
|
|
2870
|
-
runForNextAction = await upsertGoalRun(props.cwd, {
|
|
2871
|
-
...latestRun,
|
|
2872
|
-
continueRequestedAt: undefined,
|
|
2873
|
-
});
|
|
2874
|
-
}
|
|
2875
|
-
appendGoalProgress({
|
|
2876
|
-
kind: "goal_progress",
|
|
2877
|
-
phase: "continuing",
|
|
2878
|
-
title: `Choosing next Goal step: ${latestRun.title}`,
|
|
2879
|
-
detail: "Latest result is recorded; starting the next worker task or verifier automatically.",
|
|
2880
|
-
status: latestRun.status,
|
|
2881
|
-
});
|
|
2882
|
-
upsertGoalStatusEntry({
|
|
2883
|
-
runId: latestRun.id,
|
|
2884
|
-
label: latestRun.title,
|
|
2885
|
-
phase: "orchestrating",
|
|
2886
|
-
startedAt: Date.now(),
|
|
2887
|
-
detail: "choosing next step",
|
|
2888
|
-
});
|
|
2889
|
-
startGoalRunRef.current(runForNextAction);
|
|
2890
|
-
})().catch((err) => {
|
|
2891
|
-
runningGoalIdsRef.current.delete(runId);
|
|
2892
|
-
clearGoalStatusEntry(runId);
|
|
2893
|
-
log("ERROR", "goal", err instanceof Error ? err.message : String(err));
|
|
2894
|
-
setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
|
|
2895
|
-
});
|
|
2896
|
-
}, [appendGoalProgress, clearGoalStatusEntry, props.cwd, upsertGoalStatusEntry]);
|
|
2897
|
-
const handleGoalWorkerComplete = useCallback((run, completion) => {
|
|
2898
|
-
const taskTitle = run.tasks.find((task) => task.id === completion.worker.goalTaskId)?.title ??
|
|
2899
|
-
completion.worker.goalTaskId;
|
|
2900
|
-
const eventText = formatGoalWorkerCompletionEvent(run, taskTitle, completion);
|
|
2901
|
-
void summarizeGoalCounts(completion.worker.cwd).then((counts) => setGoalCount(counts.active));
|
|
2902
|
-
appendGoalProgress({
|
|
2903
|
-
kind: "goal_progress",
|
|
2904
|
-
phase: "worker_finished",
|
|
2905
|
-
title: formatGoalWorkerFinishedTitle(taskTitle, completion.status),
|
|
2906
|
-
detail: summarizeGoalCompletion(completion.summary),
|
|
2907
|
-
workerId: completion.worker.id,
|
|
2908
|
-
status: completion.status,
|
|
2909
|
-
});
|
|
2910
|
-
upsertGoalStatusEntry({
|
|
2911
|
-
runId: run.id,
|
|
2912
|
-
label: taskTitle,
|
|
2913
|
-
phase: completion.status === "done" ? "reviewing" : "failed",
|
|
2914
|
-
startedAt: Date.now(),
|
|
2915
|
-
detail: completion.status === "done" ? "reviewing result" : "task failed",
|
|
2916
|
-
workerId: completion.worker.id,
|
|
2917
|
-
goalNumber: goalNumberForRun(run.id),
|
|
2918
|
-
});
|
|
2919
|
-
runGoalSyntheticEvent(eventText);
|
|
2920
|
-
void (async () => {
|
|
2921
|
-
if (listGoalWorkers(completion.worker.cwd).some((worker) => worker.status === "running"))
|
|
2922
|
-
return;
|
|
2923
|
-
if (activeVerifierRunIdsRef.current.size > 0)
|
|
2924
|
-
return;
|
|
2925
|
-
const runs = await loadGoalRuns(completion.worker.cwd);
|
|
2926
|
-
const queued = runs.find((item) => item.continueRequestedAt && !goalHasBlockingPrerequisites(item));
|
|
2927
|
-
if (queued)
|
|
2928
|
-
setTimeout(() => continueGoalRun(queued.id), 750);
|
|
2929
|
-
})().catch((err) => log("ERROR", "goal", err instanceof Error ? err.message : String(err)));
|
|
2930
|
-
}, [
|
|
2931
|
-
appendGoalProgress,
|
|
2932
|
-
continueGoalRun,
|
|
2933
|
-
goalNumberForRun,
|
|
2934
|
-
runGoalSyntheticEvent,
|
|
2935
|
-
upsertGoalStatusEntry,
|
|
2936
|
-
]);
|
|
2937
|
-
useEffect(() => {
|
|
2938
|
-
return subscribeGoalWorkerCompletions((completion) => {
|
|
2939
|
-
void (async () => {
|
|
2940
|
-
const latestRun = (await loadGoalRuns(completion.worker.cwd)).find((item) => item.id === completion.worker.goalRunId) ?? null;
|
|
2941
|
-
if (!latestRun) {
|
|
2942
|
-
log("WARN", "goal", `Worker completion for unknown Goal ${completion.worker.goalRunId}`);
|
|
2943
|
-
return;
|
|
2944
|
-
}
|
|
2945
|
-
runningGoalIdsRef.current.add(latestRun.id);
|
|
2946
|
-
handleGoalWorkerComplete(latestRun, completion);
|
|
2947
|
-
})().catch((err) => {
|
|
2948
|
-
log("ERROR", "goal", err instanceof Error ? err.message : String(err));
|
|
2949
|
-
setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
|
|
2950
|
-
});
|
|
2951
|
-
}, props.cwd);
|
|
2952
|
-
}, [handleGoalWorkerComplete, props.cwd]);
|
|
2953
|
-
const startGoalRun = useCallback((run) => {
|
|
2954
|
-
runningGoalIdsRef.current.add(run.id);
|
|
2955
|
-
void (async () => {
|
|
2956
|
-
if (goalHasBlockingPrerequisites(run)) {
|
|
2957
|
-
setOverlay(null);
|
|
2958
|
-
const detail = formatGoalBlockingPrerequisites(run);
|
|
2959
|
-
await upsertGoalRun(props.cwd, {
|
|
2960
|
-
...run,
|
|
2961
|
-
status: "blocked",
|
|
2962
|
-
blockers: Array.from(new Set([...run.blockers, detail])),
|
|
2963
|
-
});
|
|
2964
|
-
setGoalCount((await summarizeGoalCounts(props.cwd)).active);
|
|
2965
|
-
appendGoalProgress({
|
|
2966
|
-
kind: "goal_progress",
|
|
2967
|
-
phase: "terminal",
|
|
2968
|
-
title: `Goal blocked: ${run.title}`,
|
|
2969
|
-
detail,
|
|
2970
|
-
status: "blocked",
|
|
2971
|
-
});
|
|
2972
|
-
runningGoalIdsRef.current.delete(run.id);
|
|
2973
|
-
clearGoalStatusEntry(run.id);
|
|
2974
|
-
return;
|
|
2975
|
-
}
|
|
2976
|
-
const decision = decideGoalNextAction(run);
|
|
2977
|
-
await appendGoalDecision(props.cwd, run.id, decision);
|
|
2978
|
-
if (decision.kind === "terminal") {
|
|
2979
|
-
const terminalProgress = formatGoalTerminalProgress(run);
|
|
2980
|
-
if (terminalProgress)
|
|
2981
|
-
appendGoalProgress(terminalProgress);
|
|
2982
|
-
runningGoalIdsRef.current.delete(run.id);
|
|
2983
|
-
clearGoalStatusEntry(run.id);
|
|
2984
|
-
return;
|
|
2985
|
-
}
|
|
2986
|
-
if (decision.kind === "wait") {
|
|
2987
|
-
appendGoalProgress({
|
|
2988
|
-
kind: "goal_progress",
|
|
2989
|
-
phase: "worker_started",
|
|
2990
|
-
title: decision.workerId ? `Goal working: ${run.title}` : `Goal active: ${run.title}`,
|
|
2991
|
-
detail: decision.reason,
|
|
2992
|
-
workerId: decision.workerId,
|
|
2993
|
-
});
|
|
2793
|
+
const checked = await runGoalPrerequisiteChecks(props.cwd, run);
|
|
2794
|
+
const current = await upsertGoalRun(props.cwd, checked.run);
|
|
2795
|
+
if (goalHasBlockingPrerequisites(current)) {
|
|
2994
2796
|
upsertGoalStatusEntry({
|
|
2995
|
-
runId:
|
|
2996
|
-
label:
|
|
2997
|
-
phase:
|
|
2797
|
+
runId: current.id,
|
|
2798
|
+
label: current.title,
|
|
2799
|
+
phase: "failed",
|
|
2800
|
+
detail: formatGoalBlockingPrerequisites(current),
|
|
2998
2801
|
startedAt: Date.now(),
|
|
2999
|
-
detail: decision.reason,
|
|
3000
|
-
workerId: decision.workerId,
|
|
3001
|
-
goalNumber: goalNumberForRun(run.id),
|
|
3002
|
-
});
|
|
3003
|
-
return;
|
|
3004
|
-
}
|
|
3005
|
-
if (decision.kind === "complete") {
|
|
3006
|
-
await upsertGoalRun(props.cwd, { ...run, status: "passed" });
|
|
3007
|
-
setGoalCount((await summarizeGoalCounts(props.cwd)).active);
|
|
3008
|
-
appendGoalProgress({
|
|
3009
|
-
kind: "goal_progress",
|
|
3010
|
-
phase: "terminal",
|
|
3011
|
-
title: `Goal passed: ${run.title}`,
|
|
3012
|
-
detail: decision.reason,
|
|
3013
|
-
status: "passed",
|
|
3014
|
-
});
|
|
3015
|
-
runningGoalIdsRef.current.delete(run.id);
|
|
3016
|
-
clearGoalStatusEntry(run.id);
|
|
3017
|
-
return;
|
|
3018
|
-
}
|
|
3019
|
-
if (decision.kind === "run_verifier") {
|
|
3020
|
-
await verifyGoalRun(run);
|
|
3021
|
-
return;
|
|
3022
|
-
}
|
|
3023
|
-
if (decision.kind === "create_task") {
|
|
3024
|
-
await updateGoalTask(props.cwd, run.id, `auto-${Date.now()}`, {
|
|
3025
|
-
title: decision.title,
|
|
3026
|
-
prompt: decision.prompt,
|
|
3027
|
-
status: "pending",
|
|
3028
|
-
});
|
|
3029
|
-
const latestRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id) ?? run;
|
|
3030
|
-
await upsertGoalRun(props.cwd, { ...latestRun, status: "ready" });
|
|
3031
|
-
setTimeout(() => continueGoalRun(run.id), 250);
|
|
3032
|
-
return;
|
|
3033
|
-
}
|
|
3034
|
-
if (decision.kind === "blocked") {
|
|
3035
|
-
await upsertGoalRun(props.cwd, {
|
|
3036
|
-
...run,
|
|
3037
|
-
status: "blocked",
|
|
3038
|
-
blockers: [...run.blockers, decision.reason],
|
|
3039
2802
|
});
|
|
3040
|
-
setGoalCount((await summarizeGoalCounts(props.cwd)).active);
|
|
3041
2803
|
appendGoalProgress({
|
|
3042
2804
|
kind: "goal_progress",
|
|
3043
2805
|
phase: "terminal",
|
|
3044
|
-
title: `Goal blocked: ${
|
|
3045
|
-
detail:
|
|
2806
|
+
title: `Goal blocked: ${current.title}`,
|
|
2807
|
+
detail: formatGoalBlockingPrerequisites(current),
|
|
2808
|
+
summaryRows: buildGoalSummaryRows(current),
|
|
3046
2809
|
status: "blocked",
|
|
3047
2810
|
});
|
|
3048
|
-
runningGoalIdsRef.current.delete(run.id);
|
|
3049
|
-
clearGoalStatusEntry(run.id);
|
|
3050
|
-
return;
|
|
3051
|
-
}
|
|
3052
|
-
if (decision.kind === "pause") {
|
|
3053
|
-
await updateGoalTask(props.cwd, run.id, decision.task.id, {
|
|
3054
|
-
status: "blocked",
|
|
3055
|
-
attempts: decision.attempts,
|
|
3056
|
-
lastSummary: "Paused after worker attempt limit.",
|
|
3057
|
-
});
|
|
3058
|
-
await upsertGoalRun(props.cwd, {
|
|
3059
|
-
...run,
|
|
3060
|
-
status: "paused",
|
|
3061
|
-
blockers: [...run.blockers, decision.reason],
|
|
3062
|
-
});
|
|
3063
|
-
await appendGoalEvidence(props.cwd, run.id, {
|
|
3064
|
-
kind: "summary",
|
|
3065
|
-
label: "Goal paused",
|
|
3066
|
-
content: decision.reason,
|
|
3067
|
-
});
|
|
3068
|
-
setGoalCount((await summarizeGoalCounts(props.cwd)).active);
|
|
3069
|
-
appendGoalProgress({
|
|
3070
|
-
kind: "goal_progress",
|
|
3071
|
-
phase: "terminal",
|
|
3072
|
-
title: `Goal paused: ${run.title}`,
|
|
3073
|
-
detail: decision.reason,
|
|
3074
|
-
status: "paused",
|
|
3075
|
-
});
|
|
3076
|
-
runningGoalIdsRef.current.delete(run.id);
|
|
3077
|
-
clearGoalStatusEntry(run.id);
|
|
3078
2811
|
return;
|
|
3079
2812
|
}
|
|
3080
|
-
await
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
model: currentModel,
|
|
3085
|
-
goalRunId: run.id,
|
|
3086
|
-
goalTaskId: decision.task.id,
|
|
3087
|
-
taskTitle: decision.task.title,
|
|
3088
|
-
prompt: decision.task.prompt,
|
|
2813
|
+
const resumed = await upsertGoalRun(props.cwd, {
|
|
2814
|
+
...current,
|
|
2815
|
+
status: current.status === "draft" ? "ready" : current.status,
|
|
2816
|
+
continueRequestedAt: new Date().toISOString(),
|
|
3089
2817
|
});
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
2818
|
+
setGoalAutoExpand(true);
|
|
2819
|
+
upsertGoalStatusEntry({
|
|
2820
|
+
runId: resumed.id,
|
|
2821
|
+
label: resumed.title,
|
|
2822
|
+
phase: "orchestrating",
|
|
2823
|
+
detail: "Choosing next Goal action",
|
|
2824
|
+
startedAt: Date.now(),
|
|
3095
2825
|
});
|
|
3096
|
-
setOverlay(null);
|
|
3097
|
-
setGoalCount((await summarizeGoalCounts(props.cwd)).active);
|
|
3098
2826
|
appendGoalProgress({
|
|
3099
2827
|
kind: "goal_progress",
|
|
3100
|
-
phase: "
|
|
3101
|
-
title: `
|
|
3102
|
-
detail:
|
|
3103
|
-
|
|
3104
|
-
status: worker.status,
|
|
3105
|
-
});
|
|
3106
|
-
upsertGoalStatusEntry({
|
|
3107
|
-
runId: run.id,
|
|
3108
|
-
label: decision.task.title,
|
|
3109
|
-
phase: "worker",
|
|
3110
|
-
startedAt: Date.now(),
|
|
3111
|
-
detail: "background worker running",
|
|
3112
|
-
workerId: worker.id,
|
|
3113
|
-
goalNumber: goalNumberForRun(run.id),
|
|
2828
|
+
phase: "orchestrator_reviewing",
|
|
2829
|
+
title: `Goal ${goalNumberForRun(resumed.id)} continuing`,
|
|
2830
|
+
detail: resumed.title,
|
|
2831
|
+
status: resumed.status,
|
|
3114
2832
|
});
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
}, [
|
|
3121
|
-
props.cwd,
|
|
3122
|
-
currentProvider,
|
|
3123
|
-
currentModel,
|
|
3124
|
-
appendGoalProgress,
|
|
3125
|
-
clearGoalStatusEntry,
|
|
3126
|
-
goalNumberForRun,
|
|
3127
|
-
upsertGoalStatusEntry,
|
|
3128
|
-
]);
|
|
3129
|
-
const verifyGoalRun = useCallback(async (run) => {
|
|
2833
|
+
continueGoalRun(resumed);
|
|
2834
|
+
})();
|
|
2835
|
+
};
|
|
2836
|
+
startGoalRunRef.current = handleRunGoal;
|
|
2837
|
+
const handleVerifyGoal = (run) => {
|
|
3130
2838
|
if (!run.verifier?.command) {
|
|
3131
|
-
await appendGoalEvidence(props.cwd, run.id, {
|
|
3132
|
-
kind: "summary",
|
|
3133
|
-
label: "Missing verifier",
|
|
3134
|
-
content: "No verifier command is configured.",
|
|
3135
|
-
});
|
|
3136
|
-
await upsertGoalRun(props.cwd, {
|
|
3137
|
-
...run,
|
|
3138
|
-
status: "blocked",
|
|
3139
|
-
blockers: [...run.blockers, "No verifier command configured."],
|
|
3140
|
-
});
|
|
3141
2839
|
appendGoalProgress({
|
|
3142
2840
|
kind: "goal_progress",
|
|
3143
2841
|
phase: "terminal",
|
|
3144
|
-
title: `Goal
|
|
3145
|
-
detail: "
|
|
2842
|
+
title: `Goal verifier missing: ${run.title}`,
|
|
2843
|
+
detail: "Define a verifier command before running verification.",
|
|
3146
2844
|
status: "blocked",
|
|
3147
2845
|
});
|
|
3148
|
-
runningGoalIdsRef.current.delete(run.id);
|
|
3149
|
-
clearGoalStatusEntry(run.id);
|
|
3150
2846
|
return;
|
|
3151
2847
|
}
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
kind: "goal_progress",
|
|
3160
|
-
phase: "verifier_started",
|
|
3161
|
-
title: `Verifier started: ${run.title}`,
|
|
3162
|
-
detail: run.verifier.command,
|
|
3163
|
-
status: "verifying",
|
|
3164
|
-
});
|
|
3165
|
-
const startedAt = Date.now();
|
|
3166
|
-
const verifierTimeoutMs = Number(process.env.GG_GOAL_VERIFIER_TIMEOUT_MS ?? 10 * 60 * 1000);
|
|
3167
|
-
upsertGoalStatusEntry({
|
|
3168
|
-
runId: run.id,
|
|
3169
|
-
label: run.title,
|
|
3170
|
-
phase: "verifier",
|
|
3171
|
-
startedAt,
|
|
3172
|
-
detail: run.verifier.command,
|
|
3173
|
-
goalNumber: goalNumberForRun(run.id),
|
|
3174
|
-
});
|
|
3175
|
-
void runGoalVerifierCommand({
|
|
3176
|
-
cwd: props.cwd,
|
|
3177
|
-
runId: run.id,
|
|
3178
|
-
command: run.verifier.command,
|
|
3179
|
-
timeoutMs: verifierTimeoutMs,
|
|
3180
|
-
now: () => startedAt,
|
|
3181
|
-
})
|
|
3182
|
-
.then(async ({ verification, failureClass, durationMs }) => {
|
|
3183
|
-
activeVerifierRunIdsRef.current.delete(run.id);
|
|
3184
|
-
const status = verification.status;
|
|
3185
|
-
const summary = verification.summary;
|
|
3186
|
-
const outputPath = verification.outputPath;
|
|
3187
|
-
const latestRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id) ?? run;
|
|
3188
|
-
const runWithVerifier = {
|
|
3189
|
-
...latestRun,
|
|
2848
|
+
void (async () => {
|
|
2849
|
+
const verifying = await upsertGoalRun(props.cwd, {
|
|
2850
|
+
...run,
|
|
2851
|
+
status: "ready",
|
|
2852
|
+
tasks: run.tasks.map((task) => task.status === "pending" || task.status === "failed"
|
|
2853
|
+
? { ...task, status: "done" }
|
|
2854
|
+
: task),
|
|
3190
2855
|
verifier: {
|
|
3191
|
-
...
|
|
3192
|
-
|
|
3193
|
-
command: run.verifier?.command,
|
|
3194
|
-
lastResult: verification,
|
|
2856
|
+
...run.verifier,
|
|
2857
|
+
lastResult: undefined,
|
|
3195
2858
|
},
|
|
3196
|
-
};
|
|
3197
|
-
const completionCheck = canCompleteGoalRun(runWithVerifier);
|
|
3198
|
-
const verifiedRun = await upsertGoalRun(props.cwd, {
|
|
3199
|
-
...runWithVerifier,
|
|
3200
|
-
continueRequestedAt: undefined,
|
|
3201
|
-
status: status === "pass" && completionCheck.ok
|
|
3202
|
-
? "passed"
|
|
3203
|
-
: status === "pass"
|
|
3204
|
-
? "ready"
|
|
3205
|
-
: "failed",
|
|
3206
|
-
});
|
|
3207
|
-
await appendGoalEvidence(props.cwd, run.id, {
|
|
3208
|
-
kind: "command",
|
|
3209
|
-
label: `Verifier ${status}`,
|
|
3210
|
-
content: `${failureClass}: ${summary}`.slice(0, 4000),
|
|
3211
|
-
path: outputPath,
|
|
3212
|
-
});
|
|
3213
|
-
await appendGoalDecision(props.cwd, run.id, {
|
|
3214
|
-
kind: `verifier_${status}`,
|
|
3215
|
-
reason: `${failureClass}: verifier exited with code ${verification.exitCode ?? 1}.`,
|
|
3216
|
-
content: `outputPath=${outputPath ?? ""}; durationMs=${durationMs}`,
|
|
3217
2859
|
});
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
});
|
|
3226
|
-
const runWithPendingFix = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id) ?? latestRun;
|
|
3227
|
-
await upsertGoalRun(props.cwd, { ...runWithPendingFix, status: "ready" });
|
|
2860
|
+
continueGoalRun(verifying);
|
|
2861
|
+
})();
|
|
2862
|
+
};
|
|
2863
|
+
const handlePauseGoal = (run) => {
|
|
2864
|
+
void (async () => {
|
|
2865
|
+
for (const worker of listGoalWorkers(props.cwd).filter((item) => item.goalRunId === run.id)) {
|
|
2866
|
+
await stopGoalWorker(worker.id);
|
|
3228
2867
|
}
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
title: `Verifier ${status}: ${run.title}`,
|
|
3234
|
-
detail: summarizeGoalCompletion(summary),
|
|
3235
|
-
status,
|
|
3236
|
-
});
|
|
3237
|
-
upsertGoalStatusEntry({
|
|
3238
|
-
runId: run.id,
|
|
3239
|
-
label: run.title,
|
|
3240
|
-
phase: status === "pass" ? "reviewing" : "failed",
|
|
3241
|
-
startedAt: Date.now(),
|
|
3242
|
-
detail: status === "pass" ? "reviewing verifier evidence" : "verifier failed",
|
|
3243
|
-
goalNumber: goalNumberForRun(run.id),
|
|
2868
|
+
const paused = await upsertGoalRun(props.cwd, {
|
|
2869
|
+
...run,
|
|
2870
|
+
status: "paused",
|
|
2871
|
+
blockers: [...run.blockers, "Paused by user from Goal pane."],
|
|
3244
2872
|
});
|
|
3245
|
-
const eventText = formatGoalVerifierCompletionEvent(verifiedRun, status === "pass" ? "pass" : "fail", run.verifier?.command ?? "", verification.exitCode ?? 1, summary);
|
|
3246
|
-
runGoalSyntheticEvent(eventText);
|
|
3247
|
-
const continuationRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id);
|
|
3248
|
-
if (continuationRun?.continueRequestedAt && status === "pass") {
|
|
3249
|
-
setTimeout(() => continueGoalRun(run.id), 500);
|
|
3250
|
-
}
|
|
3251
|
-
})
|
|
3252
|
-
.catch((err) => {
|
|
3253
|
-
activeVerifierRunIdsRef.current.delete(run.id);
|
|
3254
2873
|
clearGoalStatusEntry(run.id);
|
|
3255
|
-
log("ERROR", "goal", err instanceof Error ? err.message : String(err));
|
|
3256
|
-
setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal verifier")]);
|
|
3257
|
-
});
|
|
3258
|
-
}, [
|
|
3259
|
-
props.cwd,
|
|
3260
|
-
appendGoalProgress,
|
|
3261
|
-
clearGoalStatusEntry,
|
|
3262
|
-
goalNumberForRun,
|
|
3263
|
-
runGoalSyntheticEvent,
|
|
3264
|
-
upsertGoalStatusEntry,
|
|
3265
|
-
]);
|
|
3266
|
-
const pauseGoalRun = useCallback((run) => {
|
|
3267
|
-
void (async () => {
|
|
3268
|
-
runningGoalIdsRef.current.delete(run.id);
|
|
3269
|
-
if (run.activeWorkerId)
|
|
3270
|
-
await stopGoalWorker(run.activeWorkerId);
|
|
3271
|
-
await upsertGoalRun(props.cwd, { ...run, status: "paused", activeWorkerId: undefined });
|
|
3272
|
-
setGoalCount((await summarizeGoalCounts(props.cwd)).active);
|
|
3273
2874
|
appendGoalProgress({
|
|
3274
2875
|
kind: "goal_progress",
|
|
3275
2876
|
phase: "terminal",
|
|
3276
|
-
title: `Goal paused: ${
|
|
3277
|
-
detail: "
|
|
2877
|
+
title: `Goal paused: ${paused.title}`,
|
|
2878
|
+
detail: "Paused by user from Goal pane.",
|
|
2879
|
+
summaryRows: buildGoalSummaryRows(paused),
|
|
3278
2880
|
status: "paused",
|
|
3279
2881
|
});
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
2882
|
+
})();
|
|
2883
|
+
};
|
|
2884
|
+
const handleRefineGoal = (run, feedback) => {
|
|
2885
|
+
setOverlay(null);
|
|
2886
|
+
setGoalAutoExpand(false);
|
|
2887
|
+
setDoneStatus(null);
|
|
2888
|
+
appendGoalAgentTransition(`Goal refinement requested for ${run.title}`);
|
|
2889
|
+
void agentLoop
|
|
2890
|
+
.run(`Refine Goal run ${run.id} (${run.title}) using this user feedback:\n\n${feedback}\n\nUpdate the durable Goal plan with the goals tool. Do not start workers until the user approves/runs the Goal.`)
|
|
2891
|
+
.catch((err) => setLiveItems((prev) => [...prev, toErrorItem(err, getId())]));
|
|
2892
|
+
};
|
|
2893
|
+
const handleClosePlanOverlay = () => {
|
|
2894
|
+
planOverlayPendingRef.current = false;
|
|
2895
|
+
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
2896
|
+
props.sessionStore.overlay = null;
|
|
2897
|
+
props.sessionStore.planAutoExpand = false;
|
|
2898
|
+
props.resetUI();
|
|
2899
|
+
return;
|
|
2900
|
+
}
|
|
3291
2901
|
if (props.sessionStore) {
|
|
3292
|
-
props.sessionStore.
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
2902
|
+
props.sessionStore.overlay = null;
|
|
2903
|
+
props.sessionStore.planAutoExpand = false;
|
|
2904
|
+
if (agentLoop.isRunning)
|
|
2905
|
+
props.sessionStore.pendingResetUI = true;
|
|
3296
2906
|
}
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
2907
|
+
setPlanAutoExpand(false);
|
|
2908
|
+
setOverlay(null);
|
|
2909
|
+
};
|
|
2910
|
+
const handlePixelFixOne = (entry) => {
|
|
2911
|
+
setOverlay(null);
|
|
2912
|
+
startPixelFix(entry.errorId);
|
|
2913
|
+
};
|
|
2914
|
+
const handlePixelFixAll = (entries) => {
|
|
2915
|
+
const first = entries.find((entry) => entry.status === "open") ?? entries[0];
|
|
2916
|
+
if (!first)
|
|
2917
|
+
return;
|
|
2918
|
+
setOverlay(null);
|
|
2919
|
+
setRunAllPixel(true);
|
|
2920
|
+
startPixelFix(first.errorId);
|
|
2921
|
+
};
|
|
2922
|
+
const handleApprovePlan = (planPath) => {
|
|
2923
|
+
log("INFO", "plan", "Plan approved — transitioning to implementation", {
|
|
2924
|
+
planPath,
|
|
2925
|
+
});
|
|
2926
|
+
planOverlayPendingRef.current = false;
|
|
3302
2927
|
void (async () => {
|
|
3303
2928
|
try {
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
//
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
//
|
|
3313
|
-
//
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
log("WARN", "pixel", `chdir failed: ${err.message}`);
|
|
2929
|
+
// Read plan steps for progress tracking — handed to the new
|
|
2930
|
+
// mount via sessionStore.planSteps below.
|
|
2931
|
+
const planContent = await import("node:fs/promises").then(({ readFile }) => readFile(planPath, "utf-8"));
|
|
2932
|
+
const steps = extractPlanSteps(planContent);
|
|
2933
|
+
// Build the new system prompt with the approved plan baked in.
|
|
2934
|
+
const newPrompt = await rebuildSystemPrompt({
|
|
2935
|
+
approvedPlanPath: planPath,
|
|
2936
|
+
});
|
|
2937
|
+
// Create a new session file BEFORE remount so the new tree
|
|
2938
|
+
// picks it up via sessionStore.sessionPath.
|
|
2939
|
+
let newSessionPath;
|
|
2940
|
+
const sm = sessionManagerRef.current;
|
|
2941
|
+
if (sm) {
|
|
2942
|
+
const s = await sm.create(props.cwd, currentProvider, currentModel);
|
|
2943
|
+
newSessionPath = s.path;
|
|
3320
2944
|
}
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
2945
|
+
if (props.resetUI && props.sessionStore) {
|
|
2946
|
+
// Clear the overlay so the new mount lands on the chat,
|
|
2947
|
+
// not back inside the plan pane.
|
|
2948
|
+
props.sessionStore.overlay = null;
|
|
2949
|
+
props.sessionStore.planAutoExpand = false;
|
|
2950
|
+
props.resetUI({
|
|
2951
|
+
wipeSession: true,
|
|
2952
|
+
messages: [{ role: "system", content: newPrompt }],
|
|
2953
|
+
approvedPlanPath: planPath,
|
|
2954
|
+
planSteps: steps,
|
|
2955
|
+
sessionPath: newSessionPath,
|
|
2956
|
+
pendingAction: {
|
|
2957
|
+
prompt: "The plan has been approved. Implement it now, following each step in order.",
|
|
2958
|
+
planEvent: { event: "approved" },
|
|
2959
|
+
},
|
|
2960
|
+
});
|
|
2961
|
+
return;
|
|
3335
2962
|
}
|
|
3336
|
-
//
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
setupHintShownRef.current = false;
|
|
3342
|
-
const detectedForPixelFix = detectLanguages(prep.projectPath);
|
|
3343
|
-
injectedLanguagesRef.current = detectedForPixelFix;
|
|
3344
|
-
const newSystemPrompt = await rebuildSystemPrompt({
|
|
3345
|
-
cwd: prep.projectPath,
|
|
3346
|
-
planMode: false,
|
|
3347
|
-
clearApprovedPlan: true,
|
|
3348
|
-
activeLanguages: detectedForPixelFix,
|
|
3349
|
-
tools: toolsForPixelFix,
|
|
3350
|
-
});
|
|
3351
|
-
// Now that the cwd swap is committed, reset chat. Do not clear the
|
|
3352
|
-
// terminal here; terminal clear sequences can erase saved scrollback.
|
|
2963
|
+
// Fallback path (resetUI not wired — tests). Mutate in place.
|
|
2964
|
+
approvedPlanPathRef.current = planPath;
|
|
2965
|
+
planStepsRef.current = steps;
|
|
2966
|
+
setPlanSteps(steps);
|
|
2967
|
+
clearPendingHistory();
|
|
3353
2968
|
setHistory([{ kind: "banner", id: "banner" }]);
|
|
3354
2969
|
setLiveItems([]);
|
|
3355
|
-
|
|
2970
|
+
setPlanAutoExpand(false);
|
|
2971
|
+
setOverlay(null);
|
|
2972
|
+
messagesRef.current = [{ role: "system", content: newPrompt }];
|
|
3356
2973
|
agentLoop.reset();
|
|
3357
2974
|
persistedIndexRef.current = messagesRef.current.length;
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
}
|
|
3368
|
-
else {
|
|
3369
|
-
messagesRef.current.unshift({ role: "system", content: newSystemPrompt });
|
|
3370
|
-
}
|
|
3371
|
-
const title = `Fix ${errorId.slice(0, 12)}… in ${prep.projectName}`;
|
|
3372
|
-
const taskItem = { kind: "task", title, id: getId() };
|
|
3373
|
-
setLastUserMessage(title);
|
|
2975
|
+
if (newSessionPath)
|
|
2976
|
+
sessionPathRef.current = newSessionPath;
|
|
2977
|
+
setLiveItems([
|
|
2978
|
+
{
|
|
2979
|
+
kind: "info",
|
|
2980
|
+
text: "Plan approved — starting fresh session for implementation",
|
|
2981
|
+
id: getId(),
|
|
2982
|
+
},
|
|
2983
|
+
]);
|
|
3374
2984
|
setDoneStatus(null);
|
|
3375
|
-
|
|
3376
|
-
await agentLoop.run(prep.prompt);
|
|
2985
|
+
await agentLoop.run("The plan has been approved. Implement it now, following each step in order.");
|
|
3377
2986
|
}
|
|
3378
2987
|
catch (err) {
|
|
3379
|
-
const
|
|
3380
|
-
log("ERROR", "
|
|
3381
|
-
currentPixelFixRef.current = null;
|
|
3382
|
-
setRunAllPixel(false);
|
|
2988
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
2989
|
+
log("ERROR", "error", errMsg);
|
|
3383
2990
|
setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
|
|
3384
2991
|
}
|
|
3385
2992
|
})();
|
|
3386
|
-
}
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
if (agentLoop.isRunning)
|
|
3464
|
-
props.sessionStore.pendingResetUI = true;
|
|
3465
|
-
}
|
|
3466
|
-
setOverlay(null);
|
|
3467
|
-
}
|
|
3468
|
-
}, onFixOne: (entry) => {
|
|
3469
|
-
setOverlay(null);
|
|
3470
|
-
startPixelFix(entry.errorId);
|
|
3471
|
-
}, onFixAll: (entries) => {
|
|
3472
|
-
const first = entries.find((e) => e.status === "open") ?? entries[0];
|
|
3473
|
-
if (!first)
|
|
3474
|
-
return;
|
|
3475
|
-
setOverlay(null);
|
|
3476
|
-
setRunAllPixel(true);
|
|
3477
|
-
startPixelFix(first.errorId);
|
|
3478
|
-
} })) : isSkillsView ? (_jsx(SkillsOverlay, { cwd: props.cwd, onClose: () => {
|
|
3479
|
-
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
3480
|
-
props.sessionStore.overlay = null;
|
|
3481
|
-
props.resetUI();
|
|
3482
|
-
}
|
|
3483
|
-
else {
|
|
3484
|
-
if (props.sessionStore) {
|
|
3485
|
-
props.sessionStore.overlay = null;
|
|
3486
|
-
if (agentLoop.isRunning)
|
|
3487
|
-
props.sessionStore.pendingResetUI = true;
|
|
3488
|
-
}
|
|
3489
|
-
setOverlay(null);
|
|
3490
|
-
}
|
|
3491
|
-
} })) : isEyesView ? (_jsx(EyesOverlay, { cwd: props.cwd, onClose: () => {
|
|
3492
|
-
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
3493
|
-
props.sessionStore.overlay = null;
|
|
3494
|
-
props.resetUI();
|
|
3495
|
-
}
|
|
3496
|
-
else {
|
|
3497
|
-
if (props.sessionStore) {
|
|
3498
|
-
props.sessionStore.overlay = null;
|
|
3499
|
-
if (agentLoop.isRunning)
|
|
3500
|
-
props.sessionStore.pendingResetUI = true;
|
|
3501
|
-
}
|
|
3502
|
-
setEyesCount(isEyesActive(props.cwd) ? journalCount({ status: "open" }, props.cwd) : undefined);
|
|
3503
|
-
setOverlay(null);
|
|
3504
|
-
}
|
|
3505
|
-
}, onQueueMessage: (msg) => {
|
|
3506
|
-
agentLoop.queueMessage(msg);
|
|
3507
|
-
} })) : isPlanView ? (_jsx(PlanOverlay, { cwd: props.cwd, autoExpandNewest: planAutoExpand, onClose: () => {
|
|
3508
|
-
planOverlayPendingRef.current = false;
|
|
3509
|
-
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
3510
|
-
props.sessionStore.overlay = null;
|
|
3511
|
-
props.sessionStore.planAutoExpand = false;
|
|
3512
|
-
props.resetUI();
|
|
3513
|
-
}
|
|
3514
|
-
else {
|
|
3515
|
-
if (props.sessionStore) {
|
|
3516
|
-
props.sessionStore.overlay = null;
|
|
3517
|
-
props.sessionStore.planAutoExpand = false;
|
|
3518
|
-
if (agentLoop.isRunning)
|
|
3519
|
-
props.sessionStore.pendingResetUI = true;
|
|
3520
|
-
}
|
|
3521
|
-
setPlanAutoExpand(false);
|
|
3522
|
-
setOverlay(null);
|
|
3523
|
-
}
|
|
3524
|
-
}, onApprove: (planPath) => {
|
|
3525
|
-
log("INFO", "plan", "Plan approved — transitioning to implementation", {
|
|
3526
|
-
planPath,
|
|
3527
|
-
});
|
|
3528
|
-
planOverlayPendingRef.current = false;
|
|
3529
|
-
void (async () => {
|
|
3530
|
-
try {
|
|
3531
|
-
// Read plan steps for progress tracking — handed to the new
|
|
3532
|
-
// mount via sessionStore.planSteps below.
|
|
3533
|
-
const planContent = await import("node:fs/promises").then(({ readFile }) => readFile(planPath, "utf-8"));
|
|
3534
|
-
const steps = extractPlanSteps(planContent);
|
|
3535
|
-
// Build the new system prompt with the approved plan baked in.
|
|
3536
|
-
const newPrompt = await rebuildSystemPrompt({
|
|
3537
|
-
planMode: false,
|
|
3538
|
-
approvedPlanPath: planPath,
|
|
3539
|
-
});
|
|
3540
|
-
// Create a new session file BEFORE remount so the new tree
|
|
3541
|
-
// picks it up via sessionStore.sessionPath.
|
|
3542
|
-
let newSessionPath;
|
|
3543
|
-
const sm = sessionManagerRef.current;
|
|
3544
|
-
if (sm) {
|
|
3545
|
-
const s = await sm.create(props.cwd, currentProvider, currentModel);
|
|
3546
|
-
newSessionPath = s.path;
|
|
3547
|
-
}
|
|
3548
|
-
if (props.resetUI && props.sessionStore) {
|
|
3549
|
-
// Clear the overlay so the new mount lands on the chat,
|
|
3550
|
-
// not back inside the plan pane.
|
|
3551
|
-
props.sessionStore.overlay = null;
|
|
3552
|
-
props.sessionStore.planAutoExpand = false;
|
|
3553
|
-
props.resetUI({
|
|
3554
|
-
wipeSession: true,
|
|
3555
|
-
messages: [{ role: "system", content: newPrompt }],
|
|
3556
|
-
approvedPlanPath: planPath,
|
|
3557
|
-
planSteps: steps,
|
|
3558
|
-
sessionPath: newSessionPath,
|
|
3559
|
-
pendingAction: {
|
|
3560
|
-
prompt: "The plan has been approved. Implement it now, following each step in order.",
|
|
3561
|
-
planEvent: { event: "approved" },
|
|
3562
|
-
},
|
|
3563
|
-
});
|
|
3564
|
-
return;
|
|
3565
|
-
}
|
|
3566
|
-
// Fallback path (resetUI not wired — tests). Mutate in place.
|
|
3567
|
-
approvedPlanPathRef.current = planPath;
|
|
3568
|
-
planStepsRef.current = steps;
|
|
3569
|
-
setPlanSteps(steps);
|
|
3570
|
-
setHistory([{ kind: "banner", id: "banner" }]);
|
|
3571
|
-
setLiveItems([]);
|
|
3572
|
-
setPlanAutoExpand(false);
|
|
3573
|
-
setOverlay(null);
|
|
3574
|
-
messagesRef.current = [{ role: "system", content: newPrompt }];
|
|
3575
|
-
agentLoop.reset();
|
|
3576
|
-
persistedIndexRef.current = messagesRef.current.length;
|
|
3577
|
-
if (newSessionPath)
|
|
3578
|
-
sessionPathRef.current = newSessionPath;
|
|
3579
|
-
setLiveItems([
|
|
3580
|
-
{
|
|
3581
|
-
kind: "info",
|
|
3582
|
-
text: "Plan approved — starting fresh session for implementation",
|
|
3583
|
-
id: getId(),
|
|
3584
|
-
},
|
|
3585
|
-
]);
|
|
3586
|
-
setDoneStatus(null);
|
|
3587
|
-
await agentLoop.run("The plan has been approved. Implement it now, following each step in order.");
|
|
3588
|
-
}
|
|
3589
|
-
catch (err) {
|
|
3590
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
3591
|
-
log("ERROR", "error", errMsg);
|
|
3592
|
-
setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
|
|
3593
|
-
}
|
|
3594
|
-
})();
|
|
3595
|
-
}, onReject: (planPath, feedback) => {
|
|
3596
|
-
planOverlayPendingRef.current = false;
|
|
3597
|
-
const rejectionMsg = `The plan at ${planPath} was rejected.\n\nFeedback: ${feedback}\n\n` +
|
|
3598
|
-
`Please revise the plan based on this feedback.`;
|
|
3599
|
-
if (props.resetUI && props.sessionStore) {
|
|
3600
|
-
props.sessionStore.overlay = null;
|
|
3601
|
-
props.sessionStore.planAutoExpand = false;
|
|
3602
|
-
// No wipeSession — keep history, messages, plan mode etc. The
|
|
3603
|
-
// agent picks up the rejection mid-conversation.
|
|
3604
|
-
props.resetUI({
|
|
3605
|
-
pendingAction: {
|
|
3606
|
-
prompt: rejectionMsg,
|
|
3607
|
-
planEvent: { event: "rejected", detail: feedback },
|
|
3608
|
-
},
|
|
3609
|
-
});
|
|
3610
|
-
return;
|
|
3611
|
-
}
|
|
3612
|
-
setPlanAutoExpand(false);
|
|
3613
|
-
setOverlay(null);
|
|
3614
|
-
setDoneStatus(null);
|
|
3615
|
-
setLiveItems((prev) => [
|
|
3616
|
-
...prev,
|
|
3617
|
-
{ kind: "info", text: `Plan rejected — "${feedback}"`, id: getId() },
|
|
3618
|
-
]);
|
|
3619
|
-
void agentLoop.run(rejectionMsg).catch((err) => {
|
|
3620
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
3621
|
-
log("ERROR", "error", errMsg);
|
|
3622
|
-
setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
|
|
3623
|
-
});
|
|
3624
|
-
} })) : (_jsxs(_Fragment, { children: [_jsxs(Box, { flexDirection: "column", flexGrow: 1, paddingRight: 1, children: [liveItems.map((item) => renderItem(item)), _jsx(StreamingArea, { isRunning: agentLoop.isRunning, streamingText: agentLoop.streamingText, streamingThinking: agentLoop.streamingThinking, thinkingMs: agentLoop.thinkingMs, planMode: planMode })] }), agentLoop.isRunning && agentLoop.activityPhase !== "idle" ? (_jsx(Box, { marginTop: 1, borderStyle: "round", borderColor: agentLoop.activityPhase === "thinking" ? THINKING_BORDER_COLORS[0] : "transparent", paddingLeft: 1, paddingRight: 1, width: columns, children: _jsx(ActivityIndicator, { phase: agentLoop.activityPhase, elapsedMs: agentLoop.elapsedMs, runStartRef: agentLoop.runStartRef, thinkingMs: agentLoop.thinkingMs, isThinking: agentLoop.isThinking, thinkingEnabled: thinkingEnabled, tokenEstimate: agentLoop.streamedTokenEstimate, charCountRef: agentLoop.charCountRef, realTokensAccumRef: agentLoop.realTokensAccumRef, userMessage: lastUserMessage, activeToolNames: agentLoop.activeToolCalls.map((tc) => tc.name), planMode: planMode, retryInfo: agentLoop.retryInfo, planDone: planSteps.filter((s) => s.completed).length, planTotal: planSteps.length, staticDisplay: true }) })) : agentLoop.stallError ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: theme.warning, children: "⚠ API provider stream interrupted — retries exhausted." }), _jsx(Text, { color: theme.textDim, children: " Your conversation is preserved. Send a message to continue." })] })) : (doneStatus &&
|
|
3625
|
-
!agentLoop.isRunning && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.success, children: ["✻ ", doneStatus.verb, " ", formatDuration(doneStatus.durationMs)] }) }))), agentLoop.queuedCount > 0 && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.accent, children: ["⏳ ", agentLoop.queuedCount, " message", agentLoop.queuedCount > 1 ? "s" : "", " queued"] }) })), _jsx(InputArea, { onSubmit: handleSubmit, onAbort: handleAbort, disabled: agentLoop.isRunning, isActive: !taskBarFocused && !overlay, onDownAtEnd: handleFocusTaskBar, onShiftTab: handleToggleThinking, onToggleTasks: () => {
|
|
3626
|
-
// Just flip the overlay state — Ink's log-update handles the
|
|
3627
|
-
// live-area transition (chat input → TaskOverlay) natively, and
|
|
3628
|
-
// the chat history above stays in scrollback. When the overlay
|
|
3629
|
-
// closes, the history is still there (banner included).
|
|
3630
|
-
openOverlay("tasks");
|
|
3631
|
-
}, onToggleGoal: () => {
|
|
3632
|
-
openOverlay("goal");
|
|
3633
|
-
}, onToggleSkills: () => {
|
|
3634
|
-
openOverlay("skills");
|
|
3635
|
-
}, onTogglePixel: () => {
|
|
3636
|
-
openOverlay("pixel");
|
|
3637
|
-
}, onTogglePlanMode: () => {
|
|
3638
|
-
const next = !planMode;
|
|
3639
|
-
setPlanMode(next);
|
|
3640
|
-
log("INFO", "plan", `Plan mode ${next ? "enabled" : "disabled"}`);
|
|
3641
|
-
setLiveItems((items) => [
|
|
3642
|
-
...items,
|
|
3643
|
-
{
|
|
3644
|
-
kind: "plan_transition",
|
|
3645
|
-
text: next ? "Plan Mode Activated" : "Plan Mode Deactivated",
|
|
3646
|
-
active: next,
|
|
3647
|
-
id: getId(),
|
|
3648
|
-
},
|
|
3649
|
-
]);
|
|
3650
|
-
}, cwd: props.cwd, commands: allCommands, eyesCount: eyesCount }), overlay === "model" ? (_jsx(ModelSelector, { onSelect: handleModelSelect, onCancel: () => setOverlay(null), loggedInProviders: props.loggedInProviders ?? [currentProvider], currentModel: currentModel, currentProvider: currentProvider })) : overlay === "theme" ? (_jsx(ThemeSelector, { onSelect: handleThemeSelect, onCancel: () => setOverlay(null), currentTheme: theme.name })) : (_jsxs(_Fragment, { children: [_jsx(Footer, { model: currentModel, tokensIn: agentLoop.contextUsed, contextWindowOptions: contextWindowOptions, cwd: displayedCwd, gitBranch: gitBranch, thinkingLevel: thinkingEnabled ? getMaxThinkingLevel(currentModel) : undefined, planMode: planMode, exitPending: exitPending }), !exitPending && _jsx(GoalStatusBar, { entries: goalStatusEntries })] })), (footerStatusLayout.hasBackgroundTasks ||
|
|
3651
|
-
footerStatusLayout.hasEyesSignals ||
|
|
3652
|
-
footerStatusLayout.hasUpdateNotice) && (_jsxs(Box, { flexDirection: footerStatusLayout.stack ? "column" : "row", width: columns, children: [footerStatusLayout.hasBackgroundTasks && (_jsx(BackgroundTasksBar, { tasks: bgTasks, focused: taskBarFocused, expanded: taskBarExpanded, selectedIndex: selectedTaskIndex, onExpand: handleTaskBarExpand, onCollapse: handleTaskBarCollapse, onKill: handleTaskKill, onExit: handleTaskBarExit, onNavigate: handleTaskNavigate, compact: footerStatusLayout.compactBackgroundTasks })), footerStatusLayout.hasEyesSignals && (_jsx(Box, { paddingLeft: footerStatusLayout.stack || bgTasks.length === 0 ? 1 : 2, paddingRight: 1, children: _jsx(Text, { color: theme.accent, bold: true, wrap: "truncate", children: `${eyesCount} eyes signal${eyesCount === 1 ? "" : "s"} · Run /eyes-improve to enhance EZ Coder` }) })), footerStatusLayout.hasUpdateNotice && (_jsx(Box, { paddingLeft: footerStatusLayout.stack ||
|
|
3653
|
-
(!footerStatusLayout.hasBackgroundTasks && !footerStatusLayout.hasEyesSignals)
|
|
3654
|
-
? 1
|
|
3655
|
-
: 2, paddingRight: 1, children: _jsx(Text, { color: theme.success, bold: true, wrap: "truncate", children: "\u2728 Update ready \u00B7 restart to apply" }) }))] }))] }))] }));
|
|
3656
|
-
}
|
|
3657
|
-
function formatRepoMapCommandOutput(enabled, markdown, refreshed) {
|
|
3658
|
-
const status = enabled ? "on" : "off";
|
|
3659
|
-
const prefix = refreshed
|
|
3660
|
-
? `Dynamic repo map refreshed · injection: ${status}`
|
|
3661
|
-
: `Dynamic repo map · injection: ${status}`;
|
|
3662
|
-
return `${prefix}\n\n${markdown}`;
|
|
2993
|
+
};
|
|
2994
|
+
const handleRejectPlan = (planPath, feedback) => {
|
|
2995
|
+
planOverlayPendingRef.current = false;
|
|
2996
|
+
const rejectionMsg = `The plan at ${planPath} was rejected.\n\nFeedback: ${feedback}\n\n` +
|
|
2997
|
+
`Please revise the plan based on this feedback.`;
|
|
2998
|
+
if (props.resetUI && props.sessionStore) {
|
|
2999
|
+
props.sessionStore.overlay = null;
|
|
3000
|
+
props.sessionStore.planAutoExpand = false;
|
|
3001
|
+
// No wipeSession — keep history and messages so the agent picks
|
|
3002
|
+
// up the rejection mid-conversation.
|
|
3003
|
+
props.resetUI({
|
|
3004
|
+
pendingAction: {
|
|
3005
|
+
prompt: rejectionMsg,
|
|
3006
|
+
planEvent: { event: "rejected", detail: feedback },
|
|
3007
|
+
},
|
|
3008
|
+
});
|
|
3009
|
+
return;
|
|
3010
|
+
}
|
|
3011
|
+
setPlanAutoExpand(false);
|
|
3012
|
+
setOverlay(null);
|
|
3013
|
+
setDoneStatus(null);
|
|
3014
|
+
setLiveItems((prev) => [
|
|
3015
|
+
...prev,
|
|
3016
|
+
{ kind: "info", text: `Plan rejected — "${feedback}"`, id: getId() },
|
|
3017
|
+
]);
|
|
3018
|
+
void agentLoop.run(rejectionMsg).catch((err) => {
|
|
3019
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
3020
|
+
log("ERROR", "error", errMsg);
|
|
3021
|
+
setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
|
|
3022
|
+
});
|
|
3023
|
+
};
|
|
3024
|
+
const handleToggleTasks = () => {
|
|
3025
|
+
taskPicker.toggle();
|
|
3026
|
+
};
|
|
3027
|
+
const fullScreenOverlay = isPixelView
|
|
3028
|
+
? "pixel"
|
|
3029
|
+
: isSkillsView
|
|
3030
|
+
? "skills"
|
|
3031
|
+
: isPlanView
|
|
3032
|
+
? "plan"
|
|
3033
|
+
: null;
|
|
3034
|
+
if (quittingSummary) {
|
|
3035
|
+
return (_jsx(Box, { flexDirection: "column", width: columns, flexShrink: 0, flexGrow: 0, children: _jsx(SessionSummaryDisplay, { summary: quittingSummary }) }));
|
|
3036
|
+
}
|
|
3037
|
+
return (_jsx(Box, { flexDirection: "column", width: columns, flexShrink: 0, flexGrow: 0, children: rewindCheckpoints ? (_jsx(RewindOverlay, { checkpoints: rewindCheckpoints, onRestore: handleRewindRestore, onCancel: handleRewindCancel })) : fullScreenOverlay ? (_jsx(FullScreenOverlayRouter, { overlay: fullScreenOverlay, version: props.version, cwd: props.cwd, agentRunning: agentLoop.isRunning, planAutoExpand: planAutoExpand, onClosePixel: handleCloseRemountableOverlay, onPixelFixOne: handlePixelFixOne, onPixelFixAll: handlePixelFixAll, onCloseSkills: handleCloseRemountableOverlay, onClosePlan: handleClosePlanOverlay, onApprovePlan: handleApprovePlan, onRejectPlan: handleRejectPlan })) : (_jsx(ChatScreen, { columns: columns, liveItems: uniqueItemsById(liveItems), renderItem: renderItem, isRunning: agentLoop.isRunning, visibleStreamingText: visibleStreamingText, streamingThinking: agentLoop.streamingThinking, thinkingMs: agentLoop.thinkingMs, reserveStreamingSpacing: shouldReserveStreamingSpacing, renderMarkdown: renderMarkdown, measuredLiveAreaRows: measuredLiveAreaRows, fullscreen: props.fullscreen, rows: rows, transcriptLines: transcriptLines, viewportRows: viewportRows, assistantMarginTop: shouldTopSpaceStreamingText || streamingContinuesFlushed ? 1 : 0, streamingContinuation: streamingContinuesFlushed, controlsRef: mainControlsRef, hiddenQueuedCount: hiddenQueuedCount, queueIndicatorMarginTop: shouldTopSpaceQueueIndicator ? 2 : 1, theme: theme, statusSlotVisible: statusSlotVisible, activityVisible: activityVisible, stallStatusVisible: stallStatusVisible, liveToolFeed: liveToolFeed, doneStatus: doneStatus, activityPhase: agentLoop.activityPhase, elapsedMs: agentLoop.elapsedMs, runStartRef: agentLoop.runStartRef, isThinking: agentLoop.isThinking, thinkingLevel: thinkingLevel, tokenEstimate: agentLoop.streamedTokenEstimate, charCountRef: agentLoop.charCountRef, realTokensAccumRef: agentLoop.realTokensAccumRef, lastUserMessage: lastUserMessage, activeToolNames: agentLoop.activeToolCalls.map((tc) => tc.name), retryInfo: agentLoop.retryInfo, planDone: planSteps.filter((s) => s.completed).length, planTotal: planSteps.length, formatDuration: formatDuration, inputControls: {
|
|
3038
|
+
onSubmit: handleSubmit,
|
|
3039
|
+
onAbort: handleAbort,
|
|
3040
|
+
injectText: composerInject,
|
|
3041
|
+
inputActive: !taskBarFocused && !overlay,
|
|
3042
|
+
onDownAtEnd: handleFocusTaskBar,
|
|
3043
|
+
onShiftTab: handleToggleThinking,
|
|
3044
|
+
onToggleTasks: handleToggleTasks,
|
|
3045
|
+
onToggleSkills: () => openOverlay("skills"),
|
|
3046
|
+
onTogglePixel: () => openOverlay("pixel"),
|
|
3047
|
+
onToggleGoal: () => openOverlay("goal"),
|
|
3048
|
+
onToggleMarkdown: () => setRenderMarkdown((prev) => !prev),
|
|
3049
|
+
cwd: props.cwd,
|
|
3050
|
+
commands: allCommands,
|
|
3051
|
+
mouseScroll: props.fullscreen,
|
|
3052
|
+
onScroll: scrollTranscriptByLines,
|
|
3053
|
+
}, taskPicker: {
|
|
3054
|
+
open: taskPicker.open,
|
|
3055
|
+
tasks: taskPicker.tasks,
|
|
3056
|
+
onClose: taskPicker.close,
|
|
3057
|
+
onStart: taskPicker.start,
|
|
3058
|
+
onRunAll: taskPicker.runAll,
|
|
3059
|
+
onDelete: taskPicker.deleteTask,
|
|
3060
|
+
}, goalPane: {
|
|
3061
|
+
cwd: props.cwd,
|
|
3062
|
+
autoExpandNewest: goalAutoExpand,
|
|
3063
|
+
statusEntries: goalStatusEntries,
|
|
3064
|
+
onClose: handleCloseGoalOverlay,
|
|
3065
|
+
onRunGoal: handleRunGoal,
|
|
3066
|
+
onVerifyGoal: handleVerifyGoal,
|
|
3067
|
+
onPauseGoal: handlePauseGoal,
|
|
3068
|
+
onRefineGoal: handleRefineGoal,
|
|
3069
|
+
}, overlay: overlay, onModelSelect: handleModelSelect, onModelCancel: () => setOverlay(null), loggedInProviders: props.loggedInProviders ?? [currentProvider], currentModel: currentModel, currentProvider: currentProvider, onThemeSelect: handleThemeSelect, onThemeCancel: () => setOverlay(null), currentTheme: theme.name, contextUsed: agentLoop.contextUsed, contextWindowOptions: contextWindowOptions, displayedCwd: displayedCwd, gitBranch: gitBranch, planMode: planMode, exitPending: exitPending, footerStatusLayout: footerStatusLayout, backgroundTasks: bgTasks, taskBarFocused: taskBarFocused, taskBarExpanded: taskBarExpanded, selectedTaskIndex: selectedTaskIndex, onTaskBarExpand: handleTaskBarExpand, onTaskBarCollapse: handleTaskBarCollapse, onTaskKill: handleTaskKill, onTaskBarExit: handleTaskBarExit, onTaskNavigate: handleTaskNavigate })) }));
|
|
3663
3070
|
}
|
|
3664
3071
|
//# sourceMappingURL=App.js.map
|