@compilr-dev/cli 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +254 -68
- package/dist/.tsbuildinfo.app +1 -0
- package/dist/.tsbuildinfo.data +1 -0
- package/dist/.tsbuildinfo.domain +1 -0
- package/dist/.tsbuildinfo.foundation +1 -0
- package/dist/agent.d.ts +134 -4
- package/dist/agent.js +345 -166
- package/dist/anchors/index.d.ts +9 -0
- package/dist/anchors/index.js +9 -0
- package/dist/anchors/project-anchors.d.ts +79 -0
- package/dist/anchors/project-anchors.js +202 -0
- package/dist/auth/api-client.d.ts +124 -0
- package/dist/auth/api-client.js +261 -0
- package/dist/auth/index.d.ts +172 -0
- package/dist/auth/index.js +545 -0
- package/dist/auth/storage.d.ts +52 -0
- package/dist/auth/storage.js +118 -0
- package/dist/changelog/index.d.ts +16 -0
- package/dist/changelog/index.js +24 -0
- package/dist/changelog/releases.d.ts +17 -0
- package/dist/changelog/releases.js +63 -0
- package/dist/commands/index.d.ts +2 -1
- package/dist/commands-v2/handlers/auth.d.ts +10 -0
- package/dist/commands-v2/handlers/auth.js +118 -0
- package/dist/commands-v2/handlers/background.d.ts +14 -0
- package/dist/commands-v2/handlers/background.js +276 -0
- package/dist/commands-v2/handlers/context.d.ts +13 -0
- package/dist/commands-v2/handlers/context.js +533 -0
- package/dist/commands-v2/handlers/core.d.ts +14 -0
- package/dist/commands-v2/handlers/core.js +290 -0
- package/dist/commands-v2/handlers/debug.d.ts +11 -0
- package/dist/commands-v2/handlers/debug.js +177 -0
- package/dist/commands-v2/handlers/delegations.d.ts +8 -0
- package/dist/commands-v2/handlers/delegations.js +29 -0
- package/dist/commands-v2/handlers/files.d.ts +8 -0
- package/dist/commands-v2/handlers/files.js +162 -0
- package/dist/commands-v2/handlers/filter.d.ts +9 -0
- package/dist/commands-v2/handlers/filter.js +130 -0
- package/dist/commands-v2/handlers/games.d.ts +7 -0
- package/dist/commands-v2/handlers/games.js +57 -0
- package/dist/commands-v2/handlers/index.d.ts +25 -0
- package/dist/commands-v2/handlers/index.js +63 -0
- package/dist/commands-v2/handlers/mcp.d.ts +8 -0
- package/dist/commands-v2/handlers/mcp.js +39 -0
- package/dist/commands-v2/handlers/notifications.d.ts +9 -0
- package/dist/commands-v2/handlers/notifications.js +34 -0
- package/dist/commands-v2/handlers/project.d.ts +22 -0
- package/dist/commands-v2/handlers/project.js +1035 -0
- package/dist/commands-v2/handlers/reset.d.ts +11 -0
- package/dist/commands-v2/handlers/reset.js +118 -0
- package/dist/commands-v2/handlers/session.d.ts +161 -0
- package/dist/commands-v2/handlers/session.js +805 -0
- package/dist/commands-v2/handlers/settings.d.ts +17 -0
- package/dist/commands-v2/handlers/settings.js +417 -0
- package/dist/commands-v2/handlers/tasks.d.ts +5 -0
- package/dist/commands-v2/handlers/tasks.js +36 -0
- package/dist/commands-v2/handlers/team.d.ts +9 -0
- package/dist/commands-v2/handlers/team.js +549 -0
- package/dist/commands-v2/handlers/terminals.d.ts +9 -0
- package/dist/commands-v2/handlers/terminals.js +34 -0
- package/dist/commands-v2/index.d.ts +14 -0
- package/dist/commands-v2/index.js +18 -0
- package/dist/commands-v2/registry.d.ts +52 -0
- package/dist/commands-v2/registry.js +114 -0
- package/dist/commands-v2/types.d.ts +153 -0
- package/dist/commands-v2/types.js +7 -0
- package/dist/commands.js +123 -7
- package/dist/compilr-diff-companion.vsix +0 -0
- package/dist/db/index.js +98 -4
- package/dist/db/repositories/document-repository.d.ts +2 -0
- package/dist/db/repositories/document-repository.js +5 -1
- package/dist/db/repositories/index.d.ts +2 -0
- package/dist/db/repositories/index.js +1 -0
- package/dist/db/repositories/plan-repository.d.ts +101 -0
- package/dist/db/repositories/plan-repository.js +275 -0
- package/dist/db/repositories/project-repository.d.ts +6 -0
- package/dist/db/repositories/project-repository.js +41 -0
- package/dist/db/repositories/work-item-repository.d.ts +15 -0
- package/dist/db/repositories/work-item-repository.js +69 -4
- package/dist/db/schema.d.ts +40 -3
- package/dist/db/schema.js +66 -3
- package/dist/episodes/index.d.ts +20 -0
- package/dist/episodes/index.js +27 -0
- package/dist/episodes/recorder.d.ts +51 -0
- package/dist/episodes/recorder.js +195 -0
- package/dist/episodes/significant-work.d.ts +21 -0
- package/dist/episodes/significant-work.js +56 -0
- package/dist/episodes/store.d.ts +38 -0
- package/dist/episodes/store.js +199 -0
- package/dist/episodes/types.d.ts +35 -0
- package/dist/episodes/types.js +6 -0
- package/dist/episodes/work-at-risk.d.ts +12 -0
- package/dist/episodes/work-at-risk.js +38 -0
- package/dist/episodes/work-summary-anchor.d.ts +23 -0
- package/dist/episodes/work-summary-anchor.js +73 -0
- package/dist/games/coins.d.ts +66 -0
- package/dist/games/coins.js +165 -0
- package/dist/games/game-base.d.ts +84 -0
- package/dist/games/game-base.js +204 -0
- package/dist/games/index.d.ts +16 -0
- package/dist/games/index.js +49 -0
- package/dist/games/scores.d.ts +69 -0
- package/dist/games/scores.js +191 -0
- package/dist/games/tetris/board.d.ts +59 -0
- package/dist/games/tetris/board.js +170 -0
- package/dist/games/tetris/index.d.ts +109 -0
- package/dist/games/tetris/index.js +610 -0
- package/dist/games/tetris/pieces.d.ts +44 -0
- package/dist/games/tetris/pieces.js +271 -0
- package/dist/games/tetris/renderer.d.ts +26 -0
- package/dist/games/tetris/renderer.js +77 -0
- package/dist/guide/guide-content.d.ts +23 -0
- package/dist/guide/guide-content.js +196 -0
- package/dist/guide/index.d.ts +8 -0
- package/dist/guide/index.js +7 -0
- package/dist/guide/shared-content.d.ts +37 -0
- package/dist/guide/shared-content.js +1272 -0
- package/dist/guide/tutorial-helpers.d.ts +57 -0
- package/dist/guide/tutorial-helpers.js +147 -0
- package/dist/handlers/ask-user-handlers.d.ts +32 -0
- package/dist/handlers/ask-user-handlers.js +104 -0
- package/dist/handlers/delegation-handlers.d.ts +34 -0
- package/dist/handlers/delegation-handlers.js +291 -0
- package/dist/handlers/permission-handler.d.ts +30 -0
- package/dist/handlers/permission-handler.js +205 -0
- package/dist/index.d.ts +11 -1
- package/dist/index.js +615 -179
- package/dist/input-handlers/index.d.ts +7 -0
- package/dist/input-handlers/index.js +7 -0
- package/dist/input-handlers/memory-handler.d.ts +26 -0
- package/dist/input-handlers/memory-handler.js +69 -0
- package/dist/models/index.d.ts +10 -0
- package/dist/models/index.js +12 -0
- package/dist/models/model-registry.d.ts +38 -0
- package/dist/models/model-registry.js +69 -0
- package/dist/models/model-tiers.d.ts +28 -0
- package/dist/models/model-tiers.js +71 -0
- package/dist/models/model-validation.d.ts +25 -0
- package/dist/models/model-validation.js +291 -0
- package/dist/models/ollama-models.d.ts +73 -0
- package/dist/models/ollama-models.js +178 -0
- package/dist/models/provider-types.d.ts +6 -0
- package/dist/models/provider-types.js +1 -0
- package/dist/models/providers.d.ts +35 -0
- package/dist/models/providers.js +58 -0
- package/dist/models/types.d.ts +4 -0
- package/dist/models/types.js +4 -0
- package/dist/multi-agent/activity.d.ts +21 -0
- package/dist/multi-agent/activity.js +34 -0
- package/dist/multi-agent/agent-selection.d.ts +55 -0
- package/dist/multi-agent/agent-selection.js +90 -0
- package/dist/multi-agent/artifacts.d.ts +197 -0
- package/dist/multi-agent/artifacts.js +379 -0
- package/dist/multi-agent/checkpointer.d.ts +138 -0
- package/dist/multi-agent/checkpointer.js +471 -0
- package/dist/multi-agent/collision-utils.d.ts +16 -0
- package/dist/multi-agent/collision-utils.js +28 -0
- package/dist/multi-agent/context-resolver.d.ts +97 -0
- package/dist/multi-agent/context-resolver.js +316 -0
- package/dist/multi-agent/custom-agents.d.ts +83 -0
- package/dist/multi-agent/custom-agents.js +227 -0
- package/dist/multi-agent/delegation-tracker.d.ts +157 -0
- package/dist/multi-agent/delegation-tracker.js +243 -0
- package/dist/multi-agent/file-lock-hook.d.ts +29 -0
- package/dist/multi-agent/file-lock-hook.js +97 -0
- package/dist/multi-agent/file-locks.d.ts +58 -0
- package/dist/multi-agent/file-locks.js +194 -0
- package/dist/multi-agent/index.d.ts +24 -0
- package/dist/multi-agent/index.js +30 -0
- package/dist/multi-agent/mention-parser.d.ts +64 -0
- package/dist/multi-agent/mention-parser.js +146 -0
- package/dist/multi-agent/notification-manager.d.ts +84 -0
- package/dist/multi-agent/notification-manager.js +224 -0
- package/dist/multi-agent/pending-requests.d.ts +122 -0
- package/dist/multi-agent/pending-requests.js +155 -0
- package/dist/multi-agent/session-registry.d.ts +139 -0
- package/dist/multi-agent/session-registry.js +514 -0
- package/dist/multi-agent/shared-context.d.ts +293 -0
- package/dist/multi-agent/shared-context.js +671 -0
- package/dist/multi-agent/skill-requirements.d.ts +66 -0
- package/dist/multi-agent/skill-requirements.js +178 -0
- package/dist/multi-agent/task-assignment.d.ts +69 -0
- package/dist/multi-agent/task-assignment.js +123 -0
- package/dist/multi-agent/task-suggestion.d.ts +31 -0
- package/dist/multi-agent/task-suggestion.js +72 -0
- package/dist/multi-agent/team-agent.d.ts +201 -0
- package/dist/multi-agent/team-agent.js +488 -0
- package/dist/multi-agent/team.d.ts +286 -0
- package/dist/multi-agent/team.js +610 -0
- package/dist/multi-agent/tool-config.d.ts +110 -0
- package/dist/multi-agent/tool-config.js +661 -0
- package/dist/multi-agent/types.d.ts +211 -0
- package/dist/multi-agent/types.js +617 -0
- package/dist/prompts/plan-mode-prompt.d.ts +11 -0
- package/dist/prompts/plan-mode-prompt.js +95 -0
- package/dist/repl-helpers.d.ts +63 -0
- package/dist/repl-helpers.js +319 -0
- package/dist/repl-v2.d.ts +554 -0
- package/dist/repl-v2.js +3286 -0
- package/dist/session/index.d.ts +6 -0
- package/dist/session/index.js +6 -0
- package/dist/session/project-session-manager.d.ts +158 -0
- package/dist/session/project-session-manager.js +650 -0
- package/dist/settings/index.d.ts +156 -13
- package/dist/settings/index.js +377 -24
- package/dist/settings/mcp-config.d.ts +76 -0
- package/dist/settings/mcp-config.js +143 -0
- package/dist/settings/paths.d.ts +114 -0
- package/dist/settings/paths.js +270 -0
- package/dist/shared-handlers.d.ts +62 -0
- package/dist/shared-handlers.js +48 -0
- package/dist/system-prompt/builder.d.ts +5 -0
- package/dist/system-prompt/builder.js +4 -0
- package/dist/system-prompt/index.d.ts +18 -0
- package/dist/system-prompt/index.js +18 -0
- package/dist/system-prompt/modules.d.ts +5 -0
- package/dist/system-prompt/modules.js +4 -0
- package/dist/tabbed-menu.js +2 -1
- package/dist/templates/compilr-md-import.d.ts +16 -0
- package/dist/templates/compilr-md-import.js +241 -0
- package/dist/templates/compilr-md.js +10 -58
- package/dist/templates/config-json.d.ts +1 -25
- package/dist/templates/index.d.ts +2 -0
- package/dist/templates/index.js +35 -75
- package/dist/themes/colors.js +3 -1
- package/dist/themes/registry.d.ts +5 -36
- package/dist/themes/registry.js +11 -95
- package/dist/themes/types.d.ts +3 -38
- package/dist/themes/types.js +2 -2
- package/dist/tool-names.d.ts +108 -0
- package/dist/tool-names.js +227 -0
- package/dist/tools/anchor-tools.d.ts +31 -0
- package/dist/tools/anchor-tools.js +255 -0
- package/dist/tools/artifact-tools.d.ts +42 -0
- package/dist/tools/artifact-tools.js +328 -0
- package/dist/tools/ask-user-simple.d.ts +1 -1
- package/dist/tools/ask-user-simple.js +2 -1
- package/dist/tools/ask-user.d.ts +1 -1
- package/dist/tools/ask-user.js +2 -1
- package/dist/tools/backlog-wrappers.d.ts +56 -0
- package/dist/tools/backlog-wrappers.js +353 -0
- package/dist/tools/backlog.d.ts +2 -2
- package/dist/tools/backlog.js +2 -2
- package/dist/tools/db-tools.d.ts +12 -0
- package/dist/tools/db-tools.js +14 -0
- package/dist/tools/delegate-background.d.ts +27 -0
- package/dist/tools/delegate-background.js +115 -0
- package/dist/tools/delegate.d.ts +22 -0
- package/dist/tools/delegate.js +97 -0
- package/dist/tools/delegation-status.d.ts +16 -0
- package/dist/tools/delegation-status.js +128 -0
- package/dist/tools/document-db.d.ts +43 -0
- package/dist/tools/document-db.js +220 -0
- package/dist/tools/guide-tool.d.ts +12 -0
- package/dist/tools/guide-tool.js +59 -0
- package/dist/tools/handoff.d.ts +25 -0
- package/dist/tools/handoff.js +99 -0
- package/dist/tools/meta-tools.d.ts +26 -0
- package/dist/tools/meta-tools.js +47 -0
- package/dist/tools/plan-tools.d.ts +54 -0
- package/dist/tools/plan-tools.js +338 -0
- package/dist/tools/platform-adapter.d.ts +29 -0
- package/dist/tools/platform-adapter.js +394 -0
- package/dist/tools/project-db.d.ts +34 -0
- package/dist/tools/project-db.js +39 -0
- package/dist/tools/recall-work-tool.d.ts +18 -0
- package/dist/tools/recall-work-tool.js +82 -0
- package/dist/tools/workitem-db.d.ts +135 -0
- package/dist/tools/workitem-db.js +730 -0
- package/dist/tools.d.ts +67 -2
- package/dist/tools.js +238 -38
- package/dist/ui/ask-user-overlay.d.ts +2 -2
- package/dist/ui/ask-user-overlay.js +443 -535
- package/dist/ui/ask-user-simple-overlay.d.ts +2 -2
- package/dist/ui/ask-user-simple-overlay.js +182 -209
- package/dist/ui/autocomplete-controller.d.ts +42 -0
- package/dist/ui/autocomplete-controller.js +384 -0
- package/dist/ui/base/index.d.ts +26 -0
- package/dist/ui/base/index.js +33 -0
- package/dist/ui/base/inline-overlay-utils.d.ts +217 -0
- package/dist/ui/base/inline-overlay-utils.js +320 -0
- package/dist/ui/base/inline-overlay.d.ts +159 -0
- package/dist/ui/base/inline-overlay.js +257 -0
- package/dist/ui/base/key-utils.d.ts +15 -0
- package/dist/ui/base/key-utils.js +30 -0
- package/dist/ui/base/overlay-base-v2.d.ts +203 -0
- package/dist/ui/base/overlay-base-v2.js +260 -0
- package/dist/ui/base/overlay-base.d.ts +156 -0
- package/dist/ui/base/overlay-base.js +238 -0
- package/dist/ui/base/overlay-lifecycle.d.ts +65 -0
- package/dist/ui/base/overlay-lifecycle.js +159 -0
- package/dist/ui/base/overlay-types.d.ts +185 -0
- package/dist/ui/base/overlay-types.js +7 -0
- package/dist/ui/base/render-utils.d.ts +27 -0
- package/dist/ui/base/render-utils.js +36 -0
- package/dist/ui/base/screen-stack.d.ts +148 -0
- package/dist/ui/base/screen-stack.js +184 -0
- package/dist/ui/base/tabbed-list-overlay-v2.d.ts +118 -0
- package/dist/ui/base/tabbed-list-overlay-v2.js +335 -0
- package/dist/ui/base/tabbed-list-overlay.d.ts +153 -0
- package/dist/ui/base/tabbed-list-overlay.js +369 -0
- package/dist/ui/constants/labels.d.ts +14 -0
- package/dist/ui/constants/labels.js +51 -0
- package/dist/ui/conversation-store.d.ts +55 -0
- package/dist/ui/conversation-store.js +107 -0
- package/dist/ui/conversation.d.ts +75 -4
- package/dist/ui/conversation.js +376 -165
- package/dist/ui/diff.d.ts +7 -1
- package/dist/ui/diff.js +85 -48
- package/dist/ui/ephemeral.d.ts +1 -1
- package/dist/ui/ephemeral.js +4 -10
- package/dist/ui/features/index.d.ts +34 -0
- package/dist/ui/features/index.js +34 -0
- package/dist/ui/features/input-feature.d.ts +85 -0
- package/dist/ui/features/input-feature.js +238 -0
- package/dist/ui/features/list-feature.d.ts +155 -0
- package/dist/ui/features/list-feature.js +244 -0
- package/dist/ui/features/pagination-feature.d.ts +154 -0
- package/dist/ui/features/pagination-feature.js +238 -0
- package/dist/ui/features/search-feature.d.ts +148 -0
- package/dist/ui/features/search-feature.js +185 -0
- package/dist/ui/features/tab-feature.d.ts +194 -0
- package/dist/ui/features/tab-feature.js +307 -0
- package/dist/ui/file-autocomplete.d.ts +24 -0
- package/dist/ui/file-autocomplete.js +56 -0
- package/dist/ui/footer-renderer.d.ts +69 -0
- package/dist/ui/footer-renderer.js +431 -0
- package/dist/ui/footer.d.ts +181 -7
- package/dist/ui/footer.js +523 -74
- package/dist/ui/guardrail-overlay.d.ts +29 -0
- package/dist/ui/guardrail-overlay.js +145 -0
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/index.js +1 -3
- package/dist/ui/input-controller.d.ts +51 -0
- package/dist/ui/input-controller.js +176 -0
- package/dist/ui/input-prompt.d.ts +135 -33
- package/dist/ui/input-prompt.js +728 -337
- package/dist/ui/iteration-limit-overlay.d.ts +2 -2
- package/dist/ui/iteration-limit-overlay.js +92 -128
- package/dist/ui/keyboard-handler.d.ts +57 -0
- package/dist/ui/keyboard-handler.js +557 -0
- package/dist/ui/keys-overlay.d.ts +1 -0
- package/dist/ui/keys-overlay.js +203 -141
- package/dist/ui/line-utils.d.ts +88 -0
- package/dist/ui/line-utils.js +150 -0
- package/dist/ui/live-region-facade.d.ts +42 -0
- package/dist/ui/live-region-facade.js +205 -0
- package/dist/ui/live-region.d.ts +157 -0
- package/dist/ui/live-region.js +379 -0
- package/dist/ui/mascot/expressions.d.ts +32 -0
- package/dist/ui/mascot/expressions.js +213 -0
- package/dist/ui/mascot/index.d.ts +8 -0
- package/dist/ui/mascot/index.js +8 -0
- package/dist/ui/mascot/renderer.d.ts +19 -0
- package/dist/ui/mascot/renderer.js +132 -0
- package/dist/ui/overlay/data/tutorial-content.d.ts +9 -0
- package/dist/ui/overlay/data/tutorial-content.js +9 -0
- package/dist/ui/overlay/data/tutorial-registry.d.ts +12 -0
- package/dist/ui/overlay/data/tutorial-registry.js +116 -0
- package/dist/ui/overlay/data/tutorial-types.d.ts +35 -0
- package/dist/ui/overlay/data/tutorial-types.js +6 -0
- package/dist/ui/overlay/data/tutorials/basics/first-conversation.d.ts +7 -0
- package/dist/ui/overlay/data/tutorials/basics/first-conversation.js +220 -0
- package/dist/ui/overlay/data/tutorials/basics/first-project.d.ts +7 -0
- package/dist/ui/overlay/data/tutorials/basics/first-project.js +284 -0
- package/dist/ui/overlay/data/tutorials/basics/navigation.d.ts +8 -0
- package/dist/ui/overlay/data/tutorials/basics/navigation.js +22 -0
- package/dist/ui/overlay/data/tutorials/basics/welcome.d.ts +7 -0
- package/dist/ui/overlay/data/tutorials/basics/welcome.js +174 -0
- package/dist/ui/overlay/data/tutorials/config/context-management.d.ts +7 -0
- package/dist/ui/overlay/data/tutorials/config/context-management.js +158 -0
- package/dist/ui/overlay/data/tutorials/config/mcp-servers.d.ts +8 -0
- package/dist/ui/overlay/data/tutorials/config/mcp-servers.js +155 -0
- package/dist/ui/overlay/data/tutorials/config/model-selection.d.ts +7 -0
- package/dist/ui/overlay/data/tutorials/config/model-selection.js +162 -0
- package/dist/ui/overlay/data/tutorials/config/permissions-safety.d.ts +7 -0
- package/dist/ui/overlay/data/tutorials/config/permissions-safety.js +163 -0
- package/dist/ui/overlay/data/tutorials/config/settings-config.d.ts +7 -0
- package/dist/ui/overlay/data/tutorials/config/settings-config.js +166 -0
- package/dist/ui/overlay/data/tutorials/planning/arch.d.ts +7 -0
- package/dist/ui/overlay/data/tutorials/planning/arch.js +168 -0
- package/dist/ui/overlay/data/tutorials/planning/backlog.d.ts +7 -0
- package/dist/ui/overlay/data/tutorials/planning/backlog.js +103 -0
- package/dist/ui/overlay/data/tutorials/planning/build.d.ts +7 -0
- package/dist/ui/overlay/data/tutorials/planning/build.js +173 -0
- package/dist/ui/overlay/data/tutorials/planning/design.d.ts +7 -0
- package/dist/ui/overlay/data/tutorials/planning/design.js +205 -0
- package/dist/ui/overlay/data/tutorials/planning/docs.d.ts +7 -0
- package/dist/ui/overlay/data/tutorials/planning/docs.js +143 -0
- package/dist/ui/overlay/data/tutorials/planning/prd.d.ts +7 -0
- package/dist/ui/overlay/data/tutorials/planning/prd.js +173 -0
- package/dist/ui/overlay/data/tutorials/planning/scaffold.d.ts +7 -0
- package/dist/ui/overlay/data/tutorials/planning/scaffold.js +164 -0
- package/dist/ui/overlay/data/tutorials/planning/sketch.d.ts +7 -0
- package/dist/ui/overlay/data/tutorials/planning/sketch.js +58 -0
- package/dist/ui/overlay/data/tutorials/projects/anchors.d.ts +7 -0
- package/dist/ui/overlay/data/tutorials/projects/anchors.js +248 -0
- package/dist/ui/overlay/data/tutorials/projects/import-project.d.ts +7 -0
- package/dist/ui/overlay/data/tutorials/projects/import-project.js +172 -0
- package/dist/ui/overlay/data/tutorials/projects/managing-projects.d.ts +8 -0
- package/dist/ui/overlay/data/tutorials/projects/managing-projects.js +212 -0
- package/dist/ui/overlay/data/tutorials/projects/new-project.d.ts +7 -0
- package/dist/ui/overlay/data/tutorials/projects/new-project.js +251 -0
- package/dist/ui/overlay/data/tutorials/projects/session-management.d.ts +7 -0
- package/dist/ui/overlay/data/tutorials/projects/session-management.js +169 -0
- package/dist/ui/overlay/data/tutorials/teams/background-execution.d.ts +7 -0
- package/dist/ui/overlay/data/tutorials/teams/background-execution.js +171 -0
- package/dist/ui/overlay/data/tutorials/teams/multi-terminal.d.ts +8 -0
- package/dist/ui/overlay/data/tutorials/teams/multi-terminal.js +147 -0
- package/dist/ui/overlay/data/tutorials/teams/task-assignment.d.ts +7 -0
- package/dist/ui/overlay/data/tutorials/teams/task-assignment.js +204 -0
- package/dist/ui/overlay/data/tutorials/teams/team-overview.d.ts +7 -0
- package/dist/ui/overlay/data/tutorials/teams/team-overview.js +165 -0
- package/dist/ui/overlay/data/tutorials/teams/working-with-agents.d.ts +7 -0
- package/dist/ui/overlay/data/tutorials/teams/working-with-agents.js +172 -0
- package/dist/ui/overlay/impl/agents-overlay-v2.d.ts +45 -0
- package/dist/ui/overlay/impl/agents-overlay-v2.js +814 -0
- package/dist/ui/overlay/impl/anchors-overlay-v2.d.ts +47 -0
- package/dist/ui/overlay/impl/anchors-overlay-v2.js +749 -0
- package/dist/ui/overlay/impl/arch-type-overlay-v2.d.ts +37 -0
- package/dist/ui/overlay/impl/arch-type-overlay-v2.js +240 -0
- package/dist/ui/overlay/impl/artifact-detail-overlay-v2.d.ts +43 -0
- package/dist/ui/overlay/impl/artifact-detail-overlay-v2.js +232 -0
- package/dist/ui/overlay/impl/artifact-overlay-v2.d.ts +40 -0
- package/dist/ui/overlay/impl/artifact-overlay-v2.js +115 -0
- package/dist/ui/overlay/impl/ask-user-overlay-v2.d.ts +72 -0
- package/dist/ui/overlay/impl/ask-user-overlay-v2.js +581 -0
- package/dist/ui/overlay/impl/ask-user-simple-overlay-v2.d.ts +46 -0
- package/dist/ui/overlay/impl/ask-user-simple-overlay-v2.js +204 -0
- package/dist/ui/overlay/impl/background-overlay-v2.d.ts +40 -0
- package/dist/ui/overlay/impl/background-overlay-v2.js +147 -0
- package/dist/ui/overlay/impl/backlog-overlay-v2.d.ts +52 -0
- package/dist/ui/overlay/impl/backlog-overlay-v2.js +681 -0
- package/dist/ui/overlay/impl/changelog-overlay-v2.d.ts +44 -0
- package/dist/ui/overlay/impl/changelog-overlay-v2.js +165 -0
- package/dist/ui/overlay/impl/commands-overlay-v2.d.ts +33 -0
- package/dist/ui/overlay/impl/commands-overlay-v2.js +439 -0
- package/dist/ui/overlay/impl/config-overlay-v2.d.ts +111 -0
- package/dist/ui/overlay/impl/config-overlay-v2.js +718 -0
- package/dist/ui/overlay/impl/custom-agent-form-overlay-v2.d.ts +83 -0
- package/dist/ui/overlay/impl/custom-agent-form-overlay-v2.js +711 -0
- package/dist/ui/overlay/impl/dashboard-overlay-v2.d.ts +57 -0
- package/dist/ui/overlay/impl/dashboard-overlay-v2.js +382 -0
- package/dist/ui/overlay/impl/delegations-overlay-v2.d.ts +28 -0
- package/dist/ui/overlay/impl/delegations-overlay-v2.js +279 -0
- package/dist/ui/overlay/impl/docs-overlay-v2.d.ts +45 -0
- package/dist/ui/overlay/impl/docs-overlay-v2.js +117 -0
- package/dist/ui/overlay/impl/document-detail-overlay-v2.d.ts +84 -0
- package/dist/ui/overlay/impl/document-detail-overlay-v2.js +1112 -0
- package/dist/ui/overlay/impl/filter-overlay-v2.d.ts +41 -0
- package/dist/ui/overlay/impl/filter-overlay-v2.js +110 -0
- package/dist/ui/overlay/impl/games-overlay-v2.d.ts +31 -0
- package/dist/ui/overlay/impl/games-overlay-v2.js +135 -0
- package/dist/ui/overlay/impl/guardrail-overlay-v2.d.ts +43 -0
- package/dist/ui/overlay/impl/guardrail-overlay-v2.js +114 -0
- package/dist/ui/overlay/impl/help-overlay-v2.d.ts +57 -0
- package/dist/ui/overlay/impl/help-overlay-v2.js +287 -0
- package/dist/ui/overlay/impl/init-setup-overlay-v2.d.ts +25 -0
- package/dist/ui/overlay/impl/init-setup-overlay-v2.js +97 -0
- package/dist/ui/overlay/impl/iteration-limit-overlay-v2.d.ts +35 -0
- package/dist/ui/overlay/impl/iteration-limit-overlay-v2.js +105 -0
- package/dist/ui/overlay/impl/keys-overlay-v2.d.ts +41 -0
- package/dist/ui/overlay/impl/keys-overlay-v2.js +248 -0
- package/dist/ui/overlay/impl/login-overlay-v2.d.ts +49 -0
- package/dist/ui/overlay/impl/login-overlay-v2.js +277 -0
- package/dist/ui/overlay/impl/mascot-overlay-v2.d.ts +41 -0
- package/dist/ui/overlay/impl/mascot-overlay-v2.js +138 -0
- package/dist/ui/overlay/impl/mcp-overlay-v2.d.ts +63 -0
- package/dist/ui/overlay/impl/mcp-overlay-v2.js +907 -0
- package/dist/ui/overlay/impl/model-overlay-v2.d.ts +93 -0
- package/dist/ui/overlay/impl/model-overlay-v2.js +1143 -0
- package/dist/ui/overlay/impl/model-warning-overlay-v2.d.ts +46 -0
- package/dist/ui/overlay/impl/model-warning-overlay-v2.js +132 -0
- package/dist/ui/overlay/impl/new-overlay-v2.d.ts +108 -0
- package/dist/ui/overlay/impl/new-overlay-v2.js +1243 -0
- package/dist/ui/overlay/impl/notifications-overlay-v2.d.ts +20 -0
- package/dist/ui/overlay/impl/notifications-overlay-v2.js +116 -0
- package/dist/ui/overlay/impl/onboarding-wizard-overlay-v2.d.ts +76 -0
- package/dist/ui/overlay/impl/onboarding-wizard-overlay-v2.js +728 -0
- package/dist/ui/overlay/impl/pending-overlay-v2.d.ts +51 -0
- package/dist/ui/overlay/impl/pending-overlay-v2.js +445 -0
- package/dist/ui/overlay/impl/permission-overlay-v2.d.ts +36 -0
- package/dist/ui/overlay/impl/permission-overlay-v2.js +380 -0
- package/dist/ui/overlay/impl/permissions-overlay-v2.d.ts +85 -0
- package/dist/ui/overlay/impl/permissions-overlay-v2.js +820 -0
- package/dist/ui/overlay/impl/plan-approval-overlay-v2.d.ts +35 -0
- package/dist/ui/overlay/impl/plan-approval-overlay-v2.js +181 -0
- package/dist/ui/overlay/impl/project-edit-overlay-v2.d.ts +36 -0
- package/dist/ui/overlay/impl/project-edit-overlay-v2.js +195 -0
- package/dist/ui/overlay/impl/projects-overlay-v2.d.ts +37 -0
- package/dist/ui/overlay/impl/projects-overlay-v2.js +733 -0
- package/dist/ui/overlay/impl/reset-overlay-v2.d.ts +39 -0
- package/dist/ui/overlay/impl/reset-overlay-v2.js +107 -0
- package/dist/ui/overlay/impl/resume-overlay-v2.d.ts +60 -0
- package/dist/ui/overlay/impl/resume-overlay-v2.js +414 -0
- package/dist/ui/overlay/impl/session-mode-overlay-v2.d.ts +43 -0
- package/dist/ui/overlay/impl/session-mode-overlay-v2.js +124 -0
- package/dist/ui/overlay/impl/tasks-overlay-v2.d.ts +28 -0
- package/dist/ui/overlay/impl/tasks-overlay-v2.js +283 -0
- package/dist/ui/overlay/impl/team-overlay-v2.d.ts +86 -0
- package/dist/ui/overlay/impl/team-overlay-v2.js +692 -0
- package/dist/ui/overlay/impl/terminals-overlay-v2.d.ts +26 -0
- package/dist/ui/overlay/impl/terminals-overlay-v2.js +217 -0
- package/dist/ui/overlay/impl/theme-overlay-v2.d.ts +42 -0
- package/dist/ui/overlay/impl/theme-overlay-v2.js +135 -0
- package/dist/ui/overlay/impl/tools-overlay-v2.d.ts +47 -0
- package/dist/ui/overlay/impl/tools-overlay-v2.js +214 -0
- package/dist/ui/overlay/impl/tutorial-overlay-v2.d.ts +45 -0
- package/dist/ui/overlay/impl/tutorial-overlay-v2.js +212 -0
- package/dist/ui/overlay/impl/workflow-overlay-v2.d.ts +80 -0
- package/dist/ui/overlay/impl/workflow-overlay-v2.js +641 -0
- package/dist/ui/overlay/index.d.ts +52 -0
- package/dist/ui/overlay/index.js +54 -0
- package/dist/ui/overlay/key-utils.d.ts +6 -0
- package/dist/ui/overlay/key-utils.js +6 -0
- package/dist/ui/overlay/types.d.ts +140 -0
- package/dist/ui/overlay/types.js +22 -0
- package/dist/ui/overlay-manager.d.ts +43 -0
- package/dist/ui/overlay-manager.js +238 -0
- package/dist/ui/overlays.d.ts +0 -4
- package/dist/ui/overlays.js +4 -460
- package/dist/ui/permission-overlay.d.ts +1 -1
- package/dist/ui/permission-overlay.js +189 -300
- package/dist/ui/providers/types.d.ts +178 -0
- package/dist/ui/providers/types.js +9 -0
- package/dist/ui/render-modes.d.ts +36 -0
- package/dist/ui/render-modes.js +44 -0
- package/dist/ui/startup-menu.d.ts +36 -0
- package/dist/ui/startup-menu.js +236 -0
- package/dist/ui/status-bar-controller.d.ts +33 -0
- package/dist/ui/status-bar-controller.js +99 -0
- package/dist/ui/subagent-renderer.d.ts +117 -0
- package/dist/ui/subagent-renderer.js +318 -0
- package/dist/ui/terminal-autocomplete-utils.d.ts +23 -0
- package/dist/ui/terminal-autocomplete-utils.js +83 -0
- package/dist/ui/terminal-codes.d.ts +94 -0
- package/dist/ui/terminal-codes.js +124 -0
- package/dist/ui/terminal-line-builders.d.ts +17 -0
- package/dist/ui/terminal-line-builders.js +42 -0
- package/dist/ui/terminal-render-item.d.ts +16 -0
- package/dist/ui/terminal-render-item.js +267 -0
- package/dist/ui/terminal-renderer.d.ts +220 -0
- package/dist/ui/terminal-renderer.js +750 -0
- package/dist/ui/terminal-types.d.ts +179 -0
- package/dist/ui/terminal-types.js +34 -0
- package/dist/ui/terminal-ui.d.ts +331 -0
- package/dist/ui/terminal-ui.js +819 -0
- package/dist/ui/terminal.d.ts +20 -0
- package/dist/ui/terminal.js +72 -0
- package/dist/ui/todo-zone.d.ts +19 -1
- package/dist/ui/todo-zone.js +124 -38
- package/dist/ui/tool-formatters.d.ts +16 -0
- package/dist/ui/tool-formatters.js +1027 -0
- package/dist/ui/turn-metrics.d.ts +56 -0
- package/dist/ui/turn-metrics.js +75 -0
- package/dist/ui/types.d.ts +28 -0
- package/dist/ui/types.js +1 -0
- package/dist/ui/vscode-diff-ipc.d.ts +102 -0
- package/dist/ui/vscode-diff-ipc.js +385 -0
- package/dist/utils/credentials.d.ts +24 -5
- package/dist/utils/credentials.js +123 -9
- package/dist/utils/debug-log.d.ts +28 -0
- package/dist/utils/debug-log.js +57 -0
- package/dist/utils/format-tokens.d.ts +13 -0
- package/dist/utils/format-tokens.js +18 -0
- package/dist/utils/git-config.d.ts +26 -0
- package/dist/utils/git-config.js +54 -0
- package/dist/utils/message-utils.d.ts +61 -0
- package/dist/utils/message-utils.js +72 -0
- package/dist/utils/model-tiers.d.ts +8 -1
- package/dist/utils/model-tiers.js +39 -17
- package/dist/utils/open-browser.d.ts +5 -0
- package/dist/utils/open-browser.js +32 -0
- package/dist/utils/path-safety.d.ts +56 -0
- package/dist/utils/path-safety.js +240 -0
- package/dist/utils/project-detection.d.ts +58 -0
- package/dist/utils/project-detection.js +424 -0
- package/dist/utils/project-memory.js +2 -1
- package/dist/utils/project-status.d.ts +2 -2
- package/dist/utils/startup-perf.d.ts +18 -0
- package/dist/utils/startup-perf.js +60 -0
- package/dist/utils/token-tracker.d.ts +62 -0
- package/dist/utils/token-tracker.js +150 -0
- package/dist/utils/token-types.d.ts +23 -0
- package/dist/utils/token-types.js +18 -0
- package/dist/utils/types/config-types.d.ts +32 -0
- package/dist/utils/types/config-types.js +8 -0
- package/dist/utils/update-checker.d.ts +28 -0
- package/dist/utils/update-checker.js +106 -0
- package/dist/utils/version.d.ts +7 -0
- package/dist/utils/version.js +10 -0
- package/dist/utils/vscode-detect.d.ts +39 -0
- package/dist/utils/vscode-detect.js +137 -0
- package/dist/workflow/guided-mode-injector.d.ts +42 -0
- package/dist/workflow/guided-mode-injector.js +191 -0
- package/dist/workflow/index.d.ts +8 -0
- package/dist/workflow/index.js +8 -0
- package/dist/workflow/step-criteria.d.ts +62 -0
- package/dist/workflow/step-criteria.js +150 -0
- package/dist/workflow/step-tracker.d.ts +92 -0
- package/dist/workflow/step-tracker.js +141 -0
- package/package.json +32 -12
- package/dist/index.old.d.ts +0 -7
- package/dist/index.old.js +0 -1014
- package/dist/repl.d.ts +0 -121
- package/dist/repl.js +0 -1878
- package/dist/templates/claude-md.d.ts +0 -7
- package/dist/templates/claude-md.js +0 -189
- package/dist/test-autocomplete.d.ts +0 -7
- package/dist/test-autocomplete.js +0 -85
- package/dist/test-tabbed-menu.d.ts +0 -7
- package/dist/test-tabbed-menu.js +0 -25
- package/dist/tool-selector.d.ts +0 -71
- package/dist/tool-selector.js +0 -184
- package/dist/ui/agents-overlay.d.ts +0 -12
- package/dist/ui/agents-overlay.js +0 -501
- package/dist/ui/arch-type-overlay.d.ts +0 -20
- package/dist/ui/arch-type-overlay.js +0 -229
- package/dist/ui/backlog-overlay.d.ts +0 -17
- package/dist/ui/backlog-overlay.js +0 -786
- package/dist/ui/commands-overlay.d.ts +0 -11
- package/dist/ui/commands-overlay.js +0 -410
- package/dist/ui/config-overlay.d.ts +0 -34
- package/dist/ui/config-overlay.js +0 -977
- package/dist/ui/init-overlay.d.ts +0 -24
- package/dist/ui/init-overlay.js +0 -525
- package/dist/ui/input-prompt-v2.d.ts +0 -179
- package/dist/ui/input-prompt-v2.js +0 -991
- package/dist/ui/model-warning-overlay.d.ts +0 -30
- package/dist/ui/model-warning-overlay.js +0 -171
- package/dist/ui/tools-overlay.d.ts +0 -26
- package/dist/ui/tools-overlay.js +0 -278
- package/dist/ui/tutorial-overlay.d.ts +0 -10
- package/dist/ui/tutorial-overlay.js +0 -936
package/dist/repl-v2.js
ADDED
|
@@ -0,0 +1,3286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REPL V2 - New Terminal UI based REPL
|
|
3
|
+
*
|
|
4
|
+
* Uses TerminalUI with:
|
|
5
|
+
* - Persistent footer (todo list + input prompt)
|
|
6
|
+
* - Overlay system for commands
|
|
7
|
+
* - Real agent integration (or simulation mode for testing)
|
|
8
|
+
*
|
|
9
|
+
* Can be run standalone: npx tsx src/repl-v2.ts
|
|
10
|
+
* Or imported and used with a real agent from index.ts
|
|
11
|
+
*/
|
|
12
|
+
import { appendFileSync } from 'fs';
|
|
13
|
+
import { generateEditDiff, generateWriteDiff } from './ui/diff.js';
|
|
14
|
+
import { getPendingRequestsManager, setActiveSharedContext } from './multi-agent/index.js';
|
|
15
|
+
import { parseInputForMentions, hasReferences, buildMessageWithContext, ContextResolver, buildContextMap, } from './multi-agent/index.js';
|
|
16
|
+
import { getTeamCheckpointer } from './multi-agent/checkpointer.js';
|
|
17
|
+
import { getDelegationTracker } from './multi-agent/delegation-tracker.js';
|
|
18
|
+
import { initSessionRegistry, getSessionRegistry, setActiveTerminalSessionId, getActiveTerminalSessionId } from './multi-agent/session-registry.js';
|
|
19
|
+
import { initFileLockManager } from './multi-agent/file-locks.js';
|
|
20
|
+
import { initNotificationManager, getNotificationManager } from './multi-agent/notification-manager.js';
|
|
21
|
+
import { getSessionsPath } from './settings/paths.js';
|
|
22
|
+
import { setMetaToolFilter } from './tools.js';
|
|
23
|
+
import { getCurrentProject } from './tools/project-db.js';
|
|
24
|
+
import { TerminalUI } from './ui/terminal-ui.js';
|
|
25
|
+
import * as terminal from './ui/terminal.js';
|
|
26
|
+
import { getStyles } from './themes/index.js';
|
|
27
|
+
import { getSettings, getStartupMode, syncPermissionModeFromUI, getPermissionMode, permissionModeToAgentMode, getProjectSessionMode, isFirstRunComplete, getSessionRetentionDays, updateSettings } from './settings/index.js';
|
|
28
|
+
import { renderMascotWithLogo } from './ui/mascot/renderer.js';
|
|
29
|
+
import { getStartupHighlights } from './changelog/index.js';
|
|
30
|
+
import { registerCommands, executeCommand, allCommands, getAutocompleteCommands, saveCurrentSession, saveCurrentTeam, loadProjectSession, archiveCurrentSession, convertMessagesToItems, } from './commands-v2/index.js';
|
|
31
|
+
import { getCustomCommandRegistry } from './commands/custom-registry.js';
|
|
32
|
+
import { getPlanModePrompt } from './prompts/plan-mode-prompt.js';
|
|
33
|
+
import { planRepository } from './db/repositories/index.js';
|
|
34
|
+
import { PlanApprovalOverlayV2, } from './ui/overlay/impl/plan-approval-overlay-v2.js';
|
|
35
|
+
import { PermissionOverlayV2, } from './ui/overlay/impl/permission-overlay-v2.js';
|
|
36
|
+
import { stripThinkingBlocks } from './utils/message-utils.js';
|
|
37
|
+
import { truncate } from './ui/base/index.js';
|
|
38
|
+
import { AskUserSimpleOverlayV2, } from './ui/overlay/impl/ask-user-simple-overlay-v2.js';
|
|
39
|
+
import { GuardrailOverlayV2, } from './ui/overlay/impl/guardrail-overlay-v2.js';
|
|
40
|
+
import { AskUserOverlayV2, } from './ui/overlay/impl/ask-user-overlay-v2.js';
|
|
41
|
+
import { IterationLimitOverlayV2, } from './ui/overlay/impl/iteration-limit-overlay-v2.js';
|
|
42
|
+
import { OnboardingWizardOverlayV2, } from './ui/overlay/impl/onboarding-wizard-overlay-v2.js';
|
|
43
|
+
import { LoginOverlayV2, } from './ui/overlay/impl/login-overlay-v2.js';
|
|
44
|
+
import { getAuthManager } from './auth/index.js';
|
|
45
|
+
import { KeysOverlayV2 } from './ui/overlay/impl/keys-overlay-v2.js';
|
|
46
|
+
import { SessionModeOverlayV2, } from './ui/overlay/impl/session-mode-overlay-v2.js';
|
|
47
|
+
import { DocumentDetailOverlayV2 } from './ui/overlay/impl/document-detail-overlay-v2.js';
|
|
48
|
+
import { formatToolResult } from './ui/tool-formatters.js';
|
|
49
|
+
import { formatTokens } from './ui/base/index.js';
|
|
50
|
+
import { isMemoryInput } from './input-handlers/index.js';
|
|
51
|
+
import { addAnchor } from './anchors/index.js';
|
|
52
|
+
import { getActiveProject } from './tools/project-db.js';
|
|
53
|
+
import { getProjectSessionManager } from './session/project-session-manager.js';
|
|
54
|
+
import { ResumeOverlayV2 } from './ui/overlay/impl/resume-overlay-v2.js';
|
|
55
|
+
import { updateCacheInfo, updateHistoryEstimate, formatBreakdownCompact, formatCacheInfo, estimateTokens } from './utils/token-tracker.js';
|
|
56
|
+
// Version for display (when running standalone)
|
|
57
|
+
const STANDALONE_VERSION = '0.4.0-v2';
|
|
58
|
+
// Register all commands on module load
|
|
59
|
+
registerCommands(allCommands);
|
|
60
|
+
/** Maximum messages per agent queue */
|
|
61
|
+
const MAX_QUEUE_SIZE = 10;
|
|
62
|
+
// =============================================================================
|
|
63
|
+
// Debug Logging (set DEBUG_AGENT_REQUESTS=1 to enable)
|
|
64
|
+
// =============================================================================
|
|
65
|
+
const DEBUG_LOG_FILE = '/tmp/compilr-agent-debug.log';
|
|
66
|
+
const DEBUG_ENABLED = process.env.DEBUG_AGENT_REQUESTS === '1';
|
|
67
|
+
let debugRequestCount = 0;
|
|
68
|
+
// Import tool registry functions for debug logging
|
|
69
|
+
import { getDirectTools, getMetaTools, getToolStats } from './tools.js';
|
|
70
|
+
import { getRegisteredMetaTools } from './tools/meta-tools.js';
|
|
71
|
+
/**
|
|
72
|
+
* Write debug info to log file before agent request.
|
|
73
|
+
* Enable with: DEBUG_AGENT_REQUESTS=1 compilr
|
|
74
|
+
* View with: tail -f /tmp/compilr-agent-debug.log
|
|
75
|
+
*/
|
|
76
|
+
function debugLogAgentRequest(agent, message) {
|
|
77
|
+
if (!DEBUG_ENABLED)
|
|
78
|
+
return;
|
|
79
|
+
try {
|
|
80
|
+
debugRequestCount++;
|
|
81
|
+
const state = agent.serialize();
|
|
82
|
+
const timestamp = new Date().toISOString();
|
|
83
|
+
const systemPromptChars = state.systemPrompt.length;
|
|
84
|
+
const systemPromptTokens = estimateTokens(state.systemPrompt);
|
|
85
|
+
const messagesJson = JSON.stringify(state.messages);
|
|
86
|
+
const messagesTokens = estimateTokens(messagesJson);
|
|
87
|
+
// Get tool definitions for logging
|
|
88
|
+
const directTools = getDirectTools();
|
|
89
|
+
const metaTools = getMetaTools();
|
|
90
|
+
const metaRegistryTools = getRegisteredMetaTools();
|
|
91
|
+
const toolStats = getToolStats();
|
|
92
|
+
// Calculate tool definition sizes
|
|
93
|
+
const directToolsJson = JSON.stringify(directTools.map(t => ({
|
|
94
|
+
name: t.definition.name,
|
|
95
|
+
description: t.definition.description,
|
|
96
|
+
inputSchema: t.definition.inputSchema,
|
|
97
|
+
})));
|
|
98
|
+
const metaToolsJson = JSON.stringify(metaTools.map(t => ({
|
|
99
|
+
name: t.definition.name,
|
|
100
|
+
description: t.definition.description,
|
|
101
|
+
inputSchema: t.definition.inputSchema,
|
|
102
|
+
})));
|
|
103
|
+
const directToolsTokens = estimateTokens(directToolsJson);
|
|
104
|
+
const metaToolsTokens = estimateTokens(metaToolsJson);
|
|
105
|
+
const totalToolTokens = directToolsTokens + metaToolsTokens;
|
|
106
|
+
const totalEstimate = systemPromptTokens + messagesTokens + totalToolTokens;
|
|
107
|
+
const logEntry = `
|
|
108
|
+
################################################################################
|
|
109
|
+
# REQUEST #${String(debugRequestCount)} @ ${timestamp}
|
|
110
|
+
################################################################################
|
|
111
|
+
|
|
112
|
+
USER MESSAGE:
|
|
113
|
+
${message.slice(0, 500)}${message.length > 500 ? '...(truncated)' : ''}
|
|
114
|
+
|
|
115
|
+
CONTEXT SIZES:
|
|
116
|
+
- System Prompt: ${systemPromptChars.toLocaleString()} chars (~${systemPromptTokens.toLocaleString()} tokens)
|
|
117
|
+
- Messages: ${messagesJson.length.toLocaleString()} chars (~${messagesTokens.toLocaleString()} tokens)
|
|
118
|
+
- Message Count: ${String(state.messages.length)}
|
|
119
|
+
- Direct Tools: ${directToolsJson.length.toLocaleString()} chars (~${directToolsTokens.toLocaleString()} tokens) [${String(directTools.length)} tools]
|
|
120
|
+
- Meta Tools: ${metaToolsJson.length.toLocaleString()} chars (~${metaToolsTokens.toLocaleString()} tokens) [${String(metaTools.length)} tools]
|
|
121
|
+
- Meta Registry: ${String(metaRegistryTools.length)} tools (accessed via use_tool)
|
|
122
|
+
--------------------------------------------------------------------------------
|
|
123
|
+
ESTIMATED TOTAL: ~${totalEstimate.toLocaleString()} tokens
|
|
124
|
+
(prompt: ${systemPromptTokens.toLocaleString()} + messages: ${messagesTokens.toLocaleString()} + tools: ${totalToolTokens.toLocaleString()})
|
|
125
|
+
|
|
126
|
+
Tool Stats: Direct=${String(toolStats.directTools)}, MetaRegistry=${String(toolStats.metaRegistryTools)}, Savings=~${String(toolStats.tokenSavings)}
|
|
127
|
+
|
|
128
|
+
================================================================================
|
|
129
|
+
FULL SYSTEM PROMPT (${systemPromptChars.toLocaleString()} chars):
|
|
130
|
+
================================================================================
|
|
131
|
+
${state.systemPrompt}
|
|
132
|
+
|
|
133
|
+
================================================================================
|
|
134
|
+
DIRECT TOOLS (${String(directTools.length)} tools, ~${directToolsTokens.toLocaleString()} tokens):
|
|
135
|
+
================================================================================
|
|
136
|
+
${directTools.map(t => `- ${t.definition.name}: ${t.definition.description.slice(0, 100)}...`).join('\n')}
|
|
137
|
+
|
|
138
|
+
================================================================================
|
|
139
|
+
META TOOLS (${String(metaTools.length)} tools, ~${metaToolsTokens.toLocaleString()} tokens):
|
|
140
|
+
================================================================================
|
|
141
|
+
${metaTools.map(t => `- ${t.definition.name}: ${t.definition.description.slice(0, 100)}...`).join('\n')}
|
|
142
|
+
|
|
143
|
+
================================================================================
|
|
144
|
+
META REGISTRY TOOLS (${String(metaRegistryTools.length)} tools, accessed via use_tool):
|
|
145
|
+
================================================================================
|
|
146
|
+
${metaRegistryTools.map(t => `- ${t.definition.name}`).join('\n')}
|
|
147
|
+
|
|
148
|
+
================================================================================
|
|
149
|
+
MESSAGES HISTORY (${String(state.messages.length)} messages):
|
|
150
|
+
================================================================================
|
|
151
|
+
${JSON.stringify(state.messages, null, 2)}
|
|
152
|
+
|
|
153
|
+
`;
|
|
154
|
+
appendFileSync(DEBUG_LOG_FILE, logEntry);
|
|
155
|
+
console.log(`[DEBUG] Request #${String(debugRequestCount)} logged to ${DEBUG_LOG_FILE}`);
|
|
156
|
+
console.log(`[DEBUG] Estimated: sys:${String(systemPromptTokens)}tok + msg:${String(messagesTokens)}tok + tools:${String(totalToolTokens)}tok = ${String(totalEstimate)}tok`);
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
console.error('[DEBUG] Failed to log agent request:', error);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// =============================================================================
|
|
163
|
+
// REPL V2 Class
|
|
164
|
+
// =============================================================================
|
|
165
|
+
export class ReplV2 {
|
|
166
|
+
agent;
|
|
167
|
+
team;
|
|
168
|
+
agentFactory;
|
|
169
|
+
model;
|
|
170
|
+
provider;
|
|
171
|
+
version;
|
|
172
|
+
updateAvailable;
|
|
173
|
+
projectName;
|
|
174
|
+
// Multi-session tracking for background agents (Phase 3)
|
|
175
|
+
sessions = new Map();
|
|
176
|
+
foregroundAgentId = null;
|
|
177
|
+
// Per-agent message queues (Phase 3 - queued messages when agent is busy)
|
|
178
|
+
agentMessageQueues = new Map();
|
|
179
|
+
// Foreground session (backward compatible - will be replaced by sessions map)
|
|
180
|
+
currentSession = null;
|
|
181
|
+
ui;
|
|
182
|
+
onUIReady;
|
|
183
|
+
onTextBufferReady;
|
|
184
|
+
onAskUserSimpleReady;
|
|
185
|
+
onGuardrailReady;
|
|
186
|
+
onAskUserReady;
|
|
187
|
+
onIterationLimitReady;
|
|
188
|
+
onAgentFinish;
|
|
189
|
+
onSuggestionReady;
|
|
190
|
+
onSubagentReady;
|
|
191
|
+
clearSubagentTracking;
|
|
192
|
+
showOnboarding;
|
|
193
|
+
showLogin;
|
|
194
|
+
onDelegateReady;
|
|
195
|
+
onPendingDelegation;
|
|
196
|
+
onBackgroundAgentReady;
|
|
197
|
+
onBackgroundDelegationReady;
|
|
198
|
+
onSessionRegistered;
|
|
199
|
+
onTeamReplaced;
|
|
200
|
+
authOnly;
|
|
201
|
+
restoredSessionInfo = null;
|
|
202
|
+
// Phase 3b: Track which background agent is currently executing (for permission routing)
|
|
203
|
+
// This is set during the background agent's stream loop so the permission handler knows
|
|
204
|
+
// which agent is making the request.
|
|
205
|
+
currentExecutingBackgroundAgentId = null;
|
|
206
|
+
// Session stats
|
|
207
|
+
startTime = new Date();
|
|
208
|
+
sessionInputTokens = 0;
|
|
209
|
+
sessionOutputTokens = 0;
|
|
210
|
+
sessionRequests = 0;
|
|
211
|
+
// Text accumulator for agent output (class-level so it can be flushed externally)
|
|
212
|
+
textAccumulator = '';
|
|
213
|
+
// Note: Subagent tracking is now simplified - callbacks receive toolUseId directly
|
|
214
|
+
// from agent.ts, so no more FIFO queue correlation needed!
|
|
215
|
+
// Bash abort controllers for Ctrl+B backgrounding feature
|
|
216
|
+
// Maps toolUseId -> AbortController for each running bash command
|
|
217
|
+
bashAbortControllers = new Map();
|
|
218
|
+
constructor(options = {}) {
|
|
219
|
+
this.agent = options.agent;
|
|
220
|
+
this.team = options.team;
|
|
221
|
+
this.agentFactory = options.agentFactory;
|
|
222
|
+
this.model = options.model ?? 'simulation';
|
|
223
|
+
this.provider = options.provider ?? 'simulation';
|
|
224
|
+
this.version = options.version ?? STANDALONE_VERSION;
|
|
225
|
+
this.updateAvailable = options.updateAvailable ?? null;
|
|
226
|
+
this.projectName = options.projectName ?? process.cwd().split('/').pop() ?? 'Project';
|
|
227
|
+
this.onUIReady = options.onUIReady;
|
|
228
|
+
this.onTextBufferReady = options.onTextBufferReady;
|
|
229
|
+
this.onAskUserSimpleReady = options.onAskUserSimpleReady;
|
|
230
|
+
this.onGuardrailReady = options.onGuardrailReady;
|
|
231
|
+
this.onAskUserReady = options.onAskUserReady;
|
|
232
|
+
this.onIterationLimitReady = options.onIterationLimitReady;
|
|
233
|
+
this.onAgentFinish = options.onAgentFinish;
|
|
234
|
+
this.onSuggestionReady = options.onSuggestionReady;
|
|
235
|
+
this.onSubagentReady = options.onSubagentReady;
|
|
236
|
+
this.clearSubagentTracking = options.clearSubagentTracking;
|
|
237
|
+
this.showOnboarding = options.showOnboarding ?? false;
|
|
238
|
+
this.showLogin = options.showLogin ?? false;
|
|
239
|
+
this.onDelegateReady = options.onDelegateReady;
|
|
240
|
+
this.onPendingDelegation = options.onPendingDelegation;
|
|
241
|
+
this.onBackgroundAgentReady = options.onBackgroundAgentReady;
|
|
242
|
+
this.onBackgroundDelegationReady = options.onBackgroundDelegationReady;
|
|
243
|
+
this.onSessionRegistered = options.onSessionRegistered;
|
|
244
|
+
this.onTeamReplaced = options.onTeamReplaced;
|
|
245
|
+
this.authOnly = options.authOnly ?? false;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Flush accumulated agent text to the UI.
|
|
249
|
+
* Called externally (e.g., before showing permission overlay) to ensure
|
|
250
|
+
* agent's "I'll do X" message appears before the overlay.
|
|
251
|
+
*/
|
|
252
|
+
flushTextBuffer() {
|
|
253
|
+
if (this.textAccumulator.trim()) {
|
|
254
|
+
this.ui.print({
|
|
255
|
+
type: 'agent-text',
|
|
256
|
+
text: this.textAccumulator.trim(),
|
|
257
|
+
expression: this.getActiveAgentMascot(),
|
|
258
|
+
});
|
|
259
|
+
this.textAccumulator = '';
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Get the mascot expression for the currently active agent.
|
|
264
|
+
* Returns the team agent's mascot if active, otherwise default neutral.
|
|
265
|
+
*/
|
|
266
|
+
getActiveAgentMascot() {
|
|
267
|
+
if (this.team) {
|
|
268
|
+
const activeAgent = this.team.getActive();
|
|
269
|
+
return activeAgent.mascot;
|
|
270
|
+
}
|
|
271
|
+
return '[•_•]'; // Default neutral mascot
|
|
272
|
+
}
|
|
273
|
+
// ===========================================================================
|
|
274
|
+
// Session Management (Phase 3 - Background Agents)
|
|
275
|
+
// ===========================================================================
|
|
276
|
+
/**
|
|
277
|
+
* Start a new agent session (foreground or background).
|
|
278
|
+
* For foreground: sets currentSession and foregroundAgentId
|
|
279
|
+
* For background: only adds to sessions map
|
|
280
|
+
*/
|
|
281
|
+
startSession(session) {
|
|
282
|
+
this.sessions.set(session.agentId, session);
|
|
283
|
+
if (!session.isBackground) {
|
|
284
|
+
this.currentSession = session;
|
|
285
|
+
this.foregroundAgentId = session.agentId;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* End an agent session.
|
|
290
|
+
* Removes from sessions map, clears currentSession if foreground,
|
|
291
|
+
* and processes any queued messages for this agent.
|
|
292
|
+
*/
|
|
293
|
+
endSession(agentId) {
|
|
294
|
+
this.sessions.delete(agentId);
|
|
295
|
+
if (this.foregroundAgentId === agentId) {
|
|
296
|
+
this.currentSession = null;
|
|
297
|
+
this.foregroundAgentId = null;
|
|
298
|
+
}
|
|
299
|
+
// Update background agent count in UI
|
|
300
|
+
this.ui.setBackgroundAgentCount(this.getBackgroundSessions().length);
|
|
301
|
+
// Process any queued messages for this agent
|
|
302
|
+
this.processAgentQueue(agentId);
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Get a session by agent ID.
|
|
306
|
+
*/
|
|
307
|
+
getSession(agentId) {
|
|
308
|
+
return this.sessions.get(agentId);
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Get all background sessions (non-foreground).
|
|
312
|
+
*/
|
|
313
|
+
getBackgroundSessions() {
|
|
314
|
+
return Array.from(this.sessions.values()).filter(s => s.isBackground);
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Check if an agent has a running session.
|
|
318
|
+
*/
|
|
319
|
+
hasActiveSession(agentId) {
|
|
320
|
+
return this.sessions.has(agentId);
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Check if any background agents are running.
|
|
324
|
+
*/
|
|
325
|
+
hasBackgroundAgents() {
|
|
326
|
+
return this.getBackgroundSessions().length > 0;
|
|
327
|
+
}
|
|
328
|
+
// ===========================================================================
|
|
329
|
+
// Per-Agent Message Queue (Phase 3)
|
|
330
|
+
// ===========================================================================
|
|
331
|
+
/**
|
|
332
|
+
* Queue a message for an agent that is currently busy.
|
|
333
|
+
* Returns the queue position (1-based).
|
|
334
|
+
*/
|
|
335
|
+
queueMessage(agentId, message, displayMessage) {
|
|
336
|
+
const queue = this.agentMessageQueues.get(agentId) ?? [];
|
|
337
|
+
// Check max queue size
|
|
338
|
+
if (queue.length >= MAX_QUEUE_SIZE) {
|
|
339
|
+
return null; // Queue is full
|
|
340
|
+
}
|
|
341
|
+
const queuedMessage = {
|
|
342
|
+
id: `${agentId}-${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`,
|
|
343
|
+
message,
|
|
344
|
+
displayMessage,
|
|
345
|
+
queuedAt: new Date(),
|
|
346
|
+
};
|
|
347
|
+
queue.push(queuedMessage);
|
|
348
|
+
this.agentMessageQueues.set(agentId, queue);
|
|
349
|
+
return queue.length; // Return position (1-based)
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Get the number of messages queued for an agent.
|
|
353
|
+
*/
|
|
354
|
+
getQueueLength(agentId) {
|
|
355
|
+
return this.agentMessageQueues.get(agentId)?.length ?? 0;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Clear all queued messages for an agent.
|
|
359
|
+
* Returns the number of messages that were cleared.
|
|
360
|
+
*/
|
|
361
|
+
clearQueue(agentId) {
|
|
362
|
+
const queue = this.agentMessageQueues.get(agentId);
|
|
363
|
+
const count = queue?.length ?? 0;
|
|
364
|
+
this.agentMessageQueues.delete(agentId);
|
|
365
|
+
return count;
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Clear all message queues for all agents.
|
|
369
|
+
* Returns the total number of messages cleared.
|
|
370
|
+
*/
|
|
371
|
+
clearAllQueues() {
|
|
372
|
+
let total = 0;
|
|
373
|
+
for (const queue of this.agentMessageQueues.values()) {
|
|
374
|
+
total += queue.length;
|
|
375
|
+
}
|
|
376
|
+
this.agentMessageQueues.clear();
|
|
377
|
+
return total;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Process the next message in an agent's queue.
|
|
381
|
+
* Called after an agent completes a task.
|
|
382
|
+
*/
|
|
383
|
+
processAgentQueue(agentId) {
|
|
384
|
+
const queue = this.agentMessageQueues.get(agentId);
|
|
385
|
+
if (!queue || queue.length === 0) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const nextMessage = queue.shift();
|
|
389
|
+
if (!nextMessage) {
|
|
390
|
+
return; // Should never happen due to length check above
|
|
391
|
+
}
|
|
392
|
+
this.agentMessageQueues.set(agentId, queue);
|
|
393
|
+
// Show that we're processing a queued message
|
|
394
|
+
const label = this.getAgentMascotLabel(agentId);
|
|
395
|
+
const preview = truncate(nextMessage.displayMessage, 50);
|
|
396
|
+
this.ui.print({
|
|
397
|
+
type: 'info',
|
|
398
|
+
message: `${label} processing queued message: "${preview}"`,
|
|
399
|
+
});
|
|
400
|
+
// Run the queued message as a background task (non-blocking)
|
|
401
|
+
void this.runAgentBackground(agentId, nextMessage.message);
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Get detailed info about background sessions (for /bg overlay).
|
|
405
|
+
* Phase 3c: Background Status & Control
|
|
406
|
+
*/
|
|
407
|
+
getBackgroundSessionsInfo() {
|
|
408
|
+
const sessions = this.getBackgroundSessions();
|
|
409
|
+
const pendingManager = getPendingRequestsManager();
|
|
410
|
+
return sessions.map((session) => {
|
|
411
|
+
const teamAgent = this.team?.get(session.agentId);
|
|
412
|
+
const pendingForAgent = pendingManager.getByAgent(session.agentId);
|
|
413
|
+
const queuedCount = this.getQueueLength(session.agentId);
|
|
414
|
+
return {
|
|
415
|
+
agentId: session.agentId,
|
|
416
|
+
displayName: teamAgent?.displayName ?? session.agentId,
|
|
417
|
+
mascot: teamAgent?.mascot ?? '[•_•]',
|
|
418
|
+
currentAction: session.currentAction,
|
|
419
|
+
isBlocked: pendingForAgent.length > 0,
|
|
420
|
+
pendingCount: pendingForAgent.length,
|
|
421
|
+
queuedCount,
|
|
422
|
+
};
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Send the current foreground agent to background without aborting it.
|
|
427
|
+
* The agent continues running; the user regains the input prompt.
|
|
428
|
+
* Returns the agent ID that was sent to background, or null if no foreground session.
|
|
429
|
+
*/
|
|
430
|
+
sendForegroundToBackground() {
|
|
431
|
+
if (!this.currentSession || !this.foregroundAgentId) {
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
const agentId = this.foregroundAgentId;
|
|
435
|
+
// Reclassify session as background (agent keeps running)
|
|
436
|
+
this.currentSession.isBackground = true;
|
|
437
|
+
this.foregroundAgentId = null;
|
|
438
|
+
this.currentSession = null;
|
|
439
|
+
// Update UI state — user regains input prompt
|
|
440
|
+
this.ui.setAgentRunning(false);
|
|
441
|
+
this.ui.setThinking(false);
|
|
442
|
+
return agentId;
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Consolidated agent-switching method.
|
|
446
|
+
*
|
|
447
|
+
* Performs ALL operations needed to switch the foreground to a different agent:
|
|
448
|
+
* 1. Demote current foreground agent to background (if busy + different agent)
|
|
449
|
+
* 2. team.switchTo(agentId)
|
|
450
|
+
* 3. Update this.agent reference
|
|
451
|
+
* 4. Update meta-tool filter
|
|
452
|
+
* 5. Update footer UI
|
|
453
|
+
* 6. Update foreground session tracking (restore agentRunning/spinner)
|
|
454
|
+
* 7. Record telemetry
|
|
455
|
+
*
|
|
456
|
+
* Callers handle their own messaging (different styles/wording per context).
|
|
457
|
+
*/
|
|
458
|
+
async switchAgent(agentId, options = {}) {
|
|
459
|
+
const { demoteCurrentIfBusy = true, trackSession = true, recordTelemetry = true, } = options;
|
|
460
|
+
if (!this.team) {
|
|
461
|
+
throw new Error('Cannot switch agent: no team configured');
|
|
462
|
+
}
|
|
463
|
+
// 1. Demote current foreground if busy + switching to a different agent
|
|
464
|
+
let demotedAgentId = null;
|
|
465
|
+
if (demoteCurrentIfBusy &&
|
|
466
|
+
this.currentSession &&
|
|
467
|
+
this.foregroundAgentId &&
|
|
468
|
+
this.foregroundAgentId !== agentId) {
|
|
469
|
+
demotedAgentId = this.sendForegroundToBackground();
|
|
470
|
+
}
|
|
471
|
+
// 2. Switch the team's active agent
|
|
472
|
+
const teamAgent = await this.team.switchTo(agentId);
|
|
473
|
+
// 3. Update the REPL's agent reference
|
|
474
|
+
if (teamAgent.agent) {
|
|
475
|
+
this.agent = teamAgent.agent;
|
|
476
|
+
}
|
|
477
|
+
// 4. Update meta-tool filter
|
|
478
|
+
setMetaToolFilter(teamAgent.toolFilter ?? null);
|
|
479
|
+
// 5. Update footer UI
|
|
480
|
+
if (agentId !== 'default') {
|
|
481
|
+
this.ui.setActiveTeamAgent({
|
|
482
|
+
id: teamAgent.id,
|
|
483
|
+
displayName: teamAgent.displayName,
|
|
484
|
+
mascot: teamAgent.mascot,
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
else {
|
|
488
|
+
this.ui.setActiveTeamAgent(null);
|
|
489
|
+
}
|
|
490
|
+
// 5b. Update session registry with active agent
|
|
491
|
+
try {
|
|
492
|
+
const registry = getSessionRegistry();
|
|
493
|
+
if (registry) {
|
|
494
|
+
registry.setActiveAgent(agentId);
|
|
495
|
+
const agents = this.team.getAll();
|
|
496
|
+
registry.updateAgents(agents.map(a => ({
|
|
497
|
+
id: a.id,
|
|
498
|
+
displayName: a.displayName,
|
|
499
|
+
mascot: a.mascot,
|
|
500
|
+
})));
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
catch {
|
|
504
|
+
// Best effort
|
|
505
|
+
}
|
|
506
|
+
// 6. Update foreground session tracking
|
|
507
|
+
if (trackSession) {
|
|
508
|
+
const existingSession = this.sessions.get(agentId);
|
|
509
|
+
if (existingSession && existingSession.isBackground) {
|
|
510
|
+
// Agent has a background session — bring it to foreground
|
|
511
|
+
existingSession.isBackground = false;
|
|
512
|
+
this.foregroundAgentId = agentId;
|
|
513
|
+
this.currentSession = existingSession;
|
|
514
|
+
// Restore busy UI state — session exists means agent is still running
|
|
515
|
+
this.ui.setAgentRunning(true);
|
|
516
|
+
if (existingSession.currentAction) {
|
|
517
|
+
this.ui.setSpinnerText(existingSession.currentAction);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
else if (!existingSession) {
|
|
521
|
+
// No session yet — just update foreground ID so next run creates it correctly
|
|
522
|
+
this.foregroundAgentId = agentId;
|
|
523
|
+
this.currentSession = null;
|
|
524
|
+
}
|
|
525
|
+
// If existingSession exists and is already foreground, leave it as-is
|
|
526
|
+
}
|
|
527
|
+
// 7. Record telemetry
|
|
528
|
+
if (recordTelemetry) {
|
|
529
|
+
getAuthManager().recordAgent(agentId);
|
|
530
|
+
}
|
|
531
|
+
return {
|
|
532
|
+
teamAgent: {
|
|
533
|
+
id: teamAgent.id,
|
|
534
|
+
displayName: teamAgent.displayName,
|
|
535
|
+
mascot: teamAgent.mascot,
|
|
536
|
+
agent: teamAgent.agent,
|
|
537
|
+
toolFilter: teamAgent.toolFilter,
|
|
538
|
+
},
|
|
539
|
+
demotedAgentId,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Bring a background agent to foreground.
|
|
544
|
+
* Validates the agent has a background session, then delegates to switchAgent().
|
|
545
|
+
* Phase 3c: Background Status & Control
|
|
546
|
+
*/
|
|
547
|
+
async bringAgentToForeground(agentId) {
|
|
548
|
+
const session = this.sessions.get(agentId);
|
|
549
|
+
if (!session || !session.isBackground) {
|
|
550
|
+
return false;
|
|
551
|
+
}
|
|
552
|
+
const { demotedAgentId } = await this.switchAgent(agentId);
|
|
553
|
+
// Print demotion notification (if a different agent was sent to background)
|
|
554
|
+
if (demotedAgentId) {
|
|
555
|
+
const demotedLabel = this.getAgentMascotLabel(demotedAgentId);
|
|
556
|
+
this.ui.print({
|
|
557
|
+
type: 'info',
|
|
558
|
+
message: `${demotedLabel} sent to background`,
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
// Print foreground notification
|
|
562
|
+
const label = this.getAgentMascotLabel(agentId);
|
|
563
|
+
this.ui.print({
|
|
564
|
+
type: 'info',
|
|
565
|
+
message: `${label} brought to foreground`,
|
|
566
|
+
});
|
|
567
|
+
return true;
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Kill/cancel a background agent.
|
|
571
|
+
* Phase 3c: Background Status & Control
|
|
572
|
+
*/
|
|
573
|
+
killAgent(agentId) {
|
|
574
|
+
const session = this.sessions.get(agentId);
|
|
575
|
+
if (!session) {
|
|
576
|
+
return false;
|
|
577
|
+
}
|
|
578
|
+
// Abort the session
|
|
579
|
+
session.abortController.abort();
|
|
580
|
+
// Cancel any pending requests from this agent
|
|
581
|
+
const pendingManager = getPendingRequestsManager();
|
|
582
|
+
pendingManager.rejectAllForAgent(agentId, 'Agent killed by user');
|
|
583
|
+
// Clear queued messages (don't process on kill)
|
|
584
|
+
const discardedCount = this.clearQueue(agentId);
|
|
585
|
+
// Print notification
|
|
586
|
+
const label = this.getAgentMascotLabel(agentId);
|
|
587
|
+
let message = `${label} killed`;
|
|
588
|
+
if (discardedCount > 0) {
|
|
589
|
+
message += `. ${String(discardedCount)} queued message${discardedCount !== 1 ? 's' : ''} discarded.`;
|
|
590
|
+
}
|
|
591
|
+
this.ui.print({
|
|
592
|
+
type: 'warning',
|
|
593
|
+
message,
|
|
594
|
+
});
|
|
595
|
+
// Cancel any active delegations for this agent (no completion events)
|
|
596
|
+
getDelegationTracker().cancelAllForAgent(agentId);
|
|
597
|
+
// Session cleanup happens in the finally block of runAgentInBackground
|
|
598
|
+
// Note: We cleared the queue above, so endSession() won't process it
|
|
599
|
+
// Update background agent count immediately for responsive UI
|
|
600
|
+
this.ui.setBackgroundAgentCount(this.getBackgroundSessions().length);
|
|
601
|
+
return true;
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Get mascot label for an agent (for output labeling).
|
|
605
|
+
* Format: "[◈_◈] $arch" - mascot followed by agent reference
|
|
606
|
+
*/
|
|
607
|
+
getAgentMascotLabel(agentId) {
|
|
608
|
+
if (this.team) {
|
|
609
|
+
const teamAgent = this.team.get(agentId);
|
|
610
|
+
if (teamAgent) {
|
|
611
|
+
return `${teamAgent.mascot} $${agentId}`;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
return `$${agentId}`;
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Auto-expand conversation filter if user is sending to an agent not in the filter.
|
|
618
|
+
* Shows info message when filter is expanded.
|
|
619
|
+
* @returns true if filter was expanded, false otherwise
|
|
620
|
+
*/
|
|
621
|
+
autoExpandFilterIfNeeded(agentId) {
|
|
622
|
+
const wasAdded = this.ui.addToConversationFilter(agentId);
|
|
623
|
+
if (wasAdded) {
|
|
624
|
+
this.ui.print({
|
|
625
|
+
type: 'info',
|
|
626
|
+
message: `Added $${agentId} to conversation filter`,
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
return wasAdded;
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Regex to detect PLAN_READY marker in agent text.
|
|
633
|
+
* Format: [PLAN_READY:plan_id:plan_name]
|
|
634
|
+
*/
|
|
635
|
+
static PLAN_READY_REGEX = /\[PLAN_READY:(\d+):([^\]]+)\]/;
|
|
636
|
+
/**
|
|
637
|
+
* Check if text contains PLAN_READY marker and extract plan info.
|
|
638
|
+
*/
|
|
639
|
+
detectPlanReady(text) {
|
|
640
|
+
const match = ReplV2.PLAN_READY_REGEX.exec(text);
|
|
641
|
+
if (match) {
|
|
642
|
+
return {
|
|
643
|
+
planId: parseInt(match[1], 10),
|
|
644
|
+
planName: match[2],
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
return null;
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Handle plan approval flow.
|
|
651
|
+
* Shows the approval overlay and handles the user's choice.
|
|
652
|
+
*/
|
|
653
|
+
async handlePlanApproval(planId, planName) {
|
|
654
|
+
const overlay = new PlanApprovalOverlayV2({ planId, planName });
|
|
655
|
+
const result = await this.ui.showOverlay(overlay);
|
|
656
|
+
if (!result) {
|
|
657
|
+
// User cancelled or overlay closed without result
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
switch (result.action) {
|
|
661
|
+
case 'approve-auto':
|
|
662
|
+
// Update plan status to approved, switch to auto-accept mode
|
|
663
|
+
try {
|
|
664
|
+
planRepository.update(planId, { status: 'approved' });
|
|
665
|
+
this.ui.setMode('auto-accept');
|
|
666
|
+
this.ui.setActivePlan(planId, planName);
|
|
667
|
+
this.ui.print({ type: 'info', message: `Plan "${planName}" approved. Switching to auto-accept mode.` });
|
|
668
|
+
}
|
|
669
|
+
catch (e) {
|
|
670
|
+
this.ui.print({ type: 'error', message: `Failed to approve plan: ${e instanceof Error ? e.message : String(e)}` });
|
|
671
|
+
}
|
|
672
|
+
break;
|
|
673
|
+
case 'approve':
|
|
674
|
+
// Update plan status to approved, switch to normal mode
|
|
675
|
+
try {
|
|
676
|
+
planRepository.update(planId, { status: 'approved' });
|
|
677
|
+
this.ui.setMode('normal');
|
|
678
|
+
this.ui.setActivePlan(planId, planName);
|
|
679
|
+
this.ui.print({ type: 'info', message: `Plan "${planName}" approved. Switching to normal mode.` });
|
|
680
|
+
}
|
|
681
|
+
catch (e) {
|
|
682
|
+
this.ui.print({ type: 'error', message: `Failed to approve plan: ${e instanceof Error ? e.message : String(e)}` });
|
|
683
|
+
}
|
|
684
|
+
break;
|
|
685
|
+
case 'revise':
|
|
686
|
+
// Keep plan as draft, show input for feedback
|
|
687
|
+
this.ui.print({ type: 'info', message: 'Please provide feedback on what you\'d like to change:' });
|
|
688
|
+
// The user will type their feedback as the next message
|
|
689
|
+
// The agent will receive it and update the plan
|
|
690
|
+
break;
|
|
691
|
+
case 'cancel':
|
|
692
|
+
// Mark plan as abandoned
|
|
693
|
+
try {
|
|
694
|
+
planRepository.update(planId, { status: 'abandoned' });
|
|
695
|
+
this.ui.clearActivePlan();
|
|
696
|
+
this.ui.print({ type: 'warning', message: `Plan "${planName}" abandoned.` });
|
|
697
|
+
}
|
|
698
|
+
catch (e) {
|
|
699
|
+
this.ui.print({ type: 'error', message: `Failed to abandon plan: ${e instanceof Error ? e.message : String(e)}` });
|
|
700
|
+
}
|
|
701
|
+
break;
|
|
702
|
+
case 'view':
|
|
703
|
+
// Show the full plan in document detail overlay
|
|
704
|
+
try {
|
|
705
|
+
const plan = planRepository.getById(planId);
|
|
706
|
+
if (plan) {
|
|
707
|
+
// Convert Plan to ProjectDocument for the detail overlay
|
|
708
|
+
const doc = {
|
|
709
|
+
id: plan.id,
|
|
710
|
+
projectId: plan.projectId,
|
|
711
|
+
docType: 'plan',
|
|
712
|
+
title: plan.name,
|
|
713
|
+
content: plan.content,
|
|
714
|
+
status: plan.status,
|
|
715
|
+
workItemId: plan.workItemId,
|
|
716
|
+
createdAt: plan.createdAt,
|
|
717
|
+
updatedAt: plan.updatedAt,
|
|
718
|
+
};
|
|
719
|
+
const detailOverlay = new DocumentDetailOverlayV2(doc);
|
|
720
|
+
await this.ui.showOverlay(detailOverlay);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
catch (e) {
|
|
724
|
+
this.ui.print({ type: 'error', message: `Failed to load plan: ${e instanceof Error ? e.message : String(e)}` });
|
|
725
|
+
}
|
|
726
|
+
// After viewing, re-show the approval overlay
|
|
727
|
+
await this.handlePlanApproval(planId, planName);
|
|
728
|
+
break;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Restore the agent from a previous state.
|
|
733
|
+
* Called by /resume command to swap in a restored agent.
|
|
734
|
+
*/
|
|
735
|
+
restoreAgent(newAgent) {
|
|
736
|
+
this.agent = newAgent;
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Get the current agent instance.
|
|
740
|
+
* Used by commands that need to serialize/checkpoint the agent state.
|
|
741
|
+
*/
|
|
742
|
+
getAgent() {
|
|
743
|
+
return this.agent;
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Start the REPL.
|
|
747
|
+
*/
|
|
748
|
+
start() {
|
|
749
|
+
// Register this terminal in the session registry (before UI so footer can subscribe)
|
|
750
|
+
try {
|
|
751
|
+
const registry = initSessionRegistry(getSessionsPath());
|
|
752
|
+
const activeProject = getActiveProject();
|
|
753
|
+
const sessionId = registry.register(activeProject?.id ?? null);
|
|
754
|
+
setActiveTerminalSessionId(sessionId);
|
|
755
|
+
// Notify index.ts of session ID (for VS Code diff IPC prefix)
|
|
756
|
+
this.onSessionRegistered?.(sessionId);
|
|
757
|
+
// Initialize file lock manager (uses same shared DB)
|
|
758
|
+
initFileLockManager();
|
|
759
|
+
// Initialize notification manager (cross-session notifications)
|
|
760
|
+
const notifManager = initNotificationManager();
|
|
761
|
+
notifManager.startPolling();
|
|
762
|
+
// Subscribe to pending requests — notify other terminals
|
|
763
|
+
getPendingRequestsManager().on('request-added', (request) => {
|
|
764
|
+
try {
|
|
765
|
+
const sid = getActiveTerminalSessionId();
|
|
766
|
+
const proj = getActiveProject();
|
|
767
|
+
if (sid && proj) {
|
|
768
|
+
notifManager.insert({
|
|
769
|
+
projectId: proj.id,
|
|
770
|
+
fromSessionId: sid,
|
|
771
|
+
type: 'pending_request',
|
|
772
|
+
title: `${request.agentId} needs ${request.type === 'permission' ? 'approval' : 'input'}`,
|
|
773
|
+
message: request.prompt.slice(0, 200),
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
catch {
|
|
778
|
+
// Best effort
|
|
779
|
+
}
|
|
780
|
+
});
|
|
781
|
+
// Sync initial team roster to registry (covers restored teams from disk)
|
|
782
|
+
if (this.team) {
|
|
783
|
+
const agents = this.team.getAll();
|
|
784
|
+
registry.updateAgents(agents.map(a => ({
|
|
785
|
+
id: a.id,
|
|
786
|
+
displayName: a.displayName,
|
|
787
|
+
mascot: a.mascot,
|
|
788
|
+
})));
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
catch {
|
|
792
|
+
// Best effort — session registry is non-critical
|
|
793
|
+
}
|
|
794
|
+
// Create TerminalUI with mode synced from settings
|
|
795
|
+
const savedPermissionMode = getPermissionMode();
|
|
796
|
+
const initialUiMode = permissionModeToAgentMode(savedPermissionMode);
|
|
797
|
+
this.ui = new TerminalUI({
|
|
798
|
+
initialMode: initialUiMode,
|
|
799
|
+
});
|
|
800
|
+
// Print welcome screen
|
|
801
|
+
// Skip if: login overlay shows its own branded welcome,
|
|
802
|
+
// or startup mode is 'menu' (the /menu command prints its own logo)
|
|
803
|
+
if (!this.showLogin && getStartupMode() !== 'menu') {
|
|
804
|
+
this.printWelcome();
|
|
805
|
+
}
|
|
806
|
+
// Set initial state
|
|
807
|
+
this.ui.setProjectName(this.projectName);
|
|
808
|
+
this.ui.setCommandsCallback(getAutocompleteCommands);
|
|
809
|
+
// Set up agent suggestions callback for $ autocomplete (if team is available)
|
|
810
|
+
if (this.team) {
|
|
811
|
+
const team = this.team; // Capture for closure
|
|
812
|
+
this.ui.setAgentSuggestionsCallback(() => {
|
|
813
|
+
const agents = team.getAll();
|
|
814
|
+
return agents.map((agent) => ({
|
|
815
|
+
id: agent.id,
|
|
816
|
+
displayName: agent.displayName,
|
|
817
|
+
mascot: agent.mascot,
|
|
818
|
+
}));
|
|
819
|
+
});
|
|
820
|
+
// Set the active agent in the footer (for restored teams with non-default active agent)
|
|
821
|
+
const activeAgent = team.getActive();
|
|
822
|
+
if (activeAgent.id !== 'default') {
|
|
823
|
+
this.ui.setActiveTeamAgent({
|
|
824
|
+
id: activeAgent.id,
|
|
825
|
+
displayName: activeAgent.displayName,
|
|
826
|
+
mascot: activeAgent.mascot,
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
// Notify caller that UI is ready and provide the V2 permission overlay function
|
|
831
|
+
if (this.onUIReady) {
|
|
832
|
+
const ui = this.ui;
|
|
833
|
+
this.onUIReady(async (options) => {
|
|
834
|
+
const overlay = new PermissionOverlayV2(options);
|
|
835
|
+
return ui.showOverlay(overlay);
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
// Provide the text buffer flush function to the caller
|
|
839
|
+
if (this.onTextBufferReady) {
|
|
840
|
+
this.onTextBufferReady(() => {
|
|
841
|
+
this.flushTextBuffer();
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
// Provide the ask_user_simple overlay function to the caller
|
|
845
|
+
if (this.onAskUserSimpleReady) {
|
|
846
|
+
const ui = this.ui;
|
|
847
|
+
this.onAskUserSimpleReady(async (options) => {
|
|
848
|
+
const overlay = new AskUserSimpleOverlayV2(options);
|
|
849
|
+
const result = await ui.showOverlay(overlay);
|
|
850
|
+
return result ?? { answer: '', skipped: true };
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
// Provide the guardrail overlay function to the caller
|
|
854
|
+
if (this.onGuardrailReady) {
|
|
855
|
+
const ui = this.ui;
|
|
856
|
+
this.onGuardrailReady(async (options) => {
|
|
857
|
+
const overlay = new GuardrailOverlayV2(options);
|
|
858
|
+
const result = await ui.showOverlay(overlay);
|
|
859
|
+
return result ?? { approved: false };
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
// Provide the ask_user overlay function to the caller
|
|
863
|
+
if (this.onAskUserReady) {
|
|
864
|
+
const ui = this.ui;
|
|
865
|
+
this.onAskUserReady(async (options) => {
|
|
866
|
+
const overlay = new AskUserOverlayV2(options);
|
|
867
|
+
const result = await ui.showOverlay(overlay);
|
|
868
|
+
return result ?? { answers: {}, skipped: options.questions.map((q) => q.id) };
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
// Provide the iteration_limit overlay function to the caller
|
|
872
|
+
if (this.onIterationLimitReady) {
|
|
873
|
+
const ui = this.ui;
|
|
874
|
+
this.onIterationLimitReady(async (options) => {
|
|
875
|
+
const overlay = new IterationLimitOverlayV2(options);
|
|
876
|
+
const result = await ui.showOverlay(overlay);
|
|
877
|
+
return result ?? { continue: false };
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
// Provide the delegate overlay function to the caller
|
|
881
|
+
// Uses AskUserSimpleOverlayV2 with a formatted delegation question
|
|
882
|
+
if (this.onDelegateReady) {
|
|
883
|
+
const ui = this.ui;
|
|
884
|
+
this.onDelegateReady(async (options) => {
|
|
885
|
+
// Build a formatted question for the delegation approval
|
|
886
|
+
const question = options.reason
|
|
887
|
+
? `Delegate to $${options.agentId} (${options.agentDisplayName})?\nReason: ${options.reason}\nTask: ${truncate(options.task, 100)}`
|
|
888
|
+
: `Delegate to $${options.agentId} (${options.agentDisplayName})?\nTask: ${truncate(options.task, 100)}`;
|
|
889
|
+
const overlay = new AskUserSimpleOverlayV2({
|
|
890
|
+
question,
|
|
891
|
+
options: ['Yes, delegate', 'No, continue'],
|
|
892
|
+
allowCustom: false,
|
|
893
|
+
});
|
|
894
|
+
const result = await ui.showOverlay(overlay);
|
|
895
|
+
return { approved: !result?.skipped && result?.answer === 'Yes, delegate' };
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
// Provide the suggestion setter function to the caller
|
|
899
|
+
if (this.onSuggestionReady) {
|
|
900
|
+
const ui = this.ui;
|
|
901
|
+
this.onSuggestionReady((action) => {
|
|
902
|
+
ui.setSuggestion(action);
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
// Provide subagent tracking callbacks to the caller for LiveRegion rendering
|
|
906
|
+
// Now using toolUseId directly - no more FIFO queue correlation needed!
|
|
907
|
+
if (this.onSubagentReady) {
|
|
908
|
+
const ui = this.ui;
|
|
909
|
+
this.onSubagentReady({
|
|
910
|
+
onStart: (toolUseId, _agentType, _description) => {
|
|
911
|
+
// The subagent entry was already added in tool_start with toolUseId
|
|
912
|
+
// Just mark it as "started" (no-op for now, already rendering)
|
|
913
|
+
void toolUseId; // Used for correlation, entry already exists
|
|
914
|
+
},
|
|
915
|
+
onToolUse: (toolUseId, toolName, summary) => {
|
|
916
|
+
// Direct update using toolUseId - no mapping needed!
|
|
917
|
+
ui.updateSubagentTool(toolUseId, toolName, summary ?? '');
|
|
918
|
+
},
|
|
919
|
+
onEnd: (toolUseId, _success, tokenCount, _error) => {
|
|
920
|
+
// DON'T mark as complete here! That changes the line count in LiveRegion,
|
|
921
|
+
// and if render loop fires before tool_end, lastRenderHeight becomes wrong.
|
|
922
|
+
// Just store the info for tool_end to use when committing.
|
|
923
|
+
const item = ui.getLiveRegion().getItem(toolUseId);
|
|
924
|
+
if (item && item.type === 'subagent') {
|
|
925
|
+
ui.getLiveRegion().updateItem(toolUseId, {
|
|
926
|
+
tokenCount,
|
|
927
|
+
endTime: Date.now(),
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
},
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
// Phase 3b: Provide background agent tracking callbacks
|
|
934
|
+
if (this.onBackgroundAgentReady) {
|
|
935
|
+
this.onBackgroundAgentReady({
|
|
936
|
+
// Check if an agent is running in background
|
|
937
|
+
isBackgroundAgent: (agentId) => {
|
|
938
|
+
if (agentId) {
|
|
939
|
+
const session = this.sessions.get(agentId);
|
|
940
|
+
return session?.isBackground ?? false;
|
|
941
|
+
}
|
|
942
|
+
// If no agentId provided, check if we're currently executing a background agent
|
|
943
|
+
return this.currentExecutingBackgroundAgentId !== null;
|
|
944
|
+
},
|
|
945
|
+
// Get the ID of the background agent currently executing (for permission routing)
|
|
946
|
+
getActiveBackgroundAgentId: () => {
|
|
947
|
+
return this.currentExecutingBackgroundAgentId;
|
|
948
|
+
},
|
|
949
|
+
// Get info about a background agent (mascot, etc.)
|
|
950
|
+
getBackgroundAgentInfo: (agentId) => {
|
|
951
|
+
if (!this.team)
|
|
952
|
+
return null;
|
|
953
|
+
const teamAgent = this.team.get(agentId);
|
|
954
|
+
if (!teamAgent)
|
|
955
|
+
return null;
|
|
956
|
+
return { mascot: teamAgent.mascot };
|
|
957
|
+
},
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
// Phase 3d-beta: Provide background delegation callback
|
|
961
|
+
if (this.onBackgroundDelegationReady) {
|
|
962
|
+
this.onBackgroundDelegationReady((agentId, message) => {
|
|
963
|
+
if (!this.agent)
|
|
964
|
+
return null;
|
|
965
|
+
// Check if agent has an active session (busy)
|
|
966
|
+
const existingSession = this.sessions.get(agentId);
|
|
967
|
+
if (existingSession) {
|
|
968
|
+
// Agent is busy — queue the delegation message
|
|
969
|
+
const position = this.queueMessage(agentId, message, message.slice(0, 80));
|
|
970
|
+
if (position === null) {
|
|
971
|
+
return null; // Queue is full
|
|
972
|
+
}
|
|
973
|
+
return 'queued';
|
|
974
|
+
}
|
|
975
|
+
// Agent is free — start background session immediately
|
|
976
|
+
void this.runAgentBackground(agentId, message);
|
|
977
|
+
return 'running';
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
// Event handlers
|
|
981
|
+
this.ui.on('submit', (input) => {
|
|
982
|
+
void this.processInput(input);
|
|
983
|
+
});
|
|
984
|
+
this.ui.on('command', (cmd, args) => {
|
|
985
|
+
void (async () => {
|
|
986
|
+
await this.handleCommand(cmd, args);
|
|
987
|
+
await this.processQueue();
|
|
988
|
+
})();
|
|
989
|
+
});
|
|
990
|
+
this.ui.on('escape', () => {
|
|
991
|
+
// Single Esc when no agent - do nothing
|
|
992
|
+
});
|
|
993
|
+
this.ui.on('interrupt', () => {
|
|
994
|
+
if (this.currentSession) {
|
|
995
|
+
this.cancelSession();
|
|
996
|
+
}
|
|
997
|
+
else {
|
|
998
|
+
this.ui.stop();
|
|
999
|
+
console.log('\nGoodbye!\n');
|
|
1000
|
+
process.exit(0);
|
|
1001
|
+
}
|
|
1002
|
+
});
|
|
1003
|
+
this.ui.on('cancel', () => {
|
|
1004
|
+
if (this.currentSession) {
|
|
1005
|
+
this.cancelSession();
|
|
1006
|
+
}
|
|
1007
|
+
});
|
|
1008
|
+
this.ui.on('modeChange', () => {
|
|
1009
|
+
const modes = ['normal', 'auto-accept', 'plan'];
|
|
1010
|
+
const currentMode = this.ui.getMode();
|
|
1011
|
+
const currentIndex = modes.indexOf(currentMode);
|
|
1012
|
+
const nextMode = modes[(currentIndex + 1) % modes.length];
|
|
1013
|
+
this.ui.setMode(nextMode);
|
|
1014
|
+
// Sync the permission mode setting with UI mode
|
|
1015
|
+
syncPermissionModeFromUI(nextMode);
|
|
1016
|
+
// Clear session grants when switching FROM auto-accept TO a more restrictive mode
|
|
1017
|
+
let grantsCleared = 0;
|
|
1018
|
+
if (currentMode === 'auto-accept' && nextMode !== 'auto-accept' && this.agent) {
|
|
1019
|
+
const grants = this.agent.getSessionPermissions();
|
|
1020
|
+
if (grants.length > 0) {
|
|
1021
|
+
grantsCleared = grants.length;
|
|
1022
|
+
this.agent.clearSessionPermissions();
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
// Show mode change message (with grant clearing info if applicable)
|
|
1026
|
+
if (grantsCleared > 0) {
|
|
1027
|
+
this.ui.print({
|
|
1028
|
+
type: 'info',
|
|
1029
|
+
message: `Mode: ${nextMode} (${String(grantsCleared)} grant${grantsCleared > 1 ? 's' : ''} cleared)`,
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
else {
|
|
1033
|
+
this.ui.print({ type: 'info', message: `Mode: ${nextMode}` });
|
|
1034
|
+
}
|
|
1035
|
+
});
|
|
1036
|
+
// Ctrl+B - show background tasks overlay
|
|
1037
|
+
this.ui.on('showTasks', () => {
|
|
1038
|
+
void (async () => {
|
|
1039
|
+
const { TasksOverlayV2 } = await import('./ui/overlay/impl/tasks-overlay-v2.js');
|
|
1040
|
+
const overlay = new TasksOverlayV2();
|
|
1041
|
+
await this.ui.showOverlay(overlay);
|
|
1042
|
+
})();
|
|
1043
|
+
});
|
|
1044
|
+
// Double Tab - show pending requests overlay
|
|
1045
|
+
this.ui.on('showPending', () => {
|
|
1046
|
+
void executeCommand('pending', '', this.createCommandContext());
|
|
1047
|
+
});
|
|
1048
|
+
// Ctrl+B during bash execution - move to background
|
|
1049
|
+
this.ui.on('backgroundBash', (info) => {
|
|
1050
|
+
const controller = this.bashAbortControllers.get(info.toolUseId);
|
|
1051
|
+
if (controller) {
|
|
1052
|
+
// Signal the bash tool to move process to ShellManager
|
|
1053
|
+
controller.abort('background');
|
|
1054
|
+
this.bashAbortControllers.delete(info.toolUseId);
|
|
1055
|
+
}
|
|
1056
|
+
});
|
|
1057
|
+
// Start UI (skip initial render if a startup overlay will appear immediately)
|
|
1058
|
+
const hasStartupOverlay = this.showLogin || this.showOnboarding || getStartupMode() === 'menu';
|
|
1059
|
+
this.ui.start(hasStartupOverlay);
|
|
1060
|
+
// Startup flow: login → onboarding → normal startup
|
|
1061
|
+
if (this.showLogin) {
|
|
1062
|
+
// Not authenticated — show branded login overlay first
|
|
1063
|
+
void this.handleLoginFlow();
|
|
1064
|
+
}
|
|
1065
|
+
else if (this.showOnboarding) {
|
|
1066
|
+
// Authenticated but first run — show onboarding wizard
|
|
1067
|
+
void this.showOnboardingWizard();
|
|
1068
|
+
}
|
|
1069
|
+
else {
|
|
1070
|
+
// Normal startup — authenticated, onboarding complete
|
|
1071
|
+
void this.handleStartupSequence();
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
/**
|
|
1075
|
+
* Handle login flow: show branded login overlay, then continue to onboarding or normal startup.
|
|
1076
|
+
*/
|
|
1077
|
+
async handleLoginFlow() {
|
|
1078
|
+
const loginOverlay = new LoginOverlayV2(this.version);
|
|
1079
|
+
const result = await this.ui.showOverlay(loginOverlay);
|
|
1080
|
+
if (!result?.success) {
|
|
1081
|
+
// Login failed or cancelled — exit
|
|
1082
|
+
this.stop();
|
|
1083
|
+
process.exit(1);
|
|
1084
|
+
}
|
|
1085
|
+
// Send forced heartbeat in background (don't block UI)
|
|
1086
|
+
const auth = getAuthManager();
|
|
1087
|
+
void auth.sendForcedHeartbeat();
|
|
1088
|
+
// Now check if first run
|
|
1089
|
+
if (!isFirstRunComplete()) {
|
|
1090
|
+
await this.showOnboardingWizard();
|
|
1091
|
+
}
|
|
1092
|
+
else {
|
|
1093
|
+
// Re-login (user did /logout then /login or restarted)
|
|
1094
|
+
// Print welcome banner now (was skipped before login overlay)
|
|
1095
|
+
// Skip if startup mode is 'menu' — the /menu command prints its own logo
|
|
1096
|
+
if (getStartupMode() !== 'menu') {
|
|
1097
|
+
this.printWelcome();
|
|
1098
|
+
}
|
|
1099
|
+
// Start session in background, show startup immediately
|
|
1100
|
+
void auth.startSession();
|
|
1101
|
+
auth.setLLMInfo(this.provider, this.model);
|
|
1102
|
+
await this.handleStartupSequence();
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Handle startup sequence: session restoration first, then menu if applicable.
|
|
1107
|
+
* This ensures proper ordering of startup overlays.
|
|
1108
|
+
*/
|
|
1109
|
+
async handleStartupSequence() {
|
|
1110
|
+
// Start heartbeat session in background (don't block UI)
|
|
1111
|
+
// Note: auth was already verified in main() — no need to re-check here
|
|
1112
|
+
if (!this.showLogin) {
|
|
1113
|
+
const auth = getAuthManager();
|
|
1114
|
+
void auth.startSession();
|
|
1115
|
+
auth.setLLMInfo(this.provider, this.model);
|
|
1116
|
+
}
|
|
1117
|
+
// Non-blocking cleanup of legacy (pre-multi-terminal) files and old history
|
|
1118
|
+
void getProjectSessionManager().cleanupLegacyFiles();
|
|
1119
|
+
const retentionDays = getSessionRetentionDays();
|
|
1120
|
+
if (retentionDays > 0) {
|
|
1121
|
+
void getProjectSessionManager().cleanupOldSessions(retentionDays);
|
|
1122
|
+
}
|
|
1123
|
+
// If startup mode is 'menu', show menu first (project selection),
|
|
1124
|
+
// then handle session restoration for the selected project.
|
|
1125
|
+
const startupMode = getStartupMode();
|
|
1126
|
+
if (startupMode === 'menu') {
|
|
1127
|
+
await this.handleCommand('menu', '');
|
|
1128
|
+
}
|
|
1129
|
+
// Check session mode setting (runs after project is known)
|
|
1130
|
+
const sessionMode = getProjectSessionMode();
|
|
1131
|
+
if (sessionMode === 'auto') {
|
|
1132
|
+
// Auto mode: silently restore session + team
|
|
1133
|
+
await this.autoRestoreSession();
|
|
1134
|
+
}
|
|
1135
|
+
else if (sessionMode === 'ask') {
|
|
1136
|
+
// Ask mode: show overlay to let user choose
|
|
1137
|
+
await this.handleSessionModePrompt();
|
|
1138
|
+
}
|
|
1139
|
+
// 'fresh' mode: do nothing - start with clean session
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* Restore a persisted team for a given terminal prefix.
|
|
1143
|
+
* Replaces the current (fresh) team with the one from disk.
|
|
1144
|
+
* Returns true if a team was restored.
|
|
1145
|
+
*/
|
|
1146
|
+
restoreTeamForPrefix(projectId, terminalPrefix) {
|
|
1147
|
+
if (!this.team || !this.agentFactory)
|
|
1148
|
+
return false;
|
|
1149
|
+
const checkpointer = getTeamCheckpointer();
|
|
1150
|
+
const persistedTeam = checkpointer.loadTeamByPrefix(projectId, terminalPrefix, this.agentFactory);
|
|
1151
|
+
if (!persistedTeam || persistedTeam.size <= 1)
|
|
1152
|
+
return false;
|
|
1153
|
+
// Set the current agent as default in the restored team
|
|
1154
|
+
const currentAgent = this.agent;
|
|
1155
|
+
if (currentAgent) {
|
|
1156
|
+
persistedTeam.setDefaultAgent(currentAgent);
|
|
1157
|
+
}
|
|
1158
|
+
// Replace the team and update UI autocomplete
|
|
1159
|
+
this.team = persistedTeam;
|
|
1160
|
+
this.onTeamReplaced?.(persistedTeam);
|
|
1161
|
+
setActiveSharedContext(persistedTeam.sharedContext);
|
|
1162
|
+
this.ui.setAgentSuggestionsCallback(() => {
|
|
1163
|
+
const agents = persistedTeam.getAll();
|
|
1164
|
+
return agents.map(a => ({ id: a.id, displayName: a.displayName, mascot: a.mascot }));
|
|
1165
|
+
});
|
|
1166
|
+
// Populate project info in shared context
|
|
1167
|
+
const activeProject = getActiveProject();
|
|
1168
|
+
if (activeProject) {
|
|
1169
|
+
persistedTeam.sharedContext.setProject({
|
|
1170
|
+
id: activeProject.id,
|
|
1171
|
+
name: activeProject.displayName,
|
|
1172
|
+
path: activeProject.path,
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
// Set team roster as anchor on the default agent
|
|
1176
|
+
if (persistedTeam.sharedContext.hasTeamRoster() && currentAgent?.hasAnchors()) {
|
|
1177
|
+
currentAgent.addAnchor({
|
|
1178
|
+
id: 'team-roster',
|
|
1179
|
+
content: persistedTeam.sharedContext.formatTeamRoster(),
|
|
1180
|
+
priority: 'info',
|
|
1181
|
+
scope: 'session',
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
// Sync roster to session registry
|
|
1185
|
+
const registry = getSessionRegistry();
|
|
1186
|
+
if (registry) {
|
|
1187
|
+
const agents = persistedTeam.getAll();
|
|
1188
|
+
registry.updateAgents(agents.map(a => ({
|
|
1189
|
+
id: a.id,
|
|
1190
|
+
displayName: a.displayName,
|
|
1191
|
+
mascot: a.mascot,
|
|
1192
|
+
})));
|
|
1193
|
+
}
|
|
1194
|
+
return true;
|
|
1195
|
+
}
|
|
1196
|
+
/**
|
|
1197
|
+
* Auto-restore session silently (for 'auto' mode).
|
|
1198
|
+
* Restores both conversation history AND team roster.
|
|
1199
|
+
* Called at startup when projectSessionMode='auto'.
|
|
1200
|
+
*
|
|
1201
|
+
* Behavior depends on whether other terminals are active on the same project:
|
|
1202
|
+
* - Single terminal: silently restore the most recent available session + claim it
|
|
1203
|
+
* - Multiple terminals: fall back to 'ask' mode (show overlay)
|
|
1204
|
+
*/
|
|
1205
|
+
async autoRestoreSession() {
|
|
1206
|
+
if (!this.agent)
|
|
1207
|
+
return;
|
|
1208
|
+
const activeProject = getActiveProject();
|
|
1209
|
+
const projectId = activeProject?.id ?? null;
|
|
1210
|
+
const ownPrefix = getActiveTerminalSessionId()?.slice(0, 8) ?? null;
|
|
1211
|
+
// Check how many terminals are active on this project
|
|
1212
|
+
// (our own terminal is already registered, so count=1 means we're alone)
|
|
1213
|
+
const registry = getSessionRegistry();
|
|
1214
|
+
const activeCount = registry?.getActiveSessionCount(projectId) ?? 1;
|
|
1215
|
+
if (activeCount > 1) {
|
|
1216
|
+
// Multiple terminals — can't auto-pick, fall back to ask mode (overlay)
|
|
1217
|
+
await this.offerOtherTerminalSessions(projectId);
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
// Single terminal — silently restore the most recent available session
|
|
1221
|
+
const psm = getProjectSessionManager();
|
|
1222
|
+
const allSessions = await psm.listProjectSessions(projectId);
|
|
1223
|
+
// Filter to available sessions (not from active terminals, has content)
|
|
1224
|
+
const available = allSessions.filter(s => {
|
|
1225
|
+
if (s.isActiveTerminal)
|
|
1226
|
+
return false;
|
|
1227
|
+
return s.messageCount > 0 || (s.teamAgentCount !== undefined && s.teamAgentCount > 1);
|
|
1228
|
+
});
|
|
1229
|
+
if (available.length === 0) {
|
|
1230
|
+
// No sessions to restore — start fresh silently
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
// Pick the most recent session (already sorted by updatedAt descending)
|
|
1234
|
+
const target = available[0];
|
|
1235
|
+
const targetPrefix = target.terminalPrefix;
|
|
1236
|
+
// Restore team roster
|
|
1237
|
+
if (targetPrefix) {
|
|
1238
|
+
this.restoreTeamForPrefix(projectId, targetPrefix);
|
|
1239
|
+
}
|
|
1240
|
+
// Load and restore conversation
|
|
1241
|
+
const session = targetPrefix
|
|
1242
|
+
? await psm.loadProjectSessionByPrefix(projectId, targetPrefix)
|
|
1243
|
+
: null;
|
|
1244
|
+
// Claim the session files (rename to our prefix)
|
|
1245
|
+
if (targetPrefix && ownPrefix && targetPrefix !== ownPrefix) {
|
|
1246
|
+
psm.claimSession(projectId, targetPrefix, ownPrefix);
|
|
1247
|
+
getTeamCheckpointer().claimTeamFiles(projectId, targetPrefix, ownPrefix);
|
|
1248
|
+
}
|
|
1249
|
+
if (session && session.messages.length > 0) {
|
|
1250
|
+
try {
|
|
1251
|
+
const filteredMessages = stripThinkingBlocks(session.messages);
|
|
1252
|
+
await this.agent.setHistory(filteredMessages, { turnCount: session.turnCount });
|
|
1253
|
+
const historyItems = convertMessagesToItems(filteredMessages);
|
|
1254
|
+
this.ui.setRestoredHistory(historyItems);
|
|
1255
|
+
const parts = [`${String(session.messages.length)} messages`];
|
|
1256
|
+
if (this.team && this.team.size > 1) {
|
|
1257
|
+
parts.push(`${String(this.team.size)} agents`);
|
|
1258
|
+
}
|
|
1259
|
+
const msg = `Session restored (${parts.join(', ')}). Tip: Use Ctrl+R to view conversation history.`;
|
|
1260
|
+
if (getStartupMode() === 'menu') {
|
|
1261
|
+
this.restoredSessionInfo = msg;
|
|
1262
|
+
}
|
|
1263
|
+
else {
|
|
1264
|
+
this.ui.print({ type: 'info', message: msg });
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
catch {
|
|
1268
|
+
// Silently fail - session restoration is best-effort
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
else if (this.team && this.team.size > 1) {
|
|
1272
|
+
// 0-message session but team was restored
|
|
1273
|
+
const msg = `Team restored (${String(this.team.size)} agents).`;
|
|
1274
|
+
if (getStartupMode() === 'menu') {
|
|
1275
|
+
this.restoredSessionInfo = msg;
|
|
1276
|
+
}
|
|
1277
|
+
else {
|
|
1278
|
+
this.ui.print({ type: 'info', message: msg });
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
/**
|
|
1283
|
+
* When this terminal has no session but other terminals do, show the resume
|
|
1284
|
+
* overlay so the user can pick one or start fresh.
|
|
1285
|
+
*/
|
|
1286
|
+
async offerOtherTerminalSessions(projectId) {
|
|
1287
|
+
if (!this.agent)
|
|
1288
|
+
return;
|
|
1289
|
+
const psm = getProjectSessionManager();
|
|
1290
|
+
const otherSessions = await psm.listProjectSessions(projectId);
|
|
1291
|
+
// Filter out sessions from active terminals (they're "in use")
|
|
1292
|
+
// but include 0-message sessions (they may have a team roster)
|
|
1293
|
+
const validSessions = otherSessions.filter(s => {
|
|
1294
|
+
// Exclude sessions that are actively in use by another terminal
|
|
1295
|
+
if (s.isActiveTerminal)
|
|
1296
|
+
return false;
|
|
1297
|
+
// Include sessions that have messages OR a team roster
|
|
1298
|
+
return s.messageCount > 0 || (s.teamAgentCount !== undefined && s.teamAgentCount > 1);
|
|
1299
|
+
});
|
|
1300
|
+
if (validSessions.length === 0)
|
|
1301
|
+
return;
|
|
1302
|
+
// Show info about available sessions
|
|
1303
|
+
const count = validSessions.length;
|
|
1304
|
+
this.ui.print({ type: 'info', message: `Found ${String(count)} previous session${count > 1 ? 's' : ''} for this project.` });
|
|
1305
|
+
// Show resume overlay
|
|
1306
|
+
const currentProject = getActiveProject();
|
|
1307
|
+
const overlay = new ResumeOverlayV2({
|
|
1308
|
+
sessions: validSessions,
|
|
1309
|
+
currentProjectId: currentProject?.id ?? null,
|
|
1310
|
+
onDelete: async (sessionId) => {
|
|
1311
|
+
// Inline delete handler (same logic as the /resume command)
|
|
1312
|
+
const parsed = this.parseCurrentSessionIdForStartup(sessionId);
|
|
1313
|
+
if (parsed?.terminalPrefix) {
|
|
1314
|
+
return psm.deleteSessionByPrefix(parsed.projectId, parsed.terminalPrefix);
|
|
1315
|
+
}
|
|
1316
|
+
if (parsed) {
|
|
1317
|
+
return psm.deleteCurrentSession(parsed.projectId);
|
|
1318
|
+
}
|
|
1319
|
+
return false;
|
|
1320
|
+
},
|
|
1321
|
+
});
|
|
1322
|
+
const result = await this.ui.showOverlay(overlay);
|
|
1323
|
+
if (result?.deleted) {
|
|
1324
|
+
this.ui.print({ type: 'success', message: 'Session deleted.' });
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
if (result?.sessionId) {
|
|
1328
|
+
const parsed = this.parseCurrentSessionIdForStartup(result.sessionId);
|
|
1329
|
+
if (!parsed)
|
|
1330
|
+
return;
|
|
1331
|
+
// Restore team roster if the selected session has a terminal prefix
|
|
1332
|
+
if (parsed.terminalPrefix) {
|
|
1333
|
+
this.restoreTeamForPrefix(parsed.projectId, parsed.terminalPrefix);
|
|
1334
|
+
}
|
|
1335
|
+
const selectedSession = parsed.terminalPrefix
|
|
1336
|
+
? await psm.loadProjectSessionByPrefix(parsed.projectId, parsed.terminalPrefix)
|
|
1337
|
+
: await psm.loadProjectSession(parsed.projectId);
|
|
1338
|
+
if (!selectedSession) {
|
|
1339
|
+
this.ui.print({ type: 'error', message: 'Failed to load session.' });
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
// Claim: rename files from old prefix to this terminal's prefix
|
|
1343
|
+
const ownPrefix = getActiveTerminalSessionId()?.slice(0, 8) ?? null;
|
|
1344
|
+
if (parsed.terminalPrefix && ownPrefix && parsed.terminalPrefix !== ownPrefix) {
|
|
1345
|
+
psm.claimSession(parsed.projectId, parsed.terminalPrefix, ownPrefix);
|
|
1346
|
+
getTeamCheckpointer().claimTeamFiles(parsed.projectId, parsed.terminalPrefix, ownPrefix);
|
|
1347
|
+
}
|
|
1348
|
+
// Restore conversation messages (if any)
|
|
1349
|
+
if (selectedSession.messages.length > 0) {
|
|
1350
|
+
try {
|
|
1351
|
+
const filteredMessages = stripThinkingBlocks(selectedSession.messages);
|
|
1352
|
+
await this.agent.setHistory(filteredMessages, { turnCount: selectedSession.turnCount });
|
|
1353
|
+
this.ui.setRestoredHistory(convertMessagesToItems(filteredMessages));
|
|
1354
|
+
}
|
|
1355
|
+
catch {
|
|
1356
|
+
this.ui.print({ type: 'error', message: 'Failed to restore session.' });
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
// Report what was restored
|
|
1361
|
+
const parts = [];
|
|
1362
|
+
if (selectedSession.messages.length > 0) {
|
|
1363
|
+
parts.push(`${String(selectedSession.messages.length)} messages`);
|
|
1364
|
+
}
|
|
1365
|
+
if (this.team && this.team.size > 1) {
|
|
1366
|
+
parts.push(`${String(this.team.size)} agents`);
|
|
1367
|
+
}
|
|
1368
|
+
if (parts.length > 0) {
|
|
1369
|
+
this.ui.print({ type: 'success', message: `Session restored (${parts.join(', ')}).` });
|
|
1370
|
+
}
|
|
1371
|
+
else {
|
|
1372
|
+
this.ui.print({ type: 'success', message: 'Session restored.' });
|
|
1373
|
+
}
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1376
|
+
// User cancelled — start fresh
|
|
1377
|
+
}
|
|
1378
|
+
/**
|
|
1379
|
+
* Parse session ID format for startup flow (duplicates session handler logic
|
|
1380
|
+
* to avoid circular import).
|
|
1381
|
+
*/
|
|
1382
|
+
parseCurrentSessionIdForStartup(sessionId) {
|
|
1383
|
+
if (!sessionId.startsWith('current-'))
|
|
1384
|
+
return null;
|
|
1385
|
+
const rest = sessionId.slice('current-'.length);
|
|
1386
|
+
const hexMatch = /^([a-f0-9]{8})-(.+)$/.exec(rest);
|
|
1387
|
+
if (hexMatch) {
|
|
1388
|
+
const projectIdStr = hexMatch[2];
|
|
1389
|
+
return { terminalPrefix: hexMatch[1], projectId: projectIdStr === 'global' ? null : parseInt(projectIdStr, 10) };
|
|
1390
|
+
}
|
|
1391
|
+
return { terminalPrefix: null, projectId: rest === 'global' ? null : parseInt(rest, 10) };
|
|
1392
|
+
}
|
|
1393
|
+
/**
|
|
1394
|
+
* Handle session mode prompt at startup when projectSessionMode='ask'.
|
|
1395
|
+
* Shows overlay to let user choose how to handle existing session.
|
|
1396
|
+
*/
|
|
1397
|
+
async handleSessionModePrompt() {
|
|
1398
|
+
// Get current project
|
|
1399
|
+
const activeProject = getActiveProject();
|
|
1400
|
+
const projectId = activeProject?.id ?? null;
|
|
1401
|
+
const projectName = activeProject?.displayName ?? 'Global';
|
|
1402
|
+
// Check if there's a session to load for THIS terminal
|
|
1403
|
+
const session = await loadProjectSession(projectId);
|
|
1404
|
+
if (!session) {
|
|
1405
|
+
// No own session — check other terminals
|
|
1406
|
+
await this.offerOtherTerminalSessions(projectId);
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
// Restore team for own session prefix
|
|
1410
|
+
const ownPrefix = getActiveTerminalSessionId()?.slice(0, 8) ?? null;
|
|
1411
|
+
if (ownPrefix) {
|
|
1412
|
+
this.restoreTeamForPrefix(projectId, ownPrefix);
|
|
1413
|
+
}
|
|
1414
|
+
if (session.messages.length === 0) {
|
|
1415
|
+
// Session exists but no messages — team was already restored above
|
|
1416
|
+
if (this.team && this.team.size > 1) {
|
|
1417
|
+
this.ui.print({ type: 'info', message: `Team restored (${String(this.team.size)} agents).` });
|
|
1418
|
+
}
|
|
1419
|
+
// Also check other terminals for sessions to resume
|
|
1420
|
+
await this.offerOtherTerminalSessions(projectId);
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
// Get last user message for preview
|
|
1424
|
+
let lastMessagePreview;
|
|
1425
|
+
const lastUserMsg = session.messages.filter(m => m.role === 'user').pop();
|
|
1426
|
+
if (lastUserMsg) {
|
|
1427
|
+
if (typeof lastUserMsg.content === 'string') {
|
|
1428
|
+
lastMessagePreview = lastUserMsg.content;
|
|
1429
|
+
}
|
|
1430
|
+
else if (Array.isArray(lastUserMsg.content)) {
|
|
1431
|
+
const textBlock = lastUserMsg.content.find((b) => b.type === 'text');
|
|
1432
|
+
if (textBlock) {
|
|
1433
|
+
lastMessagePreview = textBlock.text;
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
// Show the session mode overlay
|
|
1438
|
+
const overlay = new SessionModeOverlayV2({
|
|
1439
|
+
projectName,
|
|
1440
|
+
messageCount: session.messages.length,
|
|
1441
|
+
lastActivity: new Date(session.updatedAt),
|
|
1442
|
+
lastMessagePreview,
|
|
1443
|
+
});
|
|
1444
|
+
const result = await this.ui.showOverlay(overlay);
|
|
1445
|
+
if (!result) {
|
|
1446
|
+
// Cancelled - default to resume-full
|
|
1447
|
+
return this.handleSessionModeResult('resume-full', session);
|
|
1448
|
+
}
|
|
1449
|
+
await this.handleSessionModeResult(result.option, session);
|
|
1450
|
+
}
|
|
1451
|
+
/**
|
|
1452
|
+
* Apply the user's session mode choice.
|
|
1453
|
+
*/
|
|
1454
|
+
async handleSessionModeResult(option, session) {
|
|
1455
|
+
if (!session)
|
|
1456
|
+
return;
|
|
1457
|
+
switch (option) {
|
|
1458
|
+
case 'resume-full':
|
|
1459
|
+
// Load the full session
|
|
1460
|
+
if (this.agent) {
|
|
1461
|
+
try {
|
|
1462
|
+
const filteredFull = stripThinkingBlocks(session.messages);
|
|
1463
|
+
await this.agent.setHistory(filteredFull, { turnCount: session.turnCount });
|
|
1464
|
+
this.ui.setRestoredHistory(convertMessagesToItems(filteredFull));
|
|
1465
|
+
this.ui.print({ type: 'success', message: `Session restored (${String(session.messages.length)} messages).` });
|
|
1466
|
+
this.ui.print({ type: 'info', message: 'Tip: Use Ctrl+R to view conversation history.' });
|
|
1467
|
+
}
|
|
1468
|
+
catch {
|
|
1469
|
+
this.ui.print({ type: 'error', message: 'Failed to restore session.' });
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
break;
|
|
1473
|
+
case 'resume-messages':
|
|
1474
|
+
// Load messages only (context refresh is deferred - same as resume-full for now)
|
|
1475
|
+
if (this.agent) {
|
|
1476
|
+
try {
|
|
1477
|
+
const filteredMsgs = stripThinkingBlocks(session.messages);
|
|
1478
|
+
await this.agent.setHistory(filteredMsgs, { turnCount: session.turnCount });
|
|
1479
|
+
this.ui.setRestoredHistory(convertMessagesToItems(filteredMsgs));
|
|
1480
|
+
this.ui.print({ type: 'success', message: `Messages restored (${String(session.messages.length)} messages).` });
|
|
1481
|
+
this.ui.print({ type: 'info', message: 'Tip: Use Ctrl+R to view conversation history.' });
|
|
1482
|
+
}
|
|
1483
|
+
catch {
|
|
1484
|
+
this.ui.print({ type: 'error', message: 'Failed to restore session.' });
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
break;
|
|
1488
|
+
case 'start-fresh':
|
|
1489
|
+
// Archive old session and start fresh
|
|
1490
|
+
try {
|
|
1491
|
+
await archiveCurrentSession();
|
|
1492
|
+
this.ui.print({ type: 'info', message: 'Previous session archived. Starting fresh.' });
|
|
1493
|
+
}
|
|
1494
|
+
catch {
|
|
1495
|
+
// Silent fail - archiving is best-effort
|
|
1496
|
+
this.ui.print({ type: 'info', message: 'Starting fresh.' });
|
|
1497
|
+
}
|
|
1498
|
+
break;
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
/**
|
|
1502
|
+
* Show the onboarding wizard for first-time users.
|
|
1503
|
+
* Called on first run, or manually via /init command.
|
|
1504
|
+
*/
|
|
1505
|
+
async showOnboardingWizard() {
|
|
1506
|
+
const wizard = new OnboardingWizardOverlayV2();
|
|
1507
|
+
const result = await this.ui.showOverlay(wizard);
|
|
1508
|
+
// Handle result
|
|
1509
|
+
if (result?.openKeys) {
|
|
1510
|
+
// User chose to configure keys - show keys overlay
|
|
1511
|
+
const keysOverlay = new KeysOverlayV2();
|
|
1512
|
+
await this.ui.showOverlay(keysOverlay);
|
|
1513
|
+
// After keys, re-show the onboarding wizard to continue
|
|
1514
|
+
await this.showOnboardingWizard();
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1517
|
+
// Start session heartbeat in background (don't block UI)
|
|
1518
|
+
const auth = getAuthManager();
|
|
1519
|
+
void auth.startSession();
|
|
1520
|
+
const startupMode = getStartupMode();
|
|
1521
|
+
if (startupMode === 'menu') {
|
|
1522
|
+
await this.handleCommand('menu', '');
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
/**
|
|
1526
|
+
* Stop the REPL.
|
|
1527
|
+
* Aborts all running sessions (foreground and background).
|
|
1528
|
+
*/
|
|
1529
|
+
/** Set MCP loading state on the footer status bar. */
|
|
1530
|
+
setMCPLoading(loading, toolCount) {
|
|
1531
|
+
this.ui.setMCPLoading(loading, toolCount);
|
|
1532
|
+
}
|
|
1533
|
+
stop() {
|
|
1534
|
+
// Stop notification polling before unregistering session
|
|
1535
|
+
try {
|
|
1536
|
+
const notifMgr = getNotificationManager();
|
|
1537
|
+
if (notifMgr) {
|
|
1538
|
+
notifMgr.stopPolling();
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
catch {
|
|
1542
|
+
// Best effort
|
|
1543
|
+
}
|
|
1544
|
+
// Unregister from session registry
|
|
1545
|
+
try {
|
|
1546
|
+
const registry = getSessionRegistry();
|
|
1547
|
+
if (registry) {
|
|
1548
|
+
registry.unregister();
|
|
1549
|
+
}
|
|
1550
|
+
setActiveTerminalSessionId(null);
|
|
1551
|
+
}
|
|
1552
|
+
catch {
|
|
1553
|
+
// Best effort
|
|
1554
|
+
}
|
|
1555
|
+
// Abort all sessions
|
|
1556
|
+
for (const [agentId, session] of this.sessions) {
|
|
1557
|
+
session.abortController.abort();
|
|
1558
|
+
this.sessions.delete(agentId);
|
|
1559
|
+
}
|
|
1560
|
+
// Clear all message queues (don't process on shutdown)
|
|
1561
|
+
this.agentMessageQueues.clear();
|
|
1562
|
+
this.currentSession = null;
|
|
1563
|
+
this.foregroundAgentId = null;
|
|
1564
|
+
this.ui.stop();
|
|
1565
|
+
}
|
|
1566
|
+
// ===========================================================================
|
|
1567
|
+
// Welcome Message
|
|
1568
|
+
// ===========================================================================
|
|
1569
|
+
/**
|
|
1570
|
+
* Print just the logo to the scrolling area.
|
|
1571
|
+
* Used for dashboard mode where hints are not needed.
|
|
1572
|
+
*/
|
|
1573
|
+
printLogo() {
|
|
1574
|
+
const settings = getSettings();
|
|
1575
|
+
const s = getStyles();
|
|
1576
|
+
terminal.clearScreen();
|
|
1577
|
+
// Show "What's New" highlights only when version has changed since last seen
|
|
1578
|
+
const isNewVersion = settings.lastSeenVersion !== this.version;
|
|
1579
|
+
const highlights = isNewVersion ? getStartupHighlights(this.version) : undefined;
|
|
1580
|
+
const logoLines = renderMascotWithLogo(settings.mascot, this.version, null, highlights);
|
|
1581
|
+
if (isNewVersion) {
|
|
1582
|
+
updateSettings({ lastSeenVersion: this.version });
|
|
1583
|
+
}
|
|
1584
|
+
console.log('');
|
|
1585
|
+
for (const line of logoLines) {
|
|
1586
|
+
console.log(line);
|
|
1587
|
+
}
|
|
1588
|
+
console.log('');
|
|
1589
|
+
// Show agent status
|
|
1590
|
+
if (this.agent) {
|
|
1591
|
+
console.log(s.success(' ✓ Agent connected'));
|
|
1592
|
+
}
|
|
1593
|
+
else {
|
|
1594
|
+
console.log(s.warning(' ⚠ Simulation mode (no agent)'));
|
|
1595
|
+
}
|
|
1596
|
+
console.log('');
|
|
1597
|
+
// Show model info
|
|
1598
|
+
console.log(s.muted('Model: ') + s.secondary(this.model));
|
|
1599
|
+
console.log(s.muted('Provider: ') + s.secondary(this.provider));
|
|
1600
|
+
console.log('');
|
|
1601
|
+
}
|
|
1602
|
+
/**
|
|
1603
|
+
* Print full welcome message for REPL mode.
|
|
1604
|
+
*/
|
|
1605
|
+
printWelcome() {
|
|
1606
|
+
const s = getStyles();
|
|
1607
|
+
// Print logo with agent/model info
|
|
1608
|
+
this.printLogo();
|
|
1609
|
+
// Show update notification if available
|
|
1610
|
+
if (this.updateAvailable) {
|
|
1611
|
+
console.log(s.warning(` ⬆ Update available: v${this.updateAvailable.current} → v${this.updateAvailable.latest}`));
|
|
1612
|
+
console.log(s.muted(' Run: npm update -g @compilr-dev/cli'));
|
|
1613
|
+
console.log('');
|
|
1614
|
+
}
|
|
1615
|
+
// Show session hint if recent session exists (< 24h)
|
|
1616
|
+
// Uses fast O(1) stat check instead of scanning all session directories
|
|
1617
|
+
try {
|
|
1618
|
+
const activeProject = getActiveProject();
|
|
1619
|
+
const psm = getProjectSessionManager();
|
|
1620
|
+
if (psm.hasRecentSessionFast(activeProject?.id ?? null)) {
|
|
1621
|
+
console.log(s.info('Tip: Recent session available. Use /resume to continue.'));
|
|
1622
|
+
console.log('');
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
catch {
|
|
1626
|
+
// Silently ignore errors checking for sessions
|
|
1627
|
+
}
|
|
1628
|
+
// Show in_progress plans notification
|
|
1629
|
+
try {
|
|
1630
|
+
const activeProject = getActiveProject();
|
|
1631
|
+
if (activeProject) {
|
|
1632
|
+
const inProgressPlans = planRepository.getInProgress(activeProject.id);
|
|
1633
|
+
if (inProgressPlans.length === 1) {
|
|
1634
|
+
console.log(s.info(`Tip: Plan "${inProgressPlans[0].name}" is in progress. Use /docs or Plan Mode to continue.`));
|
|
1635
|
+
console.log('');
|
|
1636
|
+
}
|
|
1637
|
+
else if (inProgressPlans.length > 1) {
|
|
1638
|
+
console.log(s.info(`Tip: You have ${String(inProgressPlans.length)} plan(s) in progress. Use /docs to view.`));
|
|
1639
|
+
console.log('');
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
catch {
|
|
1644
|
+
// Silently ignore errors checking for plans
|
|
1645
|
+
}
|
|
1646
|
+
// Hints
|
|
1647
|
+
console.log(s.muted('Type a message to start'));
|
|
1648
|
+
console.log(s.muted('Type /help for commands, /exit to quit'));
|
|
1649
|
+
console.log('');
|
|
1650
|
+
}
|
|
1651
|
+
// ===========================================================================
|
|
1652
|
+
// Command Handling
|
|
1653
|
+
// ===========================================================================
|
|
1654
|
+
createCommandContext() {
|
|
1655
|
+
return {
|
|
1656
|
+
ui: this.ui,
|
|
1657
|
+
version: this.version,
|
|
1658
|
+
updateAvailable: this.updateAvailable,
|
|
1659
|
+
printWelcome: () => { this.printWelcome(); return Promise.resolve(); },
|
|
1660
|
+
printLogo: () => { this.printLogo(); },
|
|
1661
|
+
consumeRestoredSessionInfo: () => {
|
|
1662
|
+
const info = this.restoredSessionInfo;
|
|
1663
|
+
this.restoredSessionInfo = null;
|
|
1664
|
+
return info;
|
|
1665
|
+
},
|
|
1666
|
+
// Agent integration
|
|
1667
|
+
agent: this.agent,
|
|
1668
|
+
team: this.team,
|
|
1669
|
+
agentFactory: this.agentFactory,
|
|
1670
|
+
setTeam: (newTeam) => {
|
|
1671
|
+
const oldTeam = this.team;
|
|
1672
|
+
this.team = newTeam;
|
|
1673
|
+
this.onTeamReplaced?.(newTeam);
|
|
1674
|
+
// Update agent suggestions callback with new team
|
|
1675
|
+
this.ui.setAgentSuggestionsCallback(() => {
|
|
1676
|
+
const agents = newTeam.getAll();
|
|
1677
|
+
return agents.map((agent) => ({
|
|
1678
|
+
id: agent.id,
|
|
1679
|
+
displayName: agent.displayName,
|
|
1680
|
+
mascot: agent.mascot,
|
|
1681
|
+
}));
|
|
1682
|
+
});
|
|
1683
|
+
return oldTeam;
|
|
1684
|
+
},
|
|
1685
|
+
restoreAgent: (newAgent) => {
|
|
1686
|
+
this.restoreAgent(newAgent);
|
|
1687
|
+
},
|
|
1688
|
+
model: this.model,
|
|
1689
|
+
provider: this.provider,
|
|
1690
|
+
getContextManager: () => {
|
|
1691
|
+
return this.agent?.getContextManager() ?? null;
|
|
1692
|
+
},
|
|
1693
|
+
getHistory: () => {
|
|
1694
|
+
return this.agent?.getHistory() ?? [];
|
|
1695
|
+
},
|
|
1696
|
+
// Queue agent message (for commands that invoke the agent)
|
|
1697
|
+
queueAgentMessage: (options) => {
|
|
1698
|
+
this.ui.queueAgentMessage(options);
|
|
1699
|
+
},
|
|
1700
|
+
// Session stats
|
|
1701
|
+
startTime: this.startTime,
|
|
1702
|
+
sessionInputTokens: this.sessionInputTokens,
|
|
1703
|
+
sessionOutputTokens: this.sessionOutputTokens,
|
|
1704
|
+
sessionRequests: this.sessionRequests,
|
|
1705
|
+
// Background session management (Phase 3c)
|
|
1706
|
+
getBackgroundSessions: () => {
|
|
1707
|
+
return this.getBackgroundSessionsInfo();
|
|
1708
|
+
},
|
|
1709
|
+
bringToForeground: async (agentId) => {
|
|
1710
|
+
return this.bringAgentToForeground(agentId);
|
|
1711
|
+
},
|
|
1712
|
+
switchAgent: async (agentId) => {
|
|
1713
|
+
await this.switchAgent(agentId);
|
|
1714
|
+
},
|
|
1715
|
+
killBackgroundAgent: (agentId) => {
|
|
1716
|
+
return this.killAgent(agentId);
|
|
1717
|
+
},
|
|
1718
|
+
sendForegroundToBackground: () => {
|
|
1719
|
+
return this.sendForegroundToBackground();
|
|
1720
|
+
},
|
|
1721
|
+
clearAllQueues: () => {
|
|
1722
|
+
return this.clearAllQueues();
|
|
1723
|
+
},
|
|
1724
|
+
};
|
|
1725
|
+
}
|
|
1726
|
+
async handleCommand(cmd, args) {
|
|
1727
|
+
// Record command usage for telemetry
|
|
1728
|
+
getAuthManager().recordCommand(cmd);
|
|
1729
|
+
const ctx = this.createCommandContext();
|
|
1730
|
+
const result = await executeCommand(cmd, args, ctx);
|
|
1731
|
+
if (result === null) {
|
|
1732
|
+
// Check if it's a custom command
|
|
1733
|
+
const customRegistry = getCustomCommandRegistry();
|
|
1734
|
+
if (customRegistry.has(cmd)) {
|
|
1735
|
+
// Expand custom command and send as message
|
|
1736
|
+
const customArgs = args ? args.split(' ').filter(a => a.trim()) : [];
|
|
1737
|
+
const expanded = customRegistry.expand(cmd, customArgs);
|
|
1738
|
+
if (expanded) {
|
|
1739
|
+
// Show user what command is being run
|
|
1740
|
+
this.ui.print({ type: 'user-message', text: `/${cmd}${args ? ' ' + args : ''}` });
|
|
1741
|
+
// Send expanded prompt to agent
|
|
1742
|
+
if (this.agent) {
|
|
1743
|
+
await this.runAgentReal(expanded);
|
|
1744
|
+
}
|
|
1745
|
+
else {
|
|
1746
|
+
await this.runAgentSimulation(expanded);
|
|
1747
|
+
}
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
this.ui.print({ type: 'warning', message: `Unknown command: /${cmd}` });
|
|
1752
|
+
this.ui.print({ type: 'info', message: 'Type /help to see available commands' });
|
|
1753
|
+
}
|
|
1754
|
+
else if (!result) {
|
|
1755
|
+
// result === false means exit — clean up before exiting
|
|
1756
|
+
this.stop();
|
|
1757
|
+
process.exit(0);
|
|
1758
|
+
}
|
|
1759
|
+
// Process any queued agent messages (from commands like /arch, /design)
|
|
1760
|
+
await this.processAgentMessageQueue();
|
|
1761
|
+
}
|
|
1762
|
+
/**
|
|
1763
|
+
* Process queued agent messages.
|
|
1764
|
+
* Commands can queue messages to invoke the agent programmatically.
|
|
1765
|
+
*/
|
|
1766
|
+
async processAgentMessageQueue() {
|
|
1767
|
+
while (this.ui.hasAgentMessage()) {
|
|
1768
|
+
const agentMsg = this.ui.popAgentMessage();
|
|
1769
|
+
if (agentMsg) {
|
|
1770
|
+
// Show display message to user (or full message if no display message)
|
|
1771
|
+
this.ui.print({
|
|
1772
|
+
type: 'user-message',
|
|
1773
|
+
text: agentMsg.displayMessage ?? agentMsg.message,
|
|
1774
|
+
});
|
|
1775
|
+
// Send full message to agent
|
|
1776
|
+
if (this.agent) {
|
|
1777
|
+
await this.runAgentReal(agentMsg.message);
|
|
1778
|
+
}
|
|
1779
|
+
else {
|
|
1780
|
+
await this.runAgentSimulation(agentMsg.message);
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
// ===========================================================================
|
|
1786
|
+
// Input Processing
|
|
1787
|
+
// ===========================================================================
|
|
1788
|
+
async processInput(input) {
|
|
1789
|
+
// Ignore empty input
|
|
1790
|
+
if (!input.trim()) {
|
|
1791
|
+
return;
|
|
1792
|
+
}
|
|
1793
|
+
// Auth-only mode: restrict to /login, /logout, /exit, /help
|
|
1794
|
+
if (this.authOnly) {
|
|
1795
|
+
const allowedCommands = ['login', 'logout', 'exit', 'help'];
|
|
1796
|
+
if (input.startsWith('/')) {
|
|
1797
|
+
const spaceIndex = input.indexOf(' ');
|
|
1798
|
+
const cmd = spaceIndex > 0 ? input.slice(1, spaceIndex) : input.slice(1);
|
|
1799
|
+
if (allowedCommands.includes(cmd.toLowerCase())) {
|
|
1800
|
+
const args = spaceIndex > 0 ? input.slice(spaceIndex + 1) : '';
|
|
1801
|
+
await this.handleCommand(cmd, args);
|
|
1802
|
+
return;
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
// Block all other input
|
|
1806
|
+
this.ui.print({ type: 'warning', message: 'Please log in first to use the CLI.' });
|
|
1807
|
+
this.ui.print({ type: 'info', message: 'Use /login to authenticate or /exit to quit.' });
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1810
|
+
// Handle $name prefix (agent switching)
|
|
1811
|
+
// e.g., "$arch Design the API" → switch to arch, send message
|
|
1812
|
+
// e.g., "$arch Design the API &" → switch to arch, run in background
|
|
1813
|
+
// e.g., "$pm /backlog" → switch to pm, run /backlog command
|
|
1814
|
+
// e.g., "$arch" → just switch to arch (no message sent)
|
|
1815
|
+
if (input.startsWith('$') && this.team) {
|
|
1816
|
+
const parsed = await this.parseAgentPrefix(input);
|
|
1817
|
+
if (parsed) {
|
|
1818
|
+
// Only process remaining input if there is one
|
|
1819
|
+
if (parsed.remainingInput.trim()) {
|
|
1820
|
+
if (parsed.isBackground) {
|
|
1821
|
+
// Background execution - fire and forget
|
|
1822
|
+
this.processInputBackground(parsed.agentId, parsed.remainingInput);
|
|
1823
|
+
}
|
|
1824
|
+
else {
|
|
1825
|
+
// Foreground execution - await
|
|
1826
|
+
await this.processInput(parsed.remainingInput);
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
// Otherwise just switch was done, nothing more to do
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1832
|
+
// If parsing failed (unknown agent), fall through to treat as regular input
|
|
1833
|
+
}
|
|
1834
|
+
if (input.startsWith('/')) {
|
|
1835
|
+
const spaceIndex = input.indexOf(' ');
|
|
1836
|
+
const cmd = spaceIndex > 0 ? input.slice(1, spaceIndex) : input.slice(1);
|
|
1837
|
+
const args = spaceIndex > 0 ? input.slice(spaceIndex + 1) : '';
|
|
1838
|
+
await this.handleCommand(cmd, args);
|
|
1839
|
+
await this.processQueue();
|
|
1840
|
+
}
|
|
1841
|
+
else if (input.startsWith('!')) {
|
|
1842
|
+
// Handle bash command ("!ls", "!git status", etc.)
|
|
1843
|
+
await this.handleBashCommand(input.slice(1).trim());
|
|
1844
|
+
await this.processQueue();
|
|
1845
|
+
}
|
|
1846
|
+
else if (isMemoryInput(input)) {
|
|
1847
|
+
// Handle memory note ("# note")
|
|
1848
|
+
this.handleMemoryInputV2(input);
|
|
1849
|
+
}
|
|
1850
|
+
else {
|
|
1851
|
+
// Check for & suffix (background execution with current agent)
|
|
1852
|
+
const isBackground = input.trim().endsWith('&');
|
|
1853
|
+
const cleanInput = isBackground ? input.slice(0, -1).trim() : input;
|
|
1854
|
+
if (isBackground && this.team && this.agent) {
|
|
1855
|
+
// Background execution with current active agent
|
|
1856
|
+
const activeAgent = this.team.getActive();
|
|
1857
|
+
// Auto-expand filter if user is sending to agent not in filter
|
|
1858
|
+
this.autoExpandFilterIfNeeded(activeAgent.id);
|
|
1859
|
+
this.ui.print({ type: 'user-message', text: `${cleanInput} [background: $${activeAgent.id}]`, agentId: activeAgent.id });
|
|
1860
|
+
const messageToSend = this.resolveAndInjectMentions(cleanInput);
|
|
1861
|
+
const label = this.getAgentMascotLabel(activeAgent.id);
|
|
1862
|
+
this.ui.print({
|
|
1863
|
+
type: 'info',
|
|
1864
|
+
message: `${label} Started in background`,
|
|
1865
|
+
});
|
|
1866
|
+
void this.runAgentBackground(activeAgent.id, messageToSend);
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
1869
|
+
// Get active agent ID for tracking
|
|
1870
|
+
const activeAgentId = this.team?.getActive().id ?? 'default';
|
|
1871
|
+
// Auto-expand filter if user is sending to agent not in filter
|
|
1872
|
+
this.autoExpandFilterIfNeeded(activeAgentId);
|
|
1873
|
+
// Print user message
|
|
1874
|
+
this.ui.print({ type: 'user-message', text: cleanInput, agentId: activeAgentId });
|
|
1875
|
+
// Resolve $agent mention references and inject context
|
|
1876
|
+
const messageToSend = this.resolveAndInjectMentions(cleanInput);
|
|
1877
|
+
// Check if the current agent has an active session (still running)
|
|
1878
|
+
// If so, queue the message and return immediately (non-blocking)
|
|
1879
|
+
const existingSession = this.sessions.get(activeAgentId);
|
|
1880
|
+
if (existingSession) {
|
|
1881
|
+
const label = this.getAgentMascotLabel(activeAgentId);
|
|
1882
|
+
const position = this.queueMessage(activeAgentId, messageToSend, cleanInput);
|
|
1883
|
+
if (position === null) {
|
|
1884
|
+
// Queue is full
|
|
1885
|
+
this.ui.print({
|
|
1886
|
+
type: 'warning',
|
|
1887
|
+
message: `${label} queue is full (${String(MAX_QUEUE_SIZE)} messages). Wait for some to complete.`,
|
|
1888
|
+
});
|
|
1889
|
+
}
|
|
1890
|
+
else {
|
|
1891
|
+
// Message queued successfully
|
|
1892
|
+
const queueInfo = position === 1 ? 'next in queue' : `position ${String(position)} in queue`;
|
|
1893
|
+
this.ui.print({
|
|
1894
|
+
type: 'info',
|
|
1895
|
+
message: `${label} is busy, message queued (${queueInfo})`,
|
|
1896
|
+
});
|
|
1897
|
+
}
|
|
1898
|
+
return; // Return immediately - user can switch agents or continue
|
|
1899
|
+
}
|
|
1900
|
+
// Run agent (real or simulation)
|
|
1901
|
+
if (this.agent) {
|
|
1902
|
+
await this.runAgentReal(messageToSend);
|
|
1903
|
+
}
|
|
1904
|
+
else {
|
|
1905
|
+
await this.runAgentSimulation(messageToSend);
|
|
1906
|
+
}
|
|
1907
|
+
await this.processQueue();
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
/**
|
|
1911
|
+
* Process input in background mode.
|
|
1912
|
+
* Starts the agent without blocking, allowing user to continue with foreground work.
|
|
1913
|
+
*
|
|
1914
|
+
* @param agentId - The agent to run in background
|
|
1915
|
+
* @param input - The message to send to the agent
|
|
1916
|
+
*/
|
|
1917
|
+
processInputBackground(agentId, input) {
|
|
1918
|
+
// Check if agent already has a running session
|
|
1919
|
+
if (this.hasActiveSession(agentId)) {
|
|
1920
|
+
this.ui.print({
|
|
1921
|
+
type: 'error',
|
|
1922
|
+
message: `Agent '${agentId}' is already running. Use /kill $${agentId} to stop it first.`,
|
|
1923
|
+
});
|
|
1924
|
+
return;
|
|
1925
|
+
}
|
|
1926
|
+
// Get mascot label for the agent
|
|
1927
|
+
const label = this.getAgentMascotLabel(agentId);
|
|
1928
|
+
// Auto-expand filter if user is sending to agent not in filter
|
|
1929
|
+
this.autoExpandFilterIfNeeded(agentId);
|
|
1930
|
+
// Print user message with background indicator
|
|
1931
|
+
this.ui.print({ type: 'user-message', text: `${input} [background: $${agentId}]`, agentId });
|
|
1932
|
+
// Resolve $agent mention references and inject context
|
|
1933
|
+
const messageToSend = this.resolveAndInjectMentions(input);
|
|
1934
|
+
// Show that background agent is starting
|
|
1935
|
+
this.ui.print({
|
|
1936
|
+
type: 'info',
|
|
1937
|
+
message: `${label} Started in background`,
|
|
1938
|
+
});
|
|
1939
|
+
// Start agent in background (don't await)
|
|
1940
|
+
if (this.agent) {
|
|
1941
|
+
void this.runAgentBackground(agentId, messageToSend);
|
|
1942
|
+
}
|
|
1943
|
+
else {
|
|
1944
|
+
// Simulation mode - just show a message
|
|
1945
|
+
this.ui.print({
|
|
1946
|
+
type: 'info',
|
|
1947
|
+
message: `${label} [simulation] Would run: "${messageToSend}"`,
|
|
1948
|
+
});
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
/**
|
|
1952
|
+
* Parse $name prefix from input.
|
|
1953
|
+
* Returns the agent ID, remaining input, and whether this is a background request.
|
|
1954
|
+
* Returns null if the agent doesn't exist.
|
|
1955
|
+
*
|
|
1956
|
+
* Examples:
|
|
1957
|
+
* "$arch Design the API" → { agentId: 'arch', remainingInput: 'Design the API', isBackground: false }
|
|
1958
|
+
* "$arch Design the API &" → { agentId: 'arch', remainingInput: 'Design the API', isBackground: true }
|
|
1959
|
+
* "$pm /backlog" → { agentId: 'pm', remainingInput: '/backlog', isBackground: false }
|
|
1960
|
+
* "$arch" → { agentId: 'arch', remainingInput: '', isBackground: false }
|
|
1961
|
+
*/
|
|
1962
|
+
async parseAgentPrefix(input) {
|
|
1963
|
+
if (!this.team || !input.startsWith('$')) {
|
|
1964
|
+
return null;
|
|
1965
|
+
}
|
|
1966
|
+
// Check for background flag (& at end of input)
|
|
1967
|
+
const isBackground = input.trim().endsWith('&');
|
|
1968
|
+
const cleanInput = isBackground ? input.slice(0, -1).trim() : input;
|
|
1969
|
+
// Extract agent ID: everything after $ until space or end
|
|
1970
|
+
// Note: Agent IDs are case-insensitive (stored lowercase by /team command)
|
|
1971
|
+
const spaceIndex = cleanInput.indexOf(' ');
|
|
1972
|
+
const agentId = (spaceIndex > 0 ? cleanInput.slice(1, spaceIndex) : cleanInput.slice(1)).toLowerCase();
|
|
1973
|
+
const remainingInput = spaceIndex > 0 ? cleanInput.slice(spaceIndex + 1).trim() : '';
|
|
1974
|
+
// Check if agent exists in team
|
|
1975
|
+
if (!this.team.has(agentId)) {
|
|
1976
|
+
this.ui.print({
|
|
1977
|
+
type: 'error',
|
|
1978
|
+
message: `Agent '${agentId}' not found in team`,
|
|
1979
|
+
});
|
|
1980
|
+
this.ui.print({
|
|
1981
|
+
type: 'info',
|
|
1982
|
+
message: `Available agents: ${this.team.getAgentIds().join(', ')}`,
|
|
1983
|
+
});
|
|
1984
|
+
return null;
|
|
1985
|
+
}
|
|
1986
|
+
// Switch to the agent (consolidated logic)
|
|
1987
|
+
try {
|
|
1988
|
+
const { teamAgent, demotedAgentId } = await this.switchAgent(agentId);
|
|
1989
|
+
if (demotedAgentId) {
|
|
1990
|
+
const demotedLabel = this.getAgentMascotLabel(demotedAgentId);
|
|
1991
|
+
this.ui.print({
|
|
1992
|
+
type: 'info',
|
|
1993
|
+
message: `${demotedLabel} sent to background (still running)`,
|
|
1994
|
+
});
|
|
1995
|
+
}
|
|
1996
|
+
this.ui.print({
|
|
1997
|
+
type: 'info',
|
|
1998
|
+
message: `Switched to ${teamAgent.mascot} ${teamAgent.displayName}`,
|
|
1999
|
+
});
|
|
2000
|
+
}
|
|
2001
|
+
catch (err) {
|
|
2002
|
+
this.ui.print({
|
|
2003
|
+
type: 'error',
|
|
2004
|
+
message: `Failed to switch to '${agentId}': ${err instanceof Error ? err.message : 'Unknown error'}`,
|
|
2005
|
+
});
|
|
2006
|
+
return null;
|
|
2007
|
+
}
|
|
2008
|
+
return { agentId, remainingInput, isBackground };
|
|
2009
|
+
}
|
|
2010
|
+
/**
|
|
2011
|
+
* Resolve $agent mention references in input and inject context.
|
|
2012
|
+
*
|
|
2013
|
+
* This handles mid-sentence mentions like:
|
|
2014
|
+
* "What did $arch propose for the API?"
|
|
2015
|
+
* "Based on $pm's plan, estimate the timeline"
|
|
2016
|
+
*
|
|
2017
|
+
* The resolved context is prepended to the message before sending to the agent.
|
|
2018
|
+
*/
|
|
2019
|
+
resolveAndInjectMentions(input) {
|
|
2020
|
+
// Skip if no team
|
|
2021
|
+
if (!this.team) {
|
|
2022
|
+
return input;
|
|
2023
|
+
}
|
|
2024
|
+
// Get valid agent IDs
|
|
2025
|
+
const validAgents = new Set(this.team.getAgentIds());
|
|
2026
|
+
// Parse input for mentions
|
|
2027
|
+
const parsed = parseInputForMentions(input, validAgents);
|
|
2028
|
+
// If no references (just a switch or no mentions), return as-is
|
|
2029
|
+
if (!hasReferences(parsed)) {
|
|
2030
|
+
return input;
|
|
2031
|
+
}
|
|
2032
|
+
// Get artifact store for context resolution
|
|
2033
|
+
const currentProject = getCurrentProject();
|
|
2034
|
+
const projectId = currentProject?.id ?? null;
|
|
2035
|
+
const checkpointer = getTeamCheckpointer();
|
|
2036
|
+
const artifactStore = checkpointer.getArtifactStore(projectId);
|
|
2037
|
+
// Create context resolver
|
|
2038
|
+
const resolver = new ContextResolver(this.team, artifactStore);
|
|
2039
|
+
// Resolve all mentions
|
|
2040
|
+
const resolved = resolver.resolveAll(parsed.references);
|
|
2041
|
+
// Check if any were resolved
|
|
2042
|
+
let anyResolved = false;
|
|
2043
|
+
for (const r of resolved.values()) {
|
|
2044
|
+
if (r.resolved) {
|
|
2045
|
+
anyResolved = true;
|
|
2046
|
+
break;
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
if (!anyResolved) {
|
|
2050
|
+
return input;
|
|
2051
|
+
}
|
|
2052
|
+
// Build context map and inject into message
|
|
2053
|
+
const contextMap = buildContextMap(resolved);
|
|
2054
|
+
const enhancedMessage = buildMessageWithContext(parsed, contextMap);
|
|
2055
|
+
// Log that context was injected (for debugging)
|
|
2056
|
+
const resolvedAgents = Array.from(resolved.entries())
|
|
2057
|
+
.filter(([_, r]) => r.resolved)
|
|
2058
|
+
.map(([id, r]) => `$${id} (${r.source}: ${r.sourceName})`);
|
|
2059
|
+
this.ui.print({
|
|
2060
|
+
type: 'info',
|
|
2061
|
+
message: `Injected context from: ${resolvedAgents.join(', ')}`,
|
|
2062
|
+
});
|
|
2063
|
+
return enhancedMessage;
|
|
2064
|
+
}
|
|
2065
|
+
/**
|
|
2066
|
+
* Handle memory note input ("# note") - V2 compatible version
|
|
2067
|
+
*/
|
|
2068
|
+
handleMemoryInputV2(input) {
|
|
2069
|
+
const note = input.slice(1).trim();
|
|
2070
|
+
// Get active project to determine scope
|
|
2071
|
+
const activeProject = getActiveProject();
|
|
2072
|
+
const projectId = activeProject?.id;
|
|
2073
|
+
// Determine priority based on keywords
|
|
2074
|
+
let priority = 'info';
|
|
2075
|
+
const lowerNote = note.toLowerCase();
|
|
2076
|
+
if (lowerNote.includes('never') || lowerNote.includes('always') || lowerNote.includes('must')) {
|
|
2077
|
+
priority = 'safety';
|
|
2078
|
+
}
|
|
2079
|
+
if (lowerNote.includes('critical') || lowerNote.includes('important')) {
|
|
2080
|
+
priority = 'critical';
|
|
2081
|
+
}
|
|
2082
|
+
// Create the anchor (persisted to file)
|
|
2083
|
+
const anchor = addAnchor({
|
|
2084
|
+
content: note,
|
|
2085
|
+
priority,
|
|
2086
|
+
scope: 'persistent',
|
|
2087
|
+
projectId: projectId?.toString(),
|
|
2088
|
+
tags: ['user-note'],
|
|
2089
|
+
});
|
|
2090
|
+
// Show confirmation
|
|
2091
|
+
const scope = activeProject ? `project "${activeProject.displayName || activeProject.name}"` : 'global';
|
|
2092
|
+
const priorityLabel = priority === 'info' ? '' : ` (${priority})`;
|
|
2093
|
+
const truncatedNote = truncate(note, 50);
|
|
2094
|
+
this.ui.print({ type: 'success', message: `Saved${priorityLabel}: "${truncatedNote}" → ${scope}` });
|
|
2095
|
+
// Also add to agent's anchor manager if enabled (for current session)
|
|
2096
|
+
if (this.agent?.getAnchorManager()) {
|
|
2097
|
+
this.agent.addAnchor({
|
|
2098
|
+
id: anchor.id,
|
|
2099
|
+
content: note,
|
|
2100
|
+
priority,
|
|
2101
|
+
scope: 'persistent',
|
|
2102
|
+
tags: ['user-note'],
|
|
2103
|
+
});
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
/**
|
|
2107
|
+
* Handle bash command ("!ls", "!git status", etc.)
|
|
2108
|
+
* Runs the command directly and displays output.
|
|
2109
|
+
* Special handling for 'cd' to change the CLI's working directory.
|
|
2110
|
+
*/
|
|
2111
|
+
async handleBashCommand(command) {
|
|
2112
|
+
if (!command) {
|
|
2113
|
+
this.ui.print({ type: 'error', message: 'Usage: !<command> (e.g., !ls, !git status, !cd <path>)' });
|
|
2114
|
+
return;
|
|
2115
|
+
}
|
|
2116
|
+
// Special handling for 'cd' command - use process.chdir() instead of subprocess
|
|
2117
|
+
// This allows the CLI's working directory to actually change
|
|
2118
|
+
const cdMatch = command.match(/^cd\s+(.+)$/);
|
|
2119
|
+
if (cdMatch || command === 'cd') {
|
|
2120
|
+
const targetDir = cdMatch ? cdMatch[1].trim() : process.env.HOME || '/';
|
|
2121
|
+
// Remove quotes if present (e.g., cd "path with spaces")
|
|
2122
|
+
const cleanDir = targetDir.replace(/^["']|["']$/g, '');
|
|
2123
|
+
try {
|
|
2124
|
+
const path = await import('path');
|
|
2125
|
+
const resolvedPath = path.resolve(process.cwd(), cleanDir);
|
|
2126
|
+
process.chdir(resolvedPath);
|
|
2127
|
+
this.ui.print({ type: 'info', message: `$ cd ${cleanDir}` });
|
|
2128
|
+
this.ui.print({ type: 'success', message: process.cwd() });
|
|
2129
|
+
}
|
|
2130
|
+
catch (error) {
|
|
2131
|
+
this.ui.print({ type: 'info', message: `$ cd ${cleanDir}` });
|
|
2132
|
+
const err = error;
|
|
2133
|
+
this.ui.print({ type: 'error', message: err.message || `Cannot change to directory: ${cleanDir}` });
|
|
2134
|
+
}
|
|
2135
|
+
return;
|
|
2136
|
+
}
|
|
2137
|
+
// Show the command being run
|
|
2138
|
+
this.ui.print({ type: 'info', message: `$ ${command}` });
|
|
2139
|
+
try {
|
|
2140
|
+
const { execSync } = await import('child_process');
|
|
2141
|
+
const output = execSync(command, {
|
|
2142
|
+
encoding: 'utf8',
|
|
2143
|
+
cwd: process.cwd(),
|
|
2144
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
2145
|
+
timeout: 30000, // 30 second timeout
|
|
2146
|
+
maxBuffer: 10 * 1024 * 1024, // 10MB buffer
|
|
2147
|
+
});
|
|
2148
|
+
if (output.trim()) {
|
|
2149
|
+
// Print output using raw type (no formatting, proper footer handling)
|
|
2150
|
+
this.ui.print({ type: 'raw', text: output.trimEnd() });
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
catch (error) {
|
|
2154
|
+
const err = error;
|
|
2155
|
+
if (err.stderr) {
|
|
2156
|
+
// Print stderr using raw type
|
|
2157
|
+
this.ui.print({ type: 'raw', text: err.stderr.trimEnd() });
|
|
2158
|
+
}
|
|
2159
|
+
else if (err.message) {
|
|
2160
|
+
this.ui.print({ type: 'error', message: err.message });
|
|
2161
|
+
}
|
|
2162
|
+
if (err.status !== undefined) {
|
|
2163
|
+
this.ui.print({ type: 'info', message: `Exit code: ${String(err.status)}` });
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
async processQueue() {
|
|
2168
|
+
while (this.ui.hasQueuedInput()) {
|
|
2169
|
+
const queued = this.ui.popQueuedInput();
|
|
2170
|
+
if (queued) {
|
|
2171
|
+
await this.processInput(queued);
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
// ===========================================================================
|
|
2176
|
+
// Real Agent Execution
|
|
2177
|
+
// ===========================================================================
|
|
2178
|
+
async runAgentReal(userMessage) {
|
|
2179
|
+
if (!this.agent)
|
|
2180
|
+
return;
|
|
2181
|
+
const abortController = new AbortController();
|
|
2182
|
+
const signal = abortController.signal;
|
|
2183
|
+
const setAction = (action) => {
|
|
2184
|
+
if (this.currentSession) {
|
|
2185
|
+
this.currentSession.currentAction = action;
|
|
2186
|
+
}
|
|
2187
|
+
};
|
|
2188
|
+
// Clear subagent tracking state from previous runs
|
|
2189
|
+
// - Agent.ts tracking (activeSubagents map)
|
|
2190
|
+
this.clearSubagentTracking?.();
|
|
2191
|
+
// - LiveRegion entries (visual display of running tools)
|
|
2192
|
+
this.ui.clearLiveRegion();
|
|
2193
|
+
// Get active agent ID for session tracking
|
|
2194
|
+
const activeAgentId = this.team?.getActive().id ?? 'default';
|
|
2195
|
+
// Create session object
|
|
2196
|
+
const session = {
|
|
2197
|
+
agentId: activeAgentId,
|
|
2198
|
+
isBackground: false, // Foreground execution
|
|
2199
|
+
abortController,
|
|
2200
|
+
currentAction: null,
|
|
2201
|
+
promise: (async () => {
|
|
2202
|
+
try {
|
|
2203
|
+
this.ui.setAgentRunning(true);
|
|
2204
|
+
this.ui.resetTurnMetrics(); // Reset token tracking for this turn
|
|
2205
|
+
setAction('Thinking...');
|
|
2206
|
+
this.ui.setSpinnerText('Thinking...');
|
|
2207
|
+
// Reset text accumulator for this run (class-level so it can be flushed externally)
|
|
2208
|
+
this.textAccumulator = '';
|
|
2209
|
+
let lastToolInput = null;
|
|
2210
|
+
let pendingDiffLines;
|
|
2211
|
+
// Stream agent events (agent is guaranteed to exist in this method)
|
|
2212
|
+
const agent = this.agent;
|
|
2213
|
+
if (!agent)
|
|
2214
|
+
return; // Type guard
|
|
2215
|
+
// Update history token estimate for breakdown display
|
|
2216
|
+
const historyMessages = agent.getHistory();
|
|
2217
|
+
const historyJson = JSON.stringify(historyMessages);
|
|
2218
|
+
updateHistoryEstimate(estimateTokens(historyJson));
|
|
2219
|
+
// In plan mode, prepend plan mode instructions to the first message
|
|
2220
|
+
// This dynamically injects the plan mode context without recreating the agent
|
|
2221
|
+
let messageToSend = userMessage;
|
|
2222
|
+
if (this.ui.getMode() === 'plan') {
|
|
2223
|
+
const activePlan = this.ui.getActivePlan();
|
|
2224
|
+
const planModePrompt = getPlanModePrompt(activePlan.id ?? undefined, activePlan.name ?? undefined);
|
|
2225
|
+
messageToSend = `${planModePrompt}\n\n---\n\nUser request: ${userMessage}`;
|
|
2226
|
+
}
|
|
2227
|
+
// Inject delegation completion notifications (deferred delivery)
|
|
2228
|
+
// If background agents completed while coordinator was busy,
|
|
2229
|
+
// prepend their results to the next coordinator message
|
|
2230
|
+
const delegationTracker = getDelegationTracker();
|
|
2231
|
+
if (delegationTracker.hasCompletionEvents()) {
|
|
2232
|
+
const completionEvents = delegationTracker.drainCompletionEvents();
|
|
2233
|
+
const completionPrefix = this.formatCompletionMessage(completionEvents);
|
|
2234
|
+
messageToSend = `${completionPrefix}\n\n---\n\n${messageToSend}`;
|
|
2235
|
+
}
|
|
2236
|
+
// Debug logging (enable with DEBUG_AGENT_REQUESTS=1)
|
|
2237
|
+
debugLogAgentRequest(agent, messageToSend);
|
|
2238
|
+
for await (const event of agent.stream(messageToSend, {
|
|
2239
|
+
signal,
|
|
2240
|
+
chatOptions: { model: this.model },
|
|
2241
|
+
// Provide tool-specific context for bash backgrounding (Ctrl+B)
|
|
2242
|
+
getToolContext: (toolName, toolUseId) => {
|
|
2243
|
+
if (toolName === 'bash') {
|
|
2244
|
+
// Create an AbortController for this bash command
|
|
2245
|
+
const bashAbortController = new AbortController();
|
|
2246
|
+
this.bashAbortControllers.set(toolUseId, bashAbortController);
|
|
2247
|
+
return {
|
|
2248
|
+
abortSignal: bashAbortController.signal,
|
|
2249
|
+
onBackground: (_shellId, _partialOutput) => {
|
|
2250
|
+
// Clean up the controller after backgrounding
|
|
2251
|
+
this.bashAbortControllers.delete(toolUseId);
|
|
2252
|
+
// The UI already handled the notification
|
|
2253
|
+
},
|
|
2254
|
+
};
|
|
2255
|
+
}
|
|
2256
|
+
return {};
|
|
2257
|
+
},
|
|
2258
|
+
})) {
|
|
2259
|
+
if (signal.aborted)
|
|
2260
|
+
break;
|
|
2261
|
+
if (event.type === 'llm_chunk') {
|
|
2262
|
+
if (event.chunk.type === 'text' && event.chunk.text) {
|
|
2263
|
+
this.textAccumulator += event.chunk.text;
|
|
2264
|
+
}
|
|
2265
|
+
else if (event.chunk.type === 'thinking_start') {
|
|
2266
|
+
// Extended thinking started - show indicator
|
|
2267
|
+
this.ui.setThinking(true);
|
|
2268
|
+
}
|
|
2269
|
+
else if (event.chunk.type === 'thinking_end') {
|
|
2270
|
+
// Extended thinking ended - hide indicator
|
|
2271
|
+
this.ui.setThinking(false);
|
|
2272
|
+
}
|
|
2273
|
+
else if (event.chunk.type === 'done' && event.chunk.usage) {
|
|
2274
|
+
this.sessionInputTokens += event.chunk.usage.inputTokens;
|
|
2275
|
+
this.sessionOutputTokens += event.chunk.usage.outputTokens;
|
|
2276
|
+
this.sessionRequests++;
|
|
2277
|
+
// Record message for telemetry heartbeat
|
|
2278
|
+
getAuthManager().recordMessage(event.chunk.usage.inputTokens, event.chunk.usage.outputTokens);
|
|
2279
|
+
// Update live token display (including thinking tokens for Gemini 2.5+)
|
|
2280
|
+
const usage = event.chunk.usage;
|
|
2281
|
+
this.ui.addTokens(usage.inputTokens, usage.outputTokens, usage.thinkingTokens, usage.cacheReadTokens, usage.debugPayload);
|
|
2282
|
+
this.ui.incrementApiCalls();
|
|
2283
|
+
// Debug: log each API call's token usage to file (not console, to avoid UI artifacts)
|
|
2284
|
+
if (DEBUG_ENABLED) {
|
|
2285
|
+
const metrics = this.ui.getTurnMetrics();
|
|
2286
|
+
const debugLine = `[API Call #${String(metrics.apiCalls)}] +${String(usage.inputTokens)} in, +${String(usage.outputTokens)} out → Turn total: ${String(metrics.inputTokens)} in, ${String(metrics.outputTokens)} out\n`;
|
|
2287
|
+
appendFileSync(DEBUG_LOG_FILE, debugLine);
|
|
2288
|
+
// Log raw usage object to see all fields
|
|
2289
|
+
appendFileSync(DEBUG_LOG_FILE, ` Raw usage: ${JSON.stringify(event.chunk.usage)}\n`);
|
|
2290
|
+
}
|
|
2291
|
+
// Update cache info for breakdown display
|
|
2292
|
+
updateCacheInfo(usage.cacheReadTokens ?? 0, usage.cacheCreationTokens ?? 0);
|
|
2293
|
+
// Update team agent's token tracking
|
|
2294
|
+
if (this.team) {
|
|
2295
|
+
const activeTeamAgent = this.team.getActive();
|
|
2296
|
+
activeTeamAgent.updateTokenUsage(event.chunk.usage.inputTokens, event.chunk.usage.outputTokens);
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
else if (event.type === 'tool_start') {
|
|
2301
|
+
// Flush accumulated text before tool
|
|
2302
|
+
if (this.textAccumulator.trim()) {
|
|
2303
|
+
this.ui.print({
|
|
2304
|
+
type: 'agent-text',
|
|
2305
|
+
text: this.textAccumulator.trim(),
|
|
2306
|
+
expression: this.getActiveAgentMascot(),
|
|
2307
|
+
agentId: activeAgentId,
|
|
2308
|
+
});
|
|
2309
|
+
this.textAccumulator = '';
|
|
2310
|
+
}
|
|
2311
|
+
// Track tool calls for per-turn metrics
|
|
2312
|
+
this.ui.incrementToolCalls();
|
|
2313
|
+
// Record tool usage for telemetry
|
|
2314
|
+
if (event.name) {
|
|
2315
|
+
getAuthManager().recordTool(event.name);
|
|
2316
|
+
}
|
|
2317
|
+
lastToolInput = event.input;
|
|
2318
|
+
// Pre-compute diff for edit tool (must happen BEFORE tool modifies the file)
|
|
2319
|
+
pendingDiffLines = undefined;
|
|
2320
|
+
{
|
|
2321
|
+
// Determine the actual tool name (handle use_tool wrapping)
|
|
2322
|
+
const actualToolName = event.name === 'use_tool' && typeof event.input.tool_name === 'string'
|
|
2323
|
+
? event.input.tool_name
|
|
2324
|
+
: event.name;
|
|
2325
|
+
const actualInput = event.name === 'use_tool' && typeof event.input.args === 'object' && event.input.args !== null
|
|
2326
|
+
? event.input.args
|
|
2327
|
+
: event.input;
|
|
2328
|
+
if (actualToolName === 'edit') {
|
|
2329
|
+
const filePath = typeof actualInput.filePath === 'string' ? actualInput.filePath : '';
|
|
2330
|
+
const oldText = typeof actualInput.oldString === 'string' ? actualInput.oldString : '';
|
|
2331
|
+
const newText = typeof actualInput.newString === 'string' ? actualInput.newString : '';
|
|
2332
|
+
const replaceAll = Boolean(actualInput.replaceAll);
|
|
2333
|
+
if (filePath && oldText && newText) {
|
|
2334
|
+
const editDiff = generateEditDiff(filePath, oldText, newText, replaceAll);
|
|
2335
|
+
if (editDiff) {
|
|
2336
|
+
pendingDiffLines = editDiff.lines;
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
// Debug: log tool calls to file
|
|
2342
|
+
if (DEBUG_ENABLED) {
|
|
2343
|
+
const toolDebugLine = `[Tool Call] ${event.name}(${JSON.stringify(event.input).slice(0, 200)})\n`;
|
|
2344
|
+
appendFileSync(DEBUG_LOG_FILE, toolDebugLine);
|
|
2345
|
+
}
|
|
2346
|
+
// For bash tool, add to live region for streaming output
|
|
2347
|
+
if (event.name === 'bash' && event.toolUseId) {
|
|
2348
|
+
const command = typeof lastToolInput.command === 'string'
|
|
2349
|
+
? lastToolInput.command
|
|
2350
|
+
: '';
|
|
2351
|
+
this.ui.addBashCommand(event.toolUseId, command);
|
|
2352
|
+
}
|
|
2353
|
+
// Update spinner with tool name (skip silent tools)
|
|
2354
|
+
if (!this.agent?.isToolSilent(event.name)) {
|
|
2355
|
+
if (event.name === 'task' && event.toolUseId) {
|
|
2356
|
+
const subagentType = typeof lastToolInput.subagent_type === 'string'
|
|
2357
|
+
? lastToolInput.subagent_type
|
|
2358
|
+
: 'general';
|
|
2359
|
+
const description = typeof lastToolInput.description === 'string'
|
|
2360
|
+
? lastToolInput.description
|
|
2361
|
+
: '';
|
|
2362
|
+
setAction(`task(${subagentType}): ${description}`);
|
|
2363
|
+
this.ui.setSpinnerText(`task(${subagentType}): ${description}`);
|
|
2364
|
+
// Add subagent to LiveRegion with toolUseId
|
|
2365
|
+
// No queue needed - callbacks now receive toolUseId directly!
|
|
2366
|
+
this.ui.addSubagent(event.toolUseId, subagentType, description);
|
|
2367
|
+
}
|
|
2368
|
+
else if (event.name === 'use_tool') {
|
|
2369
|
+
// Show the inner tool name being called
|
|
2370
|
+
const innerToolName = typeof lastToolInput.tool_name === 'string'
|
|
2371
|
+
? lastToolInput.tool_name
|
|
2372
|
+
: 'unknown';
|
|
2373
|
+
setAction(innerToolName);
|
|
2374
|
+
this.ui.setSpinnerText(`Running ${innerToolName}...`);
|
|
2375
|
+
}
|
|
2376
|
+
else if (event.name === 'list_tools') {
|
|
2377
|
+
// Quiet display for list_tools
|
|
2378
|
+
setAction('list_tools');
|
|
2379
|
+
this.ui.setSpinnerText('Discovering tools...');
|
|
2380
|
+
}
|
|
2381
|
+
else if (event.name === 'get_tool_info') {
|
|
2382
|
+
// Quiet display for get_tool_info
|
|
2383
|
+
const toolName = typeof lastToolInput.tool_name === 'string'
|
|
2384
|
+
? lastToolInput.tool_name
|
|
2385
|
+
: '?';
|
|
2386
|
+
setAction('get_tool_info');
|
|
2387
|
+
this.ui.setSpinnerText(`Getting schema for ${toolName}...`);
|
|
2388
|
+
}
|
|
2389
|
+
else if (event.name !== 'task') {
|
|
2390
|
+
setAction(event.name);
|
|
2391
|
+
this.ui.setSpinnerText(`Running ${event.name}...`);
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
else if (event.type === 'tool_output') {
|
|
2396
|
+
// Stream bash output to live region
|
|
2397
|
+
if (event.toolUseId) {
|
|
2398
|
+
this.ui.updateBashOutput(event.toolUseId, event.output);
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
else if (event.type === 'tool_end') {
|
|
2402
|
+
const toolName = event.name;
|
|
2403
|
+
const toolInput = lastToolInput;
|
|
2404
|
+
const result = event.result;
|
|
2405
|
+
lastToolInput = null;
|
|
2406
|
+
// Complete bash command in live region
|
|
2407
|
+
if (toolName === 'bash' && event.toolUseId) {
|
|
2408
|
+
// Clean up abort controller
|
|
2409
|
+
this.bashAbortControllers.delete(event.toolUseId);
|
|
2410
|
+
// Check if command was backgrounded
|
|
2411
|
+
if (result.success && typeof result.result === 'object' && result.result !== null) {
|
|
2412
|
+
const res = result.result;
|
|
2413
|
+
if (res.backgrounded === true) {
|
|
2414
|
+
// Command was moved to background - don't commit, just clear
|
|
2415
|
+
this.ui.setCurrentTool(null);
|
|
2416
|
+
continue;
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
let exitCode = result.success ? 0 : 1;
|
|
2420
|
+
if (result.success && typeof result.result === 'object' && result.result !== null) {
|
|
2421
|
+
const res = result.result;
|
|
2422
|
+
if (typeof res.exitCode === 'number') {
|
|
2423
|
+
exitCode = res.exitCode;
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
this.ui.completeBashCommand(event.toolUseId, exitCode);
|
|
2427
|
+
this.ui.commitBashCommand(event.toolUseId);
|
|
2428
|
+
this.ui.setCurrentTool(null);
|
|
2429
|
+
continue;
|
|
2430
|
+
}
|
|
2431
|
+
// Handle todo_write - update UI state
|
|
2432
|
+
if (toolName === 'todo_write' && toolInput && Array.isArray(toolInput.todos)) {
|
|
2433
|
+
const todos = toolInput.todos.map((t) => {
|
|
2434
|
+
const content = typeof t.content === 'string' ? t.content :
|
|
2435
|
+
typeof t.title === 'string' ? t.title :
|
|
2436
|
+
typeof t.task === 'string' ? t.task :
|
|
2437
|
+
typeof t.description === 'string' ? t.description :
|
|
2438
|
+
typeof t.text === 'string' ? t.text : 'Untitled task';
|
|
2439
|
+
const status = (typeof t.status === 'string' && ['pending', 'in_progress', 'completed'].includes(t.status)
|
|
2440
|
+
? t.status
|
|
2441
|
+
: 'pending');
|
|
2442
|
+
const activeForm = typeof t.activeForm === 'string' ? t.activeForm : undefined;
|
|
2443
|
+
const owner = typeof t.owner === 'string' ? t.owner : undefined;
|
|
2444
|
+
const blockedBy = Array.isArray(t.blockedBy)
|
|
2445
|
+
? t.blockedBy.filter((n) => typeof n === 'number')
|
|
2446
|
+
: undefined;
|
|
2447
|
+
return { content, status, activeForm, owner, blockedBy };
|
|
2448
|
+
});
|
|
2449
|
+
this.ui.setTodos(todos);
|
|
2450
|
+
this.ui.setCurrentTool(null);
|
|
2451
|
+
continue;
|
|
2452
|
+
}
|
|
2453
|
+
// Skip silent tools (no output needed)
|
|
2454
|
+
if (this.agent?.isToolSilent(toolName)) {
|
|
2455
|
+
this.ui.setCurrentTool(null);
|
|
2456
|
+
continue;
|
|
2457
|
+
}
|
|
2458
|
+
// Handle list_tools - show compact summary
|
|
2459
|
+
if (toolName === 'list_tools') {
|
|
2460
|
+
const innerResult = result.success && typeof result.result === 'object'
|
|
2461
|
+
? result.result
|
|
2462
|
+
: {};
|
|
2463
|
+
const count = typeof innerResult.count === 'number' ? innerResult.count : 0;
|
|
2464
|
+
const categoryParam = toolInput && typeof toolInput.category === 'string'
|
|
2465
|
+
? `category: ${toolInput.category}`
|
|
2466
|
+
: '';
|
|
2467
|
+
this.ui.print({
|
|
2468
|
+
type: 'tool-result',
|
|
2469
|
+
name: 'list_tools',
|
|
2470
|
+
params: categoryParam,
|
|
2471
|
+
summary: `${String(count)} tools available`,
|
|
2472
|
+
success: result.success,
|
|
2473
|
+
agentId: activeAgentId,
|
|
2474
|
+
});
|
|
2475
|
+
this.ui.setCurrentTool(null);
|
|
2476
|
+
continue;
|
|
2477
|
+
}
|
|
2478
|
+
// Handle use_tool - show the inner tool name instead
|
|
2479
|
+
if (toolName === 'use_tool' && toolInput) {
|
|
2480
|
+
const innerToolName = typeof toolInput.tool_name === 'string'
|
|
2481
|
+
? toolInput.tool_name
|
|
2482
|
+
: 'unknown';
|
|
2483
|
+
const innerArgs = typeof toolInput.args === 'object' && toolInput.args !== null
|
|
2484
|
+
? toolInput.args
|
|
2485
|
+
: {};
|
|
2486
|
+
// Extract and format using the inner tool name
|
|
2487
|
+
const { content, summary, success } = this.extractToolResult(innerToolName, result);
|
|
2488
|
+
// Resolve diff lines for edit (pre-computed) or write_file (computed now)
|
|
2489
|
+
let innerDiffLines = pendingDiffLines;
|
|
2490
|
+
pendingDiffLines = undefined;
|
|
2491
|
+
if (!innerDiffLines && (innerToolName === 'write_file' || innerToolName === 'write')) {
|
|
2492
|
+
const filePath = typeof innerArgs.path === 'string' ? innerArgs.path : '';
|
|
2493
|
+
const writeContent = typeof innerArgs.content === 'string' ? innerArgs.content : '';
|
|
2494
|
+
if (filePath && writeContent && success) {
|
|
2495
|
+
const writeDiff = generateWriteDiff(filePath, writeContent, true);
|
|
2496
|
+
innerDiffLines = writeDiff.lines;
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
this.ui.print({
|
|
2500
|
+
type: 'tool-result',
|
|
2501
|
+
name: innerToolName,
|
|
2502
|
+
params: this.formatToolParams(innerArgs),
|
|
2503
|
+
summary,
|
|
2504
|
+
content: content || undefined,
|
|
2505
|
+
diffLines: innerDiffLines,
|
|
2506
|
+
success,
|
|
2507
|
+
agentId: activeAgentId,
|
|
2508
|
+
});
|
|
2509
|
+
this.ui.setCurrentTool(null);
|
|
2510
|
+
continue;
|
|
2511
|
+
}
|
|
2512
|
+
// Handle get_tool_info - show compact summary
|
|
2513
|
+
if (toolName === 'get_tool_info') {
|
|
2514
|
+
const innerResult = result.success && typeof result.result === 'object'
|
|
2515
|
+
? result.result
|
|
2516
|
+
: {};
|
|
2517
|
+
const name = typeof innerResult.name === 'string' ? innerResult.name : '?';
|
|
2518
|
+
this.ui.print({
|
|
2519
|
+
type: 'tool-result',
|
|
2520
|
+
name: 'get_tool_info',
|
|
2521
|
+
params: name,
|
|
2522
|
+
summary: `Schema for ${name}`,
|
|
2523
|
+
success: result.success,
|
|
2524
|
+
agentId: activeAgentId,
|
|
2525
|
+
});
|
|
2526
|
+
this.ui.setCurrentTool(null);
|
|
2527
|
+
continue;
|
|
2528
|
+
}
|
|
2529
|
+
// Handle task tool like bash - use atomic commitSubagent
|
|
2530
|
+
if (toolName === 'task' && event.toolUseId) {
|
|
2531
|
+
// Extract result content
|
|
2532
|
+
const { content, success } = this.extractToolResult(toolName, result);
|
|
2533
|
+
// Use atomic commitSubagent (same pattern as commitBashCommand)
|
|
2534
|
+
// This removes from LiveRegion and prints in one atomic operation
|
|
2535
|
+
this.ui.commitSubagent(event.toolUseId, content, success);
|
|
2536
|
+
this.ui.setCurrentTool(null);
|
|
2537
|
+
continue; // Skip generic handling
|
|
2538
|
+
}
|
|
2539
|
+
// Extract and format tool result
|
|
2540
|
+
const { content, summary, success } = this.extractToolResult(toolName, result);
|
|
2541
|
+
// Resolve diff lines for edit/write_file tools
|
|
2542
|
+
let diffLines = pendingDiffLines;
|
|
2543
|
+
pendingDiffLines = undefined;
|
|
2544
|
+
// For write_file, generate diff from input (doesn't need pre-edit state)
|
|
2545
|
+
if (!diffLines && toolInput) {
|
|
2546
|
+
const actualToolName = toolName === 'use_tool' && typeof toolInput.tool_name === 'string'
|
|
2547
|
+
? toolInput.tool_name
|
|
2548
|
+
: toolName;
|
|
2549
|
+
const actualInput = toolName === 'use_tool' && typeof toolInput.args === 'object' && toolInput.args !== null
|
|
2550
|
+
? toolInput.args
|
|
2551
|
+
: toolInput;
|
|
2552
|
+
if (actualToolName === 'write_file' || actualToolName === 'write') {
|
|
2553
|
+
const filePath = typeof actualInput.path === 'string' ? actualInput.path : '';
|
|
2554
|
+
const writeContent = typeof actualInput.content === 'string' ? actualInput.content : '';
|
|
2555
|
+
if (filePath && writeContent && success) {
|
|
2556
|
+
const writeDiff = generateWriteDiff(filePath, writeContent, true);
|
|
2557
|
+
diffLines = writeDiff.lines;
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
this.ui.print({
|
|
2562
|
+
type: 'tool-result',
|
|
2563
|
+
name: toolName,
|
|
2564
|
+
params: this.formatToolParams(toolInput),
|
|
2565
|
+
summary,
|
|
2566
|
+
content: content || undefined,
|
|
2567
|
+
diffLines,
|
|
2568
|
+
success,
|
|
2569
|
+
agentId: activeAgentId,
|
|
2570
|
+
});
|
|
2571
|
+
this.ui.setCurrentTool(null);
|
|
2572
|
+
}
|
|
2573
|
+
else if (event.type === 'context_compacted') {
|
|
2574
|
+
// Context was compacted - show file restoration hints
|
|
2575
|
+
const hints = agent.formatRestorationHints();
|
|
2576
|
+
const savedTokens = event.tokensBefore - event.tokensAfter;
|
|
2577
|
+
const savedPct = Math.round((savedTokens / event.tokensBefore) * 100);
|
|
2578
|
+
this.ui.print({
|
|
2579
|
+
type: 'info',
|
|
2580
|
+
message: `Context compacted: ${event.tokensBefore.toLocaleString()} → ${event.tokensAfter.toLocaleString()} tokens (${String(savedPct)}% reduction)`,
|
|
2581
|
+
});
|
|
2582
|
+
if (hints) {
|
|
2583
|
+
// Display file restoration hints
|
|
2584
|
+
this.ui.print({
|
|
2585
|
+
type: 'info',
|
|
2586
|
+
message: hints,
|
|
2587
|
+
});
|
|
2588
|
+
}
|
|
2589
|
+
// Hint about Ctrl+O
|
|
2590
|
+
this.ui.print({
|
|
2591
|
+
type: 'info',
|
|
2592
|
+
message: 'Tip: Use Ctrl+O to view full conversation history',
|
|
2593
|
+
});
|
|
2594
|
+
}
|
|
2595
|
+
else if (event.type === 'context_summarized') {
|
|
2596
|
+
// Context was summarized (LLM-based) - more aggressive
|
|
2597
|
+
const savedTokens = event.tokensBefore - event.tokensAfter;
|
|
2598
|
+
const savedPct = Math.round((savedTokens / event.tokensBefore) * 100);
|
|
2599
|
+
this.ui.print({
|
|
2600
|
+
type: 'info',
|
|
2601
|
+
message: `Context summarized (${String(event.rounds)} round${event.rounds > 1 ? 's' : ''}): ${event.tokensBefore.toLocaleString()} → ${event.tokensAfter.toLocaleString()} tokens (${String(savedPct)}% reduction)`,
|
|
2602
|
+
});
|
|
2603
|
+
const hints = agent.formatRestorationHints();
|
|
2604
|
+
if (hints) {
|
|
2605
|
+
this.ui.print({
|
|
2606
|
+
type: 'info',
|
|
2607
|
+
message: hints,
|
|
2608
|
+
});
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
else if (event.type === 'llm_retry') {
|
|
2612
|
+
// LLM call failed, retrying with exponential backoff
|
|
2613
|
+
const delaySec = (event.delayMs / 1000).toFixed(1);
|
|
2614
|
+
this.ui.print({
|
|
2615
|
+
type: 'warning',
|
|
2616
|
+
message: `Connection failed (attempt ${String(event.attempt)}/${String(event.maxAttempts)}): ${event.error}. Retrying in ${delaySec}s...`,
|
|
2617
|
+
});
|
|
2618
|
+
}
|
|
2619
|
+
else if (event.type === 'custom' && typeof event.name === 'string' && event.name.startsWith('delegation:')) {
|
|
2620
|
+
// Tool result delegation events
|
|
2621
|
+
const data = event.data;
|
|
2622
|
+
if (event.name === 'delegation:completed') {
|
|
2623
|
+
const originalTokens = data.originalTokens;
|
|
2624
|
+
const summaryTokens = data.summaryTokens;
|
|
2625
|
+
const toolName = data.toolName;
|
|
2626
|
+
this.ui.setSpinnerText(`Delegated ${toolName} result: ${String(originalTokens)} → ${String(summaryTokens)} tokens`);
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
else if (event.type === 'llm_retry_exhausted') {
|
|
2630
|
+
// All retry attempts failed
|
|
2631
|
+
this.ui.print({
|
|
2632
|
+
type: 'error',
|
|
2633
|
+
message: `All ${String(event.attempts)} retry attempts failed: ${event.error}`,
|
|
2634
|
+
});
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
// Flush remaining text and check for PLAN_READY marker
|
|
2638
|
+
if (this.textAccumulator.trim()) {
|
|
2639
|
+
const text = this.textAccumulator.trim();
|
|
2640
|
+
// Check for PLAN_READY marker
|
|
2641
|
+
const planReadyInfo = this.detectPlanReady(text);
|
|
2642
|
+
// Print the text (remove the marker from display if present)
|
|
2643
|
+
const displayText = planReadyInfo
|
|
2644
|
+
? text.replace(ReplV2.PLAN_READY_REGEX, '').trim()
|
|
2645
|
+
: text;
|
|
2646
|
+
if (displayText) {
|
|
2647
|
+
this.ui.print({ type: 'agent-text', text: displayText, expression: this.getActiveAgentMascot(), agentId: activeAgentId });
|
|
2648
|
+
}
|
|
2649
|
+
// If PLAN_READY marker found, show approval overlay
|
|
2650
|
+
if (planReadyInfo) {
|
|
2651
|
+
await this.handlePlanApproval(planReadyInfo.planId, planReadyInfo.planName);
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
// Print per-turn summary before stopping agent
|
|
2655
|
+
this.printTurnSummary();
|
|
2656
|
+
this.ui.setAgentRunning(false);
|
|
2657
|
+
this.ui.setThinking(false); // Ensure thinking indicator is cleared
|
|
2658
|
+
// NOTE: Don't clear todos here - they should persist after agent finishes
|
|
2659
|
+
// to show the final state. Only clear on explicit user action or new conversation.
|
|
2660
|
+
// Notify that agent has finished (for applying deferred suggestions)
|
|
2661
|
+
this.onAgentFinish?.();
|
|
2662
|
+
// Auto-save session after successful agent response
|
|
2663
|
+
this.autoSaveSession().catch((err) => {
|
|
2664
|
+
// Log but don't interrupt - saving is best-effort
|
|
2665
|
+
console.error('Failed to save session:', err);
|
|
2666
|
+
});
|
|
2667
|
+
}
|
|
2668
|
+
catch (err) {
|
|
2669
|
+
if (!signal.aborted) {
|
|
2670
|
+
this.ui.print({ type: 'error', message: String(err) });
|
|
2671
|
+
}
|
|
2672
|
+
this.ui.setAgentRunning(false);
|
|
2673
|
+
this.ui.setThinking(false); // Ensure thinking indicator is cleared
|
|
2674
|
+
// Still notify on error so suggestions can be applied
|
|
2675
|
+
this.onAgentFinish?.();
|
|
2676
|
+
}
|
|
2677
|
+
})(),
|
|
2678
|
+
};
|
|
2679
|
+
// Register the session (foreground)
|
|
2680
|
+
this.startSession(session);
|
|
2681
|
+
// Wait for session to complete
|
|
2682
|
+
await session.promise;
|
|
2683
|
+
this.endSession(activeAgentId);
|
|
2684
|
+
// Check for pending delegation and handle it
|
|
2685
|
+
await this.handlePendingDelegation();
|
|
2686
|
+
}
|
|
2687
|
+
// ===========================================================================
|
|
2688
|
+
// Delegation Completion (Phase 2 - Coordinator Mode)
|
|
2689
|
+
// ===========================================================================
|
|
2690
|
+
/**
|
|
2691
|
+
* Format completion events into a structured message for the coordinator.
|
|
2692
|
+
* Groups events by agent and includes task, result summary, and artifact info.
|
|
2693
|
+
*/
|
|
2694
|
+
formatCompletionMessage(events) {
|
|
2695
|
+
const lines = ['[DELEGATION COMPLETIONS]', ''];
|
|
2696
|
+
for (const event of events) {
|
|
2697
|
+
const delegation = getDelegationTracker().get(event.delegationId);
|
|
2698
|
+
const agentLabel = this.team?.get(event.agentId)?.displayName ?? event.agentId;
|
|
2699
|
+
const status = event.status === 'completed' ? 'COMPLETED' : 'FAILED';
|
|
2700
|
+
lines.push(`## $${event.agentId} (${agentLabel}) — ${status}`);
|
|
2701
|
+
if (delegation) {
|
|
2702
|
+
lines.push(`Task: ${delegation.task}`);
|
|
2703
|
+
}
|
|
2704
|
+
if (event.result.success) {
|
|
2705
|
+
lines.push(`Result: ${event.result.summary}`);
|
|
2706
|
+
}
|
|
2707
|
+
else {
|
|
2708
|
+
lines.push(`Error: ${event.result.error ?? event.result.summary}`);
|
|
2709
|
+
}
|
|
2710
|
+
if (event.result.artifactIds.length > 0) {
|
|
2711
|
+
lines.push(`Artifacts: ${event.result.artifactIds.join(', ')}`);
|
|
2712
|
+
}
|
|
2713
|
+
lines.push('');
|
|
2714
|
+
}
|
|
2715
|
+
lines.push('Review the results above and decide next steps.');
|
|
2716
|
+
return lines.join('\n');
|
|
2717
|
+
}
|
|
2718
|
+
/**
|
|
2719
|
+
* Check and process delegation completions for an agent that just finished.
|
|
2720
|
+
* Marks all running delegations for this agent as completed or failed,
|
|
2721
|
+
* then attempts to auto-resume the coordinator if idle.
|
|
2722
|
+
*
|
|
2723
|
+
* @param agentId - The agent that just finished
|
|
2724
|
+
* @param error - If present, the agent failed with this error
|
|
2725
|
+
*/
|
|
2726
|
+
checkDelegationCompletion(agentId, error) {
|
|
2727
|
+
const tracker = getDelegationTracker();
|
|
2728
|
+
const running = tracker.getByAgent(agentId).filter(d => d.status === 'running');
|
|
2729
|
+
if (running.length === 0)
|
|
2730
|
+
return;
|
|
2731
|
+
for (const delegation of running) {
|
|
2732
|
+
if (error) {
|
|
2733
|
+
tracker.fail(delegation.id, error);
|
|
2734
|
+
}
|
|
2735
|
+
else {
|
|
2736
|
+
// Gather artifacts created during this delegation
|
|
2737
|
+
const artifactIds = [];
|
|
2738
|
+
if (this.team) {
|
|
2739
|
+
const currentProject = getCurrentProject();
|
|
2740
|
+
const projectId = currentProject?.id ?? null;
|
|
2741
|
+
const artifactStore = getTeamCheckpointer().getArtifactStore(projectId);
|
|
2742
|
+
const agentArtifacts = artifactStore.listByAgent(agentId);
|
|
2743
|
+
for (const artifact of agentArtifacts) {
|
|
2744
|
+
if (artifact.createdAt >= delegation.createdAt) {
|
|
2745
|
+
artifactIds.push(artifact.id);
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
tracker.complete(delegation.id, {
|
|
2750
|
+
success: true,
|
|
2751
|
+
summary: `Task completed successfully${artifactIds.length > 0 ? ` with ${String(artifactIds.length)} artifact(s)` : ''}`,
|
|
2752
|
+
artifactIds,
|
|
2753
|
+
});
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
// Try to auto-resume the coordinator (fire-and-forget — async switch must complete before runAgentReal)
|
|
2757
|
+
void this.maybeResumeCoordinator();
|
|
2758
|
+
}
|
|
2759
|
+
/**
|
|
2760
|
+
* Auto-resume the coordinator if it's idle and there are pending completion events.
|
|
2761
|
+
* If the coordinator is busy, events stay in queue for deferred delivery.
|
|
2762
|
+
*
|
|
2763
|
+
* Must be async to properly await switchAgent before running the coordinator.
|
|
2764
|
+
* Called fire-and-forget from checkDelegationCompletion — this ensures the
|
|
2765
|
+
* background agent's endSession() runs independently (no session conflicts).
|
|
2766
|
+
*/
|
|
2767
|
+
async maybeResumeCoordinator() {
|
|
2768
|
+
const tracker = getDelegationTracker();
|
|
2769
|
+
if (!tracker.hasCompletionEvents())
|
|
2770
|
+
return;
|
|
2771
|
+
// Check if coordinator (default agent) is idle
|
|
2772
|
+
if (this.sessions.has('default'))
|
|
2773
|
+
return; // Coordinator is busy
|
|
2774
|
+
// Drain events and format the completion message
|
|
2775
|
+
const events = tracker.drainCompletionEvents();
|
|
2776
|
+
const completionMessage = this.formatCompletionMessage(events);
|
|
2777
|
+
// Switch to coordinator if not already active — MUST await before runAgentReal
|
|
2778
|
+
// so that this.team.getActive().id returns 'default' and the foreground session
|
|
2779
|
+
// is registered under the correct agent ID
|
|
2780
|
+
if (this.team && this.team.getActive().id !== 'default') {
|
|
2781
|
+
try {
|
|
2782
|
+
await this.switchAgent('default', {
|
|
2783
|
+
demoteCurrentIfBusy: false,
|
|
2784
|
+
trackSession: false,
|
|
2785
|
+
});
|
|
2786
|
+
}
|
|
2787
|
+
catch (err) {
|
|
2788
|
+
this.ui.print({
|
|
2789
|
+
type: 'error',
|
|
2790
|
+
message: `Failed to switch to coordinator: ${err instanceof Error ? err.message : String(err)}`,
|
|
2791
|
+
});
|
|
2792
|
+
return;
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
// Run coordinator foreground with the completion message
|
|
2796
|
+
this.ui.print({
|
|
2797
|
+
type: 'info',
|
|
2798
|
+
message: 'Delegation completed — resuming coordinator...',
|
|
2799
|
+
});
|
|
2800
|
+
void this.runAgentReal(completionMessage);
|
|
2801
|
+
}
|
|
2802
|
+
/**
|
|
2803
|
+
* Run an agent in background (Phase 3 - Background Agents).
|
|
2804
|
+
* Similar to runAgentReal but:
|
|
2805
|
+
* - Creates a background session (isBackground: true)
|
|
2806
|
+
* - Labels all output with agent mascot
|
|
2807
|
+
* - Doesn't block the caller
|
|
2808
|
+
* - Shows completion notification when done
|
|
2809
|
+
*
|
|
2810
|
+
* @param agentId - The agent ID to run
|
|
2811
|
+
* @param userMessage - The message to send to the agent
|
|
2812
|
+
*/
|
|
2813
|
+
async runAgentBackground(agentId, userMessage) {
|
|
2814
|
+
if (!this.agent)
|
|
2815
|
+
return;
|
|
2816
|
+
const abortController = new AbortController();
|
|
2817
|
+
const signal = abortController.signal;
|
|
2818
|
+
const label = this.getAgentMascotLabel(agentId);
|
|
2819
|
+
const mascot = this.team?.get(agentId)?.mascot ?? '[•_•]';
|
|
2820
|
+
// Create background session
|
|
2821
|
+
const session = {
|
|
2822
|
+
agentId,
|
|
2823
|
+
isBackground: true,
|
|
2824
|
+
abortController,
|
|
2825
|
+
currentAction: null,
|
|
2826
|
+
taskDescription: truncate(userMessage, 50),
|
|
2827
|
+
promise: (async () => {
|
|
2828
|
+
try {
|
|
2829
|
+
// Note: Don't set UI to "running" state - that's for foreground only
|
|
2830
|
+
let textAccumulator = '';
|
|
2831
|
+
// Stream agent events
|
|
2832
|
+
const agent = this.agent;
|
|
2833
|
+
if (!agent)
|
|
2834
|
+
return; // Type guard
|
|
2835
|
+
// Phase 3b: Set the executing context so permission handler knows which agent is calling
|
|
2836
|
+
this.currentExecutingBackgroundAgentId = agentId;
|
|
2837
|
+
for await (const event of agent.stream(userMessage, {
|
|
2838
|
+
signal,
|
|
2839
|
+
chatOptions: { model: this.model },
|
|
2840
|
+
})) {
|
|
2841
|
+
if (signal.aborted)
|
|
2842
|
+
break;
|
|
2843
|
+
if (event.type === 'llm_chunk') {
|
|
2844
|
+
if (event.chunk.type === 'text' && event.chunk.text) {
|
|
2845
|
+
textAccumulator += event.chunk.text;
|
|
2846
|
+
}
|
|
2847
|
+
else if (event.chunk.type === 'done' && event.chunk.usage) {
|
|
2848
|
+
// Update session token tracking
|
|
2849
|
+
this.sessionInputTokens += event.chunk.usage.inputTokens;
|
|
2850
|
+
this.sessionOutputTokens += event.chunk.usage.outputTokens;
|
|
2851
|
+
this.sessionRequests++;
|
|
2852
|
+
// Record message for telemetry heartbeat
|
|
2853
|
+
getAuthManager().recordMessage(event.chunk.usage.inputTokens, event.chunk.usage.outputTokens);
|
|
2854
|
+
// Update team agent's token tracking
|
|
2855
|
+
if (this.team) {
|
|
2856
|
+
const teamAgent = this.team.get(agentId);
|
|
2857
|
+
teamAgent?.updateTokenUsage(event.chunk.usage.inputTokens, event.chunk.usage.outputTokens);
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
else if (event.type === 'tool_start') {
|
|
2862
|
+
// Record tool usage for telemetry
|
|
2863
|
+
if (event.name) {
|
|
2864
|
+
getAuthManager().recordTool(event.name);
|
|
2865
|
+
}
|
|
2866
|
+
// Flush accumulated text before tool (with label)
|
|
2867
|
+
if (textAccumulator.trim()) {
|
|
2868
|
+
// Use 'background-agent' type for proper labeling without double mascot
|
|
2869
|
+
this.ui.print({
|
|
2870
|
+
type: 'background-agent',
|
|
2871
|
+
agentId,
|
|
2872
|
+
mascot,
|
|
2873
|
+
text: textAccumulator.trim(),
|
|
2874
|
+
});
|
|
2875
|
+
textAccumulator = '';
|
|
2876
|
+
}
|
|
2877
|
+
// Show tool start with label
|
|
2878
|
+
this.ui.print({
|
|
2879
|
+
type: 'background-agent',
|
|
2880
|
+
agentId,
|
|
2881
|
+
mascot,
|
|
2882
|
+
text: `Running: ${event.name}`,
|
|
2883
|
+
});
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
// Flush any remaining text
|
|
2887
|
+
if (textAccumulator.trim()) {
|
|
2888
|
+
this.ui.print({
|
|
2889
|
+
type: 'background-agent',
|
|
2890
|
+
agentId,
|
|
2891
|
+
mascot,
|
|
2892
|
+
text: textAccumulator.trim(),
|
|
2893
|
+
});
|
|
2894
|
+
}
|
|
2895
|
+
// Show completion notification
|
|
2896
|
+
this.ui.print({
|
|
2897
|
+
type: 'info',
|
|
2898
|
+
message: `${label} Finished ✓`,
|
|
2899
|
+
});
|
|
2900
|
+
// Notify other terminals about completion
|
|
2901
|
+
try {
|
|
2902
|
+
const sid = getActiveTerminalSessionId();
|
|
2903
|
+
const proj = getActiveProject();
|
|
2904
|
+
if (sid && proj) {
|
|
2905
|
+
getNotificationManager()?.insert({
|
|
2906
|
+
projectId: proj.id,
|
|
2907
|
+
fromSessionId: sid,
|
|
2908
|
+
type: 'agent_finished',
|
|
2909
|
+
title: `${agentId} finished`,
|
|
2910
|
+
message: 'Task completed successfully.',
|
|
2911
|
+
});
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2914
|
+
catch {
|
|
2915
|
+
// Best effort
|
|
2916
|
+
}
|
|
2917
|
+
// Check delegation completion (success path)
|
|
2918
|
+
this.checkDelegationCompletion(agentId);
|
|
2919
|
+
}
|
|
2920
|
+
catch (err) {
|
|
2921
|
+
if (!signal.aborted) {
|
|
2922
|
+
this.ui.print({ type: 'error', message: `${label} Error: ${String(err)}` });
|
|
2923
|
+
// Notify other terminals about failure
|
|
2924
|
+
try {
|
|
2925
|
+
const sid = getActiveTerminalSessionId();
|
|
2926
|
+
const proj = getActiveProject();
|
|
2927
|
+
if (sid && proj) {
|
|
2928
|
+
getNotificationManager()?.insert({
|
|
2929
|
+
projectId: proj.id,
|
|
2930
|
+
fromSessionId: sid,
|
|
2931
|
+
type: 'agent_finished',
|
|
2932
|
+
title: `${agentId} failed`,
|
|
2933
|
+
message: String(err).slice(0, 200),
|
|
2934
|
+
});
|
|
2935
|
+
}
|
|
2936
|
+
}
|
|
2937
|
+
catch {
|
|
2938
|
+
// Best effort
|
|
2939
|
+
}
|
|
2940
|
+
// Check delegation completion (failure path)
|
|
2941
|
+
this.checkDelegationCompletion(agentId, String(err));
|
|
2942
|
+
}
|
|
2943
|
+
else {
|
|
2944
|
+
// Killed by user — cancel delegations (no completion event)
|
|
2945
|
+
getDelegationTracker().cancelAllForAgent(agentId);
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
finally {
|
|
2949
|
+
// Phase 3b: Clear the executing context
|
|
2950
|
+
if (this.currentExecutingBackgroundAgentId === agentId) {
|
|
2951
|
+
this.currentExecutingBackgroundAgentId = null;
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
})(),
|
|
2955
|
+
};
|
|
2956
|
+
// Register the background session
|
|
2957
|
+
this.startSession(session);
|
|
2958
|
+
this.ui.setBackgroundAgentCount(this.getBackgroundSessions().length);
|
|
2959
|
+
// Wait for completion (but caller doesn't await this method)
|
|
2960
|
+
await session.promise;
|
|
2961
|
+
this.endSession(agentId);
|
|
2962
|
+
}
|
|
2963
|
+
/**
|
|
2964
|
+
* Handle pending delegation after agent finishes.
|
|
2965
|
+
* If a delegation was approved during the agent run, switch to the
|
|
2966
|
+
* target agent and inject the task as a user message.
|
|
2967
|
+
*/
|
|
2968
|
+
async handlePendingDelegation() {
|
|
2969
|
+
if (!this.onPendingDelegation || !this.team)
|
|
2970
|
+
return;
|
|
2971
|
+
const pending = this.onPendingDelegation();
|
|
2972
|
+
if (!pending)
|
|
2973
|
+
return;
|
|
2974
|
+
const { agentId, task } = pending;
|
|
2975
|
+
// Validate target agent still exists
|
|
2976
|
+
if (!this.team.has(agentId)) {
|
|
2977
|
+
this.ui.print({
|
|
2978
|
+
type: 'error',
|
|
2979
|
+
message: `Delegation failed: Agent '${agentId}' no longer exists in team.`,
|
|
2980
|
+
});
|
|
2981
|
+
return;
|
|
2982
|
+
}
|
|
2983
|
+
// Switch to target agent (no demotion — delegating agent just finished, no session tracking needed)
|
|
2984
|
+
try {
|
|
2985
|
+
const { teamAgent } = await this.switchAgent(agentId, {
|
|
2986
|
+
demoteCurrentIfBusy: false,
|
|
2987
|
+
trackSession: false,
|
|
2988
|
+
});
|
|
2989
|
+
this.ui.print({
|
|
2990
|
+
type: 'info',
|
|
2991
|
+
message: `Delegating to $${agentId} (${teamAgent.displayName})...`,
|
|
2992
|
+
});
|
|
2993
|
+
}
|
|
2994
|
+
catch (err) {
|
|
2995
|
+
this.ui.print({
|
|
2996
|
+
type: 'error',
|
|
2997
|
+
message: `Failed to switch to $${agentId}: ${err instanceof Error ? err.message : String(err)}`,
|
|
2998
|
+
});
|
|
2999
|
+
return;
|
|
3000
|
+
}
|
|
3001
|
+
if (!this.agent) {
|
|
3002
|
+
this.ui.print({
|
|
3003
|
+
type: 'error',
|
|
3004
|
+
message: `Agent $${agentId} is not initialized.`,
|
|
3005
|
+
});
|
|
3006
|
+
return;
|
|
3007
|
+
}
|
|
3008
|
+
// Inject the task as a user message and run the agent
|
|
3009
|
+
this.ui.print({
|
|
3010
|
+
type: 'user-message',
|
|
3011
|
+
text: task,
|
|
3012
|
+
});
|
|
3013
|
+
// Run the target agent with the delegated task
|
|
3014
|
+
await this.runAgentReal(task);
|
|
3015
|
+
}
|
|
3016
|
+
/**
|
|
3017
|
+
* Auto-save the current session and team state.
|
|
3018
|
+
* Called after each successful agent response.
|
|
3019
|
+
*
|
|
3020
|
+
* Saves two things:
|
|
3021
|
+
* 1. Default agent's session state (for /resume - conversation history)
|
|
3022
|
+
* 2. Team state (for team persistence - all agent states including roles)
|
|
3023
|
+
*/
|
|
3024
|
+
async autoSaveSession() {
|
|
3025
|
+
if (!this.team)
|
|
3026
|
+
return;
|
|
3027
|
+
try {
|
|
3028
|
+
// Save default agent's session for /resume functionality
|
|
3029
|
+
const defaultAgent = this.team.get('default')?.agent;
|
|
3030
|
+
if (defaultAgent) {
|
|
3031
|
+
const state = defaultAgent.serialize();
|
|
3032
|
+
await saveCurrentSession(state);
|
|
3033
|
+
}
|
|
3034
|
+
// Save team state for team persistence (all agents including custom ones)
|
|
3035
|
+
// Only save if team has more than just the default agent, or if any agent has history
|
|
3036
|
+
const teamAgents = this.team.getAll();
|
|
3037
|
+
const hasNonDefaultAgents = teamAgents.some(ta => ta.id !== 'default');
|
|
3038
|
+
const hasAnyHistory = teamAgents.some(ta => {
|
|
3039
|
+
const agent = ta.agent;
|
|
3040
|
+
if (!agent)
|
|
3041
|
+
return false;
|
|
3042
|
+
const state = agent.serialize();
|
|
3043
|
+
return state.messages.length > 0;
|
|
3044
|
+
});
|
|
3045
|
+
if (hasNonDefaultAgents || hasAnyHistory) {
|
|
3046
|
+
saveCurrentTeam(this.team);
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
catch {
|
|
3050
|
+
// Silently fail - session/team saving is best-effort
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
3053
|
+
formatToolParams(input) {
|
|
3054
|
+
if (!input)
|
|
3055
|
+
return '';
|
|
3056
|
+
// Format common params
|
|
3057
|
+
if ('path' in input)
|
|
3058
|
+
return String(input.path);
|
|
3059
|
+
if ('file_path' in input)
|
|
3060
|
+
return String(input.file_path);
|
|
3061
|
+
if ('pattern' in input)
|
|
3062
|
+
return String(input.pattern);
|
|
3063
|
+
if ('command' in input)
|
|
3064
|
+
return truncate(String(input.command), 50);
|
|
3065
|
+
// Default: show first param
|
|
3066
|
+
const keys = Object.keys(input);
|
|
3067
|
+
if (keys.length > 0) {
|
|
3068
|
+
const val = String(input[keys[0]]);
|
|
3069
|
+
return truncate(val, 50);
|
|
3070
|
+
}
|
|
3071
|
+
return '';
|
|
3072
|
+
}
|
|
3073
|
+
/**
|
|
3074
|
+
* Extract content and summary from tool result.
|
|
3075
|
+
* Delegates to tool-formatters module.
|
|
3076
|
+
*/
|
|
3077
|
+
extractToolResult(toolName, result) {
|
|
3078
|
+
return formatToolResult(toolName, result);
|
|
3079
|
+
}
|
|
3080
|
+
// ===========================================================================
|
|
3081
|
+
// Agent Simulation (for standalone testing)
|
|
3082
|
+
// ===========================================================================
|
|
3083
|
+
async runAgentSimulation(userMessage) {
|
|
3084
|
+
const abortController = new AbortController();
|
|
3085
|
+
const signal = abortController.signal;
|
|
3086
|
+
const setAction = (action) => {
|
|
3087
|
+
if (this.currentSession) {
|
|
3088
|
+
this.currentSession.currentAction = action;
|
|
3089
|
+
}
|
|
3090
|
+
};
|
|
3091
|
+
// Get active agent ID for session tracking
|
|
3092
|
+
const activeAgentId = this.team?.getActive().id ?? 'default';
|
|
3093
|
+
// Create session object
|
|
3094
|
+
const session = {
|
|
3095
|
+
agentId: activeAgentId,
|
|
3096
|
+
isBackground: false, // Foreground execution
|
|
3097
|
+
abortController,
|
|
3098
|
+
currentAction: null,
|
|
3099
|
+
promise: (async () => {
|
|
3100
|
+
try {
|
|
3101
|
+
this.ui.setAgentRunning(true);
|
|
3102
|
+
setAction('Thinking...');
|
|
3103
|
+
this.ui.setSpinnerText('Thinking...');
|
|
3104
|
+
// 1. Initial response
|
|
3105
|
+
await this.sleep(1000, signal);
|
|
3106
|
+
if (signal.aborted)
|
|
3107
|
+
return;
|
|
3108
|
+
const response = `I understand you want me to help with "${userMessage}". Let me analyze this.`;
|
|
3109
|
+
this.ui.print({ type: 'agent-text', text: response, expression: this.getActiveAgentMascot() });
|
|
3110
|
+
// 2. Add todos
|
|
3111
|
+
this.ui.setTodos([
|
|
3112
|
+
{ content: 'Analyze the request', status: 'completed' },
|
|
3113
|
+
{ content: 'Search for relevant files', status: 'in_progress' },
|
|
3114
|
+
{ content: 'Make changes', status: 'pending' },
|
|
3115
|
+
]);
|
|
3116
|
+
// 3. Simulate tool
|
|
3117
|
+
await this.sleep(800, signal);
|
|
3118
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- signal can be aborted externally
|
|
3119
|
+
if (signal.aborted)
|
|
3120
|
+
return;
|
|
3121
|
+
setAction('Glob(**/*.ts)');
|
|
3122
|
+
this.ui.setSpinnerText('Running Glob...');
|
|
3123
|
+
await this.sleep(600, signal);
|
|
3124
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- signal can be aborted externally
|
|
3125
|
+
if (signal.aborted)
|
|
3126
|
+
return;
|
|
3127
|
+
this.ui.print({
|
|
3128
|
+
type: 'tool-result',
|
|
3129
|
+
name: 'Glob',
|
|
3130
|
+
params: '**/*.ts',
|
|
3131
|
+
summary: 'Found 42 files',
|
|
3132
|
+
content: 'src/index.ts\nsrc/config.ts\nsrc/utils/helpers.ts\n... and 39 more',
|
|
3133
|
+
});
|
|
3134
|
+
this.ui.setTodos([
|
|
3135
|
+
{ content: 'Analyze the request', status: 'completed' },
|
|
3136
|
+
{ content: 'Search for relevant files', status: 'completed' },
|
|
3137
|
+
{ content: 'Make changes', status: 'in_progress' },
|
|
3138
|
+
]);
|
|
3139
|
+
// 4. Conclusion
|
|
3140
|
+
await this.sleep(500, signal);
|
|
3141
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- signal can be aborted externally
|
|
3142
|
+
if (signal.aborted)
|
|
3143
|
+
return;
|
|
3144
|
+
this.ui.setTodos([
|
|
3145
|
+
{ content: 'Analyze the request', status: 'completed' },
|
|
3146
|
+
{ content: 'Search for relevant files', status: 'completed' },
|
|
3147
|
+
{ content: 'Make changes', status: 'completed' },
|
|
3148
|
+
]);
|
|
3149
|
+
this.ui.print({
|
|
3150
|
+
type: 'agent-text',
|
|
3151
|
+
text: 'Done! I have completed the analysis.',
|
|
3152
|
+
expression: 'success',
|
|
3153
|
+
});
|
|
3154
|
+
this.ui.setAgentRunning(false);
|
|
3155
|
+
this.ui.setThinking(false); // Ensure thinking indicator is cleared
|
|
3156
|
+
// NOTE: Don't clear todos - let them persist to show final state
|
|
3157
|
+
// Notify that agent has finished (for applying deferred suggestions)
|
|
3158
|
+
this.onAgentFinish?.();
|
|
3159
|
+
}
|
|
3160
|
+
catch (err) {
|
|
3161
|
+
if (!signal.aborted) {
|
|
3162
|
+
this.ui.print({ type: 'error', message: String(err) });
|
|
3163
|
+
}
|
|
3164
|
+
this.ui.setAgentRunning(false);
|
|
3165
|
+
this.ui.setThinking(false); // Ensure thinking indicator is cleared
|
|
3166
|
+
this.onAgentFinish?.();
|
|
3167
|
+
}
|
|
3168
|
+
})(),
|
|
3169
|
+
};
|
|
3170
|
+
// Register the session (foreground)
|
|
3171
|
+
this.startSession(session);
|
|
3172
|
+
// Wait for session to complete
|
|
3173
|
+
await session.promise;
|
|
3174
|
+
this.endSession(activeAgentId);
|
|
3175
|
+
}
|
|
3176
|
+
async sleep(ms, signal) {
|
|
3177
|
+
return new Promise((resolve) => {
|
|
3178
|
+
const timeout = setTimeout(resolve, ms);
|
|
3179
|
+
signal?.addEventListener('abort', () => {
|
|
3180
|
+
clearTimeout(timeout);
|
|
3181
|
+
resolve();
|
|
3182
|
+
});
|
|
3183
|
+
});
|
|
3184
|
+
}
|
|
3185
|
+
// ===========================================================================
|
|
3186
|
+
// Turn Summary
|
|
3187
|
+
// ===========================================================================
|
|
3188
|
+
/**
|
|
3189
|
+
* Print per-turn summary (tokens, duration, tool calls)
|
|
3190
|
+
*/
|
|
3191
|
+
printTurnSummary() {
|
|
3192
|
+
const metrics = this.ui.getTurnMetrics();
|
|
3193
|
+
// Only print summary if there were tokens used
|
|
3194
|
+
if (metrics.inputTokens === 0 && metrics.outputTokens === 0) {
|
|
3195
|
+
return;
|
|
3196
|
+
}
|
|
3197
|
+
const formatDuration = (ms) => {
|
|
3198
|
+
const seconds = Math.floor(ms / 1000);
|
|
3199
|
+
if (seconds < 60)
|
|
3200
|
+
return `${String(seconds)}s`;
|
|
3201
|
+
const minutes = Math.floor(seconds / 60);
|
|
3202
|
+
const remainingSeconds = seconds % 60;
|
|
3203
|
+
return `${String(minutes)}m ${String(remainingSeconds)}s`;
|
|
3204
|
+
};
|
|
3205
|
+
// Build summary parts
|
|
3206
|
+
const parts = [];
|
|
3207
|
+
parts.push(formatDuration(metrics.durationMs));
|
|
3208
|
+
// Token display with optional thinking/cache tokens
|
|
3209
|
+
let tokenDisplay = `↓ ${formatTokens(metrics.inputTokens)} ↑ ${formatTokens(metrics.outputTokens)}`;
|
|
3210
|
+
const extras = [];
|
|
3211
|
+
if (metrics.thinkingTokens > 0) {
|
|
3212
|
+
extras.push(`think: ${formatTokens(metrics.thinkingTokens)}`);
|
|
3213
|
+
}
|
|
3214
|
+
if (metrics.cacheReadTokens > 0) {
|
|
3215
|
+
extras.push(`cache: ${formatTokens(metrics.cacheReadTokens)}`);
|
|
3216
|
+
}
|
|
3217
|
+
if (extras.length > 0) {
|
|
3218
|
+
tokenDisplay += ` (${extras.join(', ')})`;
|
|
3219
|
+
}
|
|
3220
|
+
parts.push(tokenDisplay);
|
|
3221
|
+
// Add token breakdown
|
|
3222
|
+
const cacheInfo = formatCacheInfo();
|
|
3223
|
+
if (cacheInfo) {
|
|
3224
|
+
// Show cache info if available
|
|
3225
|
+
parts.push(cacheInfo);
|
|
3226
|
+
}
|
|
3227
|
+
else {
|
|
3228
|
+
// Show component breakdown
|
|
3229
|
+
const breakdown = formatBreakdownCompact();
|
|
3230
|
+
if (breakdown) {
|
|
3231
|
+
parts.push(breakdown);
|
|
3232
|
+
}
|
|
3233
|
+
}
|
|
3234
|
+
if (metrics.toolCalls > 0) {
|
|
3235
|
+
parts.push(`${String(metrics.toolCalls)} tool${metrics.toolCalls > 1 ? 's' : ''}`);
|
|
3236
|
+
}
|
|
3237
|
+
// DEBUG: Show payload debug info (chars sent to provider)
|
|
3238
|
+
if (metrics.debugPayload) {
|
|
3239
|
+
const { systemChars, contentsChars, toolsChars } = metrics.debugPayload;
|
|
3240
|
+
const totalChars = systemChars + contentsChars + toolsChars;
|
|
3241
|
+
const estTokens = Math.ceil(totalChars / 4);
|
|
3242
|
+
// Format: [dbg 2calls sent:123k (~31k est) sys:10k cont:100k tools:13k]
|
|
3243
|
+
parts.push(`[dbg ${String(metrics.apiCalls)}calls sent:${formatTokens(totalChars)} (~${formatTokens(estTokens)} est) sys:${formatTokens(systemChars)} cont:${formatTokens(contentsChars)} tools:${formatTokens(toolsChars)}]`);
|
|
3244
|
+
}
|
|
3245
|
+
// Print as muted info line
|
|
3246
|
+
this.ui.print({ type: 'turn-summary', parts });
|
|
3247
|
+
}
|
|
3248
|
+
// ===========================================================================
|
|
3249
|
+
// Session Management
|
|
3250
|
+
// ===========================================================================
|
|
3251
|
+
/**
|
|
3252
|
+
* Cancel the foreground session.
|
|
3253
|
+
* Background agents continue running.
|
|
3254
|
+
*/
|
|
3255
|
+
cancelSession() {
|
|
3256
|
+
const action = this.currentSession?.currentAction ?? undefined;
|
|
3257
|
+
if (this.currentSession && this.foregroundAgentId) {
|
|
3258
|
+
this.currentSession.abortController.abort();
|
|
3259
|
+
this.endSession(this.foregroundAgentId);
|
|
3260
|
+
}
|
|
3261
|
+
this.ui.setAgentRunning(false);
|
|
3262
|
+
this.ui.setThinking(false); // Ensure thinking indicator is cleared
|
|
3263
|
+
this.ui.setTodos([]);
|
|
3264
|
+
this.ui.clearQueue();
|
|
3265
|
+
this.ui.print({ type: 'interrupted', action, suggestion: 'what should I do instead?' });
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
3268
|
+
// =============================================================================
|
|
3269
|
+
// Standalone Entry Point
|
|
3270
|
+
// =============================================================================
|
|
3271
|
+
// Only run main() when executed directly (not when imported)
|
|
3272
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
3273
|
+
const repl = new ReplV2();
|
|
3274
|
+
// Handle SIGINT
|
|
3275
|
+
process.on('SIGINT', () => {
|
|
3276
|
+
console.log('\n\nInterrupted\n');
|
|
3277
|
+
process.exit(0);
|
|
3278
|
+
});
|
|
3279
|
+
try {
|
|
3280
|
+
repl.start();
|
|
3281
|
+
}
|
|
3282
|
+
catch (err) {
|
|
3283
|
+
console.error('Failed to start REPL:', err);
|
|
3284
|
+
process.exit(1);
|
|
3285
|
+
}
|
|
3286
|
+
}
|