@prestyj/cli 4.3.210 → 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 -732
- 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 -3
- package/dist/tools/enter-plan.d.ts.map +1 -1
- package/dist/tools/enter-plan.js +9 -21
- 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 +15 -12
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +12 -8
- 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 -243
- 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 +31 -9
- 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 -280
- package/dist/ui/App.d.ts.map +1 -1
- package/dist/ui/App.js +1608 -2235
- 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 -42
- package/dist/ui/render.d.ts.map +1 -1
- package/dist/ui/render.js +128 -42
- 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,76 +1323,52 @@ 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);
|
|
1731
|
-
// startTaskRef.current() re-sets taskRunningRef = true before
|
|
1732
|
-
// the next agent run begins, so the brief clear-then-set is
|
|
1733
|
-
// safe — the agent isn't running in between.
|
|
1734
1364
|
startTaskRef.current(next.title, next.prompt, next.id);
|
|
1735
1365
|
}
|
|
1736
1366
|
else {
|
|
1737
1367
|
setRunAllTasks(false);
|
|
1738
|
-
if (props.taskRunningRef)
|
|
1739
|
-
props.taskRunningRef.current = false;
|
|
1740
1368
|
log("INFO", "tasks", "Run-all complete — no more pending tasks");
|
|
1741
1369
|
}
|
|
1742
1370
|
}, 500);
|
|
1743
1371
|
}
|
|
1744
|
-
else if (currentTaskIdRef.current) {
|
|
1745
|
-
// Single manual task done — clear the task-running flag so a
|
|
1746
|
-
// subsequent free-form prompt can use plan mode again.
|
|
1747
|
-
if (props.taskRunningRef)
|
|
1748
|
-
props.taskRunningRef.current = false;
|
|
1749
|
-
currentTaskIdRef.current = null;
|
|
1750
|
-
if (props.sessionStore)
|
|
1751
|
-
props.sessionStore.currentTaskId = null;
|
|
1752
|
-
}
|
|
1753
|
-
// Goal loop: after the orchestrator handles a worker/verifier event,
|
|
1754
|
-
// continue the same Goal automatically until it reaches a terminal state.
|
|
1755
|
-
for (const runId of [...runningGoalIdsRef.current]) {
|
|
1756
|
-
setTimeout(() => continueGoalRun(runId), 500);
|
|
1757
|
-
}
|
|
1758
1372
|
// Pixel fix: observe branch + commits, patch status, optionally pick
|
|
1759
1373
|
// up the next open error if run-all is active.
|
|
1760
1374
|
const pendingFix = currentPixelFixRef.current;
|
|
@@ -1796,16 +1410,6 @@ export function App(props) {
|
|
|
1796
1410
|
setRunAllTasks(false);
|
|
1797
1411
|
setRunAllPixel(false);
|
|
1798
1412
|
currentPixelFixRef.current = null;
|
|
1799
|
-
// Clear the task-running flag so a subsequent free-form prompt can
|
|
1800
|
-
// re-enable plan mode. Aborted runs never auto-advance.
|
|
1801
|
-
if (props.taskRunningRef)
|
|
1802
|
-
props.taskRunningRef.current = false;
|
|
1803
|
-
// Clear the run-all task id so a subsequent run-all does not
|
|
1804
|
-
// mistakenly mark this aborted task as done. The on-disk row stays
|
|
1805
|
-
// at `in-progress`, which is the correct truth for an aborted task.
|
|
1806
|
-
currentTaskIdRef.current = null;
|
|
1807
|
-
if (props.sessionStore)
|
|
1808
|
-
props.sessionStore.currentTaskId = null;
|
|
1809
1413
|
setDoneStatus(null);
|
|
1810
1414
|
setLiveItems((prev) => {
|
|
1811
1415
|
const next = prev.map((item) => {
|
|
@@ -1851,42 +1455,16 @@ export function App(props) {
|
|
|
1851
1455
|
}, []),
|
|
1852
1456
|
onQueuedStart: useCallback((content) => {
|
|
1853
1457
|
// When a queued message starts processing, show it as a UserItem
|
|
1854
|
-
// and flush prior items to history.
|
|
1855
|
-
// from the transcript but still routed through the main agent context.
|
|
1458
|
+
// and flush prior items to history.
|
|
1856
1459
|
const displayText = typeof content === "string"
|
|
1857
1460
|
? content
|
|
1858
1461
|
: content
|
|
1859
1462
|
.filter((c) => c.type === "text")
|
|
1860
1463
|
.map((c) => c.text)
|
|
1861
1464
|
.join("\n");
|
|
1862
|
-
if (isGoalSyntheticEvent(displayText)) {
|
|
1863
|
-
const eventInfo = parseGoalSyntheticEvent(displayText);
|
|
1864
|
-
setLiveItems((prev) => {
|
|
1865
|
-
if (prev.length > 0)
|
|
1866
|
-
queueFlush(prev);
|
|
1867
|
-
return [];
|
|
1868
|
-
});
|
|
1869
|
-
setDoneStatus(null);
|
|
1870
|
-
appendGoalProgress({
|
|
1871
|
-
kind: "goal_progress",
|
|
1872
|
-
phase: "orchestrator_reviewing",
|
|
1873
|
-
title: "Orchestrator reviewing Goal update",
|
|
1874
|
-
detail: eventInfo?.kind === "worker"
|
|
1875
|
-
? `Worker ${eventInfo.worker ?? "finished"} reported back${eventInfo.task ? ` on ${eventInfo.task}` : ""}. Inspecting Goal state.`
|
|
1876
|
-
: `Verifier reported ${eventInfo?.status ?? "status"}. Inspecting evidence and next action.`,
|
|
1877
|
-
workerId: eventInfo?.worker,
|
|
1878
|
-
status: eventInfo?.status,
|
|
1879
|
-
});
|
|
1880
|
-
return;
|
|
1881
|
-
}
|
|
1882
1465
|
const imageCount = typeof content === "string"
|
|
1883
1466
|
? undefined
|
|
1884
1467
|
: content.filter((c) => c.type === "image").length || undefined;
|
|
1885
|
-
setLiveItems((prev) => {
|
|
1886
|
-
if (prev.length > 0)
|
|
1887
|
-
queueFlush(prev);
|
|
1888
|
-
return [];
|
|
1889
|
-
});
|
|
1890
1468
|
const userItem = {
|
|
1891
1469
|
kind: "user",
|
|
1892
1470
|
text: displayText,
|
|
@@ -1895,8 +1473,8 @@ export function App(props) {
|
|
|
1895
1473
|
};
|
|
1896
1474
|
setLastUserMessage(displayText);
|
|
1897
1475
|
setDoneStatus(null);
|
|
1898
|
-
|
|
1899
|
-
}, []),
|
|
1476
|
+
finalizeSubmittedUserItem(userItem);
|
|
1477
|
+
}, [finalizeSubmittedUserItem]),
|
|
1900
1478
|
// Inject a "continue with the next step" follow-up when the agent
|
|
1901
1479
|
// would otherwise stop mid-plan. The prompt-only instruction wasn't
|
|
1902
1480
|
// enough — some models (notably Opus) treat each [DONE:n] as a
|
|
@@ -1928,9 +1506,280 @@ export function App(props) {
|
|
|
1928
1506
|
];
|
|
1929
1507
|
}, []),
|
|
1930
1508
|
});
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
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
|
|
1934
1783
|
// notice item explaining what's happening, then runs the audit prompt.
|
|
1935
1784
|
triggerAutoSetupRef.current = async () => {
|
|
1936
1785
|
const setupCmd = getPromptCommand("setup");
|
|
@@ -1966,27 +1815,6 @@ export function App(props) {
|
|
|
1966
1815
|
]);
|
|
1967
1816
|
}
|
|
1968
1817
|
};
|
|
1969
|
-
// Phase 2 of the two-phase flush: after onDone clears liveItems (phase 1)
|
|
1970
|
-
// and Ink renders the smaller live area (updating its internal line
|
|
1971
|
-
// counter), this effect pushes the stashed items into Static history.
|
|
1972
|
-
// Because the Static write happens in a SEPARATE render cycle from the
|
|
1973
|
-
// live-area shrink, Ink's log-update never needs to erase the old tall
|
|
1974
|
-
// live area AND write Static content in the same frame — avoiding the
|
|
1975
|
-
// cursor-math mismatch that caused text clipping.
|
|
1976
|
-
useEffect(() => {
|
|
1977
|
-
if (pendingFlushRef.current.length > 0) {
|
|
1978
|
-
const items = pendingFlushRef.current;
|
|
1979
|
-
pendingFlushRef.current = [];
|
|
1980
|
-
setHistory((h) => {
|
|
1981
|
-
const next = compactHistory([...h, ...trimFlushedItems(items)]);
|
|
1982
|
-
if (sessionStore)
|
|
1983
|
-
sessionStore.history = next;
|
|
1984
|
-
return next;
|
|
1985
|
-
});
|
|
1986
|
-
if (sessionStore)
|
|
1987
|
-
sessionStore.liveItems = liveItems;
|
|
1988
|
-
}
|
|
1989
|
-
}, [flushGeneration]);
|
|
1990
1818
|
// Sync terminal title with agent loop state
|
|
1991
1819
|
useEffect(() => {
|
|
1992
1820
|
setTitleRunning(agentLoop.isRunning);
|
|
@@ -2014,10 +1842,28 @@ export function App(props) {
|
|
|
2014
1842
|
return () => clearTimeout(timer);
|
|
2015
1843
|
}
|
|
2016
1844
|
}, [agentLoop.isRunning, sessionStore, props.resetUI]);
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
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.
|
|
2021
1867
|
useEffect(() => {
|
|
2022
1868
|
if (pendingActionConsumedRef.current)
|
|
2023
1869
|
return;
|
|
@@ -2025,8 +1871,10 @@ export function App(props) {
|
|
|
2025
1871
|
if (!action)
|
|
2026
1872
|
return;
|
|
2027
1873
|
pendingActionConsumedRef.current = true;
|
|
2028
|
-
if (sessionStore)
|
|
1874
|
+
if (sessionStore) {
|
|
2029
1875
|
sessionStore.pendingAction = undefined;
|
|
1876
|
+
}
|
|
1877
|
+
setDoneStatus(null);
|
|
2030
1878
|
if (action.planEvent) {
|
|
2031
1879
|
const ev = action.planEvent;
|
|
2032
1880
|
setLiveItems((prev) => [
|
|
@@ -2040,7 +1888,6 @@ export function App(props) {
|
|
|
2040
1888
|
{ kind: "info", text: action.infoText, id: getId() },
|
|
2041
1889
|
]);
|
|
2042
1890
|
}
|
|
2043
|
-
setDoneStatus(null);
|
|
2044
1891
|
void agentLoop.run(action.prompt).catch((err) => {
|
|
2045
1892
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
2046
1893
|
log("ERROR", "error", errMsg);
|
|
@@ -2048,14 +1895,6 @@ export function App(props) {
|
|
|
2048
1895
|
});
|
|
2049
1896
|
// Intentional one-shot: run once on mount, never re-fire on re-render.
|
|
2050
1897
|
}, []);
|
|
2051
|
-
// Refresh eyes badge count when the agent settles (end of a turn) — a turn
|
|
2052
|
-
// may have logged new rough/wish/blocked signals. Also covers the case where
|
|
2053
|
-
// /eyes was run for the first time (manifest now exists).
|
|
2054
|
-
useEffect(() => {
|
|
2055
|
-
if (!agentLoop.isRunning) {
|
|
2056
|
-
setEyesCount(isEyesActive(props.cwd) ? journalCount({ status: "open" }, props.cwd) : undefined);
|
|
2057
|
-
}
|
|
2058
|
-
}, [agentLoop.isRunning, props.cwd]);
|
|
2059
1898
|
const handleSubmit = useCallback(async (input, inputImages = [], pasteInfo) => {
|
|
2060
1899
|
const trimmed = input.trim();
|
|
2061
1900
|
if (trimmed.startsWith("/")) {
|
|
@@ -2070,241 +1909,141 @@ export function App(props) {
|
|
|
2070
1909
|
// has not grown.
|
|
2071
1910
|
await applyLanguageDetectionRef.current("input");
|
|
2072
1911
|
}
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
const ac = new AbortController();
|
|
2081
|
-
compactionAbortRef.current = ac;
|
|
2082
|
-
const compacted = await compactConversation(messagesRef.current, ac.signal);
|
|
2083
|
-
if (!ac.signal.aborted && compacted !== messagesRef.current) {
|
|
2084
|
-
messagesRef.current = compacted;
|
|
2085
|
-
await persistCompactedSession(compacted);
|
|
2086
|
-
}
|
|
2087
|
-
if (compactionAbortRef.current === ac)
|
|
2088
|
-
compactionAbortRef.current = null;
|
|
2089
|
-
return;
|
|
2090
|
-
}
|
|
2091
|
-
// Handle /quit — exit the agent
|
|
2092
|
-
if (trimmed === "/quit" || trimmed === "/q" || trimmed === "/exit") {
|
|
2093
|
-
process.exit(0);
|
|
2094
|
-
}
|
|
2095
|
-
// Handle /clear — tear down the entire Ink instance and rebuild fresh.
|
|
2096
|
-
// Avoid direct ANSI terminal clears here; they can erase scrollback.
|
|
2097
|
-
// Runtime state (model, provider, thinking) survives via renderApp's
|
|
2098
|
-
// closure-held `runtimeState`, mirrored from React state via the
|
|
2099
|
-
// useEffects above.
|
|
2100
|
-
if (trimmed === "/clear") {
|
|
2101
|
-
if (props.resetUI) {
|
|
2102
|
-
void (async () => {
|
|
2103
|
-
const newPrompt = await rebuildSystemPrompt({ clearApprovedPlan: true });
|
|
2104
|
-
props.resetUI?.({
|
|
2105
|
-
wipeSession: true,
|
|
2106
|
-
messages: [{ role: "system", content: newPrompt }],
|
|
2107
|
-
});
|
|
2108
|
-
})();
|
|
2109
|
-
return;
|
|
2110
|
-
}
|
|
2111
|
-
// Fallback path (resetUI not wired — e.g. tests). Best-effort: clear
|
|
2112
|
-
// React state in place without touching terminal scrollback.
|
|
2113
|
-
pendingFlushRef.current = [];
|
|
2114
|
-
setHistory([{ kind: "banner", id: "banner" }]);
|
|
2115
|
-
setLiveItems([]);
|
|
2116
|
-
setDoneStatus(null);
|
|
2117
|
-
approvedPlanPathRef.current = undefined;
|
|
2118
|
-
planStepsRef.current = [];
|
|
2119
|
-
setPlanSteps([]);
|
|
2120
|
-
void (async () => {
|
|
2121
|
-
const newPrompt = await rebuildSystemPrompt({ clearApprovedPlan: true });
|
|
2122
|
-
messagesRef.current = [{ role: "system", content: newPrompt }];
|
|
2123
|
-
persistedIndexRef.current = messagesRef.current.length;
|
|
2124
|
-
})();
|
|
2125
|
-
agentLoop.reset();
|
|
2126
|
-
setSessionTitle(undefined);
|
|
2127
|
-
sessionTitleGeneratedRef.current = false;
|
|
2128
|
-
setLiveItems([{ kind: "info", text: "Session cleared.", id: getId() }]);
|
|
2129
|
-
return;
|
|
2130
|
-
}
|
|
2131
|
-
// Handle /theme — open theme selector overlay
|
|
2132
|
-
if (trimmed === "/theme" || trimmed === "/t") {
|
|
2133
|
-
setOverlay("theme");
|
|
2134
|
-
return;
|
|
2135
|
-
}
|
|
2136
|
-
// Open the Eyes pane — read-only review of installed probes + open signals.
|
|
2137
|
-
// Gated by the ezcoder-eyes manifest: in projects without /eyes set up,
|
|
2138
|
-
// there's nothing useful to show.
|
|
2139
|
-
if (trimmed === "/eyes-view" || trimmed === "/ev") {
|
|
2140
|
-
if (!isEyesActive(props.cwd)) {
|
|
2141
|
-
setLiveItems((prev) => [
|
|
2142
|
-
...prev,
|
|
2143
|
-
{
|
|
2144
|
-
kind: "info",
|
|
2145
|
-
text: "Eyes not set up in this project. Run /setup-eyes to get started.",
|
|
2146
|
-
id: getId(),
|
|
2147
|
-
},
|
|
2148
|
-
]);
|
|
2149
|
-
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);
|
|
2150
1919
|
}
|
|
2151
|
-
setOverlay("eyes");
|
|
2152
|
-
return;
|
|
2153
|
-
}
|
|
2154
|
-
// Handle /plan — toggle plan mode
|
|
2155
|
-
if (trimmed === "/plan" || trimmed === "/plan on") {
|
|
2156
|
-
setPlanMode(true);
|
|
2157
|
-
setLiveItems((prev) => [
|
|
2158
|
-
...prev,
|
|
2159
|
-
{ kind: "plan_transition", text: "Plan Mode Activated", active: true, id: getId() },
|
|
2160
|
-
]);
|
|
2161
|
-
return;
|
|
2162
|
-
}
|
|
2163
|
-
if (trimmed === "/plan off") {
|
|
2164
|
-
setPlanMode(false);
|
|
2165
1920
|
setLiveItems((prev) => [
|
|
2166
1921
|
...prev,
|
|
2167
1922
|
{
|
|
2168
|
-
kind: "
|
|
2169
|
-
text:
|
|
2170
|
-
|
|
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.",
|
|
2171
1927
|
id: getId(),
|
|
2172
1928
|
},
|
|
2173
1929
|
]);
|
|
2174
1930
|
return;
|
|
2175
1931
|
}
|
|
2176
|
-
//
|
|
2177
|
-
if (trimmed === "/
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
setPlanSteps([]);
|
|
2181
|
-
// Rebuild system prompt without the plan
|
|
2182
|
-
void replaceSystemPrompt({ clearApprovedPlan: true });
|
|
2183
|
-
setLiveItems([{ kind: "plan_event", event: "dismissed", id: getId() }]);
|
|
2184
|
-
return;
|
|
2185
|
-
}
|
|
2186
|
-
// Handle /map — show, refresh, or toggle dynamic repo map injection
|
|
2187
|
-
if (trimmed === "/map" ||
|
|
2188
|
-
trimmed === "/map refresh" ||
|
|
2189
|
-
trimmed === "/map on" ||
|
|
2190
|
-
trimmed === "/map off") {
|
|
2191
|
-
const action = trimmed.slice("/map".length).trim();
|
|
2192
|
-
if (action === "on") {
|
|
2193
|
-
repoMapInjectionEnabledRef.current = true;
|
|
2194
|
-
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) {
|
|
2195
1936
|
setLiveItems((prev) => [
|
|
2196
1937
|
...prev,
|
|
2197
|
-
{ kind: "info", text: "
|
|
1938
|
+
{ kind: "info", text: "Checkpoints are not available in this session.", id: getId() },
|
|
2198
1939
|
]);
|
|
2199
1940
|
return;
|
|
2200
1941
|
}
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
messagesRef.current = stripRepoMapMessages(messagesRef.current);
|
|
1942
|
+
const cps = await store.listCheckpoints();
|
|
1943
|
+
if (cps.length === 0) {
|
|
2204
1944
|
setLiveItems((prev) => [
|
|
2205
1945
|
...prev,
|
|
2206
1946
|
{
|
|
2207
1947
|
kind: "info",
|
|
2208
|
-
text: "
|
|
1948
|
+
text: "No checkpoints yet \u2014 edit a file through ezcoder first.",
|
|
2209
1949
|
id: getId(),
|
|
2210
1950
|
},
|
|
2211
1951
|
]);
|
|
2212
1952
|
return;
|
|
2213
1953
|
}
|
|
2214
|
-
|
|
2215
|
-
repoMapDirtyRef.current = true;
|
|
2216
|
-
const markdown = await refreshRepoMap(getLatestUserText(messagesRef.current));
|
|
2217
|
-
setLiveItems((prev) => [
|
|
2218
|
-
...prev,
|
|
2219
|
-
{
|
|
2220
|
-
kind: "info",
|
|
2221
|
-
text: formatRepoMapCommandOutput(repoMapInjectionEnabledRef.current, markdown, action === "refresh"),
|
|
2222
|
-
id: getId(),
|
|
2223
|
-
},
|
|
2224
|
-
]);
|
|
1954
|
+
setRewindCheckpoints(cps);
|
|
2225
1955
|
return;
|
|
2226
1956
|
}
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
props.sessionStore.overlay = "goal";
|
|
2237
|
-
props.sessionStore.planAutoExpand = false;
|
|
2238
|
-
if (agentLoop.isRunning)
|
|
2239
|
-
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);
|
|
2240
1966
|
}
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
}
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
props.sessionStore.overlay = "plan";
|
|
2256
|
-
props.sessionStore.planAutoExpand = false;
|
|
2257
|
-
if (agentLoop.isRunning)
|
|
2258
|
-
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;
|
|
2259
1981
|
}
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
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
|
+
})) {
|
|
2263
2021
|
return;
|
|
2264
2022
|
}
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
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;
|
|
2274
2043
|
}
|
|
2275
|
-
|
|
2276
|
-
}
|
|
2277
|
-
|
|
2278
|
-
const modelInfo = getModel(currentModel);
|
|
2279
|
-
const modelSupportsImages = modelInfo?.supportsImages ?? true;
|
|
2280
|
-
const userContent = buildUserContentWithAttachments(fullPrompt, inputImages, modelSupportsImages);
|
|
2281
|
-
// Show the typed command as the user message
|
|
2282
|
-
const userItem = {
|
|
2283
|
-
kind: "user",
|
|
2284
|
-
text: trimmed,
|
|
2285
|
-
imageCount: hasImages ? inputImages.length : undefined,
|
|
2286
|
-
id: getId(),
|
|
2287
|
-
};
|
|
2288
|
-
setLastUserMessage(trimmed);
|
|
2289
|
-
setDoneStatus(null);
|
|
2290
|
-
setLiveItems([userItem]);
|
|
2291
|
-
// Send the full prompt to the agent, with user args appended if provided
|
|
2292
|
-
try {
|
|
2293
|
-
await agentLoop.run(userContent);
|
|
2294
|
-
}
|
|
2295
|
-
catch (err) {
|
|
2296
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
2297
|
-
log("ERROR", "error", msg);
|
|
2298
|
-
const isAbort = msg.includes("aborted") || msg.includes("abort");
|
|
2299
|
-
setLiveItems((prev) => [
|
|
2300
|
-
...prev,
|
|
2301
|
-
isAbort
|
|
2302
|
-
? { kind: "stopped", text: "Request was stopped.", id: getId() }
|
|
2303
|
-
: toErrorItem(err, getId()),
|
|
2304
|
-
]);
|
|
2305
|
-
}
|
|
2306
|
-
// Reload custom commands in case a setup command created new ones
|
|
2307
|
-
reloadCustomCommands();
|
|
2044
|
+
setOverlay("goal");
|
|
2045
|
+
},
|
|
2046
|
+
})) {
|
|
2308
2047
|
return;
|
|
2309
2048
|
}
|
|
2310
2049
|
// Check slash commands
|
|
@@ -2323,7 +2062,7 @@ export function App(props) {
|
|
|
2323
2062
|
// ── Queue message if agent is already running ──
|
|
2324
2063
|
if (agentLoop.isRunning) {
|
|
2325
2064
|
log("INFO", "queue", `Queued message: ${trimmed.length > 80 ? trimmed.slice(0, 80) + "..." : trimmed}`);
|
|
2326
|
-
agentLoop.queueMessage(userContent);
|
|
2065
|
+
agentLoop.queueMessage(userContent, input);
|
|
2327
2066
|
let displayText = input;
|
|
2328
2067
|
if (hasImages) {
|
|
2329
2068
|
const { cleanText } = await extractImagePaths(input, props.cwd);
|
|
@@ -2338,31 +2077,32 @@ export function App(props) {
|
|
|
2338
2077
|
setLiveItems((prev) => [...prev, queuedItem]);
|
|
2339
2078
|
return;
|
|
2340
2079
|
}
|
|
2341
|
-
// Move any remaining live items into history (Static) before starting a
|
|
2342
|
-
// new turn. Must go through queueFlush so flushGeneration bumps and the
|
|
2343
|
-
// drain effect actually runs — mutating pendingFlushRef directly here
|
|
2344
|
-
// stashed items that nothing was signalled to pick up, so they sat in
|
|
2345
|
-
// limbo until some unrelated later code path happened to call queueFlush.
|
|
2346
|
-
setLiveItems((prev) => {
|
|
2347
|
-
if (prev.length > 0) {
|
|
2348
|
-
queueFlush(prev);
|
|
2349
|
-
}
|
|
2350
|
-
return [];
|
|
2351
|
-
});
|
|
2352
2080
|
// Build display text — strip image paths, show badges instead
|
|
2353
2081
|
let displayText = input;
|
|
2354
2082
|
if (hasImages) {
|
|
2355
2083
|
const { cleanText } = await extractImagePaths(input, props.cwd);
|
|
2356
2084
|
displayText = cleanText;
|
|
2357
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
|
+
}
|
|
2358
2096
|
const userItem = {
|
|
2359
2097
|
kind: "user",
|
|
2360
2098
|
text: displayText,
|
|
2361
2099
|
imageCount: hasImages ? inputImages.length : undefined,
|
|
2100
|
+
imagePreviews,
|
|
2362
2101
|
pasteInfo,
|
|
2363
2102
|
id: getId(),
|
|
2364
2103
|
};
|
|
2365
2104
|
setLastUserMessage(input);
|
|
2105
|
+
setScrollResetToken((token) => token + 1);
|
|
2366
2106
|
setDoneStatus(null);
|
|
2367
2107
|
// Clear stale plan progress if there's no active approved plan
|
|
2368
2108
|
// (avoids lingering progress from a completed or abandoned plan run)
|
|
@@ -2370,7 +2110,18 @@ export function App(props) {
|
|
|
2370
2110
|
planStepsRef.current = [];
|
|
2371
2111
|
setPlanSteps([]);
|
|
2372
2112
|
}
|
|
2373
|
-
|
|
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
|
+
}
|
|
2374
2125
|
// Run agent
|
|
2375
2126
|
try {
|
|
2376
2127
|
await agentLoop.run(userContent);
|
|
@@ -2388,17 +2139,29 @@ export function App(props) {
|
|
|
2388
2139
|
}
|
|
2389
2140
|
}, [
|
|
2390
2141
|
agentLoop,
|
|
2391
|
-
props.onSlashCommand,
|
|
2392
2142
|
compactConversation,
|
|
2143
|
+
currentModel,
|
|
2144
|
+
finalizeSubmittedUserItem,
|
|
2145
|
+
liveItems,
|
|
2146
|
+
props.cwd,
|
|
2147
|
+
props.onSlashCommand,
|
|
2148
|
+
props.resetUI,
|
|
2149
|
+
props.sessionStore,
|
|
2393
2150
|
rebuildSystemPrompt,
|
|
2151
|
+
showSessionSummaryAndExit,
|
|
2152
|
+
reloadCustomCommands,
|
|
2394
2153
|
replaceSystemPrompt,
|
|
2395
|
-
refreshRepoMap,
|
|
2396
|
-
stripRepoMapMessages,
|
|
2397
2154
|
]);
|
|
2398
|
-
const handleDoubleExit = useDoublePress(setExitPending,
|
|
2155
|
+
const handleDoubleExit = useDoublePress(setExitPending, showSessionSummaryAndExit);
|
|
2399
2156
|
const handleAbort = useCallback(() => {
|
|
2400
2157
|
if (agentLoop.isRunning) {
|
|
2401
|
-
|
|
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
|
+
}
|
|
2402
2165
|
agentLoop.abort();
|
|
2403
2166
|
}
|
|
2404
2167
|
else if (compactionAbortRef.current) {
|
|
@@ -2407,22 +2170,22 @@ export function App(props) {
|
|
|
2407
2170
|
else {
|
|
2408
2171
|
handleDoubleExit();
|
|
2409
2172
|
}
|
|
2410
|
-
}, [agentLoop, handleDoubleExit]);
|
|
2173
|
+
}, [agentLoop, handleDoubleExit, setLiveItems]);
|
|
2411
2174
|
const handleToggleThinking = useCallback(() => {
|
|
2412
|
-
|
|
2413
|
-
const next =
|
|
2414
|
-
log("INFO", "thinking", `Thinking ${next
|
|
2415
|
-
setLiveItems((items) => [
|
|
2416
|
-
...items,
|
|
2417
|
-
{ kind: "thinking_transition", active: next, id: getId() },
|
|
2418
|
-
]);
|
|
2175
|
+
setThinkingLevel((prev) => {
|
|
2176
|
+
const next = getNextThinkingLevel(currentProvider, currentModel, prev);
|
|
2177
|
+
log("INFO", "thinking", next ? `Thinking ${next}` : "Thinking disabled");
|
|
2419
2178
|
if (props.settingsFile) {
|
|
2420
2179
|
const sm = new SettingsManager(props.settingsFile);
|
|
2421
|
-
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
|
+
});
|
|
2422
2185
|
}
|
|
2423
2186
|
return next;
|
|
2424
2187
|
});
|
|
2425
|
-
}, [props.settingsFile]);
|
|
2188
|
+
}, [currentProvider, currentModel, props.settingsFile]);
|
|
2426
2189
|
const handleModelSelect = useCallback((value) => {
|
|
2427
2190
|
setOverlay(null);
|
|
2428
2191
|
const colonIdx = value.indexOf(":");
|
|
@@ -2431,6 +2194,9 @@ export function App(props) {
|
|
|
2431
2194
|
const newProvider = value.slice(0, colonIdx);
|
|
2432
2195
|
const newModelId = value.slice(colonIdx + 1);
|
|
2433
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;
|
|
2434
2200
|
const rebuildPromptWithTools = (tools) => {
|
|
2435
2201
|
currentToolsRef.current = tools;
|
|
2436
2202
|
void replaceSystemPrompt({ tools });
|
|
@@ -2530,214 +2296,179 @@ export function App(props) {
|
|
|
2530
2296
|
const promptByName = new Map(PROMPT_COMMANDS.map((c) => [c.name, c]));
|
|
2531
2297
|
const fromPrompt = (name) => {
|
|
2532
2298
|
const c = promptByName.get(name);
|
|
2533
|
-
return c
|
|
2299
|
+
return c
|
|
2300
|
+
? {
|
|
2301
|
+
name: c.name,
|
|
2302
|
+
aliases: c.aliases,
|
|
2303
|
+
description: c.description,
|
|
2304
|
+
sectionTitle: "workflows",
|
|
2305
|
+
}
|
|
2306
|
+
: null;
|
|
2534
2307
|
};
|
|
2535
2308
|
const promptOrder = [
|
|
2536
2309
|
// Project audits / one-shot analysis
|
|
2537
|
-
"goal",
|
|
2538
2310
|
"init",
|
|
2539
|
-
"research",
|
|
2540
|
-
"scan",
|
|
2541
|
-
"verify",
|
|
2542
2311
|
"expand",
|
|
2543
2312
|
"bullet-proof",
|
|
2544
|
-
"simplify",
|
|
2545
2313
|
"compare",
|
|
2546
|
-
"batch",
|
|
2547
2314
|
// Setup / installers
|
|
2548
|
-
"setup-lint",
|
|
2549
|
-
"setup-tests",
|
|
2550
2315
|
"setup-commit",
|
|
2551
|
-
"setup-update",
|
|
2552
|
-
"setup-eyes",
|
|
2553
|
-
"eyes-improve",
|
|
2554
2316
|
"setup-skills",
|
|
2555
2317
|
];
|
|
2556
2318
|
const orderedPromptCommands = promptOrder
|
|
2557
2319
|
.map(fromPrompt)
|
|
2558
2320
|
.filter((c) => c !== null);
|
|
2559
2321
|
const knownPromptNames = new Set(promptOrder);
|
|
2560
|
-
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
|
+
}));
|
|
2561
2328
|
return [
|
|
2562
2329
|
// Session actions (most frequent)
|
|
2563
|
-
{ name: "model", aliases: ["m"], description: "Switch model" },
|
|
2564
|
-
{ name: "compact", aliases: ["c"], description: "Compact
|
|
2565
|
-
{ name: "clear", aliases: [], description: "Clear session
|
|
2566
|
-
{ name: "theme", aliases: ["t"], description: "Switch theme" },
|
|
2567
|
-
{
|
|
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
|
+
},
|
|
2568
2348
|
...orderedPromptCommands,
|
|
2569
2349
|
...remainingPromptCommands,
|
|
2570
2350
|
...customCommands.map((cmd) => ({
|
|
2571
2351
|
name: cmd.name,
|
|
2572
2352
|
aliases: [],
|
|
2573
2353
|
description: cmd.description,
|
|
2354
|
+
sectionTitle: "custom",
|
|
2574
2355
|
})),
|
|
2575
|
-
{
|
|
2356
|
+
{
|
|
2357
|
+
name: "quit",
|
|
2358
|
+
aliases: ["q", "exit"],
|
|
2359
|
+
description: "Exit ezcoder",
|
|
2360
|
+
sectionTitle: "built-in",
|
|
2361
|
+
},
|
|
2576
2362
|
];
|
|
2577
|
-
}, [customCommands]);
|
|
2578
|
-
const renderItem = (item) => {
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
? theme.warning
|
|
2602
|
-
: item.phase === "verifier_started"
|
|
2603
|
-
? theme.accent
|
|
2604
|
-
: item.phase === "worker_started"
|
|
2605
|
-
? theme.primary
|
|
2606
|
-
: item.phase === "terminal"
|
|
2607
|
-
? theme.success
|
|
2608
|
-
: theme.primary;
|
|
2609
|
-
const glyph = item.phase === "worker_finished" || item.phase === "verifier_finished"
|
|
2610
|
-
? "✓ "
|
|
2611
|
-
: item.phase === "terminal"
|
|
2612
|
-
? item.status === "passed"
|
|
2613
|
-
? "◆ "
|
|
2614
|
-
: "! "
|
|
2615
|
-
: "↻ ";
|
|
2616
|
-
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));
|
|
2617
|
-
}
|
|
2618
|
-
case "style_pack": {
|
|
2619
|
-
const names = item.added.map((id) => LANGUAGE_DISPLAY_NAMES[id]);
|
|
2620
|
-
const headerLabel = item.added.length > 1 ? "STYLE PACKS ACTIVE" : "STYLE PACK ACTIVE";
|
|
2621
|
-
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));
|
|
2622
|
-
}
|
|
2623
|
-
case "setup_hint":
|
|
2624
|
-
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));
|
|
2625
|
-
case "assistant":
|
|
2626
|
-
return (_jsx(AssistantMessage, { text: item.text, thinking: item.thinking, thinkingMs: item.thinkingMs, planMode: item.planMode }, item.id));
|
|
2627
|
-
case "tool_start":
|
|
2628
|
-
return (_jsx(ToolExecution, { status: "running", name: item.name, args: item.args, progressOutput: item.progressOutput, animateUntil: item.animateUntil }, item.id));
|
|
2629
|
-
case "tool_done":
|
|
2630
|
-
return (_jsx(ToolExecution, { status: "done", name: item.name, args: item.args, result: item.result, isError: item.isError, details: item.details }, item.id));
|
|
2631
|
-
case "tool_group":
|
|
2632
|
-
return _jsx(ToolGroupExecution, { tools: item.tools }, item.id);
|
|
2633
|
-
case "server_tool_start":
|
|
2634
|
-
return (_jsx(ServerToolExecution, { status: "running", name: item.name, input: item.input, startedAt: item.startedAt, animateUntil: item.animateUntil }, item.id));
|
|
2635
|
-
case "server_tool_done":
|
|
2636
|
-
return (_jsx(ServerToolExecution, { status: "done", name: item.name, input: item.input, durationMs: item.durationMs, resultType: item.resultType }, item.id));
|
|
2637
|
-
case "error": {
|
|
2638
|
-
const showMessage = item.message && item.message !== item.headline;
|
|
2639
|
-
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));
|
|
2640
|
-
}
|
|
2641
|
-
case "info":
|
|
2642
|
-
return (_jsx(Box, { marginTop: 1, flexShrink: 1, children: _jsx(Text, { color: theme.textDim, wrap: "wrap", children: item.text }) }, item.id));
|
|
2643
|
-
case "update_notice":
|
|
2644
|
-
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));
|
|
2645
|
-
case "plan_transition":
|
|
2646
|
-
return (_jsx(Box, { marginTop: 1, flexShrink: 1, children: _jsxs(Text, { color: theme.planPrimary, bold: true, wrap: "wrap", children: [item.active ? "● " : "● ", item.text] }) }, item.id));
|
|
2647
|
-
case "thinking_transition": {
|
|
2648
|
-
const glyphColor = item.active ? THINKING_BORDER_COLORS[0] : theme.textDim;
|
|
2649
|
-
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));
|
|
2650
|
-
}
|
|
2651
|
-
case "model_transition": {
|
|
2652
|
-
const glyphColor = THINKING_BORDER_COLORS[0];
|
|
2653
|
-
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));
|
|
2654
|
-
}
|
|
2655
|
-
case "theme_transition": {
|
|
2656
|
-
const glyphColor = THINKING_BORDER_COLORS[0];
|
|
2657
|
-
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));
|
|
2658
|
-
}
|
|
2659
|
-
case "plan_event": {
|
|
2660
|
-
// Plan-domain status changes (approve / reject / dismiss). Uses
|
|
2661
|
-
// theme.planPrimary to match the existing plan_transition family,
|
|
2662
|
-
// distinct from the model/thinking gradient.
|
|
2663
|
-
const label = item.event === "approved"
|
|
2664
|
-
? "Plan approved"
|
|
2665
|
-
: item.event === "rejected"
|
|
2666
|
-
? "Plan rejected"
|
|
2667
|
-
: "Plan dismissed";
|
|
2668
|
-
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));
|
|
2669
|
-
}
|
|
2670
|
-
case "stopped":
|
|
2671
|
-
// Cancellation / abort acknowledgement (ESC, auto-setup cancel, etc.).
|
|
2672
|
-
// Muted dim treatment — this is an ack, not a state change worth a
|
|
2673
|
-
// gradient. Glyph `⊘` reads as "stop" without being alarming.
|
|
2674
|
-
return (_jsx(Box, { marginTop: 1, flexShrink: 1, children: _jsxs(Text, { color: theme.textDim, bold: true, children: ["⊘ ", item.text] }) }, item.id));
|
|
2675
|
-
case "step_done":
|
|
2676
|
-
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));
|
|
2677
|
-
case "queued":
|
|
2678
|
-
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
|
|
2679
|
-
? ` (+${item.imageCount} image${item.imageCount > 1 ? "s" : ""})`
|
|
2680
|
-
: ""] })] }, item.id));
|
|
2681
|
-
case "compacting":
|
|
2682
|
-
return _jsx(CompactionSpinner, { staticDisplay: true }, item.id);
|
|
2683
|
-
case "compacted":
|
|
2684
|
-
return (_jsx(CompactionDone, { originalCount: item.originalCount, newCount: item.newCount, tokensBefore: item.tokensBefore, tokensAfter: item.tokensAfter }, item.id));
|
|
2685
|
-
case "duration":
|
|
2686
|
-
return (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.textDim, children: ["✻ ", item.verb, " ", formatDuration(item.durationMs)] }) }, item.id));
|
|
2687
|
-
case "subagent_group":
|
|
2688
|
-
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();
|
|
2689
2387
|
}
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
if (props.
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
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) {
|
|
2454
|
+
const sysMsg = messagesRef.current[0];
|
|
2455
|
+
const newMessages = sysMsg && sysMsg.role === "system" ? [sysMsg] : messagesRef.current.slice(0, 1);
|
|
2456
|
+
const taskItem = { kind: "task", title, id: getId() };
|
|
2457
|
+
const sm = sessionManagerRef.current;
|
|
2458
|
+
void (async () => {
|
|
2722
2459
|
let newSessionPath;
|
|
2723
2460
|
if (sm) {
|
|
2724
2461
|
try {
|
|
2725
|
-
const
|
|
2726
|
-
newSessionPath =
|
|
2727
|
-
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 });
|
|
2728
2465
|
}
|
|
2729
2466
|
catch {
|
|
2730
|
-
//
|
|
2467
|
+
// Session creation is best-effort.
|
|
2731
2468
|
}
|
|
2732
2469
|
}
|
|
2733
|
-
if (props.sessionStore)
|
|
2470
|
+
if (props.sessionStore)
|
|
2734
2471
|
props.sessionStore.overlay = null;
|
|
2735
|
-
// Mirror through sessionStore so the post-resetUI remount can
|
|
2736
|
-
// re-seed the ref. markTaskDone() in the run-all onDone branch
|
|
2737
|
-
// reads currentTaskIdRef.current, which is seeded from this.
|
|
2738
|
-
props.sessionStore.currentTaskId = taskId;
|
|
2739
|
-
}
|
|
2740
|
-
currentTaskIdRef.current = taskId;
|
|
2741
2472
|
props.resetUI?.({
|
|
2742
2473
|
wipeSession: true,
|
|
2743
2474
|
messages: newMessages,
|
|
@@ -2748,10 +2479,7 @@ export function App(props) {
|
|
|
2748
2479
|
})();
|
|
2749
2480
|
return;
|
|
2750
2481
|
}
|
|
2751
|
-
|
|
2752
|
-
currentTaskIdRef.current = taskId;
|
|
2753
|
-
if (props.sessionStore)
|
|
2754
|
-
props.sessionStore.currentTaskId = taskId;
|
|
2482
|
+
clearPendingHistory();
|
|
2755
2483
|
setHistory([{ kind: "banner", id: "banner" }]);
|
|
2756
2484
|
setLiveItems([]);
|
|
2757
2485
|
messagesRef.current = messagesRef.current.slice(0, 1);
|
|
@@ -2759,940 +2487,585 @@ export function App(props) {
|
|
|
2759
2487
|
persistedIndexRef.current = messagesRef.current.length;
|
|
2760
2488
|
const sm = sessionManagerRef.current;
|
|
2761
2489
|
if (sm) {
|
|
2762
|
-
void sm.create(
|
|
2763
|
-
sessionPathRef.current =
|
|
2764
|
-
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 });
|
|
2765
2493
|
});
|
|
2766
2494
|
}
|
|
2767
2495
|
const taskItem = { kind: "task", title, id: getId() };
|
|
2768
2496
|
setLastUserMessage(title);
|
|
2769
2497
|
setDoneStatus(null);
|
|
2770
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;
|
|
2771
2679
|
void (async () => {
|
|
2772
2680
|
try {
|
|
2773
|
-
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() }]);
|
|
2774
2710
|
}
|
|
2775
2711
|
catch (err) {
|
|
2776
2712
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2777
|
-
log("ERROR", "error", msg);
|
|
2778
|
-
const isAbort = msg.includes("aborted") || msg.includes("abort");
|
|
2779
2713
|
setLiveItems((prev) => [
|
|
2780
2714
|
...prev,
|
|
2781
|
-
|
|
2782
|
-
? { kind: "stopped", text: "Request was stopped.", id: getId() }
|
|
2783
|
-
: toErrorItem(err, getId()),
|
|
2715
|
+
{ kind: "info", text: `Rewind failed: ${msg}`, id: getId() },
|
|
2784
2716
|
]);
|
|
2785
|
-
setRunAllTasks(false);
|
|
2786
2717
|
}
|
|
2787
2718
|
})();
|
|
2788
|
-
}, [props.
|
|
2789
|
-
const
|
|
2719
|
+
}, [props.checkpointStore, props.resetUI, agentLoop, messagesRef, persistedIndexRef]);
|
|
2720
|
+
const handleCloseRemountableOverlay = () => {
|
|
2790
2721
|
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
2791
|
-
props.sessionStore.overlay =
|
|
2792
|
-
if (kind !== "plan")
|
|
2793
|
-
props.sessionStore.planAutoExpand = false;
|
|
2722
|
+
props.sessionStore.overlay = null;
|
|
2794
2723
|
props.resetUI();
|
|
2724
|
+
return;
|
|
2795
2725
|
}
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
props.sessionStore.planAutoExpand = false;
|
|
2801
|
-
if (agentLoop.isRunning && kind !== "goal" && kind !== "plan") {
|
|
2802
|
-
props.sessionStore.pendingResetUI = true;
|
|
2803
|
-
}
|
|
2804
|
-
}
|
|
2805
|
-
if (kind !== "plan")
|
|
2806
|
-
setPlanAutoExpand(false);
|
|
2807
|
-
setOverlay(kind);
|
|
2726
|
+
if (props.sessionStore) {
|
|
2727
|
+
props.sessionStore.overlay = null;
|
|
2728
|
+
if (agentLoop.isRunning)
|
|
2729
|
+
props.sessionStore.pendingResetUI = true;
|
|
2808
2730
|
}
|
|
2809
|
-
|
|
2810
|
-
|
|
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 = () => {
|
|
2811
2758
|
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
2812
2759
|
props.sessionStore.overlay = null;
|
|
2760
|
+
props.sessionStore.goalAutoExpand = false;
|
|
2813
2761
|
props.resetUI();
|
|
2762
|
+
return;
|
|
2814
2763
|
}
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2764
|
+
if (props.sessionStore) {
|
|
2765
|
+
props.sessionStore.overlay = null;
|
|
2766
|
+
props.sessionStore.goalAutoExpand = false;
|
|
2767
|
+
if (agentLoop.isRunning)
|
|
2768
|
+
props.sessionStore.pendingResetUI = true;
|
|
2820
2769
|
}
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
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
|
+
});
|
|
2828
2782
|
appendGoalProgress({
|
|
2829
2783
|
kind: "goal_progress",
|
|
2830
|
-
phase: "
|
|
2831
|
-
title:
|
|
2832
|
-
detail:
|
|
2833
|
-
|
|
2834
|
-
status:
|
|
2784
|
+
phase: "terminal",
|
|
2785
|
+
title: `Goal blocked: ${run.title}`,
|
|
2786
|
+
detail: formatGoalBlockingPrerequisites(run),
|
|
2787
|
+
summaryRows: buildGoalSummaryRows(run),
|
|
2788
|
+
status: "blocked",
|
|
2835
2789
|
});
|
|
2836
|
-
agentLoop.queueMessage(eventText);
|
|
2837
2790
|
return;
|
|
2838
2791
|
}
|
|
2839
|
-
appendGoalProgress({
|
|
2840
|
-
kind: "goal_progress",
|
|
2841
|
-
phase: "orchestrator_reviewing",
|
|
2842
|
-
title: "Orchestrator reviewing Goal update",
|
|
2843
|
-
detail,
|
|
2844
|
-
workerId: eventInfo?.worker,
|
|
2845
|
-
status: eventInfo?.status,
|
|
2846
|
-
});
|
|
2847
|
-
setLastUserMessage("");
|
|
2848
|
-
setDoneStatus(null);
|
|
2849
|
-
void agentLoop.run(eventText).catch((err) => {
|
|
2850
|
-
log("ERROR", "goal", err instanceof Error ? err.message : String(err));
|
|
2851
|
-
setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
|
|
2852
|
-
});
|
|
2853
|
-
}, [agentLoop, appendGoalProgress]);
|
|
2854
|
-
const continueGoalRun = useCallback((runId) => {
|
|
2855
|
-
void (async () => {
|
|
2856
|
-
const latestRun = await reconcileActiveGoalRuns(props.cwd, {
|
|
2857
|
-
isWorkerActive: (workerId) => listGoalWorkers(props.cwd).some((worker) => worker.id === workerId && worker.status === "running"),
|
|
2858
|
-
}).then(({ runs }) => runs.find((item) => item.id === runId) ?? null);
|
|
2859
|
-
if (!latestRun) {
|
|
2860
|
-
runningGoalIdsRef.current.delete(runId);
|
|
2861
|
-
clearGoalStatusEntry(runId);
|
|
2862
|
-
return;
|
|
2863
|
-
}
|
|
2864
|
-
const decision = decideGoalNextAction(latestRun);
|
|
2865
|
-
if (decision.kind === "wait")
|
|
2866
|
-
return;
|
|
2867
|
-
if (decision.kind === "terminal" ||
|
|
2868
|
-
decision.kind === "blocked" ||
|
|
2869
|
-
decision.kind === "pause") {
|
|
2870
|
-
const status = decision.kind === "terminal"
|
|
2871
|
-
? decision.status
|
|
2872
|
-
: decision.kind === "blocked"
|
|
2873
|
-
? "blocked"
|
|
2874
|
-
: "paused";
|
|
2875
|
-
const nextRun = {
|
|
2876
|
-
...latestRun,
|
|
2877
|
-
status,
|
|
2878
|
-
continueRequestedAt: undefined,
|
|
2879
|
-
blockers: decision.kind === "blocked" || decision.kind === "pause"
|
|
2880
|
-
? Array.from(new Set([...latestRun.blockers, decision.reason]))
|
|
2881
|
-
: latestRun.blockers,
|
|
2882
|
-
};
|
|
2883
|
-
await upsertGoalRun(props.cwd, nextRun);
|
|
2884
|
-
await appendGoalDecision(props.cwd, latestRun.id, {
|
|
2885
|
-
kind: "continuation_stopped",
|
|
2886
|
-
reason: decision.reason,
|
|
2887
|
-
content: `terminal=${status}`,
|
|
2888
|
-
});
|
|
2889
|
-
const terminalProgress = formatGoalTerminalProgress(nextRun);
|
|
2890
|
-
if (terminalProgress)
|
|
2891
|
-
appendGoalProgress(terminalProgress);
|
|
2892
|
-
runningGoalIdsRef.current.delete(runId);
|
|
2893
|
-
clearGoalStatusEntry(runId);
|
|
2894
|
-
return;
|
|
2895
|
-
}
|
|
2896
|
-
let runForNextAction = latestRun;
|
|
2897
|
-
if (latestRun.continueRequestedAt &&
|
|
2898
|
-
!listGoalWorkers(props.cwd).some((worker) => worker.status === "running") &&
|
|
2899
|
-
activeVerifierRunIdsRef.current.size === 0) {
|
|
2900
|
-
await appendGoalDecision(props.cwd, latestRun.id, {
|
|
2901
|
-
kind: "continuation_consumed",
|
|
2902
|
-
reason: `Continuation request consumed by ${decision.kind}.`,
|
|
2903
|
-
});
|
|
2904
|
-
runForNextAction = await upsertGoalRun(props.cwd, {
|
|
2905
|
-
...latestRun,
|
|
2906
|
-
continueRequestedAt: undefined,
|
|
2907
|
-
});
|
|
2908
|
-
}
|
|
2909
|
-
appendGoalProgress({
|
|
2910
|
-
kind: "goal_progress",
|
|
2911
|
-
phase: "continuing",
|
|
2912
|
-
title: `Choosing next Goal step: ${latestRun.title}`,
|
|
2913
|
-
detail: "Latest result is recorded; starting the next worker task or verifier automatically.",
|
|
2914
|
-
status: latestRun.status,
|
|
2915
|
-
});
|
|
2916
|
-
upsertGoalStatusEntry({
|
|
2917
|
-
runId: latestRun.id,
|
|
2918
|
-
label: latestRun.title,
|
|
2919
|
-
phase: "orchestrating",
|
|
2920
|
-
startedAt: Date.now(),
|
|
2921
|
-
detail: "choosing next step",
|
|
2922
|
-
});
|
|
2923
|
-
startGoalRunRef.current(runForNextAction);
|
|
2924
|
-
})().catch((err) => {
|
|
2925
|
-
runningGoalIdsRef.current.delete(runId);
|
|
2926
|
-
clearGoalStatusEntry(runId);
|
|
2927
|
-
log("ERROR", "goal", err instanceof Error ? err.message : String(err));
|
|
2928
|
-
setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
|
|
2929
|
-
});
|
|
2930
|
-
}, [appendGoalProgress, clearGoalStatusEntry, props.cwd, upsertGoalStatusEntry]);
|
|
2931
|
-
const handleGoalWorkerComplete = useCallback((run, completion) => {
|
|
2932
|
-
const taskTitle = run.tasks.find((task) => task.id === completion.worker.goalTaskId)?.title ??
|
|
2933
|
-
completion.worker.goalTaskId;
|
|
2934
|
-
const eventText = formatGoalWorkerCompletionEvent(run, taskTitle, completion);
|
|
2935
|
-
void summarizeGoalCounts(completion.worker.cwd).then((counts) => setGoalCount(counts.active));
|
|
2936
|
-
appendGoalProgress({
|
|
2937
|
-
kind: "goal_progress",
|
|
2938
|
-
phase: "worker_finished",
|
|
2939
|
-
title: formatGoalWorkerFinishedTitle(taskTitle, completion.status),
|
|
2940
|
-
detail: summarizeGoalCompletion(completion.summary),
|
|
2941
|
-
workerId: completion.worker.id,
|
|
2942
|
-
status: completion.status,
|
|
2943
|
-
});
|
|
2944
|
-
upsertGoalStatusEntry({
|
|
2945
|
-
runId: run.id,
|
|
2946
|
-
label: taskTitle,
|
|
2947
|
-
phase: completion.status === "done" ? "reviewing" : "failed",
|
|
2948
|
-
startedAt: Date.now(),
|
|
2949
|
-
detail: completion.status === "done" ? "reviewing result" : "task failed",
|
|
2950
|
-
workerId: completion.worker.id,
|
|
2951
|
-
goalNumber: goalNumberForRun(run.id),
|
|
2952
|
-
});
|
|
2953
|
-
runGoalSyntheticEvent(eventText);
|
|
2954
2792
|
void (async () => {
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
if (
|
|
2958
|
-
return;
|
|
2959
|
-
const runs = await loadGoalRuns(completion.worker.cwd);
|
|
2960
|
-
const queued = runs.find((item) => item.continueRequestedAt && !goalHasBlockingPrerequisites(item));
|
|
2961
|
-
if (queued)
|
|
2962
|
-
setTimeout(() => continueGoalRun(queued.id), 750);
|
|
2963
|
-
})().catch((err) => log("ERROR", "goal", err instanceof Error ? err.message : String(err)));
|
|
2964
|
-
}, [
|
|
2965
|
-
appendGoalProgress,
|
|
2966
|
-
continueGoalRun,
|
|
2967
|
-
goalNumberForRun,
|
|
2968
|
-
runGoalSyntheticEvent,
|
|
2969
|
-
upsertGoalStatusEntry,
|
|
2970
|
-
]);
|
|
2971
|
-
useEffect(() => {
|
|
2972
|
-
return subscribeGoalWorkerCompletions((completion) => {
|
|
2973
|
-
void (async () => {
|
|
2974
|
-
const latestRun = (await loadGoalRuns(completion.worker.cwd)).find((item) => item.id === completion.worker.goalRunId) ?? null;
|
|
2975
|
-
if (!latestRun) {
|
|
2976
|
-
log("WARN", "goal", `Worker completion for unknown Goal ${completion.worker.goalRunId}`);
|
|
2977
|
-
return;
|
|
2978
|
-
}
|
|
2979
|
-
runningGoalIdsRef.current.add(latestRun.id);
|
|
2980
|
-
handleGoalWorkerComplete(latestRun, completion);
|
|
2981
|
-
})().catch((err) => {
|
|
2982
|
-
log("ERROR", "goal", err instanceof Error ? err.message : String(err));
|
|
2983
|
-
setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal")]);
|
|
2984
|
-
});
|
|
2985
|
-
}, props.cwd);
|
|
2986
|
-
}, [handleGoalWorkerComplete, props.cwd]);
|
|
2987
|
-
const startGoalRun = useCallback((run) => {
|
|
2988
|
-
runningGoalIdsRef.current.add(run.id);
|
|
2989
|
-
void (async () => {
|
|
2990
|
-
if (goalHasBlockingPrerequisites(run)) {
|
|
2991
|
-
setOverlay(null);
|
|
2992
|
-
const detail = formatGoalBlockingPrerequisites(run);
|
|
2993
|
-
await upsertGoalRun(props.cwd, {
|
|
2994
|
-
...run,
|
|
2995
|
-
status: "blocked",
|
|
2996
|
-
blockers: Array.from(new Set([...run.blockers, detail])),
|
|
2997
|
-
});
|
|
2998
|
-
setGoalCount((await summarizeGoalCounts(props.cwd)).active);
|
|
2999
|
-
appendGoalProgress({
|
|
3000
|
-
kind: "goal_progress",
|
|
3001
|
-
phase: "terminal",
|
|
3002
|
-
title: `Goal blocked: ${run.title}`,
|
|
3003
|
-
detail,
|
|
3004
|
-
status: "blocked",
|
|
3005
|
-
});
|
|
3006
|
-
runningGoalIdsRef.current.delete(run.id);
|
|
3007
|
-
clearGoalStatusEntry(run.id);
|
|
3008
|
-
return;
|
|
3009
|
-
}
|
|
3010
|
-
const decision = decideGoalNextAction(run);
|
|
3011
|
-
await appendGoalDecision(props.cwd, run.id, decision);
|
|
3012
|
-
if (decision.kind === "terminal") {
|
|
3013
|
-
const terminalProgress = formatGoalTerminalProgress(run);
|
|
3014
|
-
if (terminalProgress)
|
|
3015
|
-
appendGoalProgress(terminalProgress);
|
|
3016
|
-
runningGoalIdsRef.current.delete(run.id);
|
|
3017
|
-
clearGoalStatusEntry(run.id);
|
|
3018
|
-
return;
|
|
3019
|
-
}
|
|
3020
|
-
if (decision.kind === "wait") {
|
|
3021
|
-
appendGoalProgress({
|
|
3022
|
-
kind: "goal_progress",
|
|
3023
|
-
phase: "worker_started",
|
|
3024
|
-
title: decision.workerId ? `Goal working: ${run.title}` : `Goal active: ${run.title}`,
|
|
3025
|
-
detail: decision.reason,
|
|
3026
|
-
workerId: decision.workerId,
|
|
3027
|
-
});
|
|
2793
|
+
const checked = await runGoalPrerequisiteChecks(props.cwd, run);
|
|
2794
|
+
const current = await upsertGoalRun(props.cwd, checked.run);
|
|
2795
|
+
if (goalHasBlockingPrerequisites(current)) {
|
|
3028
2796
|
upsertGoalStatusEntry({
|
|
3029
|
-
runId:
|
|
3030
|
-
label:
|
|
3031
|
-
phase:
|
|
2797
|
+
runId: current.id,
|
|
2798
|
+
label: current.title,
|
|
2799
|
+
phase: "failed",
|
|
2800
|
+
detail: formatGoalBlockingPrerequisites(current),
|
|
3032
2801
|
startedAt: Date.now(),
|
|
3033
|
-
detail: decision.reason,
|
|
3034
|
-
workerId: decision.workerId,
|
|
3035
|
-
goalNumber: goalNumberForRun(run.id),
|
|
3036
2802
|
});
|
|
3037
|
-
return;
|
|
3038
|
-
}
|
|
3039
|
-
if (decision.kind === "complete") {
|
|
3040
|
-
await upsertGoalRun(props.cwd, { ...run, status: "passed" });
|
|
3041
|
-
setGoalCount((await summarizeGoalCounts(props.cwd)).active);
|
|
3042
2803
|
appendGoalProgress({
|
|
3043
2804
|
kind: "goal_progress",
|
|
3044
2805
|
phase: "terminal",
|
|
3045
|
-
title: `Goal
|
|
3046
|
-
detail:
|
|
3047
|
-
|
|
3048
|
-
});
|
|
3049
|
-
runningGoalIdsRef.current.delete(run.id);
|
|
3050
|
-
clearGoalStatusEntry(run.id);
|
|
3051
|
-
return;
|
|
3052
|
-
}
|
|
3053
|
-
if (decision.kind === "run_verifier") {
|
|
3054
|
-
await verifyGoalRun(run);
|
|
3055
|
-
return;
|
|
3056
|
-
}
|
|
3057
|
-
if (decision.kind === "create_task") {
|
|
3058
|
-
await updateGoalTask(props.cwd, run.id, `auto-${Date.now()}`, {
|
|
3059
|
-
title: decision.title,
|
|
3060
|
-
prompt: decision.prompt,
|
|
3061
|
-
status: "pending",
|
|
3062
|
-
});
|
|
3063
|
-
const latestRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id) ?? run;
|
|
3064
|
-
await upsertGoalRun(props.cwd, { ...latestRun, status: "ready" });
|
|
3065
|
-
setTimeout(() => continueGoalRun(run.id), 250);
|
|
3066
|
-
return;
|
|
3067
|
-
}
|
|
3068
|
-
if (decision.kind === "blocked") {
|
|
3069
|
-
await upsertGoalRun(props.cwd, {
|
|
3070
|
-
...run,
|
|
3071
|
-
status: "blocked",
|
|
3072
|
-
blockers: [...run.blockers, decision.reason],
|
|
3073
|
-
});
|
|
3074
|
-
setGoalCount((await summarizeGoalCounts(props.cwd)).active);
|
|
3075
|
-
appendGoalProgress({
|
|
3076
|
-
kind: "goal_progress",
|
|
3077
|
-
phase: "terminal",
|
|
3078
|
-
title: `Goal blocked: ${run.title}`,
|
|
3079
|
-
detail: decision.reason,
|
|
3080
|
-
status: "blocked",
|
|
3081
|
-
});
|
|
3082
|
-
runningGoalIdsRef.current.delete(run.id);
|
|
3083
|
-
clearGoalStatusEntry(run.id);
|
|
3084
|
-
return;
|
|
3085
|
-
}
|
|
3086
|
-
if (decision.kind === "pause") {
|
|
3087
|
-
await updateGoalTask(props.cwd, run.id, decision.task.id, {
|
|
2806
|
+
title: `Goal blocked: ${current.title}`,
|
|
2807
|
+
detail: formatGoalBlockingPrerequisites(current),
|
|
2808
|
+
summaryRows: buildGoalSummaryRows(current),
|
|
3088
2809
|
status: "blocked",
|
|
3089
|
-
attempts: decision.attempts,
|
|
3090
|
-
lastSummary: "Paused after worker attempt limit.",
|
|
3091
2810
|
});
|
|
3092
|
-
await upsertGoalRun(props.cwd, {
|
|
3093
|
-
...run,
|
|
3094
|
-
status: "paused",
|
|
3095
|
-
blockers: [...run.blockers, decision.reason],
|
|
3096
|
-
});
|
|
3097
|
-
await appendGoalEvidence(props.cwd, run.id, {
|
|
3098
|
-
kind: "summary",
|
|
3099
|
-
label: "Goal paused",
|
|
3100
|
-
content: decision.reason,
|
|
3101
|
-
});
|
|
3102
|
-
setGoalCount((await summarizeGoalCounts(props.cwd)).active);
|
|
3103
|
-
appendGoalProgress({
|
|
3104
|
-
kind: "goal_progress",
|
|
3105
|
-
phase: "terminal",
|
|
3106
|
-
title: `Goal paused: ${run.title}`,
|
|
3107
|
-
detail: decision.reason,
|
|
3108
|
-
status: "paused",
|
|
3109
|
-
});
|
|
3110
|
-
runningGoalIdsRef.current.delete(run.id);
|
|
3111
|
-
clearGoalStatusEntry(run.id);
|
|
3112
2811
|
return;
|
|
3113
2812
|
}
|
|
3114
|
-
await
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
model: currentModel,
|
|
3119
|
-
goalRunId: run.id,
|
|
3120
|
-
goalTaskId: decision.task.id,
|
|
3121
|
-
taskTitle: decision.task.title,
|
|
3122
|
-
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(),
|
|
3123
2817
|
});
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
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(),
|
|
3129
2825
|
});
|
|
3130
|
-
setOverlay(null);
|
|
3131
|
-
setGoalCount((await summarizeGoalCounts(props.cwd)).active);
|
|
3132
2826
|
appendGoalProgress({
|
|
3133
2827
|
kind: "goal_progress",
|
|
3134
|
-
phase: "
|
|
3135
|
-
title: `
|
|
3136
|
-
detail:
|
|
3137
|
-
|
|
3138
|
-
status: worker.status,
|
|
3139
|
-
});
|
|
3140
|
-
upsertGoalStatusEntry({
|
|
3141
|
-
runId: run.id,
|
|
3142
|
-
label: decision.task.title,
|
|
3143
|
-
phase: "worker",
|
|
3144
|
-
startedAt: Date.now(),
|
|
3145
|
-
detail: "background worker running",
|
|
3146
|
-
workerId: worker.id,
|
|
3147
|
-
goalNumber: goalNumberForRun(run.id),
|
|
2828
|
+
phase: "orchestrator_reviewing",
|
|
2829
|
+
title: `Goal ${goalNumberForRun(resumed.id)} continuing`,
|
|
2830
|
+
detail: resumed.title,
|
|
2831
|
+
status: resumed.status,
|
|
3148
2832
|
});
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
}, [
|
|
3155
|
-
props.cwd,
|
|
3156
|
-
currentProvider,
|
|
3157
|
-
currentModel,
|
|
3158
|
-
appendGoalProgress,
|
|
3159
|
-
clearGoalStatusEntry,
|
|
3160
|
-
goalNumberForRun,
|
|
3161
|
-
upsertGoalStatusEntry,
|
|
3162
|
-
]);
|
|
3163
|
-
const verifyGoalRun = useCallback(async (run) => {
|
|
2833
|
+
continueGoalRun(resumed);
|
|
2834
|
+
})();
|
|
2835
|
+
};
|
|
2836
|
+
startGoalRunRef.current = handleRunGoal;
|
|
2837
|
+
const handleVerifyGoal = (run) => {
|
|
3164
2838
|
if (!run.verifier?.command) {
|
|
3165
|
-
await appendGoalEvidence(props.cwd, run.id, {
|
|
3166
|
-
kind: "summary",
|
|
3167
|
-
label: "Missing verifier",
|
|
3168
|
-
content: "No verifier command is configured.",
|
|
3169
|
-
});
|
|
3170
|
-
await upsertGoalRun(props.cwd, {
|
|
3171
|
-
...run,
|
|
3172
|
-
status: "blocked",
|
|
3173
|
-
blockers: [...run.blockers, "No verifier command configured."],
|
|
3174
|
-
});
|
|
3175
2839
|
appendGoalProgress({
|
|
3176
2840
|
kind: "goal_progress",
|
|
3177
2841
|
phase: "terminal",
|
|
3178
|
-
title: `Goal
|
|
3179
|
-
detail: "
|
|
2842
|
+
title: `Goal verifier missing: ${run.title}`,
|
|
2843
|
+
detail: "Define a verifier command before running verification.",
|
|
3180
2844
|
status: "blocked",
|
|
3181
2845
|
});
|
|
3182
|
-
runningGoalIdsRef.current.delete(run.id);
|
|
3183
|
-
clearGoalStatusEntry(run.id);
|
|
3184
2846
|
return;
|
|
3185
2847
|
}
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
kind: "goal_progress",
|
|
3194
|
-
phase: "verifier_started",
|
|
3195
|
-
title: `Verifier started: ${run.title}`,
|
|
3196
|
-
detail: run.verifier.command,
|
|
3197
|
-
status: "verifying",
|
|
3198
|
-
});
|
|
3199
|
-
const startedAt = Date.now();
|
|
3200
|
-
const verifierTimeoutMs = Number(process.env.GG_GOAL_VERIFIER_TIMEOUT_MS ?? 10 * 60 * 1000);
|
|
3201
|
-
upsertGoalStatusEntry({
|
|
3202
|
-
runId: run.id,
|
|
3203
|
-
label: run.title,
|
|
3204
|
-
phase: "verifier",
|
|
3205
|
-
startedAt,
|
|
3206
|
-
detail: run.verifier.command,
|
|
3207
|
-
goalNumber: goalNumberForRun(run.id),
|
|
3208
|
-
});
|
|
3209
|
-
void runGoalVerifierCommand({
|
|
3210
|
-
cwd: props.cwd,
|
|
3211
|
-
runId: run.id,
|
|
3212
|
-
command: run.verifier.command,
|
|
3213
|
-
timeoutMs: verifierTimeoutMs,
|
|
3214
|
-
now: () => startedAt,
|
|
3215
|
-
})
|
|
3216
|
-
.then(async ({ verification, failureClass, durationMs }) => {
|
|
3217
|
-
activeVerifierRunIdsRef.current.delete(run.id);
|
|
3218
|
-
const status = verification.status;
|
|
3219
|
-
const summary = verification.summary;
|
|
3220
|
-
const outputPath = verification.outputPath;
|
|
3221
|
-
const latestRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id) ?? run;
|
|
3222
|
-
const runWithVerifier = {
|
|
3223
|
-
...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),
|
|
3224
2855
|
verifier: {
|
|
3225
|
-
...
|
|
3226
|
-
|
|
3227
|
-
command: run.verifier?.command,
|
|
3228
|
-
lastResult: verification,
|
|
2856
|
+
...run.verifier,
|
|
2857
|
+
lastResult: undefined,
|
|
3229
2858
|
},
|
|
3230
|
-
};
|
|
3231
|
-
const completionCheck = canCompleteGoalRun(runWithVerifier);
|
|
3232
|
-
const verifiedRun = await upsertGoalRun(props.cwd, {
|
|
3233
|
-
...runWithVerifier,
|
|
3234
|
-
continueRequestedAt: undefined,
|
|
3235
|
-
status: status === "pass" && completionCheck.ok
|
|
3236
|
-
? "passed"
|
|
3237
|
-
: status === "pass"
|
|
3238
|
-
? "ready"
|
|
3239
|
-
: "failed",
|
|
3240
|
-
});
|
|
3241
|
-
await appendGoalEvidence(props.cwd, run.id, {
|
|
3242
|
-
kind: "command",
|
|
3243
|
-
label: `Verifier ${status}`,
|
|
3244
|
-
content: `${failureClass}: ${summary}`.slice(0, 4000),
|
|
3245
|
-
path: outputPath,
|
|
3246
2859
|
});
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
await
|
|
3254
|
-
title: "Fix verifier failure",
|
|
3255
|
-
prompt: `Goal verifier failed after ${durationMs}ms. Original goal: ${run.goal}\n\n` +
|
|
3256
|
-
`Verifier command: ${run.verifier?.command}\n\n` +
|
|
3257
|
-
`Failure output:\n${summary.slice(-6000)}\n\nFix the cause, record evidence with the goals tool, and rerun relevant verification.`,
|
|
3258
|
-
status: "pending",
|
|
3259
|
-
});
|
|
3260
|
-
const runWithPendingFix = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id) ?? latestRun;
|
|
3261
|
-
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);
|
|
3262
2867
|
}
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
title: `Verifier ${status}: ${run.title}`,
|
|
3268
|
-
detail: summarizeGoalCompletion(summary),
|
|
3269
|
-
status,
|
|
3270
|
-
});
|
|
3271
|
-
upsertGoalStatusEntry({
|
|
3272
|
-
runId: run.id,
|
|
3273
|
-
label: run.title,
|
|
3274
|
-
phase: status === "pass" ? "reviewing" : "failed",
|
|
3275
|
-
startedAt: Date.now(),
|
|
3276
|
-
detail: status === "pass" ? "reviewing verifier evidence" : "verifier failed",
|
|
3277
|
-
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."],
|
|
3278
2872
|
});
|
|
3279
|
-
const eventText = formatGoalVerifierCompletionEvent(verifiedRun, status === "pass" ? "pass" : "fail", run.verifier?.command ?? "", verification.exitCode ?? 1, summary);
|
|
3280
|
-
runGoalSyntheticEvent(eventText);
|
|
3281
|
-
const continuationRun = (await loadGoalRuns(props.cwd)).find((item) => item.id === run.id);
|
|
3282
|
-
if (continuationRun?.continueRequestedAt && status === "pass") {
|
|
3283
|
-
setTimeout(() => continueGoalRun(run.id), 500);
|
|
3284
|
-
}
|
|
3285
|
-
})
|
|
3286
|
-
.catch((err) => {
|
|
3287
|
-
activeVerifierRunIdsRef.current.delete(run.id);
|
|
3288
2873
|
clearGoalStatusEntry(run.id);
|
|
3289
|
-
log("ERROR", "goal", err instanceof Error ? err.message : String(err));
|
|
3290
|
-
setLiveItems((prev) => [...prev, toErrorItem(err, getId(), "Goal verifier")]);
|
|
3291
|
-
});
|
|
3292
|
-
}, [
|
|
3293
|
-
props.cwd,
|
|
3294
|
-
appendGoalProgress,
|
|
3295
|
-
clearGoalStatusEntry,
|
|
3296
|
-
goalNumberForRun,
|
|
3297
|
-
runGoalSyntheticEvent,
|
|
3298
|
-
upsertGoalStatusEntry,
|
|
3299
|
-
]);
|
|
3300
|
-
const pauseGoalRun = useCallback((run) => {
|
|
3301
|
-
void (async () => {
|
|
3302
|
-
runningGoalIdsRef.current.delete(run.id);
|
|
3303
|
-
if (run.activeWorkerId)
|
|
3304
|
-
await stopGoalWorker(run.activeWorkerId);
|
|
3305
|
-
await upsertGoalRun(props.cwd, { ...run, status: "paused", activeWorkerId: undefined });
|
|
3306
|
-
setGoalCount((await summarizeGoalCounts(props.cwd)).active);
|
|
3307
2874
|
appendGoalProgress({
|
|
3308
2875
|
kind: "goal_progress",
|
|
3309
2876
|
phase: "terminal",
|
|
3310
|
-
title: `Goal paused: ${
|
|
3311
|
-
detail: "
|
|
2877
|
+
title: `Goal paused: ${paused.title}`,
|
|
2878
|
+
detail: "Paused by user from Goal pane.",
|
|
2879
|
+
summaryRows: buildGoalSummaryRows(paused),
|
|
3312
2880
|
status: "paused",
|
|
3313
2881
|
});
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
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
|
+
}
|
|
3325
2901
|
if (props.sessionStore) {
|
|
3326
|
-
props.sessionStore.
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
2902
|
+
props.sessionStore.overlay = null;
|
|
2903
|
+
props.sessionStore.planAutoExpand = false;
|
|
2904
|
+
if (agentLoop.isRunning)
|
|
2905
|
+
props.sessionStore.pendingResetUI = true;
|
|
3330
2906
|
}
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
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;
|
|
3336
2927
|
void (async () => {
|
|
3337
2928
|
try {
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
//
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
//
|
|
3347
|
-
//
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
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;
|
|
3354
2944
|
}
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
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;
|
|
3369
2962
|
}
|
|
3370
|
-
//
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
setupHintShownRef.current = false;
|
|
3376
|
-
const detectedForPixelFix = detectLanguages(prep.projectPath);
|
|
3377
|
-
injectedLanguagesRef.current = detectedForPixelFix;
|
|
3378
|
-
const newSystemPrompt = await rebuildSystemPrompt({
|
|
3379
|
-
cwd: prep.projectPath,
|
|
3380
|
-
planMode: false,
|
|
3381
|
-
clearApprovedPlan: true,
|
|
3382
|
-
activeLanguages: detectedForPixelFix,
|
|
3383
|
-
tools: toolsForPixelFix,
|
|
3384
|
-
});
|
|
3385
|
-
// Now that the cwd swap is committed, reset chat. Do not clear the
|
|
3386
|
-
// 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();
|
|
3387
2968
|
setHistory([{ kind: "banner", id: "banner" }]);
|
|
3388
2969
|
setLiveItems([]);
|
|
3389
|
-
|
|
2970
|
+
setPlanAutoExpand(false);
|
|
2971
|
+
setOverlay(null);
|
|
2972
|
+
messagesRef.current = [{ role: "system", content: newPrompt }];
|
|
3390
2973
|
agentLoop.reset();
|
|
3391
2974
|
persistedIndexRef.current = messagesRef.current.length;
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
}
|
|
3402
|
-
else {
|
|
3403
|
-
messagesRef.current.unshift({ role: "system", content: newSystemPrompt });
|
|
3404
|
-
}
|
|
3405
|
-
const title = `Fix ${errorId.slice(0, 12)}… in ${prep.projectName}`;
|
|
3406
|
-
const taskItem = { kind: "task", title, id: getId() };
|
|
3407
|
-
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
|
+
]);
|
|
3408
2984
|
setDoneStatus(null);
|
|
3409
|
-
|
|
3410
|
-
await agentLoop.run(prep.prompt);
|
|
2985
|
+
await agentLoop.run("The plan has been approved. Implement it now, following each step in order.");
|
|
3411
2986
|
}
|
|
3412
2987
|
catch (err) {
|
|
3413
|
-
const
|
|
3414
|
-
log("ERROR", "
|
|
3415
|
-
currentPixelFixRef.current = null;
|
|
3416
|
-
setRunAllPixel(false);
|
|
2988
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
2989
|
+
log("ERROR", "error", errMsg);
|
|
3417
2990
|
setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
|
|
3418
2991
|
}
|
|
3419
2992
|
})();
|
|
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
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
if (agentLoop.isRunning)
|
|
3498
|
-
props.sessionStore.pendingResetUI = true;
|
|
3499
|
-
}
|
|
3500
|
-
setOverlay(null);
|
|
3501
|
-
}
|
|
3502
|
-
}, onFixOne: (entry) => {
|
|
3503
|
-
setOverlay(null);
|
|
3504
|
-
startPixelFix(entry.errorId);
|
|
3505
|
-
}, onFixAll: (entries) => {
|
|
3506
|
-
const first = entries.find((e) => e.status === "open") ?? entries[0];
|
|
3507
|
-
if (!first)
|
|
3508
|
-
return;
|
|
3509
|
-
setOverlay(null);
|
|
3510
|
-
setRunAllPixel(true);
|
|
3511
|
-
startPixelFix(first.errorId);
|
|
3512
|
-
} })) : isSkillsView ? (_jsx(SkillsOverlay, { cwd: props.cwd, onClose: () => {
|
|
3513
|
-
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
3514
|
-
props.sessionStore.overlay = null;
|
|
3515
|
-
props.resetUI();
|
|
3516
|
-
}
|
|
3517
|
-
else {
|
|
3518
|
-
if (props.sessionStore) {
|
|
3519
|
-
props.sessionStore.overlay = null;
|
|
3520
|
-
if (agentLoop.isRunning)
|
|
3521
|
-
props.sessionStore.pendingResetUI = true;
|
|
3522
|
-
}
|
|
3523
|
-
setOverlay(null);
|
|
3524
|
-
}
|
|
3525
|
-
} })) : isEyesView ? (_jsx(EyesOverlay, { cwd: props.cwd, onClose: () => {
|
|
3526
|
-
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
3527
|
-
props.sessionStore.overlay = null;
|
|
3528
|
-
props.resetUI();
|
|
3529
|
-
}
|
|
3530
|
-
else {
|
|
3531
|
-
if (props.sessionStore) {
|
|
3532
|
-
props.sessionStore.overlay = null;
|
|
3533
|
-
if (agentLoop.isRunning)
|
|
3534
|
-
props.sessionStore.pendingResetUI = true;
|
|
3535
|
-
}
|
|
3536
|
-
setEyesCount(isEyesActive(props.cwd) ? journalCount({ status: "open" }, props.cwd) : undefined);
|
|
3537
|
-
setOverlay(null);
|
|
3538
|
-
}
|
|
3539
|
-
}, onQueueMessage: (msg) => {
|
|
3540
|
-
agentLoop.queueMessage(msg);
|
|
3541
|
-
} })) : isPlanView ? (_jsx(PlanOverlay, { cwd: props.cwd, autoExpandNewest: planAutoExpand, onClose: () => {
|
|
3542
|
-
planOverlayPendingRef.current = false;
|
|
3543
|
-
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
3544
|
-
props.sessionStore.overlay = null;
|
|
3545
|
-
props.sessionStore.planAutoExpand = false;
|
|
3546
|
-
props.resetUI();
|
|
3547
|
-
}
|
|
3548
|
-
else {
|
|
3549
|
-
if (props.sessionStore) {
|
|
3550
|
-
props.sessionStore.overlay = null;
|
|
3551
|
-
props.sessionStore.planAutoExpand = false;
|
|
3552
|
-
if (agentLoop.isRunning)
|
|
3553
|
-
props.sessionStore.pendingResetUI = true;
|
|
3554
|
-
}
|
|
3555
|
-
setPlanAutoExpand(false);
|
|
3556
|
-
setOverlay(null);
|
|
3557
|
-
}
|
|
3558
|
-
}, onApprove: (planPath) => {
|
|
3559
|
-
log("INFO", "plan", "Plan approved — transitioning to implementation", {
|
|
3560
|
-
planPath,
|
|
3561
|
-
});
|
|
3562
|
-
planOverlayPendingRef.current = false;
|
|
3563
|
-
void (async () => {
|
|
3564
|
-
try {
|
|
3565
|
-
// Read plan steps for progress tracking — handed to the new
|
|
3566
|
-
// mount via sessionStore.planSteps below.
|
|
3567
|
-
const planContent = await import("node:fs/promises").then(({ readFile }) => readFile(planPath, "utf-8"));
|
|
3568
|
-
const steps = extractPlanSteps(planContent);
|
|
3569
|
-
// Build the new system prompt with the approved plan baked in.
|
|
3570
|
-
const newPrompt = await rebuildSystemPrompt({
|
|
3571
|
-
planMode: false,
|
|
3572
|
-
approvedPlanPath: planPath,
|
|
3573
|
-
});
|
|
3574
|
-
// Create a new session file BEFORE remount so the new tree
|
|
3575
|
-
// picks it up via sessionStore.sessionPath.
|
|
3576
|
-
let newSessionPath;
|
|
3577
|
-
const sm = sessionManagerRef.current;
|
|
3578
|
-
if (sm) {
|
|
3579
|
-
const s = await sm.create(props.cwd, currentProvider, currentModel);
|
|
3580
|
-
newSessionPath = s.path;
|
|
3581
|
-
}
|
|
3582
|
-
if (props.resetUI && props.sessionStore) {
|
|
3583
|
-
// Clear the overlay so the new mount lands on the chat,
|
|
3584
|
-
// not back inside the plan pane.
|
|
3585
|
-
props.sessionStore.overlay = null;
|
|
3586
|
-
props.sessionStore.planAutoExpand = false;
|
|
3587
|
-
props.resetUI({
|
|
3588
|
-
wipeSession: true,
|
|
3589
|
-
messages: [{ role: "system", content: newPrompt }],
|
|
3590
|
-
approvedPlanPath: planPath,
|
|
3591
|
-
planSteps: steps,
|
|
3592
|
-
sessionPath: newSessionPath,
|
|
3593
|
-
pendingAction: {
|
|
3594
|
-
prompt: "The plan has been approved. Implement it now, following each step in order.",
|
|
3595
|
-
planEvent: { event: "approved" },
|
|
3596
|
-
},
|
|
3597
|
-
});
|
|
3598
|
-
return;
|
|
3599
|
-
}
|
|
3600
|
-
// Fallback path (resetUI not wired — tests). Mutate in place.
|
|
3601
|
-
approvedPlanPathRef.current = planPath;
|
|
3602
|
-
planStepsRef.current = steps;
|
|
3603
|
-
setPlanSteps(steps);
|
|
3604
|
-
setHistory([{ kind: "banner", id: "banner" }]);
|
|
3605
|
-
setLiveItems([]);
|
|
3606
|
-
setPlanAutoExpand(false);
|
|
3607
|
-
setOverlay(null);
|
|
3608
|
-
messagesRef.current = [{ role: "system", content: newPrompt }];
|
|
3609
|
-
agentLoop.reset();
|
|
3610
|
-
persistedIndexRef.current = messagesRef.current.length;
|
|
3611
|
-
if (newSessionPath)
|
|
3612
|
-
sessionPathRef.current = newSessionPath;
|
|
3613
|
-
setLiveItems([
|
|
3614
|
-
{
|
|
3615
|
-
kind: "info",
|
|
3616
|
-
text: "Plan approved — starting fresh session for implementation",
|
|
3617
|
-
id: getId(),
|
|
3618
|
-
},
|
|
3619
|
-
]);
|
|
3620
|
-
setDoneStatus(null);
|
|
3621
|
-
await agentLoop.run("The plan has been approved. Implement it now, following each step in order.");
|
|
3622
|
-
}
|
|
3623
|
-
catch (err) {
|
|
3624
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
3625
|
-
log("ERROR", "error", errMsg);
|
|
3626
|
-
setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
|
|
3627
|
-
}
|
|
3628
|
-
})();
|
|
3629
|
-
}, onReject: (planPath, feedback) => {
|
|
3630
|
-
planOverlayPendingRef.current = false;
|
|
3631
|
-
const rejectionMsg = `The plan at ${planPath} was rejected.\n\nFeedback: ${feedback}\n\n` +
|
|
3632
|
-
`Please revise the plan based on this feedback.`;
|
|
3633
|
-
if (props.resetUI && props.sessionStore) {
|
|
3634
|
-
props.sessionStore.overlay = null;
|
|
3635
|
-
props.sessionStore.planAutoExpand = false;
|
|
3636
|
-
// No wipeSession — keep history, messages, plan mode etc. The
|
|
3637
|
-
// agent picks up the rejection mid-conversation.
|
|
3638
|
-
props.resetUI({
|
|
3639
|
-
pendingAction: {
|
|
3640
|
-
prompt: rejectionMsg,
|
|
3641
|
-
planEvent: { event: "rejected", detail: feedback },
|
|
3642
|
-
},
|
|
3643
|
-
});
|
|
3644
|
-
return;
|
|
3645
|
-
}
|
|
3646
|
-
setPlanAutoExpand(false);
|
|
3647
|
-
setOverlay(null);
|
|
3648
|
-
setDoneStatus(null);
|
|
3649
|
-
setLiveItems((prev) => [
|
|
3650
|
-
...prev,
|
|
3651
|
-
{ kind: "info", text: `Plan rejected — "${feedback}"`, id: getId() },
|
|
3652
|
-
]);
|
|
3653
|
-
void agentLoop.run(rejectionMsg).catch((err) => {
|
|
3654
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
3655
|
-
log("ERROR", "error", errMsg);
|
|
3656
|
-
setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
|
|
3657
|
-
});
|
|
3658
|
-
} })) : (_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 &&
|
|
3659
|
-
!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: () => {
|
|
3660
|
-
// Just flip the overlay state — Ink's log-update handles the
|
|
3661
|
-
// live-area transition (chat input → TaskOverlay) natively, and
|
|
3662
|
-
// the chat history above stays in scrollback. When the overlay
|
|
3663
|
-
// closes, the history is still there (banner included).
|
|
3664
|
-
openOverlay("tasks");
|
|
3665
|
-
}, onToggleGoal: () => {
|
|
3666
|
-
openOverlay("goal");
|
|
3667
|
-
}, onToggleSkills: () => {
|
|
3668
|
-
openOverlay("skills");
|
|
3669
|
-
}, onTogglePixel: () => {
|
|
3670
|
-
openOverlay("pixel");
|
|
3671
|
-
}, onTogglePlanMode: () => {
|
|
3672
|
-
const next = !planMode;
|
|
3673
|
-
setPlanMode(next);
|
|
3674
|
-
log("INFO", "plan", `Plan mode ${next ? "enabled" : "disabled"}`);
|
|
3675
|
-
setLiveItems((items) => [
|
|
3676
|
-
...items,
|
|
3677
|
-
{
|
|
3678
|
-
kind: "plan_transition",
|
|
3679
|
-
text: next ? "Plan Mode Activated" : "Plan Mode Deactivated",
|
|
3680
|
-
active: next,
|
|
3681
|
-
id: getId(),
|
|
3682
|
-
},
|
|
3683
|
-
]);
|
|
3684
|
-
}, 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 ||
|
|
3685
|
-
footerStatusLayout.hasEyesSignals ||
|
|
3686
|
-
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 ||
|
|
3687
|
-
(!footerStatusLayout.hasBackgroundTasks && !footerStatusLayout.hasEyesSignals)
|
|
3688
|
-
? 1
|
|
3689
|
-
: 2, paddingRight: 1, children: _jsx(Text, { color: theme.success, bold: true, wrap: "truncate", children: "\u2728 Update ready \u00B7 restart to apply" }) }))] }))] }))] }));
|
|
3690
|
-
}
|
|
3691
|
-
function formatRepoMapCommandOutput(enabled, markdown, refreshed) {
|
|
3692
|
-
const status = enabled ? "on" : "off";
|
|
3693
|
-
const prefix = refreshed
|
|
3694
|
-
? `Dynamic repo map refreshed · injection: ${status}`
|
|
3695
|
-
: `Dynamic repo map · injection: ${status}`;
|
|
3696
|
-
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 })) }));
|
|
3697
3070
|
}
|
|
3698
3071
|
//# sourceMappingURL=App.js.map
|