@compass-ai/nova 1.0.74 → 1.0.76
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/dist/cli.js +557 -555
- package/dist/index.js +10 -8
- package/package.json +1 -1
- package/dist/acp/agent.d.ts +0 -318
- package/dist/acp/agent.d.ts.map +0 -1
- package/dist/acp/agent.js +0 -795
- package/dist/acp/agent.js.map +0 -1
- package/dist/acp/backpressure-writer.d.ts +0 -49
- package/dist/acp/backpressure-writer.d.ts.map +0 -1
- package/dist/acp/backpressure-writer.js +0 -153
- package/dist/acp/backpressure-writer.js.map +0 -1
- package/dist/acp/event-adapter.d.ts +0 -242
- package/dist/acp/event-adapter.d.ts.map +0 -1
- package/dist/acp/event-adapter.js +0 -456
- package/dist/acp/event-adapter.js.map +0 -1
- package/dist/acp/index.d.ts +0 -30
- package/dist/acp/index.d.ts.map +0 -1
- package/dist/acp/index.js +0 -36
- package/dist/acp/index.js.map +0 -1
- package/dist/acp/modes.d.ts +0 -56
- package/dist/acp/modes.d.ts.map +0 -1
- package/dist/acp/modes.js +0 -135
- package/dist/acp/modes.js.map +0 -1
- package/dist/acp/session-manager.d.ts +0 -170
- package/dist/acp/session-manager.d.ts.map +0 -1
- package/dist/acp/session-manager.js +0 -381
- package/dist/acp/session-manager.js.map +0 -1
- package/dist/acp/text-coalescer.d.ts +0 -45
- package/dist/acp/text-coalescer.d.ts.map +0 -1
- package/dist/acp/text-coalescer.js +0 -110
- package/dist/acp/text-coalescer.js.map +0 -1
- package/dist/acp/tool-bridge.d.ts +0 -156
- package/dist/acp/tool-bridge.d.ts.map +0 -1
- package/dist/acp/tool-bridge.js +0 -381
- package/dist/acp/tool-bridge.js.map +0 -1
- package/dist/acp/types.d.ts +0 -314
- package/dist/acp/types.d.ts.map +0 -1
- package/dist/acp/types.js +0 -8
- package/dist/acp/types.js.map +0 -1
- package/dist/cli.d.ts +0 -9
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/commands/acp.d.ts +0 -26
- package/dist/commands/acp.d.ts.map +0 -1
- package/dist/commands/acp.js +0 -492
- package/dist/commands/acp.js.map +0 -1
- package/dist/commands/cache.d.ts +0 -33
- package/dist/commands/cache.d.ts.map +0 -1
- package/dist/commands/cache.js +0 -537
- package/dist/commands/cache.js.map +0 -1
- package/dist/commands/config.d.ts +0 -10
- package/dist/commands/config.d.ts.map +0 -1
- package/dist/commands/config.js +0 -367
- package/dist/commands/config.js.map +0 -1
- package/dist/commands/consent.d.ts +0 -21
- package/dist/commands/consent.d.ts.map +0 -1
- package/dist/commands/consent.js +0 -334
- package/dist/commands/consent.js.map +0 -1
- package/dist/commands/data.d.ts +0 -24
- package/dist/commands/data.d.ts.map +0 -1
- package/dist/commands/data.js +0 -586
- package/dist/commands/data.js.map +0 -1
- package/dist/commands/index.d.ts +0 -145
- package/dist/commands/index.d.ts.map +0 -1
- package/dist/commands/index.js +0 -210
- package/dist/commands/index.js.map +0 -1
- package/dist/commands/init.d.ts +0 -106
- package/dist/commands/init.d.ts.map +0 -1
- package/dist/commands/init.js +0 -349
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/logs.d.ts +0 -13
- package/dist/commands/logs.d.ts.map +0 -1
- package/dist/commands/logs.js +0 -359
- package/dist/commands/logs.js.map +0 -1
- package/dist/commands/mcp.d.ts +0 -20
- package/dist/commands/mcp.d.ts.map +0 -1
- package/dist/commands/mcp.js +0 -687
- package/dist/commands/mcp.js.map +0 -1
- package/dist/commands/reset.d.ts +0 -20
- package/dist/commands/reset.d.ts.map +0 -1
- package/dist/commands/reset.js +0 -372
- package/dist/commands/reset.js.map +0 -1
- package/dist/commands/setup.d.ts +0 -74
- package/dist/commands/setup.d.ts.map +0 -1
- package/dist/commands/setup.js +0 -863
- package/dist/commands/setup.js.map +0 -1
- package/dist/commands/slash/agents.d.ts +0 -40
- package/dist/commands/slash/agents.d.ts.map +0 -1
- package/dist/commands/slash/agents.js +0 -519
- package/dist/commands/slash/agents.js.map +0 -1
- package/dist/commands/slash/approve.d.ts +0 -46
- package/dist/commands/slash/approve.d.ts.map +0 -1
- package/dist/commands/slash/approve.js +0 -239
- package/dist/commands/slash/approve.js.map +0 -1
- package/dist/commands/slash/attach.d.ts +0 -70
- package/dist/commands/slash/attach.d.ts.map +0 -1
- package/dist/commands/slash/attach.js +0 -333
- package/dist/commands/slash/attach.js.map +0 -1
- package/dist/commands/slash/clear.d.ts +0 -47
- package/dist/commands/slash/clear.d.ts.map +0 -1
- package/dist/commands/slash/clear.js +0 -261
- package/dist/commands/slash/clear.js.map +0 -1
- package/dist/commands/slash/commit.d.ts +0 -40
- package/dist/commands/slash/commit.d.ts.map +0 -1
- package/dist/commands/slash/commit.js +0 -337
- package/dist/commands/slash/commit.js.map +0 -1
- package/dist/commands/slash/compact.d.ts +0 -48
- package/dist/commands/slash/compact.d.ts.map +0 -1
- package/dist/commands/slash/compact.js +0 -288
- package/dist/commands/slash/compact.js.map +0 -1
- package/dist/commands/slash/config.d.ts +0 -20
- package/dist/commands/slash/config.d.ts.map +0 -1
- package/dist/commands/slash/config.js +0 -136
- package/dist/commands/slash/config.js.map +0 -1
- package/dist/commands/slash/context.d.ts +0 -49
- package/dist/commands/slash/context.d.ts.map +0 -1
- package/dist/commands/slash/context.js +0 -430
- package/dist/commands/slash/context.js.map +0 -1
- package/dist/commands/slash/cost.d.ts +0 -34
- package/dist/commands/slash/cost.d.ts.map +0 -1
- package/dist/commands/slash/cost.js +0 -312
- package/dist/commands/slash/cost.js.map +0 -1
- package/dist/commands/slash/custom.d.ts +0 -41
- package/dist/commands/slash/custom.d.ts.map +0 -1
- package/dist/commands/slash/custom.js +0 -126
- package/dist/commands/slash/custom.js.map +0 -1
- package/dist/commands/slash/exit.d.ts +0 -25
- package/dist/commands/slash/exit.d.ts.map +0 -1
- package/dist/commands/slash/exit.js +0 -186
- package/dist/commands/slash/exit.js.map +0 -1
- package/dist/commands/slash/export.d.ts +0 -27
- package/dist/commands/slash/export.d.ts.map +0 -1
- package/dist/commands/slash/export.js +0 -318
- package/dist/commands/slash/export.js.map +0 -1
- package/dist/commands/slash/files.d.ts +0 -47
- package/dist/commands/slash/files.d.ts.map +0 -1
- package/dist/commands/slash/files.js +0 -521
- package/dist/commands/slash/files.js.map +0 -1
- package/dist/commands/slash/help.d.ts +0 -50
- package/dist/commands/slash/help.d.ts.map +0 -1
- package/dist/commands/slash/help.js +0 -282
- package/dist/commands/slash/help.js.map +0 -1
- package/dist/commands/slash/index-cmd.d.ts +0 -41
- package/dist/commands/slash/index-cmd.d.ts.map +0 -1
- package/dist/commands/slash/index-cmd.js +0 -349
- package/dist/commands/slash/index-cmd.js.map +0 -1
- package/dist/commands/slash/index.d.ts +0 -97
- package/dist/commands/slash/index.d.ts.map +0 -1
- package/dist/commands/slash/index.js +0 -251
- package/dist/commands/slash/index.js.map +0 -1
- package/dist/commands/slash/login.d.ts +0 -23
- package/dist/commands/slash/login.d.ts.map +0 -1
- package/dist/commands/slash/login.js +0 -175
- package/dist/commands/slash/login.js.map +0 -1
- package/dist/commands/slash/logout.d.ts +0 -23
- package/dist/commands/slash/logout.d.ts.map +0 -1
- package/dist/commands/slash/logout.js +0 -153
- package/dist/commands/slash/logout.js.map +0 -1
- package/dist/commands/slash/logs.d.ts +0 -29
- package/dist/commands/slash/logs.d.ts.map +0 -1
- package/dist/commands/slash/logs.js +0 -423
- package/dist/commands/slash/logs.js.map +0 -1
- package/dist/commands/slash/mcp.d.ts +0 -29
- package/dist/commands/slash/mcp.d.ts.map +0 -1
- package/dist/commands/slash/mcp.js +0 -1026
- package/dist/commands/slash/mcp.js.map +0 -1
- package/dist/commands/slash/model.d.ts +0 -60
- package/dist/commands/slash/model.d.ts.map +0 -1
- package/dist/commands/slash/model.js +0 -466
- package/dist/commands/slash/model.js.map +0 -1
- package/dist/commands/slash/personality.d.ts +0 -40
- package/dist/commands/slash/personality.d.ts.map +0 -1
- package/dist/commands/slash/personality.js +0 -272
- package/dist/commands/slash/personality.js.map +0 -1
- package/dist/commands/slash/purge.d.ts +0 -42
- package/dist/commands/slash/purge.d.ts.map +0 -1
- package/dist/commands/slash/purge.js +0 -233
- package/dist/commands/slash/purge.js.map +0 -1
- package/dist/commands/slash/reset.d.ts +0 -44
- package/dist/commands/slash/reset.d.ts.map +0 -1
- package/dist/commands/slash/reset.js +0 -326
- package/dist/commands/slash/reset.js.map +0 -1
- package/dist/commands/slash/skills.d.ts +0 -40
- package/dist/commands/slash/skills.d.ts.map +0 -1
- package/dist/commands/slash/skills.js +0 -207
- package/dist/commands/slash/skills.js.map +0 -1
- package/dist/commands/slash/tokens.d.ts +0 -34
- package/dist/commands/slash/tokens.d.ts.map +0 -1
- package/dist/commands/slash/tokens.js +0 -205
- package/dist/commands/slash/tokens.js.map +0 -1
- package/dist/commands/slash/unleash.d.ts +0 -50
- package/dist/commands/slash/unleash.d.ts.map +0 -1
- package/dist/commands/slash/unleash.js +0 -262
- package/dist/commands/slash/unleash.js.map +0 -1
- package/dist/commands/slash/update.d.ts +0 -34
- package/dist/commands/slash/update.d.ts.map +0 -1
- package/dist/commands/slash/update.js +0 -364
- package/dist/commands/slash/update.js.map +0 -1
- package/dist/commands/slash/wrap.d.ts +0 -18
- package/dist/commands/slash/wrap.d.ts.map +0 -1
- package/dist/commands/slash/wrap.js +0 -21
- package/dist/commands/slash/wrap.js.map +0 -1
- package/dist/commands/tokens.d.ts +0 -26
- package/dist/commands/tokens.d.ts.map +0 -1
- package/dist/commands/tokens.js +0 -245
- package/dist/commands/tokens.js.map +0 -1
- package/dist/constants/builtin-agents.d.ts +0 -27
- package/dist/constants/builtin-agents.d.ts.map +0 -1
- package/dist/constants/builtin-agents.js +0 -710
- package/dist/constants/builtin-agents.js.map +0 -1
- package/dist/constants/builtin-skills.d.ts +0 -32
- package/dist/constants/builtin-skills.d.ts.map +0 -1
- package/dist/constants/builtin-skills.js +0 -389
- package/dist/constants/builtin-skills.js.map +0 -1
- package/dist/constants/defaults.d.ts +0 -448
- package/dist/constants/defaults.d.ts.map +0 -1
- package/dist/constants/defaults.js +0 -829
- package/dist/constants/defaults.js.map +0 -1
- package/dist/constants/index.d.ts +0 -27
- package/dist/constants/index.d.ts.map +0 -1
- package/dist/constants/index.js +0 -85
- package/dist/constants/index.js.map +0 -1
- package/dist/constants/install-hints.d.ts +0 -7
- package/dist/constants/install-hints.d.ts.map +0 -1
- package/dist/constants/install-hints.js +0 -123
- package/dist/constants/install-hints.js.map +0 -1
- package/dist/constants/models.d.ts +0 -255
- package/dist/constants/models.d.ts.map +0 -1
- package/dist/constants/models.js +0 -596
- package/dist/constants/models.js.map +0 -1
- package/dist/constants/schedule.d.ts +0 -43
- package/dist/constants/schedule.d.ts.map +0 -1
- package/dist/constants/schedule.js +0 -110
- package/dist/constants/schedule.js.map +0 -1
- package/dist/constants/system-utilities.d.ts +0 -57
- package/dist/constants/system-utilities.d.ts.map +0 -1
- package/dist/constants/system-utilities.js +0 -421
- package/dist/constants/system-utilities.js.map +0 -1
- package/dist/constants/token-limits.d.ts +0 -102
- package/dist/constants/token-limits.d.ts.map +0 -1
- package/dist/constants/token-limits.js +0 -286
- package/dist/constants/token-limits.js.map +0 -1
- package/dist/core/autocomplete.d.ts +0 -132
- package/dist/core/autocomplete.d.ts.map +0 -1
- package/dist/core/autocomplete.js +0 -653
- package/dist/core/autocomplete.js.map +0 -1
- package/dist/core/command-parser.d.ts +0 -301
- package/dist/core/command-parser.d.ts.map +0 -1
- package/dist/core/command-parser.js +0 -526
- package/dist/core/command-parser.js.map +0 -1
- package/dist/core/context-builder.d.ts +0 -264
- package/dist/core/context-builder.d.ts.map +0 -1
- package/dist/core/context-builder.js +0 -1018
- package/dist/core/context-builder.js.map +0 -1
- package/dist/core/event-emitter.d.ts +0 -411
- package/dist/core/event-emitter.d.ts.map +0 -1
- package/dist/core/event-emitter.js +0 -138
- package/dist/core/event-emitter.js.map +0 -1
- package/dist/core/history-manager.d.ts +0 -62
- package/dist/core/history-manager.d.ts.map +0 -1
- package/dist/core/history-manager.js +0 -151
- package/dist/core/history-manager.js.map +0 -1
- package/dist/core/slash-command-handler.d.ts +0 -352
- package/dist/core/slash-command-handler.d.ts.map +0 -1
- package/dist/core/slash-command-handler.js +0 -563
- package/dist/core/slash-command-handler.js.map +0 -1
- package/dist/core/task-processor.d.ts +0 -179
- package/dist/core/task-processor.d.ts.map +0 -1
- package/dist/core/task-processor.js +0 -519
- package/dist/core/task-processor.js.map +0 -1
- package/dist/index.d.ts +0 -90
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/prompts/agent-prompt-generator.d.ts +0 -26
- package/dist/prompts/agent-prompt-generator.d.ts.map +0 -1
- package/dist/prompts/agent-prompt-generator.js +0 -244
- package/dist/prompts/agent-prompt-generator.js.map +0 -1
- package/dist/prompts/commit-message.d.ts +0 -35
- package/dist/prompts/commit-message.d.ts.map +0 -1
- package/dist/prompts/commit-message.js +0 -187
- package/dist/prompts/commit-message.js.map +0 -1
- package/dist/prompts/conversation.d.ts +0 -51
- package/dist/prompts/conversation.d.ts.map +0 -1
- package/dist/prompts/conversation.js +0 -115
- package/dist/prompts/conversation.js.map +0 -1
- package/dist/prompts/index.d.ts +0 -39
- package/dist/prompts/index.d.ts.map +0 -1
- package/dist/prompts/index.js +0 -74
- package/dist/prompts/index.js.map +0 -1
- package/dist/prompts/memory.d.ts +0 -6
- package/dist/prompts/memory.d.ts.map +0 -1
- package/dist/prompts/memory.js +0 -29
- package/dist/prompts/memory.js.map +0 -1
- package/dist/prompts/personality.d.ts +0 -77
- package/dist/prompts/personality.d.ts.map +0 -1
- package/dist/prompts/personality.js +0 -393
- package/dist/prompts/personality.js.map +0 -1
- package/dist/prompts/plan-generator.d.ts +0 -144
- package/dist/prompts/plan-generator.d.ts.map +0 -1
- package/dist/prompts/plan-generator.js +0 -553
- package/dist/prompts/plan-generator.js.map +0 -1
- package/dist/prompts/system.d.ts +0 -95
- package/dist/prompts/system.d.ts.map +0 -1
- package/dist/prompts/system.js +0 -461
- package/dist/prompts/system.js.map +0 -1
- package/dist/prompts/task-processor.d.ts +0 -94
- package/dist/prompts/task-processor.d.ts.map +0 -1
- package/dist/prompts/task-processor.js +0 -554
- package/dist/prompts/task-processor.js.map +0 -1
- package/dist/prompts/unguarded.d.ts +0 -78
- package/dist/prompts/unguarded.d.ts.map +0 -1
- package/dist/prompts/unguarded.js +0 -418
- package/dist/prompts/unguarded.js.map +0 -1
- package/dist/prompts/utils.d.ts +0 -73
- package/dist/prompts/utils.d.ts.map +0 -1
- package/dist/prompts/utils.js +0 -114
- package/dist/prompts/utils.js.map +0 -1
- package/dist/prompts/workflow.d.ts +0 -241
- package/dist/prompts/workflow.d.ts.map +0 -1
- package/dist/prompts/workflow.js +0 -608
- package/dist/prompts/workflow.js.map +0 -1
- package/dist/services/action-logger.d.ts +0 -383
- package/dist/services/action-logger.d.ts.map +0 -1
- package/dist/services/action-logger.js +0 -544
- package/dist/services/action-logger.js.map +0 -1
- package/dist/services/agent-budget-allocator.d.ts +0 -111
- package/dist/services/agent-budget-allocator.d.ts.map +0 -1
- package/dist/services/agent-budget-allocator.js +0 -278
- package/dist/services/agent-budget-allocator.js.map +0 -1
- package/dist/services/agent-manager.d.ts +0 -181
- package/dist/services/agent-manager.d.ts.map +0 -1
- package/dist/services/agent-manager.js +0 -749
- package/dist/services/agent-manager.js.map +0 -1
- package/dist/services/agent-spawner.d.ts +0 -138
- package/dist/services/agent-spawner.d.ts.map +0 -1
- package/dist/services/agent-spawner.js +0 -748
- package/dist/services/agent-spawner.js.map +0 -1
- package/dist/services/agent-state-service.d.ts +0 -145
- package/dist/services/agent-state-service.d.ts.map +0 -1
- package/dist/services/agent-state-service.js +0 -247
- package/dist/services/agent-state-service.js.map +0 -1
- package/dist/services/anthropic-client.d.ts +0 -357
- package/dist/services/anthropic-client.d.ts.map +0 -1
- package/dist/services/anthropic-client.js +0 -1451
- package/dist/services/anthropic-client.js.map +0 -1
- package/dist/services/approval-manager.d.ts +0 -385
- package/dist/services/approval-manager.d.ts.map +0 -1
- package/dist/services/approval-manager.js +0 -1044
- package/dist/services/approval-manager.js.map +0 -1
- package/dist/services/audit-logger.d.ts +0 -245
- package/dist/services/audit-logger.d.ts.map +0 -1
- package/dist/services/audit-logger.js +0 -324
- package/dist/services/audit-logger.js.map +0 -1
- package/dist/services/backup-manager.d.ts +0 -136
- package/dist/services/backup-manager.d.ts.map +0 -1
- package/dist/services/backup-manager.js +0 -260
- package/dist/services/backup-manager.js.map +0 -1
- package/dist/services/cache-service.d.ts +0 -247
- package/dist/services/cache-service.d.ts.map +0 -1
- package/dist/services/cache-service.js +0 -558
- package/dist/services/cache-service.js.map +0 -1
- package/dist/services/chat-archival-service.d.ts +0 -108
- package/dist/services/chat-archival-service.d.ts.map +0 -1
- package/dist/services/chat-archival-service.js +0 -465
- package/dist/services/chat-archival-service.js.map +0 -1
- package/dist/services/codebase-indexer.d.ts +0 -272
- package/dist/services/codebase-indexer.d.ts.map +0 -1
- package/dist/services/codebase-indexer.js +0 -863
- package/dist/services/codebase-indexer.js.map +0 -1
- package/dist/services/compass-auth-service.d.ts +0 -204
- package/dist/services/compass-auth-service.d.ts.map +0 -1
- package/dist/services/compass-auth-service.js +0 -391
- package/dist/services/compass-auth-service.js.map +0 -1
- package/dist/services/complexity-classifier.d.ts +0 -208
- package/dist/services/complexity-classifier.d.ts.map +0 -1
- package/dist/services/complexity-classifier.js +0 -1410
- package/dist/services/complexity-classifier.js.map +0 -1
- package/dist/services/config-manager.d.ts +0 -278
- package/dist/services/config-manager.d.ts.map +0 -1
- package/dist/services/config-manager.js +0 -651
- package/dist/services/config-manager.js.map +0 -1
- package/dist/services/consent-manager.d.ts +0 -239
- package/dist/services/consent-manager.d.ts.map +0 -1
- package/dist/services/consent-manager.js +0 -516
- package/dist/services/consent-manager.js.map +0 -1
- package/dist/services/conversation-compactor.d.ts +0 -223
- package/dist/services/conversation-compactor.d.ts.map +0 -1
- package/dist/services/conversation-compactor.js +0 -750
- package/dist/services/conversation-compactor.js.map +0 -1
- package/dist/services/cost-tracker.d.ts +0 -167
- package/dist/services/cost-tracker.d.ts.map +0 -1
- package/dist/services/cost-tracker.js +0 -199
- package/dist/services/cost-tracker.js.map +0 -1
- package/dist/services/credential-store.d.ts +0 -273
- package/dist/services/credential-store.d.ts.map +0 -1
- package/dist/services/credential-store.js +0 -877
- package/dist/services/credential-store.js.map +0 -1
- package/dist/services/custom-command-service.d.ts +0 -112
- package/dist/services/custom-command-service.d.ts.map +0 -1
- package/dist/services/custom-command-service.js +0 -464
- package/dist/services/custom-command-service.js.map +0 -1
- package/dist/services/default-statusline-renderer.d.ts +0 -60
- package/dist/services/default-statusline-renderer.d.ts.map +0 -1
- package/dist/services/default-statusline-renderer.js +0 -110
- package/dist/services/default-statusline-renderer.js.map +0 -1
- package/dist/services/enhanced-context-gatherer.d.ts +0 -116
- package/dist/services/enhanced-context-gatherer.d.ts.map +0 -1
- package/dist/services/enhanced-context-gatherer.js +0 -605
- package/dist/services/enhanced-context-gatherer.js.map +0 -1
- package/dist/services/file-hash-tracker.d.ts +0 -95
- package/dist/services/file-hash-tracker.d.ts.map +0 -1
- package/dist/services/file-hash-tracker.js +0 -199
- package/dist/services/file-hash-tracker.js.map +0 -1
- package/dist/services/file-service.d.ts +0 -274
- package/dist/services/file-service.d.ts.map +0 -1
- package/dist/services/file-service.js +0 -876
- package/dist/services/file-service.js.map +0 -1
- package/dist/services/git-service.d.ts +0 -536
- package/dist/services/git-service.d.ts.map +0 -1
- package/dist/services/git-service.js +0 -1215
- package/dist/services/git-service.js.map +0 -1
- package/dist/services/hook-service.d.ts +0 -148
- package/dist/services/hook-service.d.ts.map +0 -1
- package/dist/services/hook-service.js +0 -705
- package/dist/services/hook-service.js.map +0 -1
- package/dist/services/ide-state-service.d.ts +0 -114
- package/dist/services/ide-state-service.d.ts.map +0 -1
- package/dist/services/ide-state-service.js +0 -204
- package/dist/services/ide-state-service.js.map +0 -1
- package/dist/services/interactive-clarifier.d.ts +0 -90
- package/dist/services/interactive-clarifier.d.ts.map +0 -1
- package/dist/services/interactive-clarifier.js +0 -446
- package/dist/services/interactive-clarifier.js.map +0 -1
- package/dist/services/iteration-scoper.d.ts +0 -225
- package/dist/services/iteration-scoper.d.ts.map +0 -1
- package/dist/services/iteration-scoper.js +0 -387
- package/dist/services/iteration-scoper.js.map +0 -1
- package/dist/services/llm-plan-generator.d.ts +0 -44
- package/dist/services/llm-plan-generator.d.ts.map +0 -1
- package/dist/services/llm-plan-generator.js +0 -863
- package/dist/services/llm-plan-generator.js.map +0 -1
- package/dist/services/llm-system-prompt-generator.d.ts +0 -85
- package/dist/services/llm-system-prompt-generator.d.ts.map +0 -1
- package/dist/services/llm-system-prompt-generator.js +0 -257
- package/dist/services/llm-system-prompt-generator.js.map +0 -1
- package/dist/services/log-interpreter.d.ts +0 -190
- package/dist/services/log-interpreter.d.ts.map +0 -1
- package/dist/services/log-interpreter.js +0 -520
- package/dist/services/log-interpreter.js.map +0 -1
- package/dist/services/mcp-config-manager.d.ts +0 -141
- package/dist/services/mcp-config-manager.d.ts.map +0 -1
- package/dist/services/mcp-config-manager.js +0 -678
- package/dist/services/mcp-config-manager.js.map +0 -1
- package/dist/services/mcp-oauth-service.d.ts +0 -170
- package/dist/services/mcp-oauth-service.d.ts.map +0 -1
- package/dist/services/mcp-oauth-service.js +0 -892
- package/dist/services/mcp-oauth-service.js.map +0 -1
- package/dist/services/mcp-plugin-support.d.ts +0 -81
- package/dist/services/mcp-plugin-support.d.ts.map +0 -1
- package/dist/services/mcp-plugin-support.js +0 -305
- package/dist/services/mcp-plugin-support.js.map +0 -1
- package/dist/services/mcp-server-manager.d.ts +0 -134
- package/dist/services/mcp-server-manager.d.ts.map +0 -1
- package/dist/services/mcp-server-manager.js +0 -613
- package/dist/services/mcp-server-manager.js.map +0 -1
- package/dist/services/mcp-tool-integration.d.ts +0 -119
- package/dist/services/mcp-tool-integration.d.ts.map +0 -1
- package/dist/services/mcp-tool-integration.js +0 -381
- package/dist/services/mcp-tool-integration.js.map +0 -1
- package/dist/services/mcp-transport.d.ts +0 -105
- package/dist/services/mcp-transport.d.ts.map +0 -1
- package/dist/services/mcp-transport.js +0 -1316
- package/dist/services/mcp-transport.js.map +0 -1
- package/dist/services/memory-service.d.ts +0 -55
- package/dist/services/memory-service.d.ts.map +0 -1
- package/dist/services/memory-service.js +0 -251
- package/dist/services/memory-service.js.map +0 -1
- package/dist/services/model-availability.d.ts +0 -64
- package/dist/services/model-availability.d.ts.map +0 -1
- package/dist/services/model-availability.js +0 -114
- package/dist/services/model-availability.js.map +0 -1
- package/dist/services/plan-generator.d.ts +0 -98
- package/dist/services/plan-generator.d.ts.map +0 -1
- package/dist/services/plan-generator.js +0 -658
- package/dist/services/plan-generator.js.map +0 -1
- package/dist/services/plan-mode-fallback.d.ts +0 -80
- package/dist/services/plan-mode-fallback.d.ts.map +0 -1
- package/dist/services/plan-mode-fallback.js +0 -307
- package/dist/services/plan-mode-fallback.js.map +0 -1
- package/dist/services/plan-mode-handler.d.ts +0 -42
- package/dist/services/plan-mode-handler.d.ts.map +0 -1
- package/dist/services/plan-mode-handler.js +0 -388
- package/dist/services/plan-mode-handler.js.map +0 -1
- package/dist/services/plan-persistence.d.ts +0 -203
- package/dist/services/plan-persistence.d.ts.map +0 -1
- package/dist/services/plan-persistence.js +0 -538
- package/dist/services/plan-persistence.js.map +0 -1
- package/dist/services/prompt-preprocessor.d.ts +0 -73
- package/dist/services/prompt-preprocessor.d.ts.map +0 -1
- package/dist/services/prompt-preprocessor.js +0 -146
- package/dist/services/prompt-preprocessor.js.map +0 -1
- package/dist/services/rating-service.d.ts +0 -84
- package/dist/services/rating-service.d.ts.map +0 -1
- package/dist/services/rating-service.js +0 -171
- package/dist/services/rating-service.js.map +0 -1
- package/dist/services/rating-state-manager.d.ts +0 -131
- package/dist/services/rating-state-manager.d.ts.map +0 -1
- package/dist/services/rating-state-manager.js +0 -270
- package/dist/services/rating-state-manager.js.map +0 -1
- package/dist/services/sdk-runner.d.ts +0 -113
- package/dist/services/sdk-runner.d.ts.map +0 -1
- package/dist/services/sdk-runner.js +0 -1424
- package/dist/services/sdk-runner.js.map +0 -1
- package/dist/services/session-manager.d.ts +0 -528
- package/dist/services/session-manager.d.ts.map +0 -1
- package/dist/services/session-manager.js +0 -1184
- package/dist/services/session-manager.js.map +0 -1
- package/dist/services/shell-executor.d.ts +0 -337
- package/dist/services/shell-executor.d.ts.map +0 -1
- package/dist/services/shell-executor.js +0 -1201
- package/dist/services/shell-executor.js.map +0 -1
- package/dist/services/skill-service.d.ts +0 -149
- package/dist/services/skill-service.d.ts.map +0 -1
- package/dist/services/skill-service.js +0 -594
- package/dist/services/skill-service.js.map +0 -1
- package/dist/services/statusline-executor.d.ts +0 -102
- package/dist/services/statusline-executor.d.ts.map +0 -1
- package/dist/services/statusline-executor.js +0 -305
- package/dist/services/statusline-executor.js.map +0 -1
- package/dist/services/step-tracker.d.ts +0 -356
- package/dist/services/step-tracker.d.ts.map +0 -1
- package/dist/services/step-tracker.js +0 -634
- package/dist/services/step-tracker.js.map +0 -1
- package/dist/services/system-event-logger.d.ts +0 -473
- package/dist/services/system-event-logger.d.ts.map +0 -1
- package/dist/services/system-event-logger.js +0 -790
- package/dist/services/system-event-logger.js.map +0 -1
- package/dist/services/system-utility-detector.d.ts +0 -91
- package/dist/services/system-utility-detector.d.ts.map +0 -1
- package/dist/services/system-utility-detector.js +0 -238
- package/dist/services/system-utility-detector.js.map +0 -1
- package/dist/services/team-context-store.d.ts +0 -100
- package/dist/services/team-context-store.d.ts.map +0 -1
- package/dist/services/team-context-store.js +0 -513
- package/dist/services/team-context-store.js.map +0 -1
- package/dist/services/temp-file-service.d.ts +0 -164
- package/dist/services/temp-file-service.d.ts.map +0 -1
- package/dist/services/temp-file-service.js +0 -303
- package/dist/services/temp-file-service.js.map +0 -1
- package/dist/services/token-limit-enforcer.d.ts +0 -53
- package/dist/services/token-limit-enforcer.d.ts.map +0 -1
- package/dist/services/token-limit-enforcer.js +0 -90
- package/dist/services/token-limit-enforcer.js.map +0 -1
- package/dist/services/token-limit-store.d.ts +0 -105
- package/dist/services/token-limit-store.d.ts.map +0 -1
- package/dist/services/token-limit-store.js +0 -288
- package/dist/services/token-limit-store.js.map +0 -1
- package/dist/services/token-tracker.d.ts +0 -290
- package/dist/services/token-tracker.d.ts.map +0 -1
- package/dist/services/token-tracker.js +0 -751
- package/dist/services/token-tracker.js.map +0 -1
- package/dist/services/tool-registry.d.ts +0 -302
- package/dist/services/tool-registry.d.ts.map +0 -1
- package/dist/services/tool-registry.js +0 -606
- package/dist/services/tool-registry.js.map +0 -1
- package/dist/services/tools-logger.d.ts +0 -152
- package/dist/services/tools-logger.d.ts.map +0 -1
- package/dist/services/tools-logger.js +0 -222
- package/dist/services/tools-logger.js.map +0 -1
- package/dist/services/update-plan-handler.d.ts +0 -56
- package/dist/services/update-plan-handler.d.ts.map +0 -1
- package/dist/services/update-plan-handler.js +0 -372
- package/dist/services/update-plan-handler.js.map +0 -1
- package/dist/services/update-service.d.ts +0 -197
- package/dist/services/update-service.d.ts.map +0 -1
- package/dist/services/update-service.js +0 -749
- package/dist/services/update-service.js.map +0 -1
- package/dist/services/verifier.d.ts +0 -113
- package/dist/services/verifier.d.ts.map +0 -1
- package/dist/services/verifier.js +0 -541
- package/dist/services/verifier.js.map +0 -1
- package/dist/services/workflow-manager.d.ts +0 -277
- package/dist/services/workflow-manager.d.ts.map +0 -1
- package/dist/services/workflow-manager.js +0 -616
- package/dist/services/workflow-manager.js.map +0 -1
- package/dist/services/workflow-orchestrator.d.ts +0 -148
- package/dist/services/workflow-orchestrator.d.ts.map +0 -1
- package/dist/services/workflow-orchestrator.js +0 -617
- package/dist/services/workflow-orchestrator.js.map +0 -1
- package/dist/services/worktree-manager.d.ts +0 -36
- package/dist/services/worktree-manager.d.ts.map +0 -1
- package/dist/services/worktree-manager.js +0 -185
- package/dist/services/worktree-manager.js.map +0 -1
- package/dist/templates/ascii-art.d.ts +0 -136
- package/dist/templates/ascii-art.d.ts.map +0 -1
- package/dist/templates/ascii-art.js +0 -286
- package/dist/templates/ascii-art.js.map +0 -1
- package/dist/templates/help.d.ts +0 -186
- package/dist/templates/help.d.ts.map +0 -1
- package/dist/templates/help.js +0 -588
- package/dist/templates/help.js.map +0 -1
- package/dist/templates/prompts/workflow-prompts.d.ts +0 -9
- package/dist/templates/prompts/workflow-prompts.d.ts.map +0 -1
- package/dist/templates/prompts/workflow-prompts.js +0 -9
- package/dist/templates/prompts/workflow-prompts.js.map +0 -1
- package/dist/tools/agent-tools.d.ts +0 -9
- package/dist/tools/agent-tools.d.ts.map +0 -1
- package/dist/tools/agent-tools.js +0 -349
- package/dist/tools/agent-tools.js.map +0 -1
- package/dist/tools/edit-replacers.d.ts +0 -90
- package/dist/tools/edit-replacers.d.ts.map +0 -1
- package/dist/tools/edit-replacers.js +0 -553
- package/dist/tools/edit-replacers.js.map +0 -1
- package/dist/tools/file-tools.d.ts +0 -13
- package/dist/tools/file-tools.d.ts.map +0 -1
- package/dist/tools/file-tools.js +0 -954
- package/dist/tools/file-tools.js.map +0 -1
- package/dist/tools/git-tools.d.ts +0 -9
- package/dist/tools/git-tools.d.ts.map +0 -1
- package/dist/tools/git-tools.js +0 -261
- package/dist/tools/git-tools.js.map +0 -1
- package/dist/tools/index.d.ts +0 -13
- package/dist/tools/index.d.ts.map +0 -1
- package/dist/tools/index.js +0 -70
- package/dist/tools/index.js.map +0 -1
- package/dist/tools/network-tools.d.ts +0 -8
- package/dist/tools/network-tools.d.ts.map +0 -1
- package/dist/tools/network-tools.js +0 -261
- package/dist/tools/network-tools.js.map +0 -1
- package/dist/tools/openai-tools.d.ts +0 -16
- package/dist/tools/openai-tools.d.ts.map +0 -1
- package/dist/tools/openai-tools.js +0 -385
- package/dist/tools/openai-tools.js.map +0 -1
- package/dist/tools/plan-tools.d.ts +0 -9
- package/dist/tools/plan-tools.d.ts.map +0 -1
- package/dist/tools/plan-tools.js +0 -223
- package/dist/tools/plan-tools.js.map +0 -1
- package/dist/tools/schedule-tools.d.ts +0 -9
- package/dist/tools/schedule-tools.d.ts.map +0 -1
- package/dist/tools/schedule-tools.js +0 -405
- package/dist/tools/schedule-tools.js.map +0 -1
- package/dist/tools/search-tools.d.ts +0 -8
- package/dist/tools/search-tools.d.ts.map +0 -1
- package/dist/tools/search-tools.js +0 -357
- package/dist/tools/search-tools.js.map +0 -1
- package/dist/tools/shared-utils.d.ts +0 -91
- package/dist/tools/shared-utils.d.ts.map +0 -1
- package/dist/tools/shared-utils.js +0 -385
- package/dist/tools/shared-utils.js.map +0 -1
- package/dist/tools/shell-tools.d.ts +0 -9
- package/dist/tools/shell-tools.d.ts.map +0 -1
- package/dist/tools/shell-tools.js +0 -409
- package/dist/tools/shell-tools.js.map +0 -1
- package/dist/tools/skill-tools.d.ts +0 -13
- package/dist/tools/skill-tools.d.ts.map +0 -1
- package/dist/tools/skill-tools.js +0 -244
- package/dist/tools/skill-tools.js.map +0 -1
- package/dist/tools/swarm-tools.d.ts +0 -9
- package/dist/tools/swarm-tools.d.ts.map +0 -1
- package/dist/tools/swarm-tools.js +0 -422
- package/dist/tools/swarm-tools.js.map +0 -1
- package/dist/tools/user-tools.d.ts +0 -13
- package/dist/tools/user-tools.d.ts.map +0 -1
- package/dist/tools/user-tools.js +0 -232
- package/dist/tools/user-tools.js.map +0 -1
- package/dist/types/agent-process.d.ts +0 -244
- package/dist/types/agent-process.d.ts.map +0 -1
- package/dist/types/agent-process.js +0 -93
- package/dist/types/agent-process.js.map +0 -1
- package/dist/types/agent.d.ts +0 -358
- package/dist/types/agent.d.ts.map +0 -1
- package/dist/types/agent.js +0 -171
- package/dist/types/agent.js.map +0 -1
- package/dist/types/anthropic.d.ts +0 -438
- package/dist/types/anthropic.d.ts.map +0 -1
- package/dist/types/anthropic.js +0 -9
- package/dist/types/anthropic.js.map +0 -1
- package/dist/types/approval.d.ts +0 -332
- package/dist/types/approval.d.ts.map +0 -1
- package/dist/types/approval.js +0 -44
- package/dist/types/approval.js.map +0 -1
- package/dist/types/autocomplete.d.ts +0 -57
- package/dist/types/autocomplete.d.ts.map +0 -1
- package/dist/types/autocomplete.js +0 -7
- package/dist/types/autocomplete.js.map +0 -1
- package/dist/types/chat-archive.d.ts +0 -161
- package/dist/types/chat-archive.d.ts.map +0 -1
- package/dist/types/chat-archive.js +0 -36
- package/dist/types/chat-archive.js.map +0 -1
- package/dist/types/config.d.ts +0 -268
- package/dist/types/config.d.ts.map +0 -1
- package/dist/types/config.js +0 -188
- package/dist/types/config.js.map +0 -1
- package/dist/types/consent.d.ts +0 -191
- package/dist/types/consent.d.ts.map +0 -1
- package/dist/types/consent.js +0 -119
- package/dist/types/consent.js.map +0 -1
- package/dist/types/custom-command.d.ts +0 -139
- package/dist/types/custom-command.d.ts.map +0 -1
- package/dist/types/custom-command.js +0 -7
- package/dist/types/custom-command.js.map +0 -1
- package/dist/types/git.d.ts +0 -20
- package/dist/types/git.d.ts.map +0 -1
- package/dist/types/git.js +0 -2
- package/dist/types/git.js.map +0 -1
- package/dist/types/hook.d.ts +0 -342
- package/dist/types/hook.d.ts.map +0 -1
- package/dist/types/hook.js +0 -84
- package/dist/types/hook.js.map +0 -1
- package/dist/types/index.d.ts +0 -86
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js +0 -71
- package/dist/types/index.js.map +0 -1
- package/dist/types/mcp.d.ts +0 -456
- package/dist/types/mcp.d.ts.map +0 -1
- package/dist/types/mcp.js +0 -94
- package/dist/types/mcp.js.map +0 -1
- package/dist/types/rating.d.ts +0 -110
- package/dist/types/rating.d.ts.map +0 -1
- package/dist/types/rating.js +0 -53
- package/dist/types/rating.js.map +0 -1
- package/dist/types/schedule.d.ts +0 -91
- package/dist/types/schedule.d.ts.map +0 -1
- package/dist/types/schedule.js +0 -8
- package/dist/types/schedule.js.map +0 -1
- package/dist/types/session.d.ts +0 -361
- package/dist/types/session.d.ts.map +0 -1
- package/dist/types/session.js +0 -9
- package/dist/types/session.js.map +0 -1
- package/dist/types/skill.d.ts +0 -258
- package/dist/types/skill.d.ts.map +0 -1
- package/dist/types/skill.js +0 -79
- package/dist/types/skill.js.map +0 -1
- package/dist/types/statusline.d.ts +0 -212
- package/dist/types/statusline.d.ts.map +0 -1
- package/dist/types/statusline.js +0 -8
- package/dist/types/statusline.js.map +0 -1
- package/dist/types/stream.d.ts +0 -61
- package/dist/types/stream.d.ts.map +0 -1
- package/dist/types/stream.js +0 -17
- package/dist/types/stream.js.map +0 -1
- package/dist/types/swarm.d.ts +0 -132
- package/dist/types/swarm.d.ts.map +0 -1
- package/dist/types/swarm.js +0 -21
- package/dist/types/swarm.js.map +0 -1
- package/dist/types/token-limits.d.ts +0 -107
- package/dist/types/token-limits.d.ts.map +0 -1
- package/dist/types/token-limits.js +0 -57
- package/dist/types/token-limits.js.map +0 -1
- package/dist/types/token.d.ts +0 -329
- package/dist/types/token.d.ts.map +0 -1
- package/dist/types/token.js +0 -9
- package/dist/types/token.js.map +0 -1
- package/dist/types/update.d.ts +0 -189
- package/dist/types/update.d.ts.map +0 -1
- package/dist/types/update.js +0 -55
- package/dist/types/update.js.map +0 -1
- package/dist/types/workflow.d.ts +0 -396
- package/dist/types/workflow.d.ts.map +0 -1
- package/dist/types/workflow.js +0 -46
- package/dist/types/workflow.js.map +0 -1
- package/dist/ui/App.d.ts +0 -62
- package/dist/ui/App.d.ts.map +0 -1
- package/dist/ui/App.js +0 -511
- package/dist/ui/App.js.map +0 -1
- package/dist/ui/InteractiveSession.d.ts +0 -34
- package/dist/ui/InteractiveSession.d.ts.map +0 -1
- package/dist/ui/InteractiveSession.js +0 -3611
- package/dist/ui/InteractiveSession.js.map +0 -1
- package/dist/ui/MCPApprovalPrompt.d.ts +0 -27
- package/dist/ui/MCPApprovalPrompt.d.ts.map +0 -1
- package/dist/ui/MCPApprovalPrompt.js +0 -51
- package/dist/ui/MCPApprovalPrompt.js.map +0 -1
- package/dist/ui/SetupWizard.d.ts +0 -26
- package/dist/ui/SetupWizard.d.ts.map +0 -1
- package/dist/ui/SetupWizard.js +0 -396
- package/dist/ui/SetupWizard.js.map +0 -1
- package/dist/ui/components/AgentCreationWizard.d.ts +0 -36
- package/dist/ui/components/AgentCreationWizard.d.ts.map +0 -1
- package/dist/ui/components/AgentCreationWizard.js +0 -619
- package/dist/ui/components/AgentCreationWizard.js.map +0 -1
- package/dist/ui/components/AgentManager.d.ts +0 -41
- package/dist/ui/components/AgentManager.d.ts.map +0 -1
- package/dist/ui/components/AgentManager.js +0 -343
- package/dist/ui/components/AgentManager.js.map +0 -1
- package/dist/ui/components/ApprovalDialog.d.ts +0 -18
- package/dist/ui/components/ApprovalDialog.d.ts.map +0 -1
- package/dist/ui/components/ApprovalDialog.js +0 -439
- package/dist/ui/components/ApprovalDialog.js.map +0 -1
- package/dist/ui/components/AsciiArt.d.ts +0 -54
- package/dist/ui/components/AsciiArt.d.ts.map +0 -1
- package/dist/ui/components/AsciiArt.js +0 -89
- package/dist/ui/components/AsciiArt.js.map +0 -1
- package/dist/ui/components/ClarificationWizard.d.ts +0 -36
- package/dist/ui/components/ClarificationWizard.d.ts.map +0 -1
- package/dist/ui/components/ClarificationWizard.js +0 -407
- package/dist/ui/components/ClarificationWizard.js.map +0 -1
- package/dist/ui/components/CompassSpinner.d.ts +0 -15
- package/dist/ui/components/CompassSpinner.d.ts.map +0 -1
- package/dist/ui/components/CompassSpinner.js +0 -50
- package/dist/ui/components/CompassSpinner.js.map +0 -1
- package/dist/ui/components/ConfirmationSelector.d.ts +0 -45
- package/dist/ui/components/ConfirmationSelector.d.ts.map +0 -1
- package/dist/ui/components/ConfirmationSelector.js +0 -106
- package/dist/ui/components/ConfirmationSelector.js.map +0 -1
- package/dist/ui/components/ContextUsage.d.ts +0 -76
- package/dist/ui/components/ContextUsage.d.ts.map +0 -1
- package/dist/ui/components/ContextUsage.js +0 -188
- package/dist/ui/components/ContextUsage.js.map +0 -1
- package/dist/ui/components/DiffPreview.d.ts +0 -13
- package/dist/ui/components/DiffPreview.d.ts.map +0 -1
- package/dist/ui/components/DiffPreview.js +0 -30
- package/dist/ui/components/DiffPreview.js.map +0 -1
- package/dist/ui/components/ExecutionModeSelector.d.ts +0 -45
- package/dist/ui/components/ExecutionModeSelector.d.ts.map +0 -1
- package/dist/ui/components/ExecutionModeSelector.js +0 -120
- package/dist/ui/components/ExecutionModeSelector.js.map +0 -1
- package/dist/ui/components/FileTree.d.ts +0 -47
- package/dist/ui/components/FileTree.d.ts.map +0 -1
- package/dist/ui/components/FileTree.js +0 -258
- package/dist/ui/components/FileTree.js.map +0 -1
- package/dist/ui/components/HelpMenu.d.ts +0 -49
- package/dist/ui/components/HelpMenu.d.ts.map +0 -1
- package/dist/ui/components/HelpMenu.js +0 -91
- package/dist/ui/components/HelpMenu.js.map +0 -1
- package/dist/ui/components/InterleavedStream.d.ts +0 -42
- package/dist/ui/components/InterleavedStream.d.ts.map +0 -1
- package/dist/ui/components/InterleavedStream.js +0 -1500
- package/dist/ui/components/InterleavedStream.js.map +0 -1
- package/dist/ui/components/ModelSelector.d.ts +0 -81
- package/dist/ui/components/ModelSelector.d.ts.map +0 -1
- package/dist/ui/components/ModelSelector.js +0 -305
- package/dist/ui/components/ModelSelector.js.map +0 -1
- package/dist/ui/components/PlanApprovalDialog.d.ts +0 -21
- package/dist/ui/components/PlanApprovalDialog.d.ts.map +0 -1
- package/dist/ui/components/PlanApprovalDialog.js +0 -189
- package/dist/ui/components/PlanApprovalDialog.js.map +0 -1
- package/dist/ui/components/PlanExecutionTracker.d.ts +0 -53
- package/dist/ui/components/PlanExecutionTracker.d.ts.map +0 -1
- package/dist/ui/components/PlanExecutionTracker.js +0 -113
- package/dist/ui/components/PlanExecutionTracker.js.map +0 -1
- package/dist/ui/components/ProgressIndicator.d.ts +0 -151
- package/dist/ui/components/ProgressIndicator.d.ts.map +0 -1
- package/dist/ui/components/ProgressIndicator.js +0 -171
- package/dist/ui/components/ProgressIndicator.js.map +0 -1
- package/dist/ui/components/Prompt.d.ts +0 -47
- package/dist/ui/components/Prompt.d.ts.map +0 -1
- package/dist/ui/components/Prompt.js +0 -632
- package/dist/ui/components/Prompt.js.map +0 -1
- package/dist/ui/components/RatingPanel.d.ts +0 -45
- package/dist/ui/components/RatingPanel.d.ts.map +0 -1
- package/dist/ui/components/RatingPanel.js +0 -119
- package/dist/ui/components/RatingPanel.js.map +0 -1
- package/dist/ui/components/StatusLine.d.ts +0 -43
- package/dist/ui/components/StatusLine.d.ts.map +0 -1
- package/dist/ui/components/StatusLine.js +0 -44
- package/dist/ui/components/StatusLine.js.map +0 -1
- package/dist/ui/components/StreamingResponse.d.ts +0 -45
- package/dist/ui/components/StreamingResponse.d.ts.map +0 -1
- package/dist/ui/components/StreamingResponse.js +0 -56
- package/dist/ui/components/StreamingResponse.js.map +0 -1
- package/dist/ui/components/TokenUsage.d.ts +0 -89
- package/dist/ui/components/TokenUsage.d.ts.map +0 -1
- package/dist/ui/components/TokenUsage.js +0 -99
- package/dist/ui/components/TokenUsage.js.map +0 -1
- package/dist/ui/components/ToolSummary.d.ts +0 -77
- package/dist/ui/components/ToolSummary.d.ts.map +0 -1
- package/dist/ui/components/ToolSummary.js +0 -162
- package/dist/ui/components/ToolSummary.js.map +0 -1
- package/dist/ui/components/UpdateNotification.d.ts +0 -65
- package/dist/ui/components/UpdateNotification.d.ts.map +0 -1
- package/dist/ui/components/UpdateNotification.js +0 -166
- package/dist/ui/components/UpdateNotification.js.map +0 -1
- package/dist/ui/diff-renderer.d.ts +0 -18
- package/dist/ui/diff-renderer.d.ts.map +0 -1
- package/dist/ui/diff-renderer.js +0 -206
- package/dist/ui/diff-renderer.js.map +0 -1
- package/dist/ui/themes/markdown-theme.d.ts +0 -48
- package/dist/ui/themes/markdown-theme.d.ts.map +0 -1
- package/dist/ui/themes/markdown-theme.js +0 -79
- package/dist/ui/themes/markdown-theme.js.map +0 -1
- package/dist/ui/themes/ui-theme.d.ts +0 -301
- package/dist/ui/themes/ui-theme.d.ts.map +0 -1
- package/dist/ui/themes/ui-theme.js +0 -204
- package/dist/ui/themes/ui-theme.js.map +0 -1
- package/dist/utils/attachment-handler.d.ts +0 -129
- package/dist/utils/attachment-handler.d.ts.map +0 -1
- package/dist/utils/attachment-handler.js +0 -280
- package/dist/utils/attachment-handler.js.map +0 -1
- package/dist/utils/backup-cleanup.d.ts +0 -28
- package/dist/utils/backup-cleanup.d.ts.map +0 -1
- package/dist/utils/backup-cleanup.js +0 -99
- package/dist/utils/backup-cleanup.js.map +0 -1
- package/dist/utils/clipboard-handler.d.ts +0 -82
- package/dist/utils/clipboard-handler.d.ts.map +0 -1
- package/dist/utils/clipboard-handler.js +0 -311
- package/dist/utils/clipboard-handler.js.map +0 -1
- package/dist/utils/cloud-detection.d.ts +0 -14
- package/dist/utils/cloud-detection.d.ts.map +0 -1
- package/dist/utils/cloud-detection.js +0 -92
- package/dist/utils/cloud-detection.js.map +0 -1
- package/dist/utils/command-parser.d.ts +0 -56
- package/dist/utils/command-parser.d.ts.map +0 -1
- package/dist/utils/command-parser.js +0 -206
- package/dist/utils/command-parser.js.map +0 -1
- package/dist/utils/console-capture.d.ts +0 -30
- package/dist/utils/console-capture.d.ts.map +0 -1
- package/dist/utils/console-capture.js +0 -88
- package/dist/utils/console-capture.js.map +0 -1
- package/dist/utils/cron-parser.d.ts +0 -52
- package/dist/utils/cron-parser.d.ts.map +0 -1
- package/dist/utils/cron-parser.js +0 -455
- package/dist/utils/cron-parser.js.map +0 -1
- package/dist/utils/crypto.d.ts +0 -351
- package/dist/utils/crypto.d.ts.map +0 -1
- package/dist/utils/crypto.js +0 -615
- package/dist/utils/crypto.js.map +0 -1
- package/dist/utils/diff.d.ts +0 -311
- package/dist/utils/diff.d.ts.map +0 -1
- package/dist/utils/diff.js +0 -566
- package/dist/utils/diff.js.map +0 -1
- package/dist/utils/editor.d.ts +0 -12
- package/dist/utils/editor.d.ts.map +0 -1
- package/dist/utils/editor.js +0 -30
- package/dist/utils/editor.js.map +0 -1
- package/dist/utils/file-system.d.ts +0 -512
- package/dist/utils/file-system.d.ts.map +0 -1
- package/dist/utils/file-system.js +0 -798
- package/dist/utils/file-system.js.map +0 -1
- package/dist/utils/format.d.ts +0 -318
- package/dist/utils/format.d.ts.map +0 -1
- package/dist/utils/format.js +0 -587
- package/dist/utils/format.js.map +0 -1
- package/dist/utils/ignore-patterns.d.ts +0 -93
- package/dist/utils/ignore-patterns.d.ts.map +0 -1
- package/dist/utils/ignore-patterns.js +0 -710
- package/dist/utils/ignore-patterns.js.map +0 -1
- package/dist/utils/log-cleanup.d.ts +0 -16
- package/dist/utils/log-cleanup.d.ts.map +0 -1
- package/dist/utils/log-cleanup.js +0 -51
- package/dist/utils/log-cleanup.js.map +0 -1
- package/dist/utils/logger.d.ts +0 -305
- package/dist/utils/logger.d.ts.map +0 -1
- package/dist/utils/logger.js +0 -447
- package/dist/utils/logger.js.map +0 -1
- package/dist/utils/path.d.ts +0 -406
- package/dist/utils/path.d.ts.map +0 -1
- package/dist/utils/path.js +0 -549
- package/dist/utils/path.js.map +0 -1
- package/dist/utils/schedule-file.d.ts +0 -63
- package/dist/utils/schedule-file.d.ts.map +0 -1
- package/dist/utils/schedule-file.js +0 -244
- package/dist/utils/schedule-file.js.map +0 -1
- package/dist/utils/task-id.d.ts +0 -29
- package/dist/utils/task-id.d.ts.map +0 -1
- package/dist/utils/task-id.js +0 -53
- package/dist/utils/task-id.js.map +0 -1
- package/dist/utils/temp-cleanup.d.ts +0 -46
- package/dist/utils/temp-cleanup.d.ts.map +0 -1
- package/dist/utils/temp-cleanup.js +0 -95
- package/dist/utils/temp-cleanup.js.map +0 -1
- package/dist/utils/terminal-output.d.ts +0 -34
- package/dist/utils/terminal-output.d.ts.map +0 -1
- package/dist/utils/terminal-output.js +0 -40
- package/dist/utils/terminal-output.js.map +0 -1
- package/dist/utils/token-counter.d.ts +0 -224
- package/dist/utils/token-counter.d.ts.map +0 -1
- package/dist/utils/token-counter.js +0 -332
- package/dist/utils/token-counter.js.map +0 -1
- package/dist/utils/tool-mapper.d.ts +0 -70
- package/dist/utils/tool-mapper.d.ts.map +0 -1
- package/dist/utils/tool-mapper.js +0 -234
- package/dist/utils/tool-mapper.js.map +0 -1
|
@@ -1,3611 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
/**
|
|
3
|
-
* React/ink component for main interactive session
|
|
4
|
-
* Manages the prompt, streaming responses, slash command handling,
|
|
5
|
-
* approval dialogs, and conversation state.
|
|
6
|
-
*/
|
|
7
|
-
import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
|
8
|
-
import { Box, Text, useInput, Static } from 'ink';
|
|
9
|
-
import chalk from 'chalk';
|
|
10
|
-
import { Prompt } from './components/Prompt.js';
|
|
11
|
-
import { Markdown } from './components/StreamingResponse.js';
|
|
12
|
-
import { InterleavedStream } from './components/InterleavedStream.js';
|
|
13
|
-
import { ApprovalDialog } from './components/ApprovalDialog.js';
|
|
14
|
-
import { PlanApprovalDialog } from './components/PlanApprovalDialog.js';
|
|
15
|
-
import { ClarificationWizard } from './components/ClarificationWizard.js';
|
|
16
|
-
import { HelpMenu } from './components/HelpMenu.js';
|
|
17
|
-
import { TokenUsage } from './components/TokenUsage.js';
|
|
18
|
-
import { AgentManager } from './components/AgentManager.js';
|
|
19
|
-
import { AgentCreationWizard } from './components/AgentCreationWizard.js';
|
|
20
|
-
import { DiffPreview } from './components/DiffPreview.js';
|
|
21
|
-
import { PlanExecutionTracker } from './components/PlanExecutionTracker.js';
|
|
22
|
-
import { ExecutionModeSelector } from './components/ExecutionModeSelector.js';
|
|
23
|
-
import { ConfirmationSelector } from './components/ConfirmationSelector.js';
|
|
24
|
-
import { RatingPanel } from './components/RatingPanel.js';
|
|
25
|
-
import { CompassSpinner } from './components/CompassSpinner.js';
|
|
26
|
-
import { createTextBlock, createToolBlock } from '../types/stream.js';
|
|
27
|
-
import { parseInput } from '../core/command-parser.js';
|
|
28
|
-
import { executeSlashCommand, registerSlashCommand, getSlashCommandRegistry, CommandCategory, } from '../core/slash-command-handler.js';
|
|
29
|
-
import { registerAllSlashCommands } from '../commands/slash/index.js';
|
|
30
|
-
import { getSessionManager } from '../services/session-manager.js';
|
|
31
|
-
import { getAnthropicClient } from '../services/anthropic-client.js';
|
|
32
|
-
import { getApprovalManager } from '../services/approval-manager.js';
|
|
33
|
-
import { getTokenTracker } from '../services/token-tracker.js';
|
|
34
|
-
import { getCompassAuthService } from '../services/compass-auth-service.js';
|
|
35
|
-
import { getEventEmitter } from '../core/event-emitter.js';
|
|
36
|
-
import { getCodebaseIndexer } from '../services/codebase-indexer.js';
|
|
37
|
-
import { getAuditLogger } from '../services/audit-logger.js';
|
|
38
|
-
import { getSystemEventLogger } from '../services/system-event-logger.js';
|
|
39
|
-
import { getContextBuilder } from '../core/context-builder.js';
|
|
40
|
-
import { getToolRegistry } from '../services/tool-registry.js';
|
|
41
|
-
import { registerBuiltInTools } from '../tools/index.js';
|
|
42
|
-
import { getFileService } from '../services/file-service.js';
|
|
43
|
-
import { getBackupManager } from '../services/backup-manager.js';
|
|
44
|
-
import { getStatusLineExecutor } from '../services/statusline-executor.js';
|
|
45
|
-
import { getConfigManager } from '../services/config-manager.js';
|
|
46
|
-
import { getAgentStateService } from '../services/agent-state-service.js';
|
|
47
|
-
import { checkPlanModeFallback } from '../services/plan-mode-fallback.js';
|
|
48
|
-
import { pathExists } from '../utils/file-system.js';
|
|
49
|
-
import { getProjectCompassDir } from '../utils/path.js';
|
|
50
|
-
import { detectCloudSync } from '../utils/cloud-detection.js';
|
|
51
|
-
import path from 'path';
|
|
52
|
-
import { ApprovalMode, } from '../types/approval.js';
|
|
53
|
-
import { logger } from '../utils/logger.js';
|
|
54
|
-
import { getIdeStateService } from '../services/ide-state-service.js';
|
|
55
|
-
import { getPromptPreprocessor } from '../services/prompt-preprocessor.js';
|
|
56
|
-
import { DEFAULT_MAX_TOKENS, DEFAULT_MAX_AGENTIC_ITERATIONS, DEFAULT_MAX_ITERATIONS_PER_TASK, CONTEXT_WINDOW_SAFETY_MARGIN, SLIDING_WINDOW_MAX_MESSAGES, MAX_DYNAMIC_STREAM_BLOCKS } from '../constants/defaults.js';
|
|
57
|
-
import { estimateTokens, estimateSystemPromptTokens } from '../utils/token-counter.js';
|
|
58
|
-
import { buildSystemPrompt as buildSystemPromptFromModule } from '../prompts/index.js';
|
|
59
|
-
import { getIterationScoper } from '../services/iteration-scoper.js';
|
|
60
|
-
import { getPlanPersistence } from '../services/plan-persistence.js';
|
|
61
|
-
import { captureConsoleOutput } from '../utils/console-capture.js';
|
|
62
|
-
import { hash as sha256Hash } from '../utils/crypto.js';
|
|
63
|
-
import { getConversationCompactor } from '../services/conversation-compactor.js';
|
|
64
|
-
import { getRatingStateManager } from '../services/rating-state-manager.js';
|
|
65
|
-
import { getRatingService } from '../services/rating-service.js';
|
|
66
|
-
import { buildSkillDescriptionsForPrompt } from '../services/skill-service.js';
|
|
67
|
-
import { loadAllMemory } from '../services/memory-service.js';
|
|
68
|
-
import { getModelConfig } from '../constants/models.js';
|
|
69
|
-
import { getSystemUtilitiesPromptSection } from '../services/system-utility-detector.js';
|
|
70
|
-
import { hasSupportedClipboardContent, extractAttachmentsFromClipboard, getClipboardContent } from '../utils/clipboard-handler.js';
|
|
71
|
-
import { buildMessageContent } from '../utils/attachment-handler.js';
|
|
72
|
-
/**
|
|
73
|
-
* Estimates token count for a messages array that may contain complex content blocks.
|
|
74
|
-
* Handles both simple string content and arrays of tool_use/tool_result blocks.
|
|
75
|
-
*
|
|
76
|
-
* @param messages - Array of messages with role and content
|
|
77
|
-
* @returns Estimated token count
|
|
78
|
-
*/
|
|
79
|
-
function estimateMessagesArrayTokens(messages) {
|
|
80
|
-
let totalTokens = 0;
|
|
81
|
-
const MESSAGE_OVERHEAD = 4; // Overhead per message for role markers
|
|
82
|
-
for (const message of messages) {
|
|
83
|
-
totalTokens += MESSAGE_OVERHEAD;
|
|
84
|
-
if (typeof message.content === 'string') {
|
|
85
|
-
totalTokens += estimateTokens(message.content);
|
|
86
|
-
}
|
|
87
|
-
else if (Array.isArray(message.content)) {
|
|
88
|
-
// Handle content blocks (tool_use, tool_result, text, etc.)
|
|
89
|
-
for (const block of message.content) {
|
|
90
|
-
if (typeof block === 'string') {
|
|
91
|
-
totalTokens += estimateTokens(block);
|
|
92
|
-
}
|
|
93
|
-
else if (block && typeof block === 'object') {
|
|
94
|
-
// Content block object
|
|
95
|
-
const contentBlock = block;
|
|
96
|
-
if (contentBlock.type === 'text' && typeof contentBlock.text === 'string') {
|
|
97
|
-
totalTokens += estimateTokens(contentBlock.text);
|
|
98
|
-
}
|
|
99
|
-
else if (contentBlock.type === 'tool_use') {
|
|
100
|
-
// Tool use block: name + input JSON
|
|
101
|
-
totalTokens += estimateTokens(contentBlock.name || '');
|
|
102
|
-
totalTokens += estimateTokens(JSON.stringify(contentBlock.input || {}));
|
|
103
|
-
totalTokens += 10; // Overhead for tool_use structure
|
|
104
|
-
}
|
|
105
|
-
else if (contentBlock.type === 'tool_result') {
|
|
106
|
-
// Tool result block: can have string content or nested content
|
|
107
|
-
const resultContent = contentBlock.content;
|
|
108
|
-
if (typeof resultContent === 'string') {
|
|
109
|
-
totalTokens += estimateTokens(resultContent);
|
|
110
|
-
}
|
|
111
|
-
else if (resultContent && typeof resultContent === 'object') {
|
|
112
|
-
totalTokens += estimateTokens(JSON.stringify(resultContent));
|
|
113
|
-
}
|
|
114
|
-
totalTokens += 10; // Overhead for tool_result structure
|
|
115
|
-
}
|
|
116
|
-
else {
|
|
117
|
-
// Unknown block type - estimate from JSON
|
|
118
|
-
totalTokens += estimateTokens(JSON.stringify(contentBlock));
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
return totalTokens;
|
|
125
|
-
}
|
|
126
|
-
/**
|
|
127
|
-
* Formats a duration in milliseconds to a human-readable string.
|
|
128
|
-
*/
|
|
129
|
-
function formatDuration(ms) {
|
|
130
|
-
const totalSeconds = Math.round(ms / 1000);
|
|
131
|
-
if (totalSeconds < 60)
|
|
132
|
-
return `${totalSeconds}s`;
|
|
133
|
-
const hours = Math.floor(totalSeconds / 3600);
|
|
134
|
-
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
135
|
-
const seconds = totalSeconds % 60;
|
|
136
|
-
if (hours > 0)
|
|
137
|
-
return `${hours}h${minutes > 0 ? `${minutes}m` : ''}`;
|
|
138
|
-
return seconds > 0 ? `${minutes}m${seconds}s` : `${minutes}m`;
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* Extracts a short suggested reply when the assistant's response ends with a question.
|
|
142
|
-
* Returns undefined when no actionable question is detected.
|
|
143
|
-
*/
|
|
144
|
-
function deriveQuestionSuggestion(response) {
|
|
145
|
-
if (!response)
|
|
146
|
-
return undefined;
|
|
147
|
-
// Get the last meaningful line (skip trailing whitespace / blank lines)
|
|
148
|
-
const lines = response.trimEnd().split('\n');
|
|
149
|
-
const lastLine = lines[lines.length - 1]?.trim();
|
|
150
|
-
if (!lastLine || !lastLine.endsWith('?'))
|
|
151
|
-
return undefined;
|
|
152
|
-
const lower = lastLine.toLowerCase();
|
|
153
|
-
// Yes/no style questions — suggest "yes"
|
|
154
|
-
const yesNoPatterns = [
|
|
155
|
-
/^(shall|should|do you want|would you like|want me to|can i|may i|ready to)\b/i,
|
|
156
|
-
/\b(proceed|go ahead|continue|start|do that|make sense)\s*\?$/i,
|
|
157
|
-
];
|
|
158
|
-
for (const pat of yesNoPatterns) {
|
|
159
|
-
if (pat.test(lower))
|
|
160
|
-
return 'yes';
|
|
161
|
-
}
|
|
162
|
-
// "Which" / "what option" questions with numbered or lettered choices above
|
|
163
|
-
const hasChoices = lines.slice(-6).some(l => /^\s*(\d+[\.\):]|[a-c][\.\):]|-\s)/.test(l));
|
|
164
|
-
if (hasChoices && /^(which|what)\b/i.test(lower))
|
|
165
|
-
return '1';
|
|
166
|
-
// Generic question — just offer "yes" as a safe default for action-oriented questions
|
|
167
|
-
if (/\b(ok|okay|alright|right|correct)\s*\?$/i.test(lower))
|
|
168
|
-
return 'yes';
|
|
169
|
-
return undefined;
|
|
170
|
-
}
|
|
171
|
-
/**
|
|
172
|
-
* InteractiveSession component manages the main interactive CLI experience.
|
|
173
|
-
* It handles:
|
|
174
|
-
* - User input via the Prompt component
|
|
175
|
-
* - Streaming responses from Claude
|
|
176
|
-
* - Slash command execution
|
|
177
|
-
* - Approval dialogs for file modifications
|
|
178
|
-
* - Token usage tracking and display
|
|
179
|
-
* - Conversation history management
|
|
180
|
-
*
|
|
181
|
-
* @example
|
|
182
|
-
* ```tsx
|
|
183
|
-
* <InteractiveSession
|
|
184
|
-
* onExit={() => process.exit(0)}
|
|
185
|
-
* projectRoot={process.cwd()}
|
|
186
|
-
* />
|
|
187
|
-
* ```
|
|
188
|
-
*/
|
|
189
|
-
/**
|
|
190
|
-
* Renders a single conversation entry (user, assistant, or system message).
|
|
191
|
-
* Extracted as a standalone component for use with both <Static> and dynamic rendering.
|
|
192
|
-
*/
|
|
193
|
-
const ConversationEntryComponent = ({ entry, showToolBlocks, collapseUserPrompts, expandMCPResults, formatTime }) => {
|
|
194
|
-
return (_jsxs(Box, { marginBottom: 1, flexDirection: "column", children: [entry.role !== 'system' && (_jsxs(Box, { marginBottom: 1, children: [entry.role === 'user' ? (_jsx(Text, { color: "cyan", bold: true, children: "\u276F You" })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: "#a855f7", bold: true, children: "\u2B21" }), _jsx(Text, { color: "#c4b5fd", bold: true, children: " Nova" })] })), _jsxs(Text, { dimColor: true, children: [" ", formatTime(entry.timestamp)] })] })), _jsx(Box, { paddingLeft: entry.role === 'system' ? 0 : 2, children: entry.role === 'assistant' ? (showToolBlocks && entry.blocks && entry.blocks.length > 0 ? (_jsx(InterleavedStream, { blocks: entry.blocks, isStreaming: false, showThinkingIndicator: false, expandMCPResults: expandMCPResults })) : (_jsx(Markdown, { children: entry.content }))) : entry.role === 'user' ? ((() => {
|
|
195
|
-
const lines = entry.content.split('\n');
|
|
196
|
-
const maxCollapsedLines = 3;
|
|
197
|
-
const maxCollapsedChars = 300;
|
|
198
|
-
const hasMultipleLines = lines.length > maxCollapsedLines;
|
|
199
|
-
const isVeryLong = entry.content.length > maxCollapsedChars;
|
|
200
|
-
const shouldCollapse = collapseUserPrompts && (hasMultipleLines || isVeryLong);
|
|
201
|
-
let displayContent;
|
|
202
|
-
let truncationHint = null;
|
|
203
|
-
if (!shouldCollapse) {
|
|
204
|
-
displayContent = entry.content;
|
|
205
|
-
}
|
|
206
|
-
else if (hasMultipleLines) {
|
|
207
|
-
displayContent = lines.slice(0, maxCollapsedLines).join('\n');
|
|
208
|
-
const hiddenLines = lines.length - maxCollapsedLines;
|
|
209
|
-
truncationHint = `... +${hiddenLines} lines`;
|
|
210
|
-
}
|
|
211
|
-
else {
|
|
212
|
-
displayContent = entry.content.slice(0, maxCollapsedChars);
|
|
213
|
-
const hiddenChars = entry.content.length - maxCollapsedChars;
|
|
214
|
-
truncationHint = `... +${hiddenChars} chars`;
|
|
215
|
-
}
|
|
216
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { wrap: "wrap", dimColor: true, children: displayContent }), truncationHint && (_jsxs(Text, { dimColor: true, italic: true, children: [" ", truncationHint] }))] }));
|
|
217
|
-
})()) : (_jsx(Text, { wrap: "wrap", children: entry.content })) }), entry.role === 'assistant' && entry.durationMs != null && entry.durationMs >= 60000 && (_jsx(Box, { paddingLeft: 2, marginTop: 0, children: _jsxs(Text, { dimColor: true, children: ["\uD83D\uDD50 ", formatDuration(entry.durationMs)] }) }))] }));
|
|
218
|
-
};
|
|
219
|
-
/**
|
|
220
|
-
* Self-contained "Working..." indicator with pulsing dot.
|
|
221
|
-
* Uses direct stdout writes for the dot animation to avoid React/Ink re-renders entirely.
|
|
222
|
-
* The offset (lines from cursor to dot) is passed via a ref so it can be updated
|
|
223
|
-
* without triggering re-renders.
|
|
224
|
-
* Memoized so parent re-renders don't affect it either.
|
|
225
|
-
*/
|
|
226
|
-
const WorkingIndicator = React.memo(({ linesBelowRef }) => {
|
|
227
|
-
const dotRef = useRef(false);
|
|
228
|
-
useEffect(() => {
|
|
229
|
-
const timer = setInterval(() => {
|
|
230
|
-
dotRef.current = !dotRef.current;
|
|
231
|
-
const dot = dotRef.current ? '●' : '○';
|
|
232
|
-
const n = linesBelowRef.current;
|
|
233
|
-
const cyan = '\x1b[36m';
|
|
234
|
-
const reset = '\x1b[0m';
|
|
235
|
-
// Save cursor → move up N lines to dot position → write dot → restore cursor
|
|
236
|
-
process.stdout.write(`\x1b7\x1b[${n}F\r ${cyan}${dot}${reset}\x1b8`);
|
|
237
|
-
}, 700);
|
|
238
|
-
return () => clearInterval(timer);
|
|
239
|
-
}, [linesBelowRef]);
|
|
240
|
-
return (_jsx(Box, { paddingX: 1 }));
|
|
241
|
-
});
|
|
242
|
-
export const InteractiveSession = ({ onExit, projectRoot = process.cwd(), showWelcome = true, smartMode = false, initialPrompt, continueSession = false, initialAttachments,
|
|
243
|
-
// planStatus and orgName are rendered in the banner by App before session mounts
|
|
244
|
-
planStatus: _planStatus, orgName: _orgName, }) => {
|
|
245
|
-
// Conversation state
|
|
246
|
-
const [conversation, setConversation] = useState([]);
|
|
247
|
-
const [isProcessing, setIsProcessing] = useState(false);
|
|
248
|
-
const [queuedMessages, setQueuedMessages] = useState([]);
|
|
249
|
-
const queuedMessagesRef = useRef([]);
|
|
250
|
-
// Lines between the working indicator dot and the terminal cursor (bottom of Ink output).
|
|
251
|
-
// Layout: WorkingIndicator | queued msgs (N) | Prompt top hr | input | bottom hr | status line
|
|
252
|
-
// = 4 fixed lines + N queued messages
|
|
253
|
-
const workingDotOffsetRef = useRef(3);
|
|
254
|
-
const [sessionPhase, setSessionPhase] = useState('idle');
|
|
255
|
-
// NOTE: streamingContent state removed - was causing flicker by triggering renders on every chunk
|
|
256
|
-
// The InterleavedStream component now handles all streaming display via streamBlocksRef
|
|
257
|
-
const [error, setError] = useState(null);
|
|
258
|
-
const [showHistory, setShowHistory] = useState(false);
|
|
259
|
-
const [, setShouldShowWelcome] = useState(showWelcome);
|
|
260
|
-
const [isInitialized, setIsInitialized] = useState(false);
|
|
261
|
-
const initialPromptSubmittedRef = useRef(false);
|
|
262
|
-
// Compass spinner state for visual feedback during operations
|
|
263
|
-
const [compassSpinner, setCompassSpinner] = useState(null);
|
|
264
|
-
// Interleaved stream blocks for Claude Code style display
|
|
265
|
-
// Using ref + throttled render counter pattern for proper Ink re-rendering during rapid updates
|
|
266
|
-
const streamBlocksRef = useRef([]);
|
|
267
|
-
// Stable empty array for InterleavedStream when idle — same reference prevents React.memo re-renders
|
|
268
|
-
const emptyBlocksRef = useRef([]);
|
|
269
|
-
const [, forceRender] = useState(0);
|
|
270
|
-
const renderScheduledRef = useRef(false);
|
|
271
|
-
// Refs to track modal states for use in scheduleRender (avoids stale closures)
|
|
272
|
-
const modalActiveRef = useRef(false);
|
|
273
|
-
// Throttled render - batches multiple streaming updates into a single re-render
|
|
274
|
-
// This prevents flickering by coalescing rapid chunk updates (~10fps for CLI)
|
|
275
|
-
// Using 100ms provides smooth CLI updates without visible flicker
|
|
276
|
-
const scheduleRender = useCallback(() => {
|
|
277
|
-
// Skip scheduling when approval modals are active to prevent flicker
|
|
278
|
-
if (modalActiveRef.current) {
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
if (!renderScheduledRef.current) {
|
|
282
|
-
renderScheduledRef.current = true;
|
|
283
|
-
setTimeout(() => {
|
|
284
|
-
renderScheduledRef.current = false;
|
|
285
|
-
// Double-check modal state before forcing render
|
|
286
|
-
if (!modalActiveRef.current) {
|
|
287
|
-
// Create a new array reference so React.memo on InterleavedStream
|
|
288
|
-
// detects the change. In-place mutations (appendTextChunk, updateToolBlockById)
|
|
289
|
-
// are batched between renders — this single shallow copy per frame (~10fps)
|
|
290
|
-
// is cheaper than full JSON.parse and markdown parsing on every chunk.
|
|
291
|
-
streamBlocksRef.current = [...streamBlocksRef.current];
|
|
292
|
-
// Flush batched plan ref updates into React state in the same render frame.
|
|
293
|
-
// This avoids separate re-renders for plan events vs streaming events.
|
|
294
|
-
setActivePlan(activePlanRef.current);
|
|
295
|
-
forceRender(n => n + 1);
|
|
296
|
-
}
|
|
297
|
-
}, 100); // ~10fps - optimal for CLI, eliminates flicker while staying responsive
|
|
298
|
-
}
|
|
299
|
-
}, []);
|
|
300
|
-
// Lightweight plan-only render: updates plan state without copying stream blocks.
|
|
301
|
-
// Used by plan event handlers to avoid triggering InterleavedStream re-renders
|
|
302
|
-
// (React.memo on InterleavedStream checks array reference equality).
|
|
303
|
-
const planRenderScheduledRef = useRef(false);
|
|
304
|
-
const schedulePlanRender = useCallback(() => {
|
|
305
|
-
if (modalActiveRef.current)
|
|
306
|
-
return;
|
|
307
|
-
if (!planRenderScheduledRef.current) {
|
|
308
|
-
planRenderScheduledRef.current = true;
|
|
309
|
-
setTimeout(() => {
|
|
310
|
-
planRenderScheduledRef.current = false;
|
|
311
|
-
if (!modalActiveRef.current) {
|
|
312
|
-
setActivePlan(activePlanRef.current);
|
|
313
|
-
}
|
|
314
|
-
}, 100);
|
|
315
|
-
}
|
|
316
|
-
}, []);
|
|
317
|
-
// Helper to update stream blocks and trigger throttled re-render
|
|
318
|
-
const updateStreamBlocks = useCallback((updater) => {
|
|
319
|
-
streamBlocksRef.current = updater(streamBlocksRef.current);
|
|
320
|
-
scheduleRender();
|
|
321
|
-
}, [scheduleRender]);
|
|
322
|
-
// Optimized helper: append text to the last text block or create a new one.
|
|
323
|
-
// Mutates the ref array in-place (replacing only the changed element) to avoid
|
|
324
|
-
// creating a full new array on every streamed token (~100+ times per response).
|
|
325
|
-
const appendTextChunk = useCallback((chunk) => {
|
|
326
|
-
const blocks = streamBlocksRef.current;
|
|
327
|
-
const lastBlock = blocks[blocks.length - 1];
|
|
328
|
-
if (lastBlock && lastBlock.type === 'text') {
|
|
329
|
-
// Replace last element in-place — no spread/slice needed
|
|
330
|
-
blocks[blocks.length - 1] = { ...lastBlock, content: lastBlock.content + chunk };
|
|
331
|
-
}
|
|
332
|
-
else {
|
|
333
|
-
blocks.push(createTextBlock(chunk));
|
|
334
|
-
}
|
|
335
|
-
scheduleRender();
|
|
336
|
-
}, [scheduleRender]);
|
|
337
|
-
// Optimized helper: update a single tool block by ID without mapping the entire array.
|
|
338
|
-
// Finds the block by ID and replaces only that element in-place.
|
|
339
|
-
const updateToolBlockById = useCallback((toolId, updates, statusFilter) => {
|
|
340
|
-
const blocks = streamBlocksRef.current;
|
|
341
|
-
for (let i = blocks.length - 1; i >= 0; i--) {
|
|
342
|
-
const block = blocks[i];
|
|
343
|
-
if (block.type === 'tool' && block.id === toolId && (!statusFilter || block.status === statusFilter)) {
|
|
344
|
-
blocks[i] = { ...block, ...updates };
|
|
345
|
-
break;
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
scheduleRender();
|
|
349
|
-
}, [scheduleRender]);
|
|
350
|
-
// Helper to reset stream blocks (immediate render for clean state)
|
|
351
|
-
const resetStreamBlocks = useCallback(() => {
|
|
352
|
-
streamBlocksRef.current = [];
|
|
353
|
-
forceRender(n => n + 1);
|
|
354
|
-
}, []);
|
|
355
|
-
// Track partial JSON input per tool during generation (for incremental param display)
|
|
356
|
-
const toolPartialJsonRef = useRef(new Map());
|
|
357
|
-
// Track last rendered param count per tool to avoid unnecessary updates
|
|
358
|
-
const toolLastParamCountRef = useRef(new Map());
|
|
359
|
-
// Track last parsed length per tool to debounce tryParsePartialJson calls
|
|
360
|
-
const toolLastParsedLenRef = useRef(new Map());
|
|
361
|
-
// Abort flag for breaking the agentic loop (Esc to cancel during processing)
|
|
362
|
-
const abortLoopRef = useRef(false);
|
|
363
|
-
// AbortController for cancelling API requests immediately when user presses Esc
|
|
364
|
-
const abortControllerRef = useRef(null);
|
|
365
|
-
// Per-tool AbortController for skipping individual execute_command calls (Ctrl+X)
|
|
366
|
-
const toolAbortRef = useRef(null);
|
|
367
|
-
const currentToolNameRef = useRef(null);
|
|
368
|
-
// Checkpoint for conversation rollback on cancel
|
|
369
|
-
// Stores the conversation state before current request, allowing clean rollback if user cancels
|
|
370
|
-
const conversationCheckpointRef = useRef([]);
|
|
371
|
-
/**
|
|
372
|
-
* Attempts to parse partial JSON and extract any complete key-value pairs.
|
|
373
|
-
* This allows showing params like file_path as they stream in during tool generation.
|
|
374
|
-
* Also detects "likely complete" file paths by extension even without closing quote.
|
|
375
|
-
*/
|
|
376
|
-
const tryParsePartialJson = useCallback((partialJson) => {
|
|
377
|
-
const result = {};
|
|
378
|
-
// Try full parse first (handles complete JSON)
|
|
379
|
-
try {
|
|
380
|
-
const parsed = JSON.parse(partialJson);
|
|
381
|
-
if (typeof parsed === 'object' && parsed !== null) {
|
|
382
|
-
return parsed;
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
catch {
|
|
386
|
-
// Not valid JSON yet, try to extract complete key-value pairs
|
|
387
|
-
}
|
|
388
|
-
// Extract complete string values: "key": "value"
|
|
389
|
-
// Match key-value pairs where the value is a complete quoted string
|
|
390
|
-
const stringPattern = /"([^"]+)"\s*:\s*"((?:[^"\\]|\\.)*)"/g;
|
|
391
|
-
let match;
|
|
392
|
-
while ((match = stringPattern.exec(partialJson)) !== null) {
|
|
393
|
-
// Unescape the value
|
|
394
|
-
const value = match[2].replace(/\\(.)/g, '$1');
|
|
395
|
-
result[match[1]] = value;
|
|
396
|
-
}
|
|
397
|
-
// Extract complete number values: "key": 123
|
|
398
|
-
const numberPattern = /"([^"]+)"\s*:\s*(-?\d+(?:\.\d+)?)/g;
|
|
399
|
-
while ((match = numberPattern.exec(partialJson)) !== null) {
|
|
400
|
-
result[match[1]] = parseFloat(match[2]);
|
|
401
|
-
}
|
|
402
|
-
// Extract complete boolean values: "key": true/false
|
|
403
|
-
const boolPattern = /"([^"]+)"\s*:\s*(true|false)/g;
|
|
404
|
-
while ((match = boolPattern.exec(partialJson)) !== null) {
|
|
405
|
-
result[match[1]] = match[2] === 'true';
|
|
406
|
-
}
|
|
407
|
-
// Special handling: detect file paths that appear complete by extension
|
|
408
|
-
// This catches cases like "path": "file.py (without closing quote yet)
|
|
409
|
-
// Common file extensions that indicate a likely complete path
|
|
410
|
-
const fileExtensions = /\.(py|js|ts|tsx|jsx|json|md|txt|yaml|yml|toml|rs|go|java|c|cpp|h|hpp|css|scss|html|xml|sql|sh|bash|rb|php|swift|kt|scala|vue|svelte)$/i;
|
|
411
|
-
// Look for path-like keys with incomplete string values ending in file extension
|
|
412
|
-
const incompletePathPattern = /"(file_path|path|file|filename)"\s*:\s*"([^"]+)/gi;
|
|
413
|
-
while ((match = incompletePathPattern.exec(partialJson)) !== null) {
|
|
414
|
-
const key = match[1];
|
|
415
|
-
const value = match[2];
|
|
416
|
-
// Only use if not already found (complete values take precedence) and looks like a file path
|
|
417
|
-
if (!(key in result) && fileExtensions.test(value)) {
|
|
418
|
-
result[key] = value;
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
return result;
|
|
422
|
-
}, []);
|
|
423
|
-
// NOTE: Tool execution tracking state removed - was causing flicker by triggering renders
|
|
424
|
-
// during tool execution. Audit logging is done directly through the audit logger.
|
|
425
|
-
// Overlay state
|
|
426
|
-
const [activeOverlay, setActiveOverlay] = useState('none');
|
|
427
|
-
const [diffPreview] = useState(null);
|
|
428
|
-
// Approval state
|
|
429
|
-
const [approvalMode, setApprovalMode] = useState(ApprovalMode.MANUAL);
|
|
430
|
-
const [pendingApproval, setPendingApproval] = useState(null);
|
|
431
|
-
// Execution mode state
|
|
432
|
-
const [executionMode, setExecutionMode] = useState('hybrid');
|
|
433
|
-
// Tool blocks display in history (default: true, toggle with Ctrl+T)
|
|
434
|
-
const [showToolBlocks, setShowToolBlocks] = useState(true);
|
|
435
|
-
// Collapse long user prompts in history (default: true, toggle with Ctrl+O)
|
|
436
|
-
const [collapseUserPrompts, setCollapseUserPrompts] = useState(true);
|
|
437
|
-
// Expand MCP tool results (default: false, toggle with Ctrl+O)
|
|
438
|
-
const [expandMCPResults, setExpandMCPResults] = useState(false);
|
|
439
|
-
// Throttle for expand/collapse toggle to prevent yoga-layout WASM crash on rapid toggling
|
|
440
|
-
const lastExpandToggleRef = useRef(0);
|
|
441
|
-
// Note: Slash hint moved to Prompt component to avoid parent re-renders on keystroke
|
|
442
|
-
// Logout confirmation state
|
|
443
|
-
const [logoutDetails, setLogoutDetails] = useState([]);
|
|
444
|
-
// Rating state
|
|
445
|
-
const [showRatingPanel, setShowRatingPanel] = useState(false);
|
|
446
|
-
const [hasCompassApiKey, setHasCompassApiKey] = useState(false);
|
|
447
|
-
const ratingStateManagerRef = useRef(null);
|
|
448
|
-
const ratingUsageIntervalRef = useRef(null);
|
|
449
|
-
const ratingPanelShownRef = useRef(false); // Track if panel is shown to avoid stale closure
|
|
450
|
-
// Plan approval state
|
|
451
|
-
const [pendingPlanApproval, setPendingPlanApproval] = useState(null);
|
|
452
|
-
const planApprovalClearContextRef = useRef(false);
|
|
453
|
-
// Clarification wizard state
|
|
454
|
-
const [pendingClarification, setPendingClarification] = useState(null);
|
|
455
|
-
// Keep modalActiveRef in sync with approval states and session phase (for use in scheduleRender)
|
|
456
|
-
useEffect(() => {
|
|
457
|
-
const inModal = !!(pendingApproval || pendingPlanApproval || pendingClarification);
|
|
458
|
-
const inTransition = sessionPhase === 'classifying';
|
|
459
|
-
modalActiveRef.current = inModal || inTransition;
|
|
460
|
-
}, [pendingApproval, pendingPlanApproval, pendingClarification, sessionPhase]);
|
|
461
|
-
// Suggestion offer state — shown as ghost text when the LLM ends with a question
|
|
462
|
-
const [suggestionOffer, setSuggestionOffer] = useState(undefined);
|
|
463
|
-
// Plan execution state — uses ref + throttled render to avoid per-event re-renders.
|
|
464
|
-
// Plan events (task-progress, task-completed, execution-completed) fire rapidly during
|
|
465
|
-
// the agentic loop. Routing them through a ref batches multiple updates into a single
|
|
466
|
-
// render via scheduleRender(), matching the streaming token pattern.
|
|
467
|
-
const activePlanRef = useRef(null);
|
|
468
|
-
// Snapshot for rendering — updated by scheduleRender alongside streamBlocks
|
|
469
|
-
const [activePlan, setActivePlan] = useState(null);
|
|
470
|
-
const [planTrackerMode, setPlanTrackerMode] = useState('compact');
|
|
471
|
-
// Batched metrics state - single state object to prevent multiple re-renders per API call
|
|
472
|
-
const [metrics, setMetrics] = useState({
|
|
473
|
-
tokenUsage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
|
|
474
|
-
currentContextTokens: 0,
|
|
475
|
-
cost: { inputCost: 0, outputCost: 0, totalCost: 0, currency: 'USD' },
|
|
476
|
-
});
|
|
477
|
-
// Destructure for convenient access (stable references via same render)
|
|
478
|
-
const { tokenUsage, currentContextTokens, cost } = metrics;
|
|
479
|
-
// Model state
|
|
480
|
-
const [currentModel, setCurrentModel] = useState('Claude Sonnet 4.5');
|
|
481
|
-
const [contextWindowSize, setContextWindowSize] = useState(200000);
|
|
482
|
-
// Project context state
|
|
483
|
-
const [projectSummary, setProjectSummary] = useState(null);
|
|
484
|
-
const [isContextLoading, setIsContextLoading] = useState(true);
|
|
485
|
-
const [isContextStale, setIsContextStale] = useState(false);
|
|
486
|
-
const [, setIsCloudPath] = useState(false);
|
|
487
|
-
// Status line state
|
|
488
|
-
const [statusLineText, setStatusLineText] = useState('');
|
|
489
|
-
const [statusLineVisible, setStatusLineVisible] = useState(false);
|
|
490
|
-
// Active agent state (for agent switching via /agents use)
|
|
491
|
-
const [activeAgentName, setActiveAgentName] = useState(null);
|
|
492
|
-
// Slash command output state (to display output from commands like /context, /commit)
|
|
493
|
-
const [slashCommandOutput, setSlashCommandOutput] = useState(null);
|
|
494
|
-
// Streaming command output state (for real-time event-based output)
|
|
495
|
-
const [streamingCommandOutput, setStreamingCommandOutput] = useState([]);
|
|
496
|
-
const streamingOutputRef = useRef([]);
|
|
497
|
-
// Refs for cleanup
|
|
498
|
-
const unsubscribersRef = useRef([]);
|
|
499
|
-
/**
|
|
500
|
-
* Initializes the session, registers commands, and sets up event listeners
|
|
501
|
-
*/
|
|
502
|
-
useEffect(() => {
|
|
503
|
-
async function initializeSession() {
|
|
504
|
-
try {
|
|
505
|
-
// Initialize audit logger
|
|
506
|
-
const auditLogger = getAuditLogger(projectRoot);
|
|
507
|
-
// Initialize session manager
|
|
508
|
-
const sessionManager = getSessionManager();
|
|
509
|
-
const existingSession = await sessionManager.load(projectRoot);
|
|
510
|
-
if (!existingSession) {
|
|
511
|
-
sessionManager.createSession(projectRoot);
|
|
512
|
-
}
|
|
513
|
-
else {
|
|
514
|
-
// Restore state from existing session
|
|
515
|
-
const state = sessionManager.getState();
|
|
516
|
-
if (state) {
|
|
517
|
-
setApprovalMode(state.approvalMode);
|
|
518
|
-
setExecutionMode(state.executionMode || 'hybrid');
|
|
519
|
-
setShowToolBlocks(state.showToolBlocks ?? true);
|
|
520
|
-
// Restore model from session if one was persisted
|
|
521
|
-
if (state.currentModel) {
|
|
522
|
-
try {
|
|
523
|
-
const anthropicClient = await getAnthropicClient();
|
|
524
|
-
anthropicClient.setModel(state.currentModel);
|
|
525
|
-
// Update React state so the UI reflects the restored model
|
|
526
|
-
const restoredConfig = getModelConfig(state.currentModel);
|
|
527
|
-
if (restoredConfig) {
|
|
528
|
-
setCurrentModel(restoredConfig.name);
|
|
529
|
-
setContextWindowSize(restoredConfig.contextWindow);
|
|
530
|
-
}
|
|
531
|
-
logger.debug(`Restored model from session: ${state.currentModel}`);
|
|
532
|
-
}
|
|
533
|
-
catch (modelError) {
|
|
534
|
-
logger.warn('Failed to restore model from session', modelError);
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
// Archive and handle conversation history based on -c flag
|
|
538
|
-
const hasHistory = state.conversationHistory && state.conversationHistory.length > 0;
|
|
539
|
-
const hasOperations = state.operations && state.operations.length > 0;
|
|
540
|
-
if (hasHistory) {
|
|
541
|
-
if (continueSession) {
|
|
542
|
-
// -c flag: Archive, load into UI context, then clear session.json
|
|
543
|
-
const { archive, result } = await sessionManager.archiveAndClearSession();
|
|
544
|
-
if (result.success && result.filename) {
|
|
545
|
-
logger.info(`Archived previous session to: ${result.filename}`);
|
|
546
|
-
}
|
|
547
|
-
// Load the archived messages into the UI for display
|
|
548
|
-
if (archive && archive.messages.length > 0) {
|
|
549
|
-
const restoredConversation = archive.messages.map((msg, idx) => ({
|
|
550
|
-
id: `archived-${idx}-${Date.now()}`,
|
|
551
|
-
role: msg.role,
|
|
552
|
-
content: msg.content,
|
|
553
|
-
timestamp: new Date(msg.timestamp || Date.now()),
|
|
554
|
-
tokenCount: msg.tokenCount,
|
|
555
|
-
}));
|
|
556
|
-
setConversation(restoredConversation);
|
|
557
|
-
setShowHistory(true);
|
|
558
|
-
logger.info(`Loaded ${restoredConversation.length} messages from archived session`);
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
else {
|
|
562
|
-
// No -c flag: Archive and clear, don't load into UI
|
|
563
|
-
const { result } = await sessionManager.archiveAndClearSession();
|
|
564
|
-
if (result.success && result.filename) {
|
|
565
|
-
logger.info(`Archived previous session to: ${result.filename}`);
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
else {
|
|
570
|
-
// No conversation history — clear stale operations if any
|
|
571
|
-
if (hasOperations) {
|
|
572
|
-
sessionManager.clearHistory();
|
|
573
|
-
}
|
|
574
|
-
// -c flag: load latest archive even when session is clean
|
|
575
|
-
if (continueSession) {
|
|
576
|
-
const archivalService = sessionManager.getArchivalService();
|
|
577
|
-
const archive = await archivalService.getLatestArchive();
|
|
578
|
-
if (archive && archive.messages.length > 0) {
|
|
579
|
-
const restoredConversation = archive.messages.map((msg, idx) => ({
|
|
580
|
-
id: `archived-${idx}-${Date.now()}`,
|
|
581
|
-
role: msg.role,
|
|
582
|
-
content: msg.content,
|
|
583
|
-
timestamp: new Date(msg.timestamp || Date.now()),
|
|
584
|
-
tokenCount: msg.tokenCount,
|
|
585
|
-
}));
|
|
586
|
-
setConversation(restoredConversation);
|
|
587
|
-
setShowHistory(true);
|
|
588
|
-
logger.info(`Loaded ${restoredConversation.length} messages from latest archive`);
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
// If projectPath was updated during load or history was cleared, save immediately
|
|
594
|
-
if (sessionManager.isDirty()) {
|
|
595
|
-
await sessionManager.save();
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
// Log session start for audit trail
|
|
599
|
-
const session = sessionManager.getSession();
|
|
600
|
-
if (session) {
|
|
601
|
-
// Initialize system event logger with session ID
|
|
602
|
-
const systemEventLogger = getSystemEventLogger(projectRoot);
|
|
603
|
-
systemEventLogger.setSessionId(session.id);
|
|
604
|
-
await systemEventLogger.logSessionStart(session.id, projectRoot);
|
|
605
|
-
// Initialize audit logger with session ID for LLM traces
|
|
606
|
-
auditLogger.setSessionId(session.id);
|
|
607
|
-
await auditLogger.logSessionStart(session.id, projectRoot);
|
|
608
|
-
}
|
|
609
|
-
// Initialize token tracker
|
|
610
|
-
const tokenTracker = await getTokenTracker();
|
|
611
|
-
tokenTracker.startSession();
|
|
612
|
-
// Get current model from client
|
|
613
|
-
const client = await getAnthropicClient();
|
|
614
|
-
if (client.isInitialized()) {
|
|
615
|
-
const model = client.getCurrentModel();
|
|
616
|
-
setCurrentModel(model.name);
|
|
617
|
-
setContextWindowSize(model.contextWindow);
|
|
618
|
-
}
|
|
619
|
-
// Initialize status line from config
|
|
620
|
-
try {
|
|
621
|
-
const configManager = getConfigManager();
|
|
622
|
-
await configManager.load(projectRoot);
|
|
623
|
-
const statusLineConfig = configManager.getValue('ui.statusLine');
|
|
624
|
-
logger.debug('Status line config lookup', {
|
|
625
|
-
projectRoot,
|
|
626
|
-
statusLineConfig: statusLineConfig || 'not found',
|
|
627
|
-
uiConfig: configManager.getValue('ui')
|
|
628
|
-
});
|
|
629
|
-
const statusLineExecutor = getStatusLineExecutor();
|
|
630
|
-
if (statusLineConfig && statusLineConfig.enabled !== false) {
|
|
631
|
-
// User has configured a status line
|
|
632
|
-
if (statusLineConfig.type === 'default') {
|
|
633
|
-
// Default type: built-in context display
|
|
634
|
-
statusLineExecutor.configure(statusLineConfig);
|
|
635
|
-
setStatusLineVisible(true);
|
|
636
|
-
logger.info('Status line initialized (default type)');
|
|
637
|
-
}
|
|
638
|
-
else if (statusLineConfig.command) {
|
|
639
|
-
// Command type: external script
|
|
640
|
-
statusLineExecutor.configure(statusLineConfig);
|
|
641
|
-
setStatusLineVisible(true);
|
|
642
|
-
logger.info('Status line initialized (command type)', { command: statusLineConfig.command });
|
|
643
|
-
}
|
|
644
|
-
else {
|
|
645
|
-
logger.debug('Status line config missing command for command type');
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
else if (!statusLineConfig) {
|
|
649
|
-
// No config: use default status line automatically
|
|
650
|
-
const defaultConfig = {
|
|
651
|
-
type: 'default',
|
|
652
|
-
enabled: true,
|
|
653
|
-
};
|
|
654
|
-
statusLineExecutor.configure(defaultConfig);
|
|
655
|
-
setStatusLineVisible(true);
|
|
656
|
-
logger.debug('Status line initialized (auto default)');
|
|
657
|
-
}
|
|
658
|
-
else {
|
|
659
|
-
logger.debug('Status line explicitly disabled');
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
catch (err) {
|
|
663
|
-
logger.error('Failed to initialize status line', err);
|
|
664
|
-
}
|
|
665
|
-
// Register slash commands (includes custom commands from .compass/commands/)
|
|
666
|
-
await registerCommands();
|
|
667
|
-
// Initialize SMART mode with session auto-approval if enabled via CLI
|
|
668
|
-
if (smartMode) {
|
|
669
|
-
const approvalManager = getApprovalManager();
|
|
670
|
-
approvalManager.setMode(ApprovalMode.SMART);
|
|
671
|
-
approvalManager.enableSessionAutoApproval();
|
|
672
|
-
setApprovalMode(ApprovalMode.SMART);
|
|
673
|
-
logger.info('SMART mode enabled via CLI - session auto-approval active');
|
|
674
|
-
}
|
|
675
|
-
// Initialize tool registry with built-in tools
|
|
676
|
-
const toolRegistry = getToolRegistry();
|
|
677
|
-
toolRegistry.setProjectRoot(projectRoot);
|
|
678
|
-
// Initialize file service with project root
|
|
679
|
-
const fileService = getFileService();
|
|
680
|
-
fileService.setProjectRoot(projectRoot);
|
|
681
|
-
logger.debug(`File service project root set to: ${projectRoot}`);
|
|
682
|
-
// Initialize backup manager with project root
|
|
683
|
-
const backupManager = await getBackupManager();
|
|
684
|
-
backupManager.setProjectRoot(projectRoot);
|
|
685
|
-
logger.debug(`Backup manager project root set to: ${projectRoot}`);
|
|
686
|
-
registerBuiltInTools();
|
|
687
|
-
logger.debug(`Registered ${toolRegistry.getAllTools().length} tools`);
|
|
688
|
-
// Subscribe to events
|
|
689
|
-
subscribeToEvents();
|
|
690
|
-
// Emit history-cleared if we cleared on startup (ensures UI state is reset)
|
|
691
|
-
if (!continueSession) {
|
|
692
|
-
getEventEmitter().emit('history-cleared', { isStartup: true });
|
|
693
|
-
}
|
|
694
|
-
// Start auto-save
|
|
695
|
-
sessionManager.startAutoSave(60000); // Auto-save every minute
|
|
696
|
-
// Check if file index exists and trigger performant re-index in background
|
|
697
|
-
await checkAndReindexIfNeeded();
|
|
698
|
-
// Load project context summary for system prompt
|
|
699
|
-
// On cloud-synced folders, defer to background to avoid blocking the UI
|
|
700
|
-
const cloudInfo = detectCloudSync(projectRoot);
|
|
701
|
-
if (cloudInfo.isCloud) {
|
|
702
|
-
logger.info(`Cloud-synced folder detected (${cloudInfo.provider}), loading context in background`);
|
|
703
|
-
setIsCloudPath(true);
|
|
704
|
-
setIsContextLoading(false);
|
|
705
|
-
loadProjectContext().catch(err => logger.debug('Background context load failed', err));
|
|
706
|
-
}
|
|
707
|
-
else {
|
|
708
|
-
await loadProjectContext();
|
|
709
|
-
}
|
|
710
|
-
// Subscribe to file-modified events to mark context as stale
|
|
711
|
-
const eventEmitter = getEventEmitter();
|
|
712
|
-
const unsubscribeFileModified = eventEmitter.on('file-modified', ({ operation }) => {
|
|
713
|
-
// Only mark as stale for create/delete operations (not modifications to existing files)
|
|
714
|
-
if (operation === 'create' || operation === 'delete') {
|
|
715
|
-
logger.debug(`File ${operation} detected - marking project context as stale`);
|
|
716
|
-
setIsContextStale(true);
|
|
717
|
-
}
|
|
718
|
-
});
|
|
719
|
-
unsubscribersRef.current.push(unsubscribeFileModified);
|
|
720
|
-
// Subscribe to manual context refresh events (from /context refresh command)
|
|
721
|
-
const unsubscribeContextRefreshed = eventEmitter.on('project-context-refreshed', ({ summary }) => {
|
|
722
|
-
logger.debug('Project context manually refreshed via /context refresh');
|
|
723
|
-
setProjectSummary(summary);
|
|
724
|
-
setIsContextStale(false);
|
|
725
|
-
});
|
|
726
|
-
unsubscribersRef.current.push(unsubscribeContextRefreshed);
|
|
727
|
-
// Initialize rating system if Compass API key is available
|
|
728
|
-
try {
|
|
729
|
-
const { getCredentialStore } = await import('../services/credential-store.js');
|
|
730
|
-
const credentialStore = await getCredentialStore();
|
|
731
|
-
const compassApiKey = await credentialStore.getCompassApiKey();
|
|
732
|
-
if (compassApiKey) {
|
|
733
|
-
setHasCompassApiKey(true);
|
|
734
|
-
// Initialize rating state manager
|
|
735
|
-
const ratingStateManager = await getRatingStateManager();
|
|
736
|
-
ratingStateManagerRef.current = ratingStateManager;
|
|
737
|
-
// Configure rating service with API key
|
|
738
|
-
const ratingService = getRatingService();
|
|
739
|
-
ratingService.setApiKey(compassApiKey);
|
|
740
|
-
// Start periodic usage tracking (every 10 seconds)
|
|
741
|
-
ratingUsageIntervalRef.current = setInterval(() => {
|
|
742
|
-
if (ratingStateManagerRef.current && !ratingPanelShownRef.current) {
|
|
743
|
-
ratingStateManagerRef.current.updateUsageTime();
|
|
744
|
-
// Check if we should show rating
|
|
745
|
-
if (ratingStateManagerRef.current.shouldShowRating()) {
|
|
746
|
-
ratingPanelShownRef.current = true;
|
|
747
|
-
setShowRatingPanel(true);
|
|
748
|
-
ratingStateManagerRef.current.recordRatingShown();
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
}, 10000);
|
|
752
|
-
logger.debug('[Rating] Rating system initialized');
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
catch (ratingError) {
|
|
756
|
-
logger.debug('[Rating] Failed to initialize rating system', ratingError);
|
|
757
|
-
}
|
|
758
|
-
// Mark initialization as complete
|
|
759
|
-
setIsInitialized(true);
|
|
760
|
-
logger.debug('Session initialization complete');
|
|
761
|
-
}
|
|
762
|
-
catch (err) {
|
|
763
|
-
logger.error('Failed to initialize session', err);
|
|
764
|
-
setError('Failed to initialize session. Please try again.');
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
initializeSession();
|
|
768
|
-
// Cleanup on unmount
|
|
769
|
-
return () => {
|
|
770
|
-
// Clear rating usage interval
|
|
771
|
-
if (ratingUsageIntervalRef.current) {
|
|
772
|
-
clearInterval(ratingUsageIntervalRef.current);
|
|
773
|
-
ratingUsageIntervalRef.current = null;
|
|
774
|
-
}
|
|
775
|
-
// Unsubscribe from all events
|
|
776
|
-
for (const unsubscribe of unsubscribersRef.current) {
|
|
777
|
-
unsubscribe();
|
|
778
|
-
}
|
|
779
|
-
// Disconnect MCP servers
|
|
780
|
-
import('../services/mcp-server-manager.js')
|
|
781
|
-
.then(({ getMCPServerManager }) => {
|
|
782
|
-
const mcpServerManager = getMCPServerManager();
|
|
783
|
-
return mcpServerManager.disconnectAllServers();
|
|
784
|
-
})
|
|
785
|
-
.then(() => {
|
|
786
|
-
logger.debug('MCP servers disconnected on unmount');
|
|
787
|
-
})
|
|
788
|
-
.catch((err) => {
|
|
789
|
-
logger.debug('Failed to disconnect MCP servers on unmount', err);
|
|
790
|
-
});
|
|
791
|
-
// Log session end for audit trail
|
|
792
|
-
const sessionManager = getSessionManager();
|
|
793
|
-
const session = sessionManager.getSession();
|
|
794
|
-
if (session) {
|
|
795
|
-
// Log to system event logger
|
|
796
|
-
const systemEventLogger = getSystemEventLogger(projectRoot);
|
|
797
|
-
systemEventLogger.logSessionEnd().catch((err) => {
|
|
798
|
-
logger.debug('Failed to log system session end', err);
|
|
799
|
-
});
|
|
800
|
-
// Log to audit logger (LLM traces)
|
|
801
|
-
const auditLogger = getAuditLogger(projectRoot);
|
|
802
|
-
auditLogger.logSessionEnd(session.id).catch((err) => {
|
|
803
|
-
logger.debug('Failed to log audit session end', err);
|
|
804
|
-
});
|
|
805
|
-
}
|
|
806
|
-
// Stop auto-save and save final state
|
|
807
|
-
sessionManager.stopAutoSave();
|
|
808
|
-
sessionManager.save().catch((err) => {
|
|
809
|
-
logger.error('Failed to save session on exit', err);
|
|
810
|
-
});
|
|
811
|
-
};
|
|
812
|
-
}, [projectRoot, smartMode, continueSession]);
|
|
813
|
-
/**
|
|
814
|
-
* Auto-submits initial prompt when provided and session is initialized
|
|
815
|
-
*/
|
|
816
|
-
useEffect(() => {
|
|
817
|
-
if (isInitialized && initialPrompt && !initialPromptSubmittedRef.current && !isProcessing) {
|
|
818
|
-
initialPromptSubmittedRef.current = true;
|
|
819
|
-
logger.debug('Auto-submitting initial prompt', { promptLength: initialPrompt.length });
|
|
820
|
-
// Use setTimeout to ensure React has finished rendering
|
|
821
|
-
setTimeout(() => {
|
|
822
|
-
handleSubmit(initialPrompt);
|
|
823
|
-
}, 100);
|
|
824
|
-
}
|
|
825
|
-
}, [isInitialized, initialPrompt, isProcessing]);
|
|
826
|
-
/**
|
|
827
|
-
* Adds initial attachments to session manager when provided
|
|
828
|
-
*/
|
|
829
|
-
useEffect(() => {
|
|
830
|
-
if (isInitialized && initialAttachments && initialAttachments.length > 0) {
|
|
831
|
-
const sessionManager = getSessionManager();
|
|
832
|
-
sessionManager.addPendingAttachments(initialAttachments);
|
|
833
|
-
logger.info(`Added ${initialAttachments.length} initial attachments to session`);
|
|
834
|
-
}
|
|
835
|
-
}, [isInitialized, initialAttachments]);
|
|
836
|
-
/**
|
|
837
|
-
* Updates the status line when conversation, tokens, or model changes.
|
|
838
|
-
* Debounced to prevent excessive updates during rapid API calls.
|
|
839
|
-
*/
|
|
840
|
-
const statusLineTimerRef = useRef(null);
|
|
841
|
-
useEffect(() => {
|
|
842
|
-
if (!statusLineVisible) {
|
|
843
|
-
return;
|
|
844
|
-
}
|
|
845
|
-
// Debounce: wait 2s after last dependency change before updating
|
|
846
|
-
if (statusLineTimerRef.current) {
|
|
847
|
-
clearTimeout(statusLineTimerRef.current);
|
|
848
|
-
}
|
|
849
|
-
statusLineTimerRef.current = setTimeout(async () => {
|
|
850
|
-
const executor = getStatusLineExecutor();
|
|
851
|
-
if (!executor.isEnabled()) {
|
|
852
|
-
return;
|
|
853
|
-
}
|
|
854
|
-
try {
|
|
855
|
-
// Get current session info
|
|
856
|
-
const sessionManager = getSessionManager();
|
|
857
|
-
const session = sessionManager.getSession();
|
|
858
|
-
// Get current model info
|
|
859
|
-
const client = await getAnthropicClient();
|
|
860
|
-
const model = client.isInitialized() ? client.getCurrentModel() : null;
|
|
861
|
-
// Gather rolling window token limit info for the status line
|
|
862
|
-
let tokenLimitUsed;
|
|
863
|
-
let tokenLimitMax;
|
|
864
|
-
let planType;
|
|
865
|
-
try {
|
|
866
|
-
const modelId = model?.id || 'unknown';
|
|
867
|
-
const compassAuth = getCompassAuthService();
|
|
868
|
-
const planStatus = compassAuth.getPlanStatus();
|
|
869
|
-
planType = compassAuth.getPlanType();
|
|
870
|
-
const tracker = await getTokenTracker();
|
|
871
|
-
const { modelLimit } = tracker.resolveLimit(modelId, planStatus, planType);
|
|
872
|
-
if (modelLimit !== Infinity) {
|
|
873
|
-
tokenLimitUsed = await tracker.getUsageForModel(modelId);
|
|
874
|
-
tokenLimitMax = modelLimit;
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
catch {
|
|
878
|
-
// Non-critical — skip token limit display on error
|
|
879
|
-
}
|
|
880
|
-
// Build status line context
|
|
881
|
-
const data = executor.buildStatusData({
|
|
882
|
-
sessionId: session?.id,
|
|
883
|
-
projectRoot,
|
|
884
|
-
cwd: process.cwd(),
|
|
885
|
-
modelId: model?.id || 'unknown',
|
|
886
|
-
modelDisplayName: model?.name || currentModel,
|
|
887
|
-
totalCostUsd: cost.totalCost,
|
|
888
|
-
totalDurationMs: 0, // TODO: Track session duration if needed
|
|
889
|
-
totalApiDurationMs: 0,
|
|
890
|
-
totalLinesAdded: 0,
|
|
891
|
-
totalLinesRemoved: 0,
|
|
892
|
-
totalInputTokens: currentContextTokens, // Use current context, not cumulative
|
|
893
|
-
totalOutputTokens: tokenUsage.outputTokens,
|
|
894
|
-
contextWindowSize: model?.contextWindow || 200000,
|
|
895
|
-
transcriptPath: session?.id ? `${projectRoot}/.compass/sessions/${session.id}.json` : undefined,
|
|
896
|
-
tokenLimitUsed,
|
|
897
|
-
tokenLimitMax,
|
|
898
|
-
planType: planType ?? undefined,
|
|
899
|
-
});
|
|
900
|
-
const result = await executor.execute(data);
|
|
901
|
-
if (result.success || result.text) {
|
|
902
|
-
setStatusLineText(result.text);
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
catch (err) {
|
|
906
|
-
logger.debug('Failed to update status line', err);
|
|
907
|
-
}
|
|
908
|
-
}, 2000);
|
|
909
|
-
return () => {
|
|
910
|
-
if (statusLineTimerRef.current) {
|
|
911
|
-
clearTimeout(statusLineTimerRef.current);
|
|
912
|
-
}
|
|
913
|
-
};
|
|
914
|
-
}, [conversation, metrics, currentModel, statusLineVisible, projectRoot]);
|
|
915
|
-
/**
|
|
916
|
-
* Checks if file index exists and triggers performant re-index in background
|
|
917
|
-
*/
|
|
918
|
-
async function checkAndReindexIfNeeded() {
|
|
919
|
-
try {
|
|
920
|
-
const indexPath = path.join(getProjectCompassDir(projectRoot), 'index', 'file-index.json');
|
|
921
|
-
// Check if index exists
|
|
922
|
-
if (await pathExists(indexPath)) {
|
|
923
|
-
logger.debug('File index found, starting background re-index for changed files...');
|
|
924
|
-
// Trigger re-index in background (non-blocking)
|
|
925
|
-
// This only re-indexes files that have changed since last index
|
|
926
|
-
const indexer = await getCodebaseIndexer(projectRoot);
|
|
927
|
-
indexer.indexProject(projectRoot, {
|
|
928
|
-
force: false, // Only re-index changed files for performance
|
|
929
|
-
onProgress: (current, total) => {
|
|
930
|
-
// Log progress periodically (every 10%)
|
|
931
|
-
if (current % Math.max(1, Math.floor(total / 10)) === 0) {
|
|
932
|
-
logger.debug(`Re-indexing: ${current}/${total} files checked`);
|
|
933
|
-
}
|
|
934
|
-
},
|
|
935
|
-
}).then((result) => {
|
|
936
|
-
if (result.newFiles > 0 || result.updatedFiles > 0 || result.removedFiles > 0) {
|
|
937
|
-
logger.debug(`Index updated: ${result.newFiles} new, ${result.updatedFiles} updated, ${result.removedFiles} removed`);
|
|
938
|
-
}
|
|
939
|
-
else {
|
|
940
|
-
logger.debug('Index is up to date');
|
|
941
|
-
}
|
|
942
|
-
}).catch((err) => {
|
|
943
|
-
logger.debug('Background re-index failed', err);
|
|
944
|
-
});
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
catch (err) {
|
|
948
|
-
logger.debug('Failed to check for file index', err);
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
/**
|
|
952
|
-
* Loads project context summary for system prompt injection
|
|
953
|
-
*/
|
|
954
|
-
async function loadProjectContext() {
|
|
955
|
-
try {
|
|
956
|
-
logger.debug('Loading project context...');
|
|
957
|
-
const contextBuilder = getContextBuilder({ projectRoot });
|
|
958
|
-
const summary = await contextBuilder.getProjectSummary();
|
|
959
|
-
setProjectSummary(summary);
|
|
960
|
-
logger.debug(`Project context loaded: ${summary.name} (${summary.projectType})`);
|
|
961
|
-
logger.info(`Project: ${summary.name} (${summary.projectType}) - ${summary.fileCount} files`);
|
|
962
|
-
}
|
|
963
|
-
catch (err) {
|
|
964
|
-
logger.debug('Failed to load project context', err);
|
|
965
|
-
// Non-fatal - continue without project context
|
|
966
|
-
}
|
|
967
|
-
finally {
|
|
968
|
-
setIsContextLoading(false);
|
|
969
|
-
}
|
|
970
|
-
}
|
|
971
|
-
/**
|
|
972
|
-
* Refreshes project context if marked as stale (e.g., after file creation/deletion).
|
|
973
|
-
* This ensures the system prompt has up-to-date project structure information
|
|
974
|
-
* for Anthropic's prompt caching to work effectively with fresh context.
|
|
975
|
-
*
|
|
976
|
-
* Note: Anthropic's prompt cache uses ephemeral TTL (5 minutes by default).
|
|
977
|
-
* When the system prompt changes, a new cache entry is created and the old
|
|
978
|
-
* one expires after the TTL - Anthropic doesn't keep historical versions.
|
|
979
|
-
*/
|
|
980
|
-
async function refreshProjectContext() {
|
|
981
|
-
if (!isContextStale) {
|
|
982
|
-
logger.debug('Project context is not stale, skipping refresh');
|
|
983
|
-
return;
|
|
984
|
-
}
|
|
985
|
-
try {
|
|
986
|
-
logger.info('Refreshing project context (files changed)...');
|
|
987
|
-
const contextBuilder = getContextBuilder({ projectRoot });
|
|
988
|
-
const summary = await contextBuilder.getProjectSummary();
|
|
989
|
-
setProjectSummary(summary);
|
|
990
|
-
setIsContextStale(false);
|
|
991
|
-
logger.info(`Project context refreshed: ${summary.name} - ${summary.fileCount} files`);
|
|
992
|
-
}
|
|
993
|
-
catch (err) {
|
|
994
|
-
logger.debug('Failed to refresh project context', err);
|
|
995
|
-
// Non-fatal - continue with existing context
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
/**
|
|
999
|
-
* Builds the system prompt with project context, model info, and tool guidance.
|
|
1000
|
-
* Uses the centralized buildSystemPrompt from the prompts module.
|
|
1001
|
-
* If an agent is active, uses the agent's system prompt instead.
|
|
1002
|
-
*/
|
|
1003
|
-
async function buildSystemPrompt(summary, modelName, modelId) {
|
|
1004
|
-
const agentState = getAgentStateService();
|
|
1005
|
-
const activeAgent = agentState.getActiveAgent();
|
|
1006
|
-
// Get skill descriptions for model invocation awareness
|
|
1007
|
-
const skillDescriptions = await buildSkillDescriptionsForPrompt();
|
|
1008
|
-
// Get effective personality mode and unguarded mode from session
|
|
1009
|
-
const sessionMgr = getSessionManager();
|
|
1010
|
-
const personalityMode = sessionMgr.getEffectivePersonalityMode();
|
|
1011
|
-
const unguardedMode = sessionMgr.getUnguardedMode();
|
|
1012
|
-
// Get system utilities section for unguarded mode
|
|
1013
|
-
let systemUtilitiesSection;
|
|
1014
|
-
if (unguardedMode) {
|
|
1015
|
-
try {
|
|
1016
|
-
systemUtilitiesSection = await getSystemUtilitiesPromptSection();
|
|
1017
|
-
if (systemUtilitiesSection) {
|
|
1018
|
-
logger.debug('System utilities section generated for unguarded mode');
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1021
|
-
catch (error) {
|
|
1022
|
-
logger.debug('Failed to generate system utilities section', error);
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
// Load memory content (NOVA.md + auto memory)
|
|
1026
|
-
let memoryContent = '';
|
|
1027
|
-
try {
|
|
1028
|
-
memoryContent = await loadAllMemory(projectRoot);
|
|
1029
|
-
}
|
|
1030
|
-
catch (error) {
|
|
1031
|
-
logger.debug('Failed to load memory content', error);
|
|
1032
|
-
}
|
|
1033
|
-
// If an agent is active, use its system prompt
|
|
1034
|
-
if (activeAgent) {
|
|
1035
|
-
logger.debug(`Using active agent system prompt: ${activeAgent.name}`, {
|
|
1036
|
-
standalone: activeAgent.standalone,
|
|
1037
|
-
model: activeAgent.model,
|
|
1038
|
-
});
|
|
1039
|
-
// Standalone agents use ONLY their prompt, non-standalone agents
|
|
1040
|
-
// get the base prompt with agent prompt appended
|
|
1041
|
-
if (activeAgent.standalone) {
|
|
1042
|
-
// Pure standalone: just the agent's prompt + project context + skills
|
|
1043
|
-
let agentPrompt = activeAgent.systemPrompt;
|
|
1044
|
-
if (skillDescriptions) {
|
|
1045
|
-
agentPrompt = `${agentPrompt}\n${skillDescriptions}`;
|
|
1046
|
-
}
|
|
1047
|
-
if (summary) {
|
|
1048
|
-
agentPrompt = `${agentPrompt}\n\n${summary.formatted}`;
|
|
1049
|
-
}
|
|
1050
|
-
if (memoryContent) {
|
|
1051
|
-
agentPrompt = `${agentPrompt}\n\n${memoryContent}`;
|
|
1052
|
-
}
|
|
1053
|
-
return agentPrompt;
|
|
1054
|
-
}
|
|
1055
|
-
else {
|
|
1056
|
-
// Non-standalone: base prompt + agent's additional instructions
|
|
1057
|
-
const basePrompt = buildSystemPromptFromModule({
|
|
1058
|
-
projectSummary: summary ? { formatted: summary.formatted } : null,
|
|
1059
|
-
workingDirectory: projectRoot,
|
|
1060
|
-
modelName,
|
|
1061
|
-
modelId,
|
|
1062
|
-
skillDescriptions,
|
|
1063
|
-
personalityMode,
|
|
1064
|
-
unguardedMode,
|
|
1065
|
-
systemUtilitiesSection,
|
|
1066
|
-
});
|
|
1067
|
-
// Append agent's specialized instructions
|
|
1068
|
-
let combinedPrompt = `# Active Agent: ${activeAgent.name}
|
|
1069
|
-
|
|
1070
|
-
You are now operating as the "${activeAgent.name}" agent. Follow these specialized instructions:
|
|
1071
|
-
|
|
1072
|
-
${activeAgent.systemPrompt}
|
|
1073
|
-
|
|
1074
|
-
# Fallback Instructions
|
|
1075
|
-
If the above agent-specific instructions are insufficient to fully satisfy the user request, follow the general instructions below.
|
|
1076
|
-
|
|
1077
|
-
${basePrompt}
|
|
1078
|
-
`;
|
|
1079
|
-
if (memoryContent) {
|
|
1080
|
-
combinedPrompt = `${combinedPrompt}\n\n${memoryContent}`;
|
|
1081
|
-
}
|
|
1082
|
-
return combinedPrompt;
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
// Default: use the standard Compass system prompt
|
|
1086
|
-
let defaultPrompt = buildSystemPromptFromModule({
|
|
1087
|
-
projectSummary: summary ? { formatted: summary.formatted } : null,
|
|
1088
|
-
workingDirectory: projectRoot,
|
|
1089
|
-
modelName,
|
|
1090
|
-
modelId,
|
|
1091
|
-
skillDescriptions,
|
|
1092
|
-
personalityMode,
|
|
1093
|
-
unguardedMode,
|
|
1094
|
-
systemUtilitiesSection,
|
|
1095
|
-
});
|
|
1096
|
-
if (memoryContent) {
|
|
1097
|
-
defaultPrompt = `${defaultPrompt}\n\n${memoryContent}`;
|
|
1098
|
-
}
|
|
1099
|
-
return defaultPrompt;
|
|
1100
|
-
}
|
|
1101
|
-
/**
|
|
1102
|
-
* Registers all slash commands with the command handler
|
|
1103
|
-
*/
|
|
1104
|
-
async function registerCommands() {
|
|
1105
|
-
const registry = getSlashCommandRegistry();
|
|
1106
|
-
// Register all slash commands from the central registry (includes custom commands)
|
|
1107
|
-
await registerAllSlashCommands();
|
|
1108
|
-
// Override specific commands that need UI-specific behavior
|
|
1109
|
-
// (Re-registering will replace the centrally-registered versions)
|
|
1110
|
-
// Help command - shows overlay instead of text output
|
|
1111
|
-
registerSlashCommand({
|
|
1112
|
-
name: 'help',
|
|
1113
|
-
aliases: ['h', '?'],
|
|
1114
|
-
description: 'Show help menu',
|
|
1115
|
-
category: CommandCategory.INFO,
|
|
1116
|
-
requiresLLM: false,
|
|
1117
|
-
canRunOffline: true,
|
|
1118
|
-
handler: () => {
|
|
1119
|
-
setActiveOverlay((prev) => (prev === 'help' ? 'none' : 'help'));
|
|
1120
|
-
},
|
|
1121
|
-
});
|
|
1122
|
-
// Tokens command - shows overlay instead of text output
|
|
1123
|
-
registerSlashCommand({
|
|
1124
|
-
name: 'tokens',
|
|
1125
|
-
aliases: ['usage', 'cost'],
|
|
1126
|
-
description: 'Show token usage statistics',
|
|
1127
|
-
category: CommandCategory.INFO,
|
|
1128
|
-
requiresLLM: false,
|
|
1129
|
-
canRunOffline: true,
|
|
1130
|
-
handler: () => {
|
|
1131
|
-
setActiveOverlay((prev) => (prev === 'tokens' ? 'none' : 'tokens'));
|
|
1132
|
-
},
|
|
1133
|
-
});
|
|
1134
|
-
// Agents command - shows overlay for list, delegates subcommands to original handler
|
|
1135
|
-
registerSlashCommand({
|
|
1136
|
-
name: 'agents',
|
|
1137
|
-
aliases: ['agent', 'subagents'],
|
|
1138
|
-
description: 'Manage sub-agents for task delegation',
|
|
1139
|
-
category: CommandCategory.INFO,
|
|
1140
|
-
requiresLLM: false,
|
|
1141
|
-
canRunOffline: true,
|
|
1142
|
-
handler: async (args) => {
|
|
1143
|
-
// Show overlay for no args or list subcommand
|
|
1144
|
-
if (args.length === 0 || args[0].toLowerCase() === 'list' || args[0].toLowerCase() === 'ls' || args[0] === '-l') {
|
|
1145
|
-
setActiveOverlay((prev) => (prev === 'agents' ? 'none' : 'agents'));
|
|
1146
|
-
return;
|
|
1147
|
-
}
|
|
1148
|
-
// For other subcommands, use the original handler from agents.ts
|
|
1149
|
-
const { agentsHandler } = await import('../commands/slash/agents.js');
|
|
1150
|
-
await agentsHandler(args);
|
|
1151
|
-
},
|
|
1152
|
-
});
|
|
1153
|
-
// Clear command - updates React state
|
|
1154
|
-
registerSlashCommand({
|
|
1155
|
-
name: 'clear',
|
|
1156
|
-
aliases: ['cls'],
|
|
1157
|
-
description: 'Clear conversation history',
|
|
1158
|
-
category: CommandCategory.SESSION,
|
|
1159
|
-
requiresLLM: false,
|
|
1160
|
-
canRunOffline: true,
|
|
1161
|
-
handler: () => {
|
|
1162
|
-
// Reset all UI conversation state
|
|
1163
|
-
setConversation([]);
|
|
1164
|
-
setShowHistory(false);
|
|
1165
|
-
setSlashCommandOutput(null);
|
|
1166
|
-
setStreamingCommandOutput([]);
|
|
1167
|
-
streamingOutputRef.current = [];
|
|
1168
|
-
setError(null);
|
|
1169
|
-
setIsProcessing(false);
|
|
1170
|
-
setSessionPhase('idle');
|
|
1171
|
-
setCompassSpinner(null);
|
|
1172
|
-
setActiveOverlay('none');
|
|
1173
|
-
setPendingApproval(null);
|
|
1174
|
-
setPendingPlanApproval(null);
|
|
1175
|
-
setPendingClarification(null);
|
|
1176
|
-
setSuggestionOffer(undefined);
|
|
1177
|
-
activePlanRef.current = null;
|
|
1178
|
-
setActivePlan(null);
|
|
1179
|
-
setPlanTrackerMode('compact');
|
|
1180
|
-
setMetrics((prev) => ({ ...prev, currentContextTokens: 0 }));
|
|
1181
|
-
// Reset stream blocks ref (static ref is cleared by useMemo when conversation empties)
|
|
1182
|
-
streamBlocksRef.current = [];
|
|
1183
|
-
// Clear session history and stop plan monitoring
|
|
1184
|
-
const sessionManager = getSessionManager();
|
|
1185
|
-
sessionManager.clearHistory();
|
|
1186
|
-
const planPersistence = getPlanPersistence(projectRoot);
|
|
1187
|
-
planPersistence.stopFileMonitoring();
|
|
1188
|
-
// Notify listeners and confirm
|
|
1189
|
-
getEventEmitter().emit('history-cleared', {});
|
|
1190
|
-
getEventEmitter().emit('slash-command-output', { command: 'clear', message: 'Conversation cleared.', isError: false });
|
|
1191
|
-
},
|
|
1192
|
-
});
|
|
1193
|
-
// Exit command - calls onExit callback
|
|
1194
|
-
registerSlashCommand({
|
|
1195
|
-
name: 'exit',
|
|
1196
|
-
aliases: ['quit', 'q'],
|
|
1197
|
-
description: 'Exit Compass',
|
|
1198
|
-
category: CommandCategory.SYSTEM,
|
|
1199
|
-
requiresLLM: false,
|
|
1200
|
-
canRunOffline: true,
|
|
1201
|
-
executeOnComplete: true,
|
|
1202
|
-
handler: async () => {
|
|
1203
|
-
const sessionManager = getSessionManager();
|
|
1204
|
-
await sessionManager.save();
|
|
1205
|
-
// Trigger graceful cleanup, then force exit as a safety net
|
|
1206
|
-
void onExit();
|
|
1207
|
-
setTimeout(() => process.exit(0), 500);
|
|
1208
|
-
await new Promise(() => { });
|
|
1209
|
-
},
|
|
1210
|
-
});
|
|
1211
|
-
// Approve command - updates React state
|
|
1212
|
-
registerSlashCommand({
|
|
1213
|
-
name: 'approve',
|
|
1214
|
-
aliases: ['approval', 'mode'],
|
|
1215
|
-
description: 'Toggle or set approval mode',
|
|
1216
|
-
category: CommandCategory.MODEL,
|
|
1217
|
-
requiresLLM: false,
|
|
1218
|
-
canRunOffline: true,
|
|
1219
|
-
handler: async (args) => {
|
|
1220
|
-
const approvalManager = getApprovalManager();
|
|
1221
|
-
const sessionManager = getSessionManager();
|
|
1222
|
-
const eventEmitter = getEventEmitter();
|
|
1223
|
-
const emitOutput = (message, isError = false) => {
|
|
1224
|
-
eventEmitter.emit('slash-command-output', { command: 'approve', message, isError });
|
|
1225
|
-
};
|
|
1226
|
-
if (args.length === 0) {
|
|
1227
|
-
// Toggle mode
|
|
1228
|
-
const newMode = approvalManager.toggleMode();
|
|
1229
|
-
sessionManager.setApprovalMode(newMode);
|
|
1230
|
-
setApprovalMode(newMode);
|
|
1231
|
-
emitOutput(`Approval mode: ${newMode.toUpperCase()}`);
|
|
1232
|
-
}
|
|
1233
|
-
else {
|
|
1234
|
-
const modeArg = args[0].toLowerCase();
|
|
1235
|
-
let newMode;
|
|
1236
|
-
switch (modeArg) {
|
|
1237
|
-
case 'auto':
|
|
1238
|
-
case 'a':
|
|
1239
|
-
newMode = ApprovalMode.AUTO;
|
|
1240
|
-
break;
|
|
1241
|
-
case 'manual':
|
|
1242
|
-
case 'm':
|
|
1243
|
-
newMode = ApprovalMode.MANUAL;
|
|
1244
|
-
break;
|
|
1245
|
-
case 'strict':
|
|
1246
|
-
case 's':
|
|
1247
|
-
newMode = ApprovalMode.STRICT;
|
|
1248
|
-
break;
|
|
1249
|
-
case 'smart':
|
|
1250
|
-
case 'sm':
|
|
1251
|
-
newMode = ApprovalMode.SMART;
|
|
1252
|
-
break;
|
|
1253
|
-
default:
|
|
1254
|
-
emitOutput(`Unknown mode: ${modeArg}. Use: auto, manual, strict, or smart`, true);
|
|
1255
|
-
return;
|
|
1256
|
-
}
|
|
1257
|
-
approvalManager.setMode(newMode);
|
|
1258
|
-
sessionManager.setApprovalMode(newMode);
|
|
1259
|
-
setApprovalMode(newMode);
|
|
1260
|
-
emitOutput(`Approval mode set to: ${newMode.toUpperCase()}`);
|
|
1261
|
-
}
|
|
1262
|
-
},
|
|
1263
|
-
});
|
|
1264
|
-
// Model command - updates React state via events
|
|
1265
|
-
registerSlashCommand({
|
|
1266
|
-
name: 'model',
|
|
1267
|
-
description: 'Switch model',
|
|
1268
|
-
category: CommandCategory.MODEL,
|
|
1269
|
-
requiresLLM: false,
|
|
1270
|
-
canRunOffline: true,
|
|
1271
|
-
handler: async (args) => {
|
|
1272
|
-
const client = await getAnthropicClient();
|
|
1273
|
-
const eventEmitter = getEventEmitter();
|
|
1274
|
-
// Helper to emit output that will be captured by the event listener
|
|
1275
|
-
const emitOutput = (message) => {
|
|
1276
|
-
eventEmitter.emit('slash-command-output', { command: 'model', message, isError: false });
|
|
1277
|
-
};
|
|
1278
|
-
if (args.length === 0) {
|
|
1279
|
-
// Show current model
|
|
1280
|
-
const model = client.getCurrentModel();
|
|
1281
|
-
emitOutput(`Current model: ${model.name}`);
|
|
1282
|
-
emitOutput('Available: sonnet, opus, haiku');
|
|
1283
|
-
return;
|
|
1284
|
-
}
|
|
1285
|
-
const modelArg = args[0].toLowerCase();
|
|
1286
|
-
const success = client.setModel(modelArg);
|
|
1287
|
-
if (success) {
|
|
1288
|
-
const model = client.getCurrentModel();
|
|
1289
|
-
setCurrentModel(model.name);
|
|
1290
|
-
setContextWindowSize(model.contextWindow);
|
|
1291
|
-
const sessionManager = getSessionManager();
|
|
1292
|
-
sessionManager.setModel(model.id);
|
|
1293
|
-
emitOutput(`Switched to: ${model.name}`);
|
|
1294
|
-
}
|
|
1295
|
-
else {
|
|
1296
|
-
eventEmitter.emit('slash-command-output', { command: 'model', message: `Unknown model: ${modelArg}. Use: sonnet, opus, or haiku`, isError: true });
|
|
1297
|
-
}
|
|
1298
|
-
},
|
|
1299
|
-
});
|
|
1300
|
-
// Switch-plan command - toggles execution mode
|
|
1301
|
-
registerSlashCommand({
|
|
1302
|
-
name: 'switch-plan',
|
|
1303
|
-
aliases: ['sp', 'mode'],
|
|
1304
|
-
description: 'Switch execution mode (hybrid/direct/plan)',
|
|
1305
|
-
category: CommandCategory.MODEL,
|
|
1306
|
-
requiresLLM: false,
|
|
1307
|
-
canRunOffline: true,
|
|
1308
|
-
executeOnComplete: true,
|
|
1309
|
-
handler: async (args) => {
|
|
1310
|
-
const sessionManager = getSessionManager();
|
|
1311
|
-
const eventEmitter = getEventEmitter();
|
|
1312
|
-
const emitOutput = (message) => {
|
|
1313
|
-
eventEmitter.emit('slash-command-output', { command: 'switch-plan', message, isError: false });
|
|
1314
|
-
};
|
|
1315
|
-
const modeDescriptions = {
|
|
1316
|
-
hybrid: 'LLM decides when to use planning based on complexity',
|
|
1317
|
-
direct: 'Pure agentic execution, no planning',
|
|
1318
|
-
plan: 'Always forces planning for every task'
|
|
1319
|
-
};
|
|
1320
|
-
const modeAliases = {
|
|
1321
|
-
hybrid: 'h',
|
|
1322
|
-
direct: 'd',
|
|
1323
|
-
plan: 'p'
|
|
1324
|
-
};
|
|
1325
|
-
// No arguments - show interactive mode selector
|
|
1326
|
-
if (args.length === 0) {
|
|
1327
|
-
setActiveOverlay('mode-selector');
|
|
1328
|
-
return;
|
|
1329
|
-
}
|
|
1330
|
-
// Argument provided - switch to specific mode
|
|
1331
|
-
const modeArg = args[0].toLowerCase();
|
|
1332
|
-
let newMode;
|
|
1333
|
-
switch (modeArg) {
|
|
1334
|
-
case 'hybrid':
|
|
1335
|
-
case 'h':
|
|
1336
|
-
newMode = 'hybrid';
|
|
1337
|
-
break;
|
|
1338
|
-
case 'direct':
|
|
1339
|
-
case 'd':
|
|
1340
|
-
newMode = 'direct';
|
|
1341
|
-
break;
|
|
1342
|
-
case 'plan':
|
|
1343
|
-
case 'p':
|
|
1344
|
-
newMode = 'plan';
|
|
1345
|
-
break;
|
|
1346
|
-
default:
|
|
1347
|
-
emitOutput('');
|
|
1348
|
-
emitOutput(chalk.red(`Unknown mode: ${modeArg}`));
|
|
1349
|
-
emitOutput(chalk.gray('Available modes: hybrid, direct, plan'));
|
|
1350
|
-
emitOutput('');
|
|
1351
|
-
return;
|
|
1352
|
-
}
|
|
1353
|
-
setExecutionMode(newMode);
|
|
1354
|
-
sessionManager.setExecutionMode(newMode);
|
|
1355
|
-
const color = newMode === 'hybrid' ? chalk.cyan : newMode === 'direct' ? chalk.yellow : chalk.magenta;
|
|
1356
|
-
emitOutput('');
|
|
1357
|
-
emitOutput(chalk.green('✓') + ` Switched to ${color.bold(newMode.toUpperCase())} ${chalk.gray(`(${modeAliases[newMode]})`)}`);
|
|
1358
|
-
emitOutput(` ${chalk.gray(modeDescriptions[newMode])}`);
|
|
1359
|
-
emitOutput('');
|
|
1360
|
-
},
|
|
1361
|
-
});
|
|
1362
|
-
// Logout command - shows interactive confirmation selector
|
|
1363
|
-
registerSlashCommand({
|
|
1364
|
-
name: 'logout',
|
|
1365
|
-
aliases: ['signout'],
|
|
1366
|
-
description: 'Clear stored API keys and logout from Compass',
|
|
1367
|
-
category: CommandCategory.AUTH,
|
|
1368
|
-
requiresLLM: false,
|
|
1369
|
-
canRunOffline: true,
|
|
1370
|
-
executeOnComplete: true,
|
|
1371
|
-
handler: async (args) => {
|
|
1372
|
-
const forceLogout = args.includes('--force') || args.includes('-f');
|
|
1373
|
-
if (forceLogout) {
|
|
1374
|
-
// Skip confirmation with --force flag
|
|
1375
|
-
await performLogout();
|
|
1376
|
-
return;
|
|
1377
|
-
}
|
|
1378
|
-
// Check what credentials exist
|
|
1379
|
-
const { getCredentialStore } = await import('../services/credential-store.js');
|
|
1380
|
-
const credentialStore = await getCredentialStore();
|
|
1381
|
-
const hasCompassKey = await credentialStore.hasCompassApiKey();
|
|
1382
|
-
const hasAnthropicKey = await credentialStore.hasApiKey();
|
|
1383
|
-
// Check provider-specific keys
|
|
1384
|
-
const providerNames = ['ollama', 'zai', 'minimax'];
|
|
1385
|
-
const storedProviders = [];
|
|
1386
|
-
for (const provider of providerNames) {
|
|
1387
|
-
if (await credentialStore.hasProviderApiKey(provider)) {
|
|
1388
|
-
storedProviders.push(provider);
|
|
1389
|
-
}
|
|
1390
|
-
}
|
|
1391
|
-
if (!hasCompassKey && !hasAnthropicKey && storedProviders.length === 0) {
|
|
1392
|
-
const eventEmitter = getEventEmitter();
|
|
1393
|
-
eventEmitter.emit('slash-command-output', { command: 'logout', message: '', isError: false });
|
|
1394
|
-
eventEmitter.emit('slash-command-output', { command: 'logout', message: chalk.yellow('⚠️ No API keys are currently stored.'), isError: false });
|
|
1395
|
-
eventEmitter.emit('slash-command-output', { command: 'logout', message: chalk.gray(' You are already logged out.'), isError: false });
|
|
1396
|
-
eventEmitter.emit('slash-command-output', { command: 'logout', message: '', isError: false });
|
|
1397
|
-
return;
|
|
1398
|
-
}
|
|
1399
|
-
// Build details list
|
|
1400
|
-
const details = ['The following credentials will be cleared:'];
|
|
1401
|
-
if (hasCompassKey) {
|
|
1402
|
-
details.push(' • Compass API Key');
|
|
1403
|
-
}
|
|
1404
|
-
if (hasAnthropicKey) {
|
|
1405
|
-
details.push(' • Anthropic API Key');
|
|
1406
|
-
}
|
|
1407
|
-
for (const provider of storedProviders) {
|
|
1408
|
-
details.push(` • ${provider.charAt(0).toUpperCase() + provider.slice(1)} API Key`);
|
|
1409
|
-
}
|
|
1410
|
-
setLogoutDetails(details);
|
|
1411
|
-
setActiveOverlay('logout-confirm');
|
|
1412
|
-
},
|
|
1413
|
-
});
|
|
1414
|
-
// Check if save command needs UI-specific handling
|
|
1415
|
-
if (!registry.has('save')) {
|
|
1416
|
-
registerSlashCommand({
|
|
1417
|
-
name: 'save',
|
|
1418
|
-
description: 'Save current session',
|
|
1419
|
-
category: CommandCategory.SESSION,
|
|
1420
|
-
requiresLLM: false,
|
|
1421
|
-
canRunOffline: true,
|
|
1422
|
-
handler: async (args) => {
|
|
1423
|
-
const sessionManager = getSessionManager();
|
|
1424
|
-
const name = args[0] || `session-${Date.now()}`;
|
|
1425
|
-
try {
|
|
1426
|
-
await sessionManager.saveAs(name);
|
|
1427
|
-
logger.output(`Session saved as: ${name}`);
|
|
1428
|
-
}
|
|
1429
|
-
catch (err) {
|
|
1430
|
-
logger.output(`Failed to save session: ${err}`);
|
|
1431
|
-
}
|
|
1432
|
-
},
|
|
1433
|
-
});
|
|
1434
|
-
}
|
|
1435
|
-
if (!registry.has('context')) {
|
|
1436
|
-
registerSlashCommand({
|
|
1437
|
-
name: 'context',
|
|
1438
|
-
description: 'Show context window usage',
|
|
1439
|
-
category: CommandCategory.INFO,
|
|
1440
|
-
requiresLLM: false,
|
|
1441
|
-
canRunOffline: true,
|
|
1442
|
-
handler: async () => {
|
|
1443
|
-
const client = await getAnthropicClient();
|
|
1444
|
-
const model = client.getCurrentModel();
|
|
1445
|
-
const usagePercent = Math.round((tokenUsage.totalTokens / model.contextWindow) * 100);
|
|
1446
|
-
logger.output(`Context Usage:`);
|
|
1447
|
-
logger.output(` Model: ${model.name}`);
|
|
1448
|
-
logger.output(` Window: ${model.contextWindow.toLocaleString()} tokens`);
|
|
1449
|
-
logger.output(` Used: ${tokenUsage.totalTokens.toLocaleString()} tokens (${usagePercent}%)`);
|
|
1450
|
-
logger.output(` Available: ${(model.contextWindow - tokenUsage.totalTokens).toLocaleString()} tokens`);
|
|
1451
|
-
},
|
|
1452
|
-
});
|
|
1453
|
-
}
|
|
1454
|
-
if (!registry.has('reset')) {
|
|
1455
|
-
registerSlashCommand({
|
|
1456
|
-
name: 'reset',
|
|
1457
|
-
description: 'Reset session to clean state',
|
|
1458
|
-
category: CommandCategory.SESSION,
|
|
1459
|
-
requiresLLM: false,
|
|
1460
|
-
canRunOffline: true,
|
|
1461
|
-
handler: async () => {
|
|
1462
|
-
setConversation([]);
|
|
1463
|
-
setMetrics({
|
|
1464
|
-
tokenUsage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
|
|
1465
|
-
currentContextTokens: 0,
|
|
1466
|
-
cost: { inputCost: 0, outputCost: 0, totalCost: 0, currency: 'USD' },
|
|
1467
|
-
});
|
|
1468
|
-
setError(null);
|
|
1469
|
-
setShowHistory(false);
|
|
1470
|
-
// Clear plan execution UI state and persistence
|
|
1471
|
-
activePlanRef.current = null;
|
|
1472
|
-
setActivePlan(null);
|
|
1473
|
-
setPlanTrackerMode('compact');
|
|
1474
|
-
setSessionPhase('idle');
|
|
1475
|
-
setPendingApproval(null);
|
|
1476
|
-
setPendingPlanApproval(null);
|
|
1477
|
-
setPendingClarification(null);
|
|
1478
|
-
setSuggestionOffer(undefined);
|
|
1479
|
-
const planPersistence = getPlanPersistence(projectRoot);
|
|
1480
|
-
planPersistence.stopFileMonitoring();
|
|
1481
|
-
const sessionManager = getSessionManager();
|
|
1482
|
-
sessionManager.createSession(projectRoot);
|
|
1483
|
-
logger.output('Session reset to clean state.');
|
|
1484
|
-
},
|
|
1485
|
-
});
|
|
1486
|
-
}
|
|
1487
|
-
}
|
|
1488
|
-
/**
|
|
1489
|
-
* Subscribes to event emitter events for real-time updates
|
|
1490
|
-
*/
|
|
1491
|
-
function subscribeToEvents() {
|
|
1492
|
-
const eventEmitter = getEventEmitter();
|
|
1493
|
-
// Token updates - single batched state update to prevent multiple re-renders
|
|
1494
|
-
const unsubTokens = eventEmitter.on('token-update', ({ usage, cost: newCost }) => {
|
|
1495
|
-
setMetrics((prev) => ({
|
|
1496
|
-
tokenUsage: {
|
|
1497
|
-
inputTokens: prev.tokenUsage.inputTokens + usage.inputTokens,
|
|
1498
|
-
outputTokens: prev.tokenUsage.outputTokens + usage.outputTokens,
|
|
1499
|
-
totalTokens: prev.tokenUsage.totalTokens + usage.totalTokens,
|
|
1500
|
-
},
|
|
1501
|
-
currentContextTokens: usage.inputTokens,
|
|
1502
|
-
cost: {
|
|
1503
|
-
inputCost: prev.cost.inputCost + newCost.inputCost,
|
|
1504
|
-
outputCost: prev.cost.outputCost + newCost.outputCost,
|
|
1505
|
-
totalCost: prev.cost.totalCost + newCost.totalCost,
|
|
1506
|
-
currency: 'USD',
|
|
1507
|
-
},
|
|
1508
|
-
}));
|
|
1509
|
-
});
|
|
1510
|
-
unsubscribersRef.current.push(unsubTokens);
|
|
1511
|
-
// History cleared (from /clear command) - token reset and terminal clear only.
|
|
1512
|
-
// All UI state is already reset by the /clear handler itself; duplicating
|
|
1513
|
-
// setConversation/setActivePlan/etc. here triggers a second React render cycle
|
|
1514
|
-
// that can race yoga-layout node teardown and cause WASM OOB crashes.
|
|
1515
|
-
const unsubHistoryCleared = eventEmitter.on('history-cleared', (data) => {
|
|
1516
|
-
setMetrics((prev) => ({ ...prev, currentContextTokens: 0 }));
|
|
1517
|
-
// Clear the terminal screen including Static scrollback buffer (skip on startup to preserve banner).
|
|
1518
|
-
// Deferred via setTimeout so React state updates flush first — writing ANSI escape codes
|
|
1519
|
-
// synchronously corrupts Ink's internal cursor tracking, causing duplicate renders.
|
|
1520
|
-
if (!data?.isStartup) {
|
|
1521
|
-
setTimeout(() => {
|
|
1522
|
-
process.stdout.write('\x1B[2J\x1B[H');
|
|
1523
|
-
}, 0);
|
|
1524
|
-
}
|
|
1525
|
-
});
|
|
1526
|
-
unsubscribersRef.current.push(unsubHistoryCleared);
|
|
1527
|
-
// Agent state changed (from /agents use and /agents reset)
|
|
1528
|
-
const unsubAgentState = eventEmitter.on('agent-state-changed', ({ activeAgent }) => {
|
|
1529
|
-
setActiveAgentName(activeAgent?.name ?? null);
|
|
1530
|
-
});
|
|
1531
|
-
unsubscribersRef.current.push(unsubAgentState);
|
|
1532
|
-
// Approval requests
|
|
1533
|
-
const unsubApproval = eventEmitter.on('approval-required', ({ request }) => {
|
|
1534
|
-
setPendingApproval(request);
|
|
1535
|
-
});
|
|
1536
|
-
unsubscribersRef.current.push(unsubApproval);
|
|
1537
|
-
// Plan approval requests
|
|
1538
|
-
const unsubPlanApproval = eventEmitter.on('plan-approval-required', ({ request }) => {
|
|
1539
|
-
setPendingPlanApproval(request);
|
|
1540
|
-
});
|
|
1541
|
-
unsubscribersRef.current.push(unsubPlanApproval);
|
|
1542
|
-
// Clarification wizard requests
|
|
1543
|
-
const unsubClarification = eventEmitter.on('clarification-required', ({ request }) => {
|
|
1544
|
-
setPendingClarification(request);
|
|
1545
|
-
});
|
|
1546
|
-
unsubscribersRef.current.push(unsubClarification);
|
|
1547
|
-
// Plan update started — hide the current plan tracker while a new plan is being generated
|
|
1548
|
-
const unsubPlanUpdateStarted = eventEmitter.on('plan-update-started', () => {
|
|
1549
|
-
activePlanRef.current = null;
|
|
1550
|
-
setActivePlan(null);
|
|
1551
|
-
setPlanTrackerMode('compact'); // Reset for when the new plan is approved
|
|
1552
|
-
});
|
|
1553
|
-
unsubscribersRef.current.push(unsubPlanUpdateStarted);
|
|
1554
|
-
// Plan execution events - initialize tracker when plan approved.
|
|
1555
|
-
// All plan events mutate activePlanRef and call schedulePlanRender() to update
|
|
1556
|
-
// only the plan state without triggering InterleavedStream re-renders.
|
|
1557
|
-
const unsubPlanExecStarted = eventEmitter.on('plan-execution-started', ({ planId, tasks }) => {
|
|
1558
|
-
setPlanTrackerMode('compact'); // Reset visibility for new plan
|
|
1559
|
-
activePlanRef.current = {
|
|
1560
|
-
planId,
|
|
1561
|
-
title: '',
|
|
1562
|
-
tasks: tasks.map((task) => ({ task, status: 'pending' })),
|
|
1563
|
-
isComplete: false,
|
|
1564
|
-
success: false,
|
|
1565
|
-
};
|
|
1566
|
-
schedulePlanRender();
|
|
1567
|
-
});
|
|
1568
|
-
unsubscribersRef.current.push(unsubPlanExecStarted);
|
|
1569
|
-
// Plan task completed - update tracker
|
|
1570
|
-
const unsubPlanTaskCompleted = eventEmitter.on('plan-task-completed', ({ planId, taskId, status, summary: _summary }) => {
|
|
1571
|
-
const prev = activePlanRef.current;
|
|
1572
|
-
if (!prev || prev.planId !== planId)
|
|
1573
|
-
return;
|
|
1574
|
-
activePlanRef.current = {
|
|
1575
|
-
...prev,
|
|
1576
|
-
tasks: prev.tasks.map((t) => t.task.id === taskId ? { ...t, status } : t),
|
|
1577
|
-
};
|
|
1578
|
-
schedulePlanRender();
|
|
1579
|
-
});
|
|
1580
|
-
unsubscribersRef.current.push(unsubPlanTaskCompleted);
|
|
1581
|
-
// Plan task progress - update tracker for in-progress detection
|
|
1582
|
-
const unsubPlanTaskProgress = eventEmitter.on('plan-task-progress', ({ planId, taskId }) => {
|
|
1583
|
-
const prev = activePlanRef.current;
|
|
1584
|
-
if (!prev || prev.planId !== planId)
|
|
1585
|
-
return;
|
|
1586
|
-
activePlanRef.current = {
|
|
1587
|
-
...prev,
|
|
1588
|
-
tasks: prev.tasks.map((t) => t.task.id === taskId && t.status === 'pending'
|
|
1589
|
-
? { ...t, status: 'in_progress' }
|
|
1590
|
-
: t),
|
|
1591
|
-
};
|
|
1592
|
-
schedulePlanRender();
|
|
1593
|
-
});
|
|
1594
|
-
unsubscribersRef.current.push(unsubPlanTaskProgress);
|
|
1595
|
-
// Plan execution completed — mark complete synchronously, clear on next idle transition.
|
|
1596
|
-
// No setTimeout: the deferred clear used to cause a second layout shift 500ms after
|
|
1597
|
-
// processing ended. Instead, activePlanRef is cleared when isProcessing transitions to false.
|
|
1598
|
-
const unsubPlanExecCompleted = eventEmitter.on('plan-execution-completed', ({ planId, success }) => {
|
|
1599
|
-
const prev = activePlanRef.current;
|
|
1600
|
-
if (!prev || prev.planId !== planId)
|
|
1601
|
-
return;
|
|
1602
|
-
activePlanRef.current = { ...prev, isComplete: true, success };
|
|
1603
|
-
schedulePlanRender();
|
|
1604
|
-
});
|
|
1605
|
-
unsubscribersRef.current.push(unsubPlanExecCompleted);
|
|
1606
|
-
// Model switches
|
|
1607
|
-
const unsubModel = eventEmitter.on('model-switched', ({ model }) => {
|
|
1608
|
-
setCurrentModel(model.name);
|
|
1609
|
-
setContextWindowSize(model.contextWindow);
|
|
1610
|
-
});
|
|
1611
|
-
unsubscribersRef.current.push(unsubModel);
|
|
1612
|
-
// Errors
|
|
1613
|
-
const unsubError = eventEmitter.on('error', ({ message }) => {
|
|
1614
|
-
setError(message);
|
|
1615
|
-
});
|
|
1616
|
-
unsubscribersRef.current.push(unsubError);
|
|
1617
|
-
// Tool status messages (for granular progress during long-running tools)
|
|
1618
|
-
const unsubToolStatus = eventEmitter.on('tool-status-message', ({ toolId, message }) => {
|
|
1619
|
-
updateStreamBlocks((prev) => prev.map((block) => block.type === 'tool' && block.id === toolId
|
|
1620
|
-
? { ...block, statusMessage: message }
|
|
1621
|
-
: block));
|
|
1622
|
-
});
|
|
1623
|
-
unsubscribersRef.current.push(unsubToolStatus);
|
|
1624
|
-
// Tool complexity reasoning (persistent reasoning for plan mode)
|
|
1625
|
-
const unsubComplexityReasoning = eventEmitter.on('tool-complexity-reasoning', ({ toolId, reasoning }) => {
|
|
1626
|
-
updateStreamBlocks((prev) => prev.map((block) => block.type === 'tool' && block.id === toolId
|
|
1627
|
-
? { ...block, complexityReasoning: reasoning }
|
|
1628
|
-
: block));
|
|
1629
|
-
});
|
|
1630
|
-
unsubscribersRef.current.push(unsubComplexityReasoning);
|
|
1631
|
-
// Slash command output events (for real-time streaming output from commands like /commit)
|
|
1632
|
-
// Accumulate synchronously - React batching is fine since we add to conversation at the end
|
|
1633
|
-
const unsubSlashCommandOutput = eventEmitter.on('slash-command-output', ({ message }) => {
|
|
1634
|
-
logger.debug(`[StreamingOutput Event] Received: ${message.substring(0, 50)}, total lines: ${streamingOutputRef.current.length + 1}`);
|
|
1635
|
-
streamingOutputRef.current.push(message);
|
|
1636
|
-
// Trigger state update for real-time display during command execution
|
|
1637
|
-
setStreamingCommandOutput([...streamingOutputRef.current]);
|
|
1638
|
-
});
|
|
1639
|
-
unsubscribersRef.current.push(unsubSlashCommandOutput);
|
|
1640
|
-
// Compass spinner events (for visual feedback during operations like /commit)
|
|
1641
|
-
const unsubCompassSpinner = eventEmitter.on('compass-spinner', (data) => {
|
|
1642
|
-
logger.debug(`[CompassSpinner Event] Received: active=${data.active}, message=${data.message}`);
|
|
1643
|
-
setCompassSpinner(data.active ? data : null);
|
|
1644
|
-
});
|
|
1645
|
-
unsubscribersRef.current.push(unsubCompassSpinner);
|
|
1646
|
-
// Agent wizard events (for /agents create and /agents edit)
|
|
1647
|
-
const unsubAgentWizard = eventEmitter.on('open-agent-wizard', ({ mode, agentName }) => {
|
|
1648
|
-
logger.debug(`[AgentWizard Event] Opening wizard: mode=${mode}, agentName=${agentName}`);
|
|
1649
|
-
setActiveOverlay('agent-wizard');
|
|
1650
|
-
});
|
|
1651
|
-
unsubscribersRef.current.push(unsubAgentWizard);
|
|
1652
|
-
}
|
|
1653
|
-
/**
|
|
1654
|
-
* Helper to update a tool block in stream blocks by ID
|
|
1655
|
-
*/
|
|
1656
|
-
function updateToolBlock(toolId, updates) {
|
|
1657
|
-
// Use optimized in-place update instead of full array map
|
|
1658
|
-
updateToolBlockById(toolId, updates);
|
|
1659
|
-
}
|
|
1660
|
-
/**
|
|
1661
|
-
* Executes tool calls and returns tool results with timing records.
|
|
1662
|
-
* Updates UI state progressively as each tool completes.
|
|
1663
|
-
*/
|
|
1664
|
-
async function executeToolCalls(toolUseBlocks) {
|
|
1665
|
-
const toolRegistry = getToolRegistry();
|
|
1666
|
-
const toolResults = [];
|
|
1667
|
-
const toolRecords = [];
|
|
1668
|
-
let userRejected = false;
|
|
1669
|
-
let endTurn = false;
|
|
1670
|
-
for (const toolUse of toolUseBlocks) {
|
|
1671
|
-
// Check abort flag before each tool execution
|
|
1672
|
-
if (abortLoopRef.current) {
|
|
1673
|
-
logger.info('Tool execution aborted by user');
|
|
1674
|
-
break;
|
|
1675
|
-
}
|
|
1676
|
-
logger.debug(`Executing tool: ${toolUse.name}`);
|
|
1677
|
-
// Update existing tool block to "running" status (block was added during streaming)
|
|
1678
|
-
updateToolBlockById(toolUse.id, { status: 'running', params: toolUse.input });
|
|
1679
|
-
// Create per-tool AbortController for execute_command (allows Ctrl+X skip)
|
|
1680
|
-
const isExecuteCommand = toolUse.name === 'execute_command';
|
|
1681
|
-
if (isExecuteCommand) {
|
|
1682
|
-
toolAbortRef.current = new AbortController();
|
|
1683
|
-
currentToolNameRef.current = 'execute_command';
|
|
1684
|
-
}
|
|
1685
|
-
const startTime = Date.now();
|
|
1686
|
-
try {
|
|
1687
|
-
// Use per-tool signal for execute_command, loop-wide signal for others
|
|
1688
|
-
const abortSignal = isExecuteCommand
|
|
1689
|
-
? toolAbortRef.current.signal
|
|
1690
|
-
: abortControllerRef.current?.signal;
|
|
1691
|
-
const result = await toolRegistry.executeTool(toolUse.name, toolUse.input, { projectRoot, toolCallId: toolUse.id, abortSignal });
|
|
1692
|
-
const duration = Date.now() - startTime;
|
|
1693
|
-
// Clear per-tool abort state
|
|
1694
|
-
if (isExecuteCommand) {
|
|
1695
|
-
toolAbortRef.current = null;
|
|
1696
|
-
currentToolNameRef.current = null;
|
|
1697
|
-
}
|
|
1698
|
-
// Check if the tool was skipped by the user (Ctrl+X)
|
|
1699
|
-
const toolData = result.data;
|
|
1700
|
-
if (toolData?.skippedByUser) {
|
|
1701
|
-
const partialOutput = (toolData.stdout || '').slice(0, 200) + (toolData.stderr || '').slice(0, 200);
|
|
1702
|
-
updateToolBlock(toolUse.id, {
|
|
1703
|
-
status: 'skipped',
|
|
1704
|
-
result: JSON.stringify(result.data),
|
|
1705
|
-
duration_ms: duration,
|
|
1706
|
-
});
|
|
1707
|
-
toolResults.push({
|
|
1708
|
-
type: 'tool_result',
|
|
1709
|
-
tool_use_id: toolUse.id,
|
|
1710
|
-
content: `Command was skipped by the user.${partialOutput ? ` Partial output:\n${partialOutput}` : ''}`,
|
|
1711
|
-
});
|
|
1712
|
-
const record = {
|
|
1713
|
-
name: toolUse.name,
|
|
1714
|
-
input: toolUse.input,
|
|
1715
|
-
result: result.data,
|
|
1716
|
-
success: false,
|
|
1717
|
-
error: 'Skipped by user',
|
|
1718
|
-
duration_ms: duration,
|
|
1719
|
-
};
|
|
1720
|
-
toolRecords.push(record);
|
|
1721
|
-
logger.debug(`Tool ${toolUse.name} skipped by user after ${duration}ms`);
|
|
1722
|
-
continue;
|
|
1723
|
-
}
|
|
1724
|
-
if (result.success) {
|
|
1725
|
-
// Check if the tool's return data indicates a user rejection
|
|
1726
|
-
// (tool handler returns success: false but doesn't throw)
|
|
1727
|
-
const isToolRejection = toolData?.success === false &&
|
|
1728
|
-
(toolData?.error?.toLowerCase().includes('rejected by user') ||
|
|
1729
|
-
toolData?.error?.toLowerCase().includes('rejected'));
|
|
1730
|
-
// Check if a plan was cancelled — this signals the turn should end
|
|
1731
|
-
if (toolData?.planCancelled === true) {
|
|
1732
|
-
endTurn = true;
|
|
1733
|
-
logger.info(`Plan cancelled via ${toolUse.name} — will end turn`);
|
|
1734
|
-
}
|
|
1735
|
-
if (isToolRejection) {
|
|
1736
|
-
userRejected = true;
|
|
1737
|
-
// Update tool block with error
|
|
1738
|
-
updateToolBlock(toolUse.id, {
|
|
1739
|
-
status: 'error',
|
|
1740
|
-
error: toolData?.error || 'Operation rejected by user',
|
|
1741
|
-
duration_ms: duration,
|
|
1742
|
-
});
|
|
1743
|
-
// Still add the result for the LLM to see, but mark as error
|
|
1744
|
-
toolResults.push({
|
|
1745
|
-
type: 'tool_result',
|
|
1746
|
-
tool_use_id: toolUse.id,
|
|
1747
|
-
content: `Error: ${toolData?.error || 'Operation rejected by user'}`,
|
|
1748
|
-
is_error: true,
|
|
1749
|
-
});
|
|
1750
|
-
const record = {
|
|
1751
|
-
name: toolUse.name,
|
|
1752
|
-
input: toolUse.input,
|
|
1753
|
-
result: null,
|
|
1754
|
-
success: false,
|
|
1755
|
-
error: toolData?.error || 'Operation rejected by user',
|
|
1756
|
-
duration_ms: duration,
|
|
1757
|
-
};
|
|
1758
|
-
toolRecords.push(record);
|
|
1759
|
-
logger.debug(`Tool ${toolUse.name} rejected by user`);
|
|
1760
|
-
}
|
|
1761
|
-
else {
|
|
1762
|
-
// Update tool block with success.
|
|
1763
|
-
// Store parsedResult to avoid re-parsing JSON in the render path.
|
|
1764
|
-
updateToolBlock(toolUse.id, {
|
|
1765
|
-
status: 'success',
|
|
1766
|
-
result: JSON.stringify(result.data),
|
|
1767
|
-
parsedResult: result.data,
|
|
1768
|
-
duration_ms: duration,
|
|
1769
|
-
});
|
|
1770
|
-
toolResults.push({
|
|
1771
|
-
type: 'tool_result',
|
|
1772
|
-
tool_use_id: toolUse.id,
|
|
1773
|
-
content: JSON.stringify(result.data, null, 2),
|
|
1774
|
-
});
|
|
1775
|
-
const record = {
|
|
1776
|
-
name: toolUse.name,
|
|
1777
|
-
input: toolUse.input,
|
|
1778
|
-
result: result.data,
|
|
1779
|
-
success: true,
|
|
1780
|
-
duration_ms: duration,
|
|
1781
|
-
};
|
|
1782
|
-
toolRecords.push(record);
|
|
1783
|
-
logger.debug(`Tool ${toolUse.name} succeeded in ${duration}ms`);
|
|
1784
|
-
}
|
|
1785
|
-
}
|
|
1786
|
-
else {
|
|
1787
|
-
// Check if this was a user rejection (tool threw an error)
|
|
1788
|
-
const isUserRejection = result.error?.toLowerCase().includes('rejected by user') ||
|
|
1789
|
-
result.error?.toLowerCase().includes('rejected');
|
|
1790
|
-
if (isUserRejection) {
|
|
1791
|
-
userRejected = true;
|
|
1792
|
-
}
|
|
1793
|
-
// Update tool block with error
|
|
1794
|
-
updateToolBlock(toolUse.id, {
|
|
1795
|
-
status: 'error',
|
|
1796
|
-
error: result.error,
|
|
1797
|
-
duration_ms: duration,
|
|
1798
|
-
});
|
|
1799
|
-
toolResults.push({
|
|
1800
|
-
type: 'tool_result',
|
|
1801
|
-
tool_use_id: toolUse.id,
|
|
1802
|
-
content: `Error: ${result.error}`,
|
|
1803
|
-
is_error: true,
|
|
1804
|
-
});
|
|
1805
|
-
const record = {
|
|
1806
|
-
name: toolUse.name,
|
|
1807
|
-
input: toolUse.input,
|
|
1808
|
-
result: null,
|
|
1809
|
-
success: false,
|
|
1810
|
-
error: result.error,
|
|
1811
|
-
duration_ms: duration,
|
|
1812
|
-
};
|
|
1813
|
-
toolRecords.push(record);
|
|
1814
|
-
logger.debug(`Tool ${toolUse.name} failed: ${result.error}`);
|
|
1815
|
-
}
|
|
1816
|
-
}
|
|
1817
|
-
catch (err) {
|
|
1818
|
-
// Clear per-tool abort state on error
|
|
1819
|
-
if (isExecuteCommand) {
|
|
1820
|
-
toolAbortRef.current = null;
|
|
1821
|
-
currentToolNameRef.current = null;
|
|
1822
|
-
}
|
|
1823
|
-
const duration = Date.now() - startTime;
|
|
1824
|
-
const errorMsg = err instanceof Error ? err.message : 'Tool execution failed';
|
|
1825
|
-
// Update tool block with error
|
|
1826
|
-
updateToolBlock(toolUse.id, {
|
|
1827
|
-
status: 'error',
|
|
1828
|
-
error: errorMsg,
|
|
1829
|
-
duration_ms: duration,
|
|
1830
|
-
});
|
|
1831
|
-
toolResults.push({
|
|
1832
|
-
type: 'tool_result',
|
|
1833
|
-
tool_use_id: toolUse.id,
|
|
1834
|
-
content: `Error: ${errorMsg}`,
|
|
1835
|
-
is_error: true,
|
|
1836
|
-
});
|
|
1837
|
-
const record = {
|
|
1838
|
-
name: toolUse.name,
|
|
1839
|
-
input: toolUse.input,
|
|
1840
|
-
result: null,
|
|
1841
|
-
success: false,
|
|
1842
|
-
error: errorMsg,
|
|
1843
|
-
duration_ms: duration,
|
|
1844
|
-
};
|
|
1845
|
-
toolRecords.push(record);
|
|
1846
|
-
logger.error(`Tool ${toolUse.name} threw error`, err);
|
|
1847
|
-
}
|
|
1848
|
-
}
|
|
1849
|
-
return { results: toolResults, records: toolRecords, userRejected, endTurn };
|
|
1850
|
-
}
|
|
1851
|
-
/**
|
|
1852
|
-
* Handles clipboard paste (Ctrl+V)
|
|
1853
|
-
* Extracts images and files from clipboard and adds them as pending attachments
|
|
1854
|
-
*/
|
|
1855
|
-
const handlePaste = useCallback(async () => {
|
|
1856
|
-
try {
|
|
1857
|
-
// Check if clipboard contains supported content
|
|
1858
|
-
const hasContent = await hasSupportedClipboardContent();
|
|
1859
|
-
if (!hasContent) {
|
|
1860
|
-
logger.output(chalk.yellow('⚠ No supported content (images or PDFs) in clipboard'));
|
|
1861
|
-
return;
|
|
1862
|
-
}
|
|
1863
|
-
// Get clipboard content
|
|
1864
|
-
const clipboardContent = await getClipboardContent();
|
|
1865
|
-
if (clipboardContent.images.length === 0 && clipboardContent.files.length === 0) {
|
|
1866
|
-
logger.output(chalk.yellow('⚠ Could not extract any attachments from clipboard'));
|
|
1867
|
-
return;
|
|
1868
|
-
}
|
|
1869
|
-
// Extract attachments from clipboard content
|
|
1870
|
-
const attachments = await extractAttachmentsFromClipboard(clipboardContent);
|
|
1871
|
-
if (attachments.length === 0) {
|
|
1872
|
-
logger.output(chalk.yellow('⚠ Could not extract any attachments from clipboard'));
|
|
1873
|
-
return;
|
|
1874
|
-
}
|
|
1875
|
-
// Add attachments to session manager
|
|
1876
|
-
const sessionManager = getSessionManager();
|
|
1877
|
-
sessionManager.addPendingAttachments(attachments);
|
|
1878
|
-
// Show feedback to user
|
|
1879
|
-
const imageCount = attachments.filter(a => a.type === 'image').length;
|
|
1880
|
-
const pdfCount = attachments.filter(a => a.type === "document").length;
|
|
1881
|
-
let message = chalk.green(`✓ Pasted ${attachments.length} file${attachments.length > 1 ? 's' : ''}`);
|
|
1882
|
-
if (imageCount > 0)
|
|
1883
|
-
message += chalk.gray(` (${imageCount} image${imageCount > 1 ? 's' : ''})`);
|
|
1884
|
-
if (pdfCount > 0)
|
|
1885
|
-
message += chalk.gray(` (${pdfCount} PDF${pdfCount > 1 ? 's' : ''})`);
|
|
1886
|
-
logger.output('');
|
|
1887
|
-
logger.output(message);
|
|
1888
|
-
logger.output(chalk.cyan(`💡 ${attachments.length} attachment${attachments.length > 1 ? 's' : ''} pending for next message`));
|
|
1889
|
-
logger.output('');
|
|
1890
|
-
// Emit event to update UI
|
|
1891
|
-
const eventEmitter = getEventEmitter();
|
|
1892
|
-
eventEmitter.emit('attachments-updated', {
|
|
1893
|
-
attachments: sessionManager.getPendingAttachments(),
|
|
1894
|
-
});
|
|
1895
|
-
logger.info(`Pasted ${attachments.length} attachments from clipboard`);
|
|
1896
|
-
}
|
|
1897
|
-
catch (error) {
|
|
1898
|
-
const errorMessage = error instanceof Error ? error.message : 'Failed to paste from clipboard';
|
|
1899
|
-
logger.debug('Clipboard paste failed', { error });
|
|
1900
|
-
logger.output(chalk.red(`❌ ${errorMessage}`));
|
|
1901
|
-
}
|
|
1902
|
-
}, []);
|
|
1903
|
-
/**
|
|
1904
|
-
* Handles user input submission from the Prompt component
|
|
1905
|
-
* Implements an agentic loop with proper tool use
|
|
1906
|
-
*/
|
|
1907
|
-
const handleSubmit = useCallback(async (input) => {
|
|
1908
|
-
const parsed = parseInput(input);
|
|
1909
|
-
// Hide welcome message after first input
|
|
1910
|
-
setShouldShowWelcome(false);
|
|
1911
|
-
// Record activity for rating tracking
|
|
1912
|
-
if (ratingStateManagerRef.current) {
|
|
1913
|
-
ratingStateManagerRef.current.recordActivity();
|
|
1914
|
-
}
|
|
1915
|
-
// Clear any previous suggestion offer when user submits new input
|
|
1916
|
-
setSuggestionOffer(undefined);
|
|
1917
|
-
// Initialize iteration scoper (accessible in finally block)
|
|
1918
|
-
const iterationScoper = getIterationScoper();
|
|
1919
|
-
let isPlanMode = false;
|
|
1920
|
-
// Log user input for audit trail
|
|
1921
|
-
const auditLogger = getAuditLogger(projectRoot);
|
|
1922
|
-
// Handle slash commands
|
|
1923
|
-
if (parsed.type === 'slash') {
|
|
1924
|
-
await auditLogger.logUserInput(input, true);
|
|
1925
|
-
// Clear any previous slash command output
|
|
1926
|
-
setSlashCommandOutput(null);
|
|
1927
|
-
// Clear streaming command output and reset the ref
|
|
1928
|
-
streamingOutputRef.current = [];
|
|
1929
|
-
setStreamingCommandOutput([]);
|
|
1930
|
-
try {
|
|
1931
|
-
// Only show "Thinking..." indicator for commands that require LLM interaction.
|
|
1932
|
-
// Local commands (e.g. /model, /help, /clear) execute instantly and don't need it.
|
|
1933
|
-
const registry = getSlashCommandRegistry();
|
|
1934
|
-
const needsLLM = parsed.command ? registry.requiresLLM(parsed.command) : false;
|
|
1935
|
-
if (needsLLM) {
|
|
1936
|
-
setIsProcessing(true);
|
|
1937
|
-
}
|
|
1938
|
-
logger.debug(`[InteractiveSession] Starting slash command: ${parsed.command}, isProcessing=${needsLLM}`);
|
|
1939
|
-
// Capture console output during slash command execution
|
|
1940
|
-
// This supports both new-style (event emission) and old-style (console.log) commands
|
|
1941
|
-
const { result, output: capturedConsoleOutput } = await captureConsoleOutput(async () => {
|
|
1942
|
-
return await executeSlashCommand(parsed);
|
|
1943
|
-
});
|
|
1944
|
-
logger.debug(`[InteractiveSession] Slash command completed: ${parsed.command}, streamingOutputRef.length=${streamingOutputRef.current.length}, capturedConsoleOutput.length=${capturedConsoleOutput.length}`);
|
|
1945
|
-
if (!result.handled) {
|
|
1946
|
-
setError(`Unknown command: /${parsed.command}. Type /help for available commands.`);
|
|
1947
|
-
setIsProcessing(false);
|
|
1948
|
-
return;
|
|
1949
|
-
}
|
|
1950
|
-
// Check if this is a custom command that wants to continue as a prompt
|
|
1951
|
-
if (result.continueAsPrompt) {
|
|
1952
|
-
// Continue with LLM flow using the processed prompt
|
|
1953
|
-
// Fall through to natural language handling with the processed content
|
|
1954
|
-
logger.debug('Custom command continuing as prompt', {
|
|
1955
|
-
command: parsed.command,
|
|
1956
|
-
contentLength: result.continueAsPrompt.length,
|
|
1957
|
-
});
|
|
1958
|
-
// Override input for LLM processing
|
|
1959
|
-
// The original command is shown to user, but processed content goes to LLM
|
|
1960
|
-
// We need to continue below with the processed content
|
|
1961
|
-
// Store the custom command result for use below
|
|
1962
|
-
parsed.customCommandResult = result;
|
|
1963
|
-
// Set back to true for LLM processing
|
|
1964
|
-
setIsProcessing(true);
|
|
1965
|
-
}
|
|
1966
|
-
else {
|
|
1967
|
-
// Regular slash command handled - display output ephemerally (NOT added to conversation/LLM context)
|
|
1968
|
-
// Prefer streaming events (new-style), fall back to captured console output (old-style)
|
|
1969
|
-
const hasStreamingOutput = streamingOutputRef.current.length > 0;
|
|
1970
|
-
const hasCapturedOutput = capturedConsoleOutput.trim().length > 0;
|
|
1971
|
-
logger.debug(`[InteractiveSession] Processing output: streaming=${streamingOutputRef.current.length} lines, captured=${capturedConsoleOutput.length} chars`);
|
|
1972
|
-
if (hasStreamingOutput || hasCapturedOutput) {
|
|
1973
|
-
// Prefer streaming output if available, otherwise use captured console output
|
|
1974
|
-
const combinedOutput = hasStreamingOutput
|
|
1975
|
-
? streamingOutputRef.current.join('\n')
|
|
1976
|
-
: capturedConsoleOutput;
|
|
1977
|
-
logger.info(`[InteractiveSession] Setting ephemeral slash command output (${combinedOutput.length} chars, source=${hasStreamingOutput ? 'events' : 'console'})`);
|
|
1978
|
-
// Clear streaming refs FIRST to prevent both streamingCommandOutput and slashCommandOutput
|
|
1979
|
-
// from rendering simultaneously (which could show duplicate/stale content)
|
|
1980
|
-
streamingOutputRef.current = [];
|
|
1981
|
-
setStreamingCommandOutput([]);
|
|
1982
|
-
// Use ephemeral state - this output is cleared when user types next command or prompt
|
|
1983
|
-
// This prevents slash command output (like /help) from being sent to LLM as conversation context
|
|
1984
|
-
setSlashCommandOutput(combinedOutput);
|
|
1985
|
-
// Set isProcessing to false to display the output
|
|
1986
|
-
setIsProcessing(false);
|
|
1987
|
-
logger.debug('[InteractiveSession] Set ephemeral output and cleared streaming refs');
|
|
1988
|
-
}
|
|
1989
|
-
else {
|
|
1990
|
-
logger.debug(`[InteractiveSession] No output for command: ${parsed.command} (silent command)`);
|
|
1991
|
-
setIsProcessing(false);
|
|
1992
|
-
}
|
|
1993
|
-
return;
|
|
1994
|
-
}
|
|
1995
|
-
}
|
|
1996
|
-
catch (err) {
|
|
1997
|
-
const errorMessage = err instanceof Error ? err.message : 'Command failed';
|
|
1998
|
-
setError(errorMessage);
|
|
1999
|
-
logger.error(`Slash command error: ${parsed.command}`, err);
|
|
2000
|
-
await auditLogger.logError(errorMessage, err);
|
|
2001
|
-
setIsProcessing(false);
|
|
2002
|
-
// Still clear streaming output even on error
|
|
2003
|
-
streamingOutputRef.current = [];
|
|
2004
|
-
setStreamingCommandOutput([]);
|
|
2005
|
-
return;
|
|
2006
|
-
}
|
|
2007
|
-
}
|
|
2008
|
-
// Clear slash command output when starting new LLM interaction
|
|
2009
|
-
setSlashCommandOutput(null);
|
|
2010
|
-
// Check if input is just "/" (incomplete slash command)
|
|
2011
|
-
if (input.trim() === '/') {
|
|
2012
|
-
setError('Incomplete command. Type /help to see available commands.');
|
|
2013
|
-
return;
|
|
2014
|
-
}
|
|
2015
|
-
// Check if this is from a custom command that should continue as prompt
|
|
2016
|
-
const customCommandResult = parsed.customCommandResult;
|
|
2017
|
-
const isCustomCommand = !!customCommandResult?.continueAsPrompt;
|
|
2018
|
-
// Log user input for audit trail (only if not a custom command, which was already logged)
|
|
2019
|
-
if (!isCustomCommand) {
|
|
2020
|
-
await auditLogger.logUserInput(input, false);
|
|
2021
|
-
}
|
|
2022
|
-
// Parse any system-reminders in the input for IDE state
|
|
2023
|
-
const ideStateService = getIdeStateService();
|
|
2024
|
-
ideStateService.updateFromMessage(input);
|
|
2025
|
-
// For custom commands, use the processed prompt directly
|
|
2026
|
-
// Otherwise, preprocess for IDE context injection
|
|
2027
|
-
let inputForLLM;
|
|
2028
|
-
if (isCustomCommand && customCommandResult?.continueAsPrompt) {
|
|
2029
|
-
inputForLLM = customCommandResult.continueAsPrompt;
|
|
2030
|
-
logger.debug('Using custom command processed content for LLM', {
|
|
2031
|
-
contentLength: inputForLLM.length,
|
|
2032
|
-
});
|
|
2033
|
-
}
|
|
2034
|
-
else {
|
|
2035
|
-
const promptPreprocessor = getPromptPreprocessor();
|
|
2036
|
-
const { processedInput, contextInjected, injectionDetails } = await promptPreprocessor.process(input);
|
|
2037
|
-
if (contextInjected && injectionDetails) {
|
|
2038
|
-
logger.info('IDE context injected into prompt', {
|
|
2039
|
-
filePath: injectionDetails.filePath,
|
|
2040
|
-
lineCount: injectionDetails.lineCount,
|
|
2041
|
-
truncated: injectionDetails.truncated,
|
|
2042
|
-
});
|
|
2043
|
-
}
|
|
2044
|
-
inputForLLM = processedInput;
|
|
2045
|
-
}
|
|
2046
|
-
// Enable history display on first user message
|
|
2047
|
-
setShowHistory(true);
|
|
2048
|
-
// Handle natural language input - send to Claude
|
|
2049
|
-
setIsProcessing(true);
|
|
2050
|
-
setSessionPhase('classifying');
|
|
2051
|
-
setError(null);
|
|
2052
|
-
resetStreamBlocks();
|
|
2053
|
-
// Handle plan execution tracker state on new request
|
|
2054
|
-
{
|
|
2055
|
-
const prev = activePlanRef.current;
|
|
2056
|
-
if (prev) {
|
|
2057
|
-
if (prev.isComplete || executionMode === 'direct') {
|
|
2058
|
-
activePlanRef.current = null;
|
|
2059
|
-
}
|
|
2060
|
-
else if (prev.isPaused) {
|
|
2061
|
-
activePlanRef.current = { ...prev, isPaused: false };
|
|
2062
|
-
}
|
|
2063
|
-
}
|
|
2064
|
-
setActivePlan(activePlanRef.current);
|
|
2065
|
-
}
|
|
2066
|
-
// Classify the request for SMART mode approval decisions
|
|
2067
|
-
// This must happen BEFORE any tool execution so the ApprovalManager can use the context
|
|
2068
|
-
const approvalManager = getApprovalManager();
|
|
2069
|
-
approvalManager.classifyAndSetContext(input);
|
|
2070
|
-
// Save checkpoint for potential rollback if user cancels mid-execution
|
|
2071
|
-
conversationCheckpointRef.current = [...conversation];
|
|
2072
|
-
// Add user message to conversation (show original input to user)
|
|
2073
|
-
const userEntry = {
|
|
2074
|
-
id: `user-${Date.now()}`,
|
|
2075
|
-
role: 'user',
|
|
2076
|
-
content: input,
|
|
2077
|
-
timestamp: new Date(),
|
|
2078
|
-
};
|
|
2079
|
-
setConversation((prev) => [...prev, userEntry]);
|
|
2080
|
-
// Hoisted so `finally` can read the last assistant response for question detection
|
|
2081
|
-
let finalResponseForSuggestion = '';
|
|
2082
|
-
try {
|
|
2083
|
-
const client = await getAnthropicClient();
|
|
2084
|
-
if (!client.isInitialized()) {
|
|
2085
|
-
const initialized = await client.initialize();
|
|
2086
|
-
if (!initialized) {
|
|
2087
|
-
throw new Error('Failed to initialize API client. Please check your API key with: compass setup');
|
|
2088
|
-
}
|
|
2089
|
-
}
|
|
2090
|
-
// Refresh project context if stale (files created/deleted since last call)
|
|
2091
|
-
// This ensures Anthropic's prompt cache gets fresh context when project structure changes
|
|
2092
|
-
await refreshProjectContext();
|
|
2093
|
-
// Build system prompt with project context and model info
|
|
2094
|
-
const model = client.getCurrentModel();
|
|
2095
|
-
const systemPrompt = await buildSystemPrompt(projectSummary, model.name, model.id);
|
|
2096
|
-
// Log system prompt for audit trail
|
|
2097
|
-
await auditLogger.logSystemPrompt(systemPrompt);
|
|
2098
|
-
// Get tool definitions from registry
|
|
2099
|
-
const toolRegistry = getToolRegistry();
|
|
2100
|
-
let tools = toolRegistry.getToolDefinitions();
|
|
2101
|
-
// Filter tools based on execution mode
|
|
2102
|
-
if (executionMode === 'direct') {
|
|
2103
|
-
// In DIRECT mode, remove the enter_plan_mode tool
|
|
2104
|
-
tools = tools.filter(tool => tool.name !== 'enter_plan_mode');
|
|
2105
|
-
logger.debug('Execution mode: DIRECT - enter_plan_mode tool removed');
|
|
2106
|
-
}
|
|
2107
|
-
else {
|
|
2108
|
-
logger.debug(`Execution mode: ${executionMode.toUpperCase()} - all tools available`);
|
|
2109
|
-
}
|
|
2110
|
-
// Build initial messages for API from conversation history
|
|
2111
|
-
const messages = conversation.map((entry) => ({
|
|
2112
|
-
role: entry.role === 'system' ? 'user' : entry.role,
|
|
2113
|
-
content: entry.content,
|
|
2114
|
-
}));
|
|
2115
|
-
// ACTIVE PLAN CONTEXT INJECTION
|
|
2116
|
-
// If there's an active plan being executed, inject the pending tasks context
|
|
2117
|
-
// This ensures the LLM knows exactly what tasks remain when user says "continue"
|
|
2118
|
-
const planPersistence = getPlanPersistence(projectRoot);
|
|
2119
|
-
const activePlanId = planPersistence.getActivePlanId();
|
|
2120
|
-
if (activePlanId) {
|
|
2121
|
-
const pendingContext = await planPersistence.getPendingTasksContext(activePlanId);
|
|
2122
|
-
if (pendingContext) {
|
|
2123
|
-
logger.info('Injecting active plan context', {
|
|
2124
|
-
planId: activePlanId,
|
|
2125
|
-
completedTasks: pendingContext.completedTasks,
|
|
2126
|
-
totalTasks: pendingContext.totalTasks,
|
|
2127
|
-
pendingCount: pendingContext.pendingTasks.length,
|
|
2128
|
-
});
|
|
2129
|
-
// Insert plan context BEFORE the user's message
|
|
2130
|
-
messages.push({
|
|
2131
|
-
role: 'user',
|
|
2132
|
-
content: pendingContext.formattedContext,
|
|
2133
|
-
});
|
|
2134
|
-
}
|
|
2135
|
-
}
|
|
2136
|
-
// Use processedInput (with potential IDE context) for LLM
|
|
2137
|
-
// Check for pending attachments and build appropriate content
|
|
2138
|
-
const sessionMgrForAttachments = getSessionManager();
|
|
2139
|
-
let userMessageContent;
|
|
2140
|
-
if (sessionMgrForAttachments.hasPendingAttachments()) {
|
|
2141
|
-
const pendingAttachments = sessionMgrForAttachments.consumePendingAttachments();
|
|
2142
|
-
userMessageContent = buildMessageContent(inputForLLM, pendingAttachments);
|
|
2143
|
-
logger.info(`Sending message with ${pendingAttachments.length} attachment(s)`);
|
|
2144
|
-
}
|
|
2145
|
-
else {
|
|
2146
|
-
userMessageContent = inputForLLM;
|
|
2147
|
-
}
|
|
2148
|
-
messages.push({ role: 'user', content: userMessageContent });
|
|
2149
|
-
// EXECUTION MODE HANDLING
|
|
2150
|
-
if (executionMode === 'plan' && !isCustomCommand) {
|
|
2151
|
-
// PLAN MODE: Force planning for EVERY request (except custom commands/skills)
|
|
2152
|
-
// Skills have their own execution logic and should not be forced into planning
|
|
2153
|
-
// BUT: skip forcing if there's already an active plan that's been approved/is executing
|
|
2154
|
-
// This prevents re-entering plan mode on follow-up confirmations like "yes, proceed"
|
|
2155
|
-
const planPersistenceForMode = getPlanPersistence(projectRoot);
|
|
2156
|
-
const activePlanIdForMode = planPersistenceForMode.getActivePlanId();
|
|
2157
|
-
let hasActivePlan = false;
|
|
2158
|
-
if (activePlanIdForMode) {
|
|
2159
|
-
const activePlanRecord = await planPersistenceForMode.loadPlanRecord(activePlanIdForMode);
|
|
2160
|
-
if (activePlanRecord && (activePlanRecord.phase === 'approved' || activePlanRecord.phase === 'executing')) {
|
|
2161
|
-
hasActivePlan = true;
|
|
2162
|
-
logger.info('PLAN mode: skipping forced planning - active plan already approved/executing', {
|
|
2163
|
-
planId: activePlanIdForMode,
|
|
2164
|
-
phase: activePlanRecord.phase,
|
|
2165
|
-
});
|
|
2166
|
-
}
|
|
2167
|
-
}
|
|
2168
|
-
if (!hasActivePlan) {
|
|
2169
|
-
logger.info('PLAN mode active - forcing planning for this request');
|
|
2170
|
-
const forcePlanningMessage = `[SYSTEM REQUIREMENT - MANDATORY]
|
|
2171
|
-
The system is in PLAN mode. You MUST use the \`enter_plan_mode\` tool FIRST before taking any other actions.
|
|
2172
|
-
|
|
2173
|
-
Do NOT directly execute commands, create files, or modify code without first creating an implementation plan.
|
|
2174
|
-
|
|
2175
|
-
Call \`enter_plan_mode\` with:
|
|
2176
|
-
- task_summary: Brief description of what needs to be done
|
|
2177
|
-
- complexity_reason: Why planning is needed for this task
|
|
2178
|
-
|
|
2179
|
-
After the plan is approved, proceed with execution following the plan tasks.`;
|
|
2180
|
-
// Insert mandatory planning instruction before the user's message
|
|
2181
|
-
messages.splice(messages.length - 1, 0, {
|
|
2182
|
-
role: 'user',
|
|
2183
|
-
content: forcePlanningMessage,
|
|
2184
|
-
});
|
|
2185
|
-
}
|
|
2186
|
-
}
|
|
2187
|
-
else if (executionMode === 'hybrid') {
|
|
2188
|
-
// HYBRID MODE: Run complexity checks to suggest planning when appropriate
|
|
2189
|
-
// Skip pre-flight for custom commands/skills that return continueAsPrompt
|
|
2190
|
-
// These have already been processed and should execute directly
|
|
2191
|
-
// Also skip for short follow-up messages when conversation has context
|
|
2192
|
-
// This handles cases like "yes", "no", "please do" responses to assistant questions
|
|
2193
|
-
const hasConversationContext = conversation.some(entry => entry.role === 'assistant');
|
|
2194
|
-
const isShortFollowUp = input.trim().length < 50 && input.trim().split(/\s+/).length < 10;
|
|
2195
|
-
if (isCustomCommand || (hasConversationContext && isShortFollowUp)) {
|
|
2196
|
-
if (isCustomCommand) {
|
|
2197
|
-
logger.debug('Skipping pre-flight check for custom command/skill');
|
|
2198
|
-
}
|
|
2199
|
-
else {
|
|
2200
|
-
logger.debug('Skipping pre-flight check for short follow-up message', {
|
|
2201
|
-
inputLength: input.length,
|
|
2202
|
-
wordCount: input.trim().split(/\s+/).length,
|
|
2203
|
-
});
|
|
2204
|
-
}
|
|
2205
|
-
// Skip pre-flight and proceed directly to agentic loop with full conversation
|
|
2206
|
-
}
|
|
2207
|
-
else {
|
|
2208
|
-
// Create a fresh AbortController BEFORE pre-flight so a previously-aborted signal
|
|
2209
|
-
// doesn't immediately cancel the complexity classifier request
|
|
2210
|
-
abortControllerRef.current = new AbortController();
|
|
2211
|
-
try {
|
|
2212
|
-
// Get personality mode for consistent classifier tone
|
|
2213
|
-
const sessionMgrPreflight = getSessionManager();
|
|
2214
|
-
const preflightPersonality = sessionMgrPreflight.getEffectivePersonalityMode();
|
|
2215
|
-
// Convert conversation history to Message format for analyzer context
|
|
2216
|
-
const conversationForAnalyzer = conversation
|
|
2217
|
-
.filter(entry => entry.role === 'user' || entry.role === 'assistant')
|
|
2218
|
-
.map(entry => ({
|
|
2219
|
-
role: entry.role,
|
|
2220
|
-
content: entry.content
|
|
2221
|
-
}));
|
|
2222
|
-
const preflightResult = await checkPlanModeFallback(input, false, // LLM hasn't called plan mode yet
|
|
2223
|
-
projectRoot, preflightPersonality, abortControllerRef.current?.signal, // Pass abort signal for cancellation
|
|
2224
|
-
conversationForAnalyzer // Pass conversation history for context
|
|
2225
|
-
);
|
|
2226
|
-
// Handle NO_ACTION mode - display explanation and skip LLM call
|
|
2227
|
-
if (preflightResult.noActionNeeded) {
|
|
2228
|
-
logger.info('Pre-flight complexity check: no action needed', {
|
|
2229
|
-
confidence: preflightResult.confidence,
|
|
2230
|
-
explanation: preflightResult.noActionExplanation,
|
|
2231
|
-
});
|
|
2232
|
-
// Build a response message for the user
|
|
2233
|
-
const noActionResponse = preflightResult.noActionExplanation || preflightResult.reasoning;
|
|
2234
|
-
// Add assistant response to conversation
|
|
2235
|
-
const assistantEntry = {
|
|
2236
|
-
id: `assistant-${Date.now()}`,
|
|
2237
|
-
role: 'assistant',
|
|
2238
|
-
content: noActionResponse,
|
|
2239
|
-
timestamp: new Date(),
|
|
2240
|
-
};
|
|
2241
|
-
setConversation((prev) => [...prev, assistantEntry]);
|
|
2242
|
-
// Save to session manager
|
|
2243
|
-
const sessionManager = getSessionManager();
|
|
2244
|
-
sessionManager.addMessage({ role: 'user', content: input });
|
|
2245
|
-
sessionManager.addMessage({ role: 'assistant', content: noActionResponse });
|
|
2246
|
-
// Clear streaming state and stop processing
|
|
2247
|
-
setIsProcessing(false);
|
|
2248
|
-
setSessionPhase('idle');
|
|
2249
|
-
// Skip the LLM call entirely
|
|
2250
|
-
return;
|
|
2251
|
-
}
|
|
2252
|
-
// Handle CLARIFICATION mode - ask user questions before proceeding
|
|
2253
|
-
if (preflightResult.needsClarification && preflightResult.clarificationQuestions && preflightResult.clarificationQuestions.length > 0) {
|
|
2254
|
-
logger.info('Pre-flight complexity check: clarification needed', {
|
|
2255
|
-
confidence: preflightResult.confidence,
|
|
2256
|
-
questionCount: preflightResult.clarificationQuestions.length,
|
|
2257
|
-
});
|
|
2258
|
-
// Request clarification from user via the wizard
|
|
2259
|
-
const clarificationResponse = await approvalManager.requestClarification(input.substring(0, 200), // Task summary (truncated)
|
|
2260
|
-
preflightResult.clarificationQuestions, preflightResult.reasoning);
|
|
2261
|
-
if (clarificationResponse.completed && clarificationResponse.answers && clarificationResponse.answers.length > 0) {
|
|
2262
|
-
// Format answers for LLM context
|
|
2263
|
-
const answersContext = clarificationResponse.answers
|
|
2264
|
-
.map(a => `- ${a.questionId}: ${Array.isArray(a.answer) ? a.answer.join(', ') : a.answer}`)
|
|
2265
|
-
.join('\n');
|
|
2266
|
-
const clarificationContext = `[USER CLARIFICATION - IMPORTANT]
|
|
2267
|
-
The user has answered the following clarification questions about their request:
|
|
2268
|
-
|
|
2269
|
-
${answersContext}
|
|
2270
|
-
|
|
2271
|
-
Please incorporate these preferences into your implementation.`;
|
|
2272
|
-
// Insert clarification context before the user's message
|
|
2273
|
-
messages.splice(messages.length - 1, 0, {
|
|
2274
|
-
role: 'user',
|
|
2275
|
-
content: clarificationContext,
|
|
2276
|
-
});
|
|
2277
|
-
logger.info('Clarification answers injected into context', {
|
|
2278
|
-
answerCount: clarificationResponse.answers.length,
|
|
2279
|
-
});
|
|
2280
|
-
}
|
|
2281
|
-
else {
|
|
2282
|
-
// User cancelled clarification - continue without answers
|
|
2283
|
-
logger.info('Clarification cancelled by user, proceeding without');
|
|
2284
|
-
}
|
|
2285
|
-
}
|
|
2286
|
-
// Handle SPAWN_AGENT mode - inject guidance so the agentic loop calls spawn_task
|
|
2287
|
-
if (preflightResult.shouldSpawnAgent && preflightResult.spawnAgentName && preflightResult.spawnAgentTask) {
|
|
2288
|
-
logger.info('Pre-flight complexity check: advising agent spawn via agentic loop', {
|
|
2289
|
-
confidence: preflightResult.confidence,
|
|
2290
|
-
agentName: preflightResult.spawnAgentName,
|
|
2291
|
-
reasoning: preflightResult.reasoning,
|
|
2292
|
-
});
|
|
2293
|
-
const spawnAgentAdvice = `[SYSTEM GUIDANCE - IMPORTANT]
|
|
2294
|
-
This task has been analyzed and should be delegated to a specialized sub-agent (confidence: ${preflightResult.confidence}%).
|
|
2295
|
-
${preflightResult.reasoning}
|
|
2296
|
-
|
|
2297
|
-
You MUST use the \`spawn_task\` tool to delegate this task with the following parameters:
|
|
2298
|
-
- agent_name: "${preflightResult.spawnAgentName}"
|
|
2299
|
-
- task: "${preflightResult.spawnAgentTask.replace(/"/g, '\\"')}"
|
|
2300
|
-
|
|
2301
|
-
Do NOT attempt to handle this task directly. Use spawn_task immediately.`;
|
|
2302
|
-
messages.splice(messages.length - 1, 0, {
|
|
2303
|
-
role: 'user',
|
|
2304
|
-
content: spawnAgentAdvice,
|
|
2305
|
-
});
|
|
2306
|
-
}
|
|
2307
|
-
if (preflightResult.shouldSuggestPlanMode && preflightResult.suggestedPrompt) {
|
|
2308
|
-
logger.info('Pre-flight complexity check: advising plan mode', {
|
|
2309
|
-
confidence: preflightResult.confidence,
|
|
2310
|
-
detectedMode: preflightResult.detectedMode,
|
|
2311
|
-
});
|
|
2312
|
-
// Inject strong guidance BEFORE the user's request
|
|
2313
|
-
const planModeAdvice = `[SYSTEM GUIDANCE - IMPORTANT]
|
|
2314
|
-
This task has been analyzed as complex (confidence: ${preflightResult.confidence}%).
|
|
2315
|
-
${preflightResult.reasoning}
|
|
2316
|
-
|
|
2317
|
-
You MUST use the \`enter_plan_mode\` tool FIRST before taking any other actions.
|
|
2318
|
-
Do NOT directly create files or execute commands without planning first.
|
|
2319
|
-
|
|
2320
|
-
After calling enter_plan_mode, wait for the plan to be approved before proceeding.`;
|
|
2321
|
-
// Insert advice before the user's message
|
|
2322
|
-
messages.splice(messages.length - 1, 0, {
|
|
2323
|
-
role: 'user',
|
|
2324
|
-
content: planModeAdvice,
|
|
2325
|
-
});
|
|
2326
|
-
}
|
|
2327
|
-
}
|
|
2328
|
-
catch (error) {
|
|
2329
|
-
// Check if this was an abort (user cancelled)
|
|
2330
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2331
|
-
const isAbort = errorMessage === 'Request aborted' ||
|
|
2332
|
-
(error instanceof Error && error.name === 'AbortError') ||
|
|
2333
|
-
abortLoopRef.current;
|
|
2334
|
-
if (isAbort) {
|
|
2335
|
-
logger.debug('Pre-flight complexity check cancelled by user');
|
|
2336
|
-
setIsProcessing(false);
|
|
2337
|
-
setSessionPhase('idle');
|
|
2338
|
-
return; // Exit early, cancellation message already shown
|
|
2339
|
-
}
|
|
2340
|
-
logger.warn('Pre-flight complexity check failed', { error });
|
|
2341
|
-
// Continue without advice on error
|
|
2342
|
-
}
|
|
2343
|
-
} // end of else block for non-follow-up messages
|
|
2344
|
-
}
|
|
2345
|
-
else {
|
|
2346
|
-
// DIRECT MODE: Skip all complexity checks and planning
|
|
2347
|
-
logger.debug('DIRECT mode active - skipping all complexity checks');
|
|
2348
|
-
}
|
|
2349
|
-
// Track agentic loop data for logging
|
|
2350
|
-
const loopStartTime = Date.now();
|
|
2351
|
-
const agenticIterations = [];
|
|
2352
|
-
let totalToolCalls = 0;
|
|
2353
|
-
const totalUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
2354
|
-
const totalCost = { inputCost: 0, outputCost: 0, totalCost: 0 };
|
|
2355
|
-
// Load agentic configuration
|
|
2356
|
-
const configManager = getConfigManager();
|
|
2357
|
-
const agenticConfig = configManager.getValue('agentic');
|
|
2358
|
-
const maxIterations = agenticConfig?.maxIterations ?? DEFAULT_MAX_AGENTIC_ITERATIONS;
|
|
2359
|
-
const maxIterationsPerTask = agenticConfig?.maxIterationsPerTask ?? DEFAULT_MAX_ITERATIONS_PER_TASK;
|
|
2360
|
-
const costWarningThreshold = agenticConfig?.costWarningThreshold ?? 5.0;
|
|
2361
|
-
const costHardCap = agenticConfig?.costHardCap ?? 0;
|
|
2362
|
-
const autoCompactPercent = agenticConfig?.autoCompactAtPercent ?? 80;
|
|
2363
|
-
const stuckDetectionEnabled = agenticConfig?.stuckDetectionEnabled ?? true;
|
|
2364
|
-
const stuckDetectionThreshold = agenticConfig?.stuckDetectionThreshold ?? 3;
|
|
2365
|
-
// Get model context window for auto-compaction
|
|
2366
|
-
const modelConfig = getModelConfig(client.getCurrentModelId());
|
|
2367
|
-
const contextWindow = modelConfig?.contextWindow ?? 200000;
|
|
2368
|
-
// Stuck detection tracking
|
|
2369
|
-
const recentToolCalls = [];
|
|
2370
|
-
let costWarningShown = false;
|
|
2371
|
-
let lastProactiveCompactionIter = -Infinity; // Iteration when proactive compaction last ran
|
|
2372
|
-
const PROACTIVE_COMPACT_MIN_INTERVAL = 10; // Minimum iterations between proactive compactions
|
|
2373
|
-
let lastCompactionFailureTime = 0; // Cooldown tracking for compaction failures
|
|
2374
|
-
const COMPACTION_COOLDOWN_MS = 30_000; // 30s cooldown after a compaction failure
|
|
2375
|
-
// Helper: Re-inject active plan context after compaction so the LLM
|
|
2376
|
-
// retains structured plan/task ID references that survive summarization.
|
|
2377
|
-
const reinjectPlanContextAfterCompaction = async (msgs) => {
|
|
2378
|
-
try {
|
|
2379
|
-
const planPersistence = getPlanPersistence(projectRoot);
|
|
2380
|
-
const activePlanId = planPersistence.getActivePlanId();
|
|
2381
|
-
if (activePlanId) {
|
|
2382
|
-
const pendingContext = await planPersistence.getPendingTasksContext(activePlanId);
|
|
2383
|
-
if (pendingContext) {
|
|
2384
|
-
// Append plan context to the compaction summary message to avoid
|
|
2385
|
-
// message alternation issues. The summary is always the user message
|
|
2386
|
-
// containing "[CONTEXT SUMMARY".
|
|
2387
|
-
const summaryIdx = msgs.findIndex((m) => m.role === 'user' && typeof m.content === 'string' && m.content.includes('[CONTEXT SUMMARY'));
|
|
2388
|
-
if (summaryIdx >= 0 && typeof msgs[summaryIdx].content === 'string') {
|
|
2389
|
-
msgs[summaryIdx] = {
|
|
2390
|
-
...msgs[summaryIdx],
|
|
2391
|
-
content: msgs[summaryIdx].content + '\n\n' + pendingContext.formattedContext,
|
|
2392
|
-
};
|
|
2393
|
-
}
|
|
2394
|
-
else {
|
|
2395
|
-
// No summary message found (e.g., crude truncation) — append as last user message
|
|
2396
|
-
const lastUserIdx = msgs.reduce((acc, m, i) => m.role === 'user' ? i : acc, -1);
|
|
2397
|
-
if (lastUserIdx >= 0 && typeof msgs[lastUserIdx].content === 'string') {
|
|
2398
|
-
msgs[lastUserIdx] = {
|
|
2399
|
-
...msgs[lastUserIdx],
|
|
2400
|
-
content: msgs[lastUserIdx].content + '\n\n' + pendingContext.formattedContext,
|
|
2401
|
-
};
|
|
2402
|
-
}
|
|
2403
|
-
}
|
|
2404
|
-
logger.info(`Re-injected plan context after compaction (plan: ${activePlanId}, ${pendingContext.completedTasks}/${pendingContext.totalTasks} done)`);
|
|
2405
|
-
}
|
|
2406
|
-
}
|
|
2407
|
-
}
|
|
2408
|
-
catch (planErr) {
|
|
2409
|
-
logger.debug('Failed to re-inject plan context after compaction', { error: planErr });
|
|
2410
|
-
}
|
|
2411
|
-
};
|
|
2412
|
-
// Agentic loop: call LLM, execute tools, feed results back
|
|
2413
|
-
setSessionPhase('executing');
|
|
2414
|
-
let iterations = 0;
|
|
2415
|
-
let accumulatedText = '';
|
|
2416
|
-
let finalResponse = '';
|
|
2417
|
-
let reachedMaxIterations = false;
|
|
2418
|
-
let planModeTriggered = false; // Track if enter_plan_mode tool was called
|
|
2419
|
-
let fallbackCheckDone = false; // Only run fallback once
|
|
2420
|
-
let stuckDetected = false;
|
|
2421
|
-
let costCapExceeded = false;
|
|
2422
|
-
let wasAborted = false;
|
|
2423
|
-
while (iterations < maxIterations &&
|
|
2424
|
-
(!isPlanMode || iterationScoper.shouldContinueIteration(iterations))) {
|
|
2425
|
-
// Check abort flag at start of each iteration (set by Esc during clarification or double-Esc)
|
|
2426
|
-
if (abortLoopRef.current) {
|
|
2427
|
-
logger.info('Agentic loop aborted by user');
|
|
2428
|
-
abortLoopRef.current = false; // Reset for next request
|
|
2429
|
-
wasAborted = true;
|
|
2430
|
-
break;
|
|
2431
|
-
}
|
|
2432
|
-
iterations++;
|
|
2433
|
-
iterationScoper.incrementIteration();
|
|
2434
|
-
logger.debug(`Agentic loop iteration ${iterations}`);
|
|
2435
|
-
// SLIDING WINDOW CHECK: Compact if message count exceeds limit (first line of defense)
|
|
2436
|
-
const compactionOnCooldown = lastCompactionFailureTime > 0 && (Date.now() - lastCompactionFailureTime) < COMPACTION_COOLDOWN_MS;
|
|
2437
|
-
if (messages.length > SLIDING_WINDOW_MAX_MESSAGES) {
|
|
2438
|
-
if (compactionOnCooldown) {
|
|
2439
|
-
logger.debug('Sliding window compaction skipped (cooldown active)');
|
|
2440
|
-
}
|
|
2441
|
-
else {
|
|
2442
|
-
logger.info(`Sliding window: ${messages.length} messages > ${SLIDING_WINDOW_MAX_MESSAGES}, compacting`);
|
|
2443
|
-
try {
|
|
2444
|
-
const compactor = await getConversationCompactor();
|
|
2445
|
-
await compactor.initialize();
|
|
2446
|
-
const compactionResult = await compactor.compactApiMessages(messages, 10, 6000);
|
|
2447
|
-
if (compactionResult.messagesCompacted > 0) {
|
|
2448
|
-
messages.length = 0;
|
|
2449
|
-
messages.push(...compactionResult.compactedMessages);
|
|
2450
|
-
const sessionManager = getSessionManager();
|
|
2451
|
-
sessionManager.syncAfterCompaction(compactionResult.summary, compactionResult.compactedMessages.length - 2);
|
|
2452
|
-
await reinjectPlanContextAfterCompaction(messages);
|
|
2453
|
-
lastProactiveCompactionIter = iterations;
|
|
2454
|
-
lastCompactionFailureTime = 0; // Reset on success
|
|
2455
|
-
}
|
|
2456
|
-
}
|
|
2457
|
-
catch (compactError) {
|
|
2458
|
-
logger.warn('Sliding window compaction failed', { error: compactError });
|
|
2459
|
-
lastCompactionFailureTime = Date.now();
|
|
2460
|
-
}
|
|
2461
|
-
}
|
|
2462
|
-
}
|
|
2463
|
-
// PRE-API CHECK: Estimate message size and apply intelligent compaction if needed
|
|
2464
|
-
// This prevents the 200k token limit error
|
|
2465
|
-
const estimatedMessageTokens = estimateMessagesArrayTokens(messages);
|
|
2466
|
-
const estimatedSystemTokens = estimateSystemPromptTokens(systemPrompt);
|
|
2467
|
-
const estimatedTotalTokens = estimatedMessageTokens + estimatedSystemTokens + DEFAULT_MAX_TOKENS; // Reserve space for output
|
|
2468
|
-
const maxAllowedTokens = Math.floor(contextWindow * (CONTEXT_WINDOW_SAFETY_MARGIN / 100));
|
|
2469
|
-
const proactiveCompactThreshold = Math.floor(contextWindow * 0.70); // Trigger at 70% to avoid emergency
|
|
2470
|
-
// Sync estimated context tokens with session for accurate compaction checks
|
|
2471
|
-
const sessionManager = getSessionManager();
|
|
2472
|
-
sessionManager.updateEstimatedContextTokens(estimatedMessageTokens);
|
|
2473
|
-
// Log compaction evaluation on every 5th iteration or when approaching thresholds
|
|
2474
|
-
const contextUsagePct = Math.round((estimatedTotalTokens / contextWindow) * 100);
|
|
2475
|
-
if (iterations % 5 === 0 || contextUsagePct >= 50) {
|
|
2476
|
-
logger.debug(`Compaction eval [iter ${iterations}]: ${estimatedTotalTokens.toLocaleString()} tokens (${contextUsagePct}% of ${contextWindow.toLocaleString()}) | ` +
|
|
2477
|
-
`thresholds: proactive=${proactiveCompactThreshold.toLocaleString()} (70%), emergency=${maxAllowedTokens.toLocaleString()} (90%) | ` +
|
|
2478
|
-
`messages=${messages.length}, itersSinceCompaction=${iterations - lastProactiveCompactionIter}`);
|
|
2479
|
-
}
|
|
2480
|
-
// Proactive compaction at 70% context usage (percentage-based only).
|
|
2481
|
-
// Uses an iteration-based interval instead of a one-shot flag so compaction
|
|
2482
|
-
// can re-trigger in long-running agentic loops that keep accumulating context.
|
|
2483
|
-
const iterationsSinceLastCompaction = iterations - lastProactiveCompactionIter;
|
|
2484
|
-
const shouldProactiveCompact = iterationsSinceLastCompaction >= PROACTIVE_COMPACT_MIN_INTERVAL &&
|
|
2485
|
-
messages.length > 8 &&
|
|
2486
|
-
estimatedTotalTokens > proactiveCompactThreshold;
|
|
2487
|
-
if (shouldProactiveCompact) {
|
|
2488
|
-
if (compactionOnCooldown) {
|
|
2489
|
-
logger.debug('Proactive compaction skipped (cooldown active)');
|
|
2490
|
-
}
|
|
2491
|
-
else {
|
|
2492
|
-
logger.info(`Proactive compaction triggered: ${contextUsagePct}% context usage`);
|
|
2493
|
-
lastProactiveCompactionIter = iterations;
|
|
2494
|
-
try {
|
|
2495
|
-
const compactor = await getConversationCompactor();
|
|
2496
|
-
await compactor.initialize();
|
|
2497
|
-
const compactionResult = await compactor.compactApiMessages(messages, 10, 6000);
|
|
2498
|
-
if (compactionResult.messagesCompacted > 0) {
|
|
2499
|
-
logger.info(`Proactive compaction: ${compactionResult.tokensBefore} → ${compactionResult.tokensAfter} tokens (${compactionResult.messagesCompacted} messages compacted)`);
|
|
2500
|
-
messages.length = 0;
|
|
2501
|
-
messages.push(...compactionResult.compactedMessages);
|
|
2502
|
-
// Sync with session to prevent state drift
|
|
2503
|
-
const sessionManager = getSessionManager();
|
|
2504
|
-
sessionManager.syncAfterCompaction(compactionResult.summary, compactionResult.compactedMessages.length - 2); // -2 for summary message and original
|
|
2505
|
-
await reinjectPlanContextAfterCompaction(messages);
|
|
2506
|
-
lastCompactionFailureTime = 0; // Reset on success
|
|
2507
|
-
}
|
|
2508
|
-
}
|
|
2509
|
-
catch (compactError) {
|
|
2510
|
-
logger.warn('Proactive compaction failed, will use emergency fallback if needed', { error: compactError });
|
|
2511
|
-
lastCompactionFailureTime = Date.now();
|
|
2512
|
-
}
|
|
2513
|
-
}
|
|
2514
|
-
}
|
|
2515
|
-
// Emergency compaction at 90% - must succeed to continue
|
|
2516
|
-
if (estimatedTotalTokens > maxAllowedTokens) {
|
|
2517
|
-
logger.warn(`Pre-API check: estimated ${estimatedTotalTokens} tokens exceeds ${maxAllowedTokens} limit (${CONTEXT_WINDOW_SAFETY_MARGIN}% of ${contextWindow})`);
|
|
2518
|
-
const minMessagesToKeep = 4;
|
|
2519
|
-
if (messages.length > minMessagesToKeep) {
|
|
2520
|
-
// Try intelligent compaction first
|
|
2521
|
-
try {
|
|
2522
|
-
const compactor = await getConversationCompactor();
|
|
2523
|
-
await compactor.initialize();
|
|
2524
|
-
const compactionResult = await compactor.compactApiMessages(messages, 6, 4000);
|
|
2525
|
-
if (compactionResult.messagesCompacted > 0) {
|
|
2526
|
-
const newEstimate = estimateMessagesArrayTokens(compactionResult.compactedMessages) + estimatedSystemTokens + DEFAULT_MAX_TOKENS;
|
|
2527
|
-
if (newEstimate < maxAllowedTokens) {
|
|
2528
|
-
logger.info(`Emergency intelligent compaction: ${compactionResult.tokensBefore} → ${compactionResult.tokensAfter} tokens`);
|
|
2529
|
-
messages.length = 0;
|
|
2530
|
-
messages.push(...compactionResult.compactedMessages);
|
|
2531
|
-
// Sync with session to prevent state drift
|
|
2532
|
-
const sessionManager = getSessionManager();
|
|
2533
|
-
sessionManager.syncAfterCompaction(compactionResult.summary, compactionResult.compactedMessages.length - 2);
|
|
2534
|
-
await reinjectPlanContextAfterCompaction(messages);
|
|
2535
|
-
}
|
|
2536
|
-
else {
|
|
2537
|
-
// Compaction wasn't enough, fall back to crude truncation
|
|
2538
|
-
throw new Error('Compaction insufficient');
|
|
2539
|
-
}
|
|
2540
|
-
}
|
|
2541
|
-
else {
|
|
2542
|
-
throw new Error('Nothing to compact');
|
|
2543
|
-
}
|
|
2544
|
-
}
|
|
2545
|
-
catch (compactError) {
|
|
2546
|
-
// Fall back to crude truncation as last resort
|
|
2547
|
-
logger.warn('Emergency compaction failed, using crude truncation', { error: compactError });
|
|
2548
|
-
const originalFirstMessage = messages[0];
|
|
2549
|
-
const recentMessages = messages.slice(-Math.min(messages.length - 1, 4));
|
|
2550
|
-
const truncatedMessages = [
|
|
2551
|
-
originalFirstMessage,
|
|
2552
|
-
{
|
|
2553
|
-
role: 'user',
|
|
2554
|
-
content: `[EMERGENCY: Context severely truncated. ${messages.length - recentMessages.length - 1} earlier messages removed. Key context may be lost - please ask if you need clarification on previous work.]`,
|
|
2555
|
-
},
|
|
2556
|
-
...recentMessages,
|
|
2557
|
-
];
|
|
2558
|
-
const newEstimate = estimateMessagesArrayTokens(truncatedMessages) + estimatedSystemTokens + DEFAULT_MAX_TOKENS;
|
|
2559
|
-
if (newEstimate < maxAllowedTokens) {
|
|
2560
|
-
logger.info(`Crude truncation: ${messages.length} -> ${truncatedMessages.length} messages`);
|
|
2561
|
-
messages.length = 0;
|
|
2562
|
-
messages.push(...truncatedMessages);
|
|
2563
|
-
await reinjectPlanContextAfterCompaction(messages);
|
|
2564
|
-
}
|
|
2565
|
-
else {
|
|
2566
|
-
// Absolute last resort - keep only original request
|
|
2567
|
-
logger.error('All compaction strategies failed, keeping minimal context');
|
|
2568
|
-
messages.length = 0;
|
|
2569
|
-
messages.push(originalFirstMessage, {
|
|
2570
|
-
role: 'user',
|
|
2571
|
-
content: '[CRITICAL: All context removed due to size limits. Please summarize what you remember and ask for clarification.]',
|
|
2572
|
-
});
|
|
2573
|
-
await reinjectPlanContextAfterCompaction(messages);
|
|
2574
|
-
}
|
|
2575
|
-
}
|
|
2576
|
-
}
|
|
2577
|
-
}
|
|
2578
|
-
// Create AbortController for this request so Esc can cancel immediately
|
|
2579
|
-
abortControllerRef.current = new AbortController();
|
|
2580
|
-
// Call Claude with tools
|
|
2581
|
-
const response = await client.createAgenticMessage(messages, {
|
|
2582
|
-
system: systemPrompt,
|
|
2583
|
-
tools,
|
|
2584
|
-
maxTokens: DEFAULT_MAX_TOKENS,
|
|
2585
|
-
abortSignal: abortControllerRef.current.signal,
|
|
2586
|
-
}, (chunk) => {
|
|
2587
|
-
// Stream text chunks to UI (display handled by streamBlocksRef)
|
|
2588
|
-
accumulatedText += chunk;
|
|
2589
|
-
// Append text chunk using optimized in-place update (avoids full array copy)
|
|
2590
|
-
appendTextChunk(chunk);
|
|
2591
|
-
}, (toolEvent) => {
|
|
2592
|
-
// Handle tool streaming events for real-time visibility
|
|
2593
|
-
if (toolEvent.type === 'tool_start') {
|
|
2594
|
-
// Initialize trackers for this tool
|
|
2595
|
-
toolPartialJsonRef.current.set(toolEvent.id, '');
|
|
2596
|
-
toolLastParamCountRef.current.set(toolEvent.id, 0);
|
|
2597
|
-
// Add tool block in "generating" status as soon as we know the tool name
|
|
2598
|
-
streamBlocksRef.current.push(createToolBlock(toolEvent.id, toolEvent.name, {}, 'generating'));
|
|
2599
|
-
scheduleRender();
|
|
2600
|
-
}
|
|
2601
|
-
else if (toolEvent.type === 'tool_input_delta') {
|
|
2602
|
-
// Accumulate partial JSON and extract available params for display
|
|
2603
|
-
const currentJson = toolPartialJsonRef.current.get(toolEvent.id) || '';
|
|
2604
|
-
const newJson = currentJson + toolEvent.partialInput;
|
|
2605
|
-
toolPartialJsonRef.current.set(toolEvent.id, newJson);
|
|
2606
|
-
// Debounce: only attempt parse every 50 chars to avoid calling
|
|
2607
|
-
// JSON.parse() (which will throw) on every tiny delta event.
|
|
2608
|
-
const lastParsedLen = toolLastParsedLenRef.current.get(toolEvent.id) || 0;
|
|
2609
|
-
if (newJson.length - lastParsedLen < 50) {
|
|
2610
|
-
// Skip parsing until enough new data accumulated
|
|
2611
|
-
}
|
|
2612
|
-
else {
|
|
2613
|
-
toolLastParsedLenRef.current.set(toolEvent.id, newJson.length);
|
|
2614
|
-
const partialParams = tryParsePartialJson(newJson);
|
|
2615
|
-
const paramCount = Object.keys(partialParams).length;
|
|
2616
|
-
const lastParamCount = toolLastParamCountRef.current.get(toolEvent.id) || 0;
|
|
2617
|
-
// Only update if we have new params (param count increased)
|
|
2618
|
-
if (paramCount > lastParamCount) {
|
|
2619
|
-
toolLastParamCountRef.current.set(toolEvent.id, paramCount);
|
|
2620
|
-
updateToolBlockById(toolEvent.id, { params: partialParams }, 'generating');
|
|
2621
|
-
}
|
|
2622
|
-
}
|
|
2623
|
-
}
|
|
2624
|
-
else if (toolEvent.type === 'tool_complete') {
|
|
2625
|
-
// Clean up trackers
|
|
2626
|
-
toolPartialJsonRef.current.delete(toolEvent.id);
|
|
2627
|
-
toolLastParamCountRef.current.delete(toolEvent.id);
|
|
2628
|
-
toolLastParsedLenRef.current.delete(toolEvent.id);
|
|
2629
|
-
// Update tool block to "pending" with full params when generation complete
|
|
2630
|
-
updateToolBlockById(toolEvent.id, { params: toolEvent.input, status: 'pending' });
|
|
2631
|
-
}
|
|
2632
|
-
});
|
|
2633
|
-
logger.debug(`Response stop_reason: ${response.stopReason}, tool calls: ${response.toolUseBlocks.length}`);
|
|
2634
|
-
// Accumulate usage and cost
|
|
2635
|
-
totalUsage.inputTokens += response.usage.inputTokens;
|
|
2636
|
-
totalUsage.outputTokens += response.usage.outputTokens;
|
|
2637
|
-
totalUsage.totalTokens += response.usage.totalTokens;
|
|
2638
|
-
totalCost.inputCost += response.cost.inputCost;
|
|
2639
|
-
totalCost.outputCost += response.cost.outputCost;
|
|
2640
|
-
totalCost.totalCost += response.cost.totalCost;
|
|
2641
|
-
// Cost warning check (internal tracking only, not shown in UI)
|
|
2642
|
-
if (!costWarningShown && totalCost.totalCost >= costWarningThreshold) {
|
|
2643
|
-
logger.debug(`Agentic loop cost warning: $${totalCost.totalCost.toFixed(2)} exceeds threshold of $${costWarningThreshold.toFixed(2)}`);
|
|
2644
|
-
costWarningShown = true;
|
|
2645
|
-
}
|
|
2646
|
-
// Cost hard cap check
|
|
2647
|
-
if (costHardCap > 0 && totalCost.totalCost >= costHardCap) {
|
|
2648
|
-
logger.warn(`Agentic loop stopped: cost hard cap of $${costHardCap.toFixed(2)} exceeded`);
|
|
2649
|
-
costCapExceeded = true;
|
|
2650
|
-
finalResponse = accumulatedText + `\n\n⛔ *Session stopped: Cost cap of $${costHardCap.toFixed(2)} reached*`;
|
|
2651
|
-
break;
|
|
2652
|
-
}
|
|
2653
|
-
// Context window usage check for auto-compaction (post-API check using LAST call's input tokens, not cumulative)
|
|
2654
|
-
const postApiContextUsagePct = (response.usage.inputTokens / contextWindow) * 100;
|
|
2655
|
-
const postApiItersSinceCompaction = iterations - lastProactiveCompactionIter;
|
|
2656
|
-
const shouldPostApiCompact = postApiItersSinceCompaction >= PROACTIVE_COMPACT_MIN_INTERVAL &&
|
|
2657
|
-
postApiContextUsagePct >= autoCompactPercent;
|
|
2658
|
-
if (shouldPostApiCompact) {
|
|
2659
|
-
const postApiCooldown = lastCompactionFailureTime > 0 && (Date.now() - lastCompactionFailureTime) < COMPACTION_COOLDOWN_MS;
|
|
2660
|
-
if (postApiCooldown) {
|
|
2661
|
-
logger.debug('Post-API compaction skipped (cooldown active)');
|
|
2662
|
-
}
|
|
2663
|
-
else {
|
|
2664
|
-
logger.info(`Post-API compaction triggered: ${postApiContextUsagePct.toFixed(1)}% context usage`);
|
|
2665
|
-
lastProactiveCompactionIter = iterations;
|
|
2666
|
-
try {
|
|
2667
|
-
const compactor = await getConversationCompactor();
|
|
2668
|
-
await compactor.initialize();
|
|
2669
|
-
// Use API messages compaction directly since that's what's actually growing
|
|
2670
|
-
const compactionResult = await compactor.compactApiMessages(messages, 10, 6000);
|
|
2671
|
-
if (compactionResult.messagesCompacted > 0) {
|
|
2672
|
-
logger.info(`Post-API compaction: ${compactionResult.tokensBefore} → ${compactionResult.tokensAfter} tokens`);
|
|
2673
|
-
messages.length = 0;
|
|
2674
|
-
messages.push(...compactionResult.compactedMessages);
|
|
2675
|
-
// Sync with session
|
|
2676
|
-
const sessionManager = getSessionManager();
|
|
2677
|
-
sessionManager.syncAfterCompaction(compactionResult.summary, compactionResult.compactedMessages.length - 2);
|
|
2678
|
-
await reinjectPlanContextAfterCompaction(messages);
|
|
2679
|
-
lastCompactionFailureTime = 0; // Reset on success
|
|
2680
|
-
}
|
|
2681
|
-
}
|
|
2682
|
-
catch (compactError) {
|
|
2683
|
-
logger.warn('Post-API compaction failed', { error: compactError });
|
|
2684
|
-
lastCompactionFailureTime = Date.now();
|
|
2685
|
-
}
|
|
2686
|
-
}
|
|
2687
|
-
}
|
|
2688
|
-
// Handle max_tokens - prompt continuation
|
|
2689
|
-
if (response.stopReason === 'max_tokens') {
|
|
2690
|
-
logger.warn('Response truncated due to max_tokens - prompting continuation');
|
|
2691
|
-
agenticIterations.push({
|
|
2692
|
-
iteration: iterations,
|
|
2693
|
-
llm_call: {
|
|
2694
|
-
messages_count: messages.length,
|
|
2695
|
-
response_text: response.textContent,
|
|
2696
|
-
tool_calls_count: 0,
|
|
2697
|
-
usage: response.usage,
|
|
2698
|
-
cost: response.cost,
|
|
2699
|
-
},
|
|
2700
|
-
tool_calls: [],
|
|
2701
|
-
stop_reason: response.stopReason,
|
|
2702
|
-
});
|
|
2703
|
-
// Add the partial response to conversation and prompt to continue
|
|
2704
|
-
messages.push({
|
|
2705
|
-
role: 'assistant',
|
|
2706
|
-
content: response.contentBlocks,
|
|
2707
|
-
});
|
|
2708
|
-
messages.push({
|
|
2709
|
-
role: 'user',
|
|
2710
|
-
content: 'Continue from where you left off. If you were about to use tools, please proceed with those tool calls now.',
|
|
2711
|
-
});
|
|
2712
|
-
continue; // Continue the loop to get the next part of the response
|
|
2713
|
-
}
|
|
2714
|
-
// If no tool calls, we're done
|
|
2715
|
-
if (response.stopReason !== 'tool_use' || response.toolUseBlocks.length === 0) {
|
|
2716
|
-
// Check queue before ending the loop — drain ALL queued messages at once
|
|
2717
|
-
if (queuedMessagesRef.current.length > 0) {
|
|
2718
|
-
// Separate slash commands from regular messages.
|
|
2719
|
-
// Regular messages get batched; the first slash command (if any) breaks the loop after.
|
|
2720
|
-
const allQueued = [...queuedMessagesRef.current];
|
|
2721
|
-
const regularMessages = [];
|
|
2722
|
-
let slashCommand = null;
|
|
2723
|
-
for (const msg of allQueued) {
|
|
2724
|
-
const parsed = parseInput(msg);
|
|
2725
|
-
if (parsed.type === 'slash') {
|
|
2726
|
-
slashCommand = msg;
|
|
2727
|
-
break; // Stop — everything before this gets batched, slash runs after
|
|
2728
|
-
}
|
|
2729
|
-
regularMessages.push(msg);
|
|
2730
|
-
}
|
|
2731
|
-
// Remove consumed messages from the queue (regular + the slash command if found)
|
|
2732
|
-
const consumed = regularMessages.length + (slashCommand ? 1 : 0);
|
|
2733
|
-
queuedMessagesRef.current = queuedMessagesRef.current.slice(consumed);
|
|
2734
|
-
setQueuedMessages([...queuedMessagesRef.current]);
|
|
2735
|
-
// If we only got a slash command with no regular messages, handle it directly
|
|
2736
|
-
if (regularMessages.length === 0 && slashCommand) {
|
|
2737
|
-
logger.info(`Queued message is a slash command: ${slashCommand}, breaking loop to execute`);
|
|
2738
|
-
agenticIterations.push({
|
|
2739
|
-
iteration: iterations,
|
|
2740
|
-
llm_call: {
|
|
2741
|
-
messages_count: messages.length,
|
|
2742
|
-
response_text: response.textContent,
|
|
2743
|
-
tool_calls_count: 0,
|
|
2744
|
-
usage: response.usage,
|
|
2745
|
-
cost: response.cost,
|
|
2746
|
-
},
|
|
2747
|
-
tool_calls: [],
|
|
2748
|
-
stop_reason: 'queue_drain_slash',
|
|
2749
|
-
});
|
|
2750
|
-
setTimeout(() => handleSubmit(slashCommand), 0);
|
|
2751
|
-
break;
|
|
2752
|
-
}
|
|
2753
|
-
// Batch all regular messages into a single well-formatted prompt
|
|
2754
|
-
const combinedMessage = regularMessages.length === 1
|
|
2755
|
-
? regularMessages[0]
|
|
2756
|
-
: regularMessages.map((msg, i) => `${i + 1}. ${msg}`).join('\n');
|
|
2757
|
-
// Add the LLM's response to messages
|
|
2758
|
-
messages.push({
|
|
2759
|
-
role: 'assistant',
|
|
2760
|
-
content: response.contentBlocks,
|
|
2761
|
-
});
|
|
2762
|
-
// Add combined queued message to conversation display
|
|
2763
|
-
const now = new Date();
|
|
2764
|
-
setConversation((prev) => [...prev,
|
|
2765
|
-
{ id: `assistant-${Date.now()}`, role: 'assistant', content: response.textContent, timestamp: now },
|
|
2766
|
-
{ id: `user-queued-${Date.now()}`, role: 'user', content: combinedMessage, timestamp: now },
|
|
2767
|
-
]);
|
|
2768
|
-
// Add user message to API messages with reinforcement framing
|
|
2769
|
-
const preamble = regularMessages.length === 1
|
|
2770
|
-
? 'The user has provided additional input while you were working. This is likely related to your current task — treat it as a follow-up instruction, correction, or new insight. Integrate it into your ongoing work naturally.'
|
|
2771
|
-
: `The user has provided ${regularMessages.length} additional messages while you were working. These are likely related to your current task — treat them as follow-up instructions, corrections, or new insights. Address all of them together in your ongoing work.`;
|
|
2772
|
-
const framedMessage = `<system-reminder>${preamble}</system-reminder>\n\n${combinedMessage}`;
|
|
2773
|
-
messages.push({ role: 'user', content: framedMessage });
|
|
2774
|
-
// Reset stream blocks for the new response
|
|
2775
|
-
resetStreamBlocks();
|
|
2776
|
-
logger.info(`Injected ${regularMessages.length} queued message(s) as batch: "${combinedMessage.substring(0, 80)}..."`);
|
|
2777
|
-
// Record iteration metrics before continuing
|
|
2778
|
-
agenticIterations.push({
|
|
2779
|
-
iteration: iterations,
|
|
2780
|
-
llm_call: {
|
|
2781
|
-
messages_count: messages.length,
|
|
2782
|
-
response_text: response.textContent,
|
|
2783
|
-
tool_calls_count: 0,
|
|
2784
|
-
usage: response.usage,
|
|
2785
|
-
cost: response.cost,
|
|
2786
|
-
},
|
|
2787
|
-
tool_calls: [],
|
|
2788
|
-
stop_reason: 'queue_drain',
|
|
2789
|
-
});
|
|
2790
|
-
// If there's a slash command waiting after the regular messages,
|
|
2791
|
-
// schedule it to run after this loop iteration completes
|
|
2792
|
-
if (slashCommand) {
|
|
2793
|
-
const cmd = slashCommand;
|
|
2794
|
-
setTimeout(() => handleSubmit(cmd), 0);
|
|
2795
|
-
// Don't break yet — continue so the LLM processes the batched messages first
|
|
2796
|
-
}
|
|
2797
|
-
continue; // Continue loop with the batched message
|
|
2798
|
-
}
|
|
2799
|
-
// Log final iteration (no tool calls)
|
|
2800
|
-
agenticIterations.push({
|
|
2801
|
-
iteration: iterations,
|
|
2802
|
-
llm_call: {
|
|
2803
|
-
messages_count: messages.length,
|
|
2804
|
-
response_text: response.textContent,
|
|
2805
|
-
tool_calls_count: 0,
|
|
2806
|
-
usage: response.usage,
|
|
2807
|
-
cost: response.cost,
|
|
2808
|
-
},
|
|
2809
|
-
tool_calls: [],
|
|
2810
|
-
stop_reason: response.stopReason,
|
|
2811
|
-
});
|
|
2812
|
-
finalResponse = response.textContent;
|
|
2813
|
-
break;
|
|
2814
|
-
}
|
|
2815
|
-
// Execute tool calls
|
|
2816
|
-
const { results: toolResults, records: toolRecords, userRejected, endTurn } = await executeToolCalls(response.toolUseBlocks);
|
|
2817
|
-
totalToolCalls += toolRecords.length;
|
|
2818
|
-
// Stuck detection: check if we're calling the same tools with the same inputs repeatedly
|
|
2819
|
-
if (stuckDetectionEnabled && response.toolUseBlocks.length > 0) {
|
|
2820
|
-
for (const toolUse of response.toolUseBlocks) {
|
|
2821
|
-
// Create a SHA-256 hash of the full tool input for accurate comparison
|
|
2822
|
-
// Using full hash ensures different content in the same file is detected
|
|
2823
|
-
const inputHash = sha256Hash(JSON.stringify(toolUse.input || {}));
|
|
2824
|
-
recentToolCalls.push({ name: toolUse.name, inputHash });
|
|
2825
|
-
}
|
|
2826
|
-
// Keep only the last N * threshold calls for comparison
|
|
2827
|
-
const maxTracked = stuckDetectionThreshold * 3;
|
|
2828
|
-
while (recentToolCalls.length > maxTracked) {
|
|
2829
|
-
recentToolCalls.shift();
|
|
2830
|
-
}
|
|
2831
|
-
// Check for stuck pattern: same tool call repeated consecutively
|
|
2832
|
-
if (recentToolCalls.length >= stuckDetectionThreshold) {
|
|
2833
|
-
const lastCalls = recentToolCalls.slice(-stuckDetectionThreshold);
|
|
2834
|
-
const firstCall = lastCalls[0];
|
|
2835
|
-
const isStuck = lastCalls.every((call) => call.name === firstCall.name && call.inputHash === firstCall.inputHash);
|
|
2836
|
-
if (isStuck) {
|
|
2837
|
-
logger.warn(`Stuck detection triggered: tool "${firstCall.name}" called ${stuckDetectionThreshold} times with same input`);
|
|
2838
|
-
stuckDetected = true;
|
|
2839
|
-
finalResponse = accumulatedText + `\n\n⚠️ *Stuck detected: The same operation was attempted ${stuckDetectionThreshold} times. Consider rephrasing your request or providing more context.*`;
|
|
2840
|
-
break;
|
|
2841
|
-
}
|
|
2842
|
-
}
|
|
2843
|
-
// Early warning: inject guidance on 2nd consecutive identical attempt (before stuck triggers)
|
|
2844
|
-
// This gives the LLM a chance to try a different approach
|
|
2845
|
-
const warningThreshold = stuckDetectionThreshold - 1;
|
|
2846
|
-
if (warningThreshold >= 2 && recentToolCalls.length >= warningThreshold) {
|
|
2847
|
-
const lastCallsForWarning = recentToolCalls.slice(-warningThreshold);
|
|
2848
|
-
const firstCallForWarning = lastCallsForWarning[0];
|
|
2849
|
-
const isRepeating = lastCallsForWarning.every((call) => call.name === firstCallForWarning.name && call.inputHash === firstCallForWarning.inputHash);
|
|
2850
|
-
if (isRepeating) {
|
|
2851
|
-
logger.info(`Early stuck warning: tool "${firstCallForWarning.name}" called ${warningThreshold} times with same input`);
|
|
2852
|
-
// Inject warning message to the LLM in the next iteration
|
|
2853
|
-
messages.push({
|
|
2854
|
-
role: 'user',
|
|
2855
|
-
content: `⚠️ SYSTEM WARNING: You have attempted the same operation (${firstCallForWarning.name}) ${warningThreshold} times with identical parameters. The operation appears to be failing repeatedly. Please try a DIFFERENT approach - consider: using different parameters, trying an alternative tool, or explaining what's blocking you so we can help. One more identical attempt will trigger stuck detection and terminate execution.`,
|
|
2856
|
-
});
|
|
2857
|
-
}
|
|
2858
|
-
}
|
|
2859
|
-
}
|
|
2860
|
-
// Check if enter_plan_mode was called
|
|
2861
|
-
let planJustApproved = false;
|
|
2862
|
-
if (response.toolUseBlocks.some((tool) => tool.name === 'enter_plan_mode')) {
|
|
2863
|
-
planModeTriggered = true;
|
|
2864
|
-
logger.debug('Plan mode tool was called by LLM');
|
|
2865
|
-
// Check if plan was approved and execution started
|
|
2866
|
-
// activePlanId is only set when startExecution() is called (after user approval)
|
|
2867
|
-
const planPersistence = getPlanPersistence(projectRoot);
|
|
2868
|
-
const activePlanId = planPersistence.getActivePlanId();
|
|
2869
|
-
if (activePlanId) {
|
|
2870
|
-
isPlanMode = true;
|
|
2871
|
-
iterationScoper.startPlanExecution(activePlanId, maxIterationsPerTask);
|
|
2872
|
-
logger.info(`Started per-task iteration tracking for plan ${activePlanId} with ${maxIterationsPerTask} iterations per task`);
|
|
2873
|
-
planJustApproved = true;
|
|
2874
|
-
}
|
|
2875
|
-
else {
|
|
2876
|
-
// Expected when plan was clarified, edited, rejected, or errored
|
|
2877
|
-
// The LLM may call enter_plan_mode again after clarification
|
|
2878
|
-
logger.debug('Plan mode tool called but no active execution (plan may need clarification/modification)');
|
|
2879
|
-
}
|
|
2880
|
-
}
|
|
2881
|
-
// If user rejected an operation, log it but continue the loop
|
|
2882
|
-
// The LLM will see the rejection error in the tool result and can decide what to do
|
|
2883
|
-
if (userRejected) {
|
|
2884
|
-
logger.info('User rejected operation - continuing agentic loop so LLM can respond');
|
|
2885
|
-
}
|
|
2886
|
-
// If a plan was cancelled, end the turn immediately — don't let the LLM continue
|
|
2887
|
-
if (endTurn) {
|
|
2888
|
-
logger.info('End turn signalled (plan cancelled) — stopping agentic loop');
|
|
2889
|
-
finalResponse = accumulatedText;
|
|
2890
|
-
break;
|
|
2891
|
-
}
|
|
2892
|
-
// Log this iteration
|
|
2893
|
-
agenticIterations.push({
|
|
2894
|
-
iteration: iterations,
|
|
2895
|
-
llm_call: {
|
|
2896
|
-
messages_count: messages.length,
|
|
2897
|
-
response_text: response.textContent,
|
|
2898
|
-
tool_calls_count: response.toolUseBlocks.length,
|
|
2899
|
-
usage: response.usage,
|
|
2900
|
-
cost: response.cost,
|
|
2901
|
-
},
|
|
2902
|
-
tool_calls: toolRecords,
|
|
2903
|
-
stop_reason: response.stopReason,
|
|
2904
|
-
});
|
|
2905
|
-
// Add assistant response (with tool use) to messages
|
|
2906
|
-
messages.push({
|
|
2907
|
-
role: 'assistant',
|
|
2908
|
-
content: response.contentBlocks,
|
|
2909
|
-
});
|
|
2910
|
-
// Add tool results as user message
|
|
2911
|
-
messages.push({
|
|
2912
|
-
role: 'user',
|
|
2913
|
-
content: toolResults,
|
|
2914
|
-
});
|
|
2915
|
-
// Add explicit continuation instruction after plan approval
|
|
2916
|
-
// This ensures the LLM continues with tool calls instead of just responding with text
|
|
2917
|
-
if (planJustApproved) {
|
|
2918
|
-
if (planApprovalClearContextRef.current) {
|
|
2919
|
-
// Full clear: keep only the last 2 messages (enter_plan_mode tool_use + plan result)
|
|
2920
|
-
const lastTwo = messages.slice(-2);
|
|
2921
|
-
messages.length = 0;
|
|
2922
|
-
messages.push(...lastTwo);
|
|
2923
|
-
planApprovalClearContextRef.current = false;
|
|
2924
|
-
setMetrics((prev) => ({ ...prev, currentContextTokens: 0 }));
|
|
2925
|
-
logger.info('Plan approval full context clear: messages reduced to 2');
|
|
2926
|
-
}
|
|
2927
|
-
else {
|
|
2928
|
-
// Partial compaction: keep last 3 messages + LLM-generated summary
|
|
2929
|
-
try {
|
|
2930
|
-
const compactor = await getConversationCompactor();
|
|
2931
|
-
await compactor.initialize();
|
|
2932
|
-
const compactionResult = await compactor.compactForPlanApproval(messages, 3);
|
|
2933
|
-
if (compactionResult.messagesCompacted > 0) {
|
|
2934
|
-
messages.length = 0;
|
|
2935
|
-
messages.push(...compactionResult.compactedMessages);
|
|
2936
|
-
const sessionManager = getSessionManager();
|
|
2937
|
-
sessionManager.syncAfterCompaction(compactionResult.summary, compactionResult.compactedMessages.length - 2);
|
|
2938
|
-
await reinjectPlanContextAfterCompaction(messages);
|
|
2939
|
-
logger.info(`Plan approval compaction: ${compactionResult.tokensBefore} → ${compactionResult.tokensAfter} tokens`);
|
|
2940
|
-
}
|
|
2941
|
-
}
|
|
2942
|
-
catch (compactError) {
|
|
2943
|
-
logger.warn('Plan approval compaction failed, continuing with full context', { error: compactError });
|
|
2944
|
-
}
|
|
2945
|
-
}
|
|
2946
|
-
messages.push({
|
|
2947
|
-
role: 'user',
|
|
2948
|
-
content: 'Plan approved by user. Proceed immediately with executing Task 1 using the appropriate tools. Do not ask for confirmation - begin implementation now.',
|
|
2949
|
-
});
|
|
2950
|
-
logger.debug('Added plan continuation instruction to messages');
|
|
2951
|
-
}
|
|
2952
|
-
// Update accumulated text (display handled by streamBlocksRef)
|
|
2953
|
-
accumulatedText = response.textContent;
|
|
2954
|
-
// Plan mode fallback check: after first iteration, if LLM didn't call enter_plan_mode,
|
|
2955
|
-
// run heuristic check and suggest plan mode if complexity is high
|
|
2956
|
-
// Only run in HYBRID mode (PLAN mode already forces planning, DIRECT skips all planning)
|
|
2957
|
-
if (executionMode === 'hybrid' && iterations === 1 && !planModeTriggered && !fallbackCheckDone) {
|
|
2958
|
-
fallbackCheckDone = true;
|
|
2959
|
-
try {
|
|
2960
|
-
// Get personality mode for consistent classifier tone
|
|
2961
|
-
const sessionMgrFallback = getSessionManager();
|
|
2962
|
-
const fallbackPersonality = sessionMgrFallback.getEffectivePersonalityMode();
|
|
2963
|
-
// Convert conversation history to Message format for analyzer context
|
|
2964
|
-
const conversationForFallback = conversation
|
|
2965
|
-
.filter(entry => entry.role === 'user' || entry.role === 'assistant')
|
|
2966
|
-
.map(entry => ({
|
|
2967
|
-
role: entry.role,
|
|
2968
|
-
content: entry.content
|
|
2969
|
-
}));
|
|
2970
|
-
const fallbackResult = await checkPlanModeFallback(input, // Original user input
|
|
2971
|
-
planModeTriggered, projectRoot, fallbackPersonality, abortControllerRef.current?.signal, // Pass abort signal for cancellation
|
|
2972
|
-
conversationForFallback // Pass conversation history for context
|
|
2973
|
-
);
|
|
2974
|
-
if (fallbackResult.shouldSuggestPlanMode && fallbackResult.suggestedPrompt) {
|
|
2975
|
-
logger.info('Plan mode fallback triggered - suggesting plan mode to LLM', {
|
|
2976
|
-
confidence: fallbackResult.confidence,
|
|
2977
|
-
detectedMode: fallbackResult.detectedMode,
|
|
2978
|
-
});
|
|
2979
|
-
// Inject system suggestion into the conversation
|
|
2980
|
-
messages.push({
|
|
2981
|
-
role: 'user',
|
|
2982
|
-
content: fallbackResult.suggestedPrompt,
|
|
2983
|
-
});
|
|
2984
|
-
}
|
|
2985
|
-
}
|
|
2986
|
-
catch (error) {
|
|
2987
|
-
// Check if this was an abort (user cancelled)
|
|
2988
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2989
|
-
const isAbort = errorMessage === 'Request aborted' ||
|
|
2990
|
-
(error instanceof Error && error.name === 'AbortError') ||
|
|
2991
|
-
abortLoopRef.current;
|
|
2992
|
-
if (isAbort) {
|
|
2993
|
-
logger.debug('Plan mode fallback check cancelled by user');
|
|
2994
|
-
wasAborted = true;
|
|
2995
|
-
break; // Exit the loop
|
|
2996
|
-
}
|
|
2997
|
-
logger.warn('Plan mode fallback check failed', { error });
|
|
2998
|
-
// Continue without suggestion on error
|
|
2999
|
-
}
|
|
3000
|
-
}
|
|
3001
|
-
// Check iteration limits at end of iteration
|
|
3002
|
-
if (iterations >= maxIterations && !stuckDetected && !costCapExceeded) {
|
|
3003
|
-
// Global iteration limit reached (not plan mode specific)
|
|
3004
|
-
logger.warn('Agentic loop reached global maximum iterations');
|
|
3005
|
-
finalResponse = accumulatedText + '\n\n*(Reached maximum tool iterations)*';
|
|
3006
|
-
reachedMaxIterations = true;
|
|
3007
|
-
break;
|
|
3008
|
-
}
|
|
3009
|
-
else if (isPlanMode && !iterationScoper.shouldContinueIteration(iterations)) {
|
|
3010
|
-
// Task-specific iteration limit reached in plan mode
|
|
3011
|
-
const status = iterationScoper.getIterationStatus();
|
|
3012
|
-
logger.warn(`Task ${status.task} reached iteration limit (${status.iteration}/${status.max})`);
|
|
3013
|
-
// Get plan details to check if there are more tasks
|
|
3014
|
-
const planPersistence = getPlanPersistence(projectRoot);
|
|
3015
|
-
const activePlanId = planPersistence.getActivePlanId();
|
|
3016
|
-
const executionSummary = activePlanId ? await planPersistence.getExecutionSummary(activePlanId) : null;
|
|
3017
|
-
if (executionSummary && executionSummary.pending > 0) {
|
|
3018
|
-
// There are more tasks - prompt to continue with next task
|
|
3019
|
-
logger.info(`${executionSummary.pending} tasks remaining after iteration limit`);
|
|
3020
|
-
// Add continuation prompt to move to next task
|
|
3021
|
-
const continuationPrompt = `\n\n*Task iteration limit reached (${status.iteration}/${status.max}). ${executionSummary.completed} of ${executionSummary.total} tasks completed.*\n\nPlease move to the next pending task and continue execution.`;
|
|
3022
|
-
accumulatedText += continuationPrompt;
|
|
3023
|
-
// Update stream blocks with continuation message
|
|
3024
|
-
updateStreamBlocks((prev) => {
|
|
3025
|
-
const lastBlock = prev[prev.length - 1];
|
|
3026
|
-
if (lastBlock && lastBlock.type === 'text') {
|
|
3027
|
-
return [...prev.slice(0, -1), { ...lastBlock, content: lastBlock.content + continuationPrompt }];
|
|
3028
|
-
}
|
|
3029
|
-
return [...prev, createTextBlock(continuationPrompt)];
|
|
3030
|
-
});
|
|
3031
|
-
// Note: response.contentBlocks was already added to messages earlier in the iteration
|
|
3032
|
-
// Just add the continuation prompt as a user message
|
|
3033
|
-
messages.push({
|
|
3034
|
-
role: 'user',
|
|
3035
|
-
content: `The current task has used its iteration budget. Please proceed with the next task from the plan. Use complete_plan_task to mark the current task status, then move to the next task.`,
|
|
3036
|
-
});
|
|
3037
|
-
// Reset task iterations for next task (will be done automatically by event)
|
|
3038
|
-
// Continue the loop
|
|
3039
|
-
continue;
|
|
3040
|
-
}
|
|
3041
|
-
else {
|
|
3042
|
-
// No more tasks or couldn't get summary - end execution
|
|
3043
|
-
const iterationLimitMessage = `\n\n*(Task "${status.task}" reached iteration limit: ${status.iteration}/${status.max}. Type "continue" to resume work if the task is not yet complete.)*`;
|
|
3044
|
-
finalResponse = accumulatedText + iterationLimitMessage;
|
|
3045
|
-
// Ensure the message is displayed to the user
|
|
3046
|
-
updateStreamBlocks((prev) => {
|
|
3047
|
-
const lastBlock = prev[prev.length - 1];
|
|
3048
|
-
if (lastBlock && lastBlock.type === 'text') {
|
|
3049
|
-
return [...prev.slice(0, -1), { ...lastBlock, content: lastBlock.content + iterationLimitMessage }];
|
|
3050
|
-
}
|
|
3051
|
-
return [...prev, createTextBlock(iterationLimitMessage)];
|
|
3052
|
-
});
|
|
3053
|
-
reachedMaxIterations = true;
|
|
3054
|
-
break;
|
|
3055
|
-
}
|
|
3056
|
-
}
|
|
3057
|
-
}
|
|
3058
|
-
// Handle user cancellation: rollback conversation state and clean up UI
|
|
3059
|
-
// This removes the user's prompt so they can submit a corrected version
|
|
3060
|
-
if (wasAborted) {
|
|
3061
|
-
logger.info('Rolling back conversation state after user cancellation');
|
|
3062
|
-
setConversation(conversationCheckpointRef.current);
|
|
3063
|
-
resetStreamBlocks();
|
|
3064
|
-
// Exit without adding any messages - finally block still runs for cleanup
|
|
3065
|
-
return;
|
|
3066
|
-
}
|
|
3067
|
-
// Log the complete agentic loop
|
|
3068
|
-
const loopDuration = Date.now() - loopStartTime;
|
|
3069
|
-
// Collect per-task iteration data if in plan mode
|
|
3070
|
-
const taskIterationData = isPlanMode
|
|
3071
|
-
? Array.from(iterationScoper.getTaskIterationHistory().entries()).map(([taskId, record]) => ({
|
|
3072
|
-
taskId,
|
|
3073
|
-
taskName: taskId, // TODO: Could enrich with actual task name from plan
|
|
3074
|
-
iterations: record.iterations,
|
|
3075
|
-
toolCalls: 0, // TODO: Could track tool calls per task
|
|
3076
|
-
}))
|
|
3077
|
-
: undefined;
|
|
3078
|
-
await auditLogger.logAgenticLoop({
|
|
3079
|
-
user_input: input,
|
|
3080
|
-
system_prompt: systemPrompt,
|
|
3081
|
-
model: client.getCurrentModel().id,
|
|
3082
|
-
iterations: agenticIterations,
|
|
3083
|
-
final_response: finalResponse,
|
|
3084
|
-
total_iterations: iterations,
|
|
3085
|
-
total_tool_calls: totalToolCalls,
|
|
3086
|
-
total_usage: totalUsage,
|
|
3087
|
-
total_cost: totalCost,
|
|
3088
|
-
duration_ms: loopDuration,
|
|
3089
|
-
reached_max_iterations: reachedMaxIterations,
|
|
3090
|
-
isPlanMode,
|
|
3091
|
-
planId: isPlanMode ? iterationScoper.getCurrentPlanId() || undefined : undefined,
|
|
3092
|
-
task_iterations: taskIterationData,
|
|
3093
|
-
});
|
|
3094
|
-
// Expose to `finally` for post-response question detection
|
|
3095
|
-
finalResponseForSuggestion = finalResponse;
|
|
3096
|
-
// Add final assistant response to conversation (skip if empty - e.g., user rejected)
|
|
3097
|
-
if (finalResponse) {
|
|
3098
|
-
// Capture stream blocks before they get reset (for history display)
|
|
3099
|
-
const capturedBlocks = [...streamBlocksRef.current];
|
|
3100
|
-
const assistantEntry = {
|
|
3101
|
-
id: `assistant-${Date.now()}`,
|
|
3102
|
-
role: 'assistant',
|
|
3103
|
-
content: finalResponse,
|
|
3104
|
-
timestamp: new Date(),
|
|
3105
|
-
blocks: capturedBlocks.length > 0 ? capturedBlocks : undefined,
|
|
3106
|
-
durationMs: loopDuration,
|
|
3107
|
-
};
|
|
3108
|
-
setConversation((prev) => [...prev, assistantEntry]);
|
|
3109
|
-
// Save to session manager
|
|
3110
|
-
const sessionManager = getSessionManager();
|
|
3111
|
-
sessionManager.addMessage({ role: 'user', content: input });
|
|
3112
|
-
sessionManager.addMessage({ role: 'assistant', content: finalResponse });
|
|
3113
|
-
}
|
|
3114
|
-
}
|
|
3115
|
-
catch (err) {
|
|
3116
|
-
// Handle abort errors gracefully - don't show as error since user cancelled
|
|
3117
|
-
if (err instanceof Error && (err.message === 'Request aborted' || err.name === 'AbortError')) {
|
|
3118
|
-
logger.info('Request cancelled by user');
|
|
3119
|
-
// Rollback conversation state on abort
|
|
3120
|
-
setConversation(conversationCheckpointRef.current);
|
|
3121
|
-
resetStreamBlocks();
|
|
3122
|
-
}
|
|
3123
|
-
else {
|
|
3124
|
-
const errorMessage = err instanceof Error ? err.message : 'Failed to get response';
|
|
3125
|
-
setError(errorMessage);
|
|
3126
|
-
logger.error('API request failed', err);
|
|
3127
|
-
await auditLogger.logError(errorMessage, err);
|
|
3128
|
-
}
|
|
3129
|
-
}
|
|
3130
|
-
finally {
|
|
3131
|
-
setIsProcessing(false);
|
|
3132
|
-
setSessionPhase('idle');
|
|
3133
|
-
// Clear complexity context after request completes
|
|
3134
|
-
approvalManager.clearComplexityContext();
|
|
3135
|
-
// Detect if the LLM ended with a question and suggest a short reply
|
|
3136
|
-
setSuggestionOffer(deriveQuestionSuggestion(finalResponseForSuggestion));
|
|
3137
|
-
// Clear temporary personality mode after response completes
|
|
3138
|
-
const sessionMgrForCleanup = getSessionManager();
|
|
3139
|
-
if (sessionMgrForCleanup.hasTemporaryPersonalityMode()) {
|
|
3140
|
-
sessionMgrForCleanup.clearTemporaryPersonalityMode();
|
|
3141
|
-
logger.debug('Temporary personality mode cleared after response');
|
|
3142
|
-
}
|
|
3143
|
-
// Stop iteration scoping if plan mode was active
|
|
3144
|
-
if (isPlanMode) {
|
|
3145
|
-
iterationScoper.stopPlanExecution();
|
|
3146
|
-
}
|
|
3147
|
-
// Pause the plan execution tracker when processing ends but plan is not complete
|
|
3148
|
-
// This hides the tracker when the LLM asks a follow-up question
|
|
3149
|
-
{
|
|
3150
|
-
const prev = activePlanRef.current;
|
|
3151
|
-
if (prev && !prev.isComplete) {
|
|
3152
|
-
activePlanRef.current = { ...prev, isPaused: true };
|
|
3153
|
-
}
|
|
3154
|
-
setActivePlan(activePlanRef.current);
|
|
3155
|
-
}
|
|
3156
|
-
}
|
|
3157
|
-
}, [conversation, projectRoot, projectSummary, executionMode]);
|
|
3158
|
-
/**
|
|
3159
|
-
* Routes prompt submissions: queues during processing, submits directly when idle
|
|
3160
|
-
* Uses refs to keep the callback identity stable across renders, preventing
|
|
3161
|
-
* React.memo on Prompt from breaking during the agentic loop (which would cause
|
|
3162
|
-
* full Prompt re-renders on every scheduleRender tick, blocking stdin events).
|
|
3163
|
-
*/
|
|
3164
|
-
const isProcessingRef = useRef(isProcessing);
|
|
3165
|
-
isProcessingRef.current = isProcessing;
|
|
3166
|
-
const handleSubmitRef = useRef(handleSubmit);
|
|
3167
|
-
handleSubmitRef.current = handleSubmit;
|
|
3168
|
-
const handlePromptSubmit = useCallback((input) => {
|
|
3169
|
-
if (isProcessingRef.current) {
|
|
3170
|
-
const trimmed = input.trim();
|
|
3171
|
-
if (!trimmed)
|
|
3172
|
-
return;
|
|
3173
|
-
// Slash commands cannot be queued — show feedback instead of silently dropping
|
|
3174
|
-
if (trimmed.startsWith('/')) {
|
|
3175
|
-
setError(`Slash commands cannot be queued while processing. Wait for the current operation to complete, then retry: ${trimmed.length > 60 ? trimmed.substring(0, 60) + '...' : trimmed}`);
|
|
3176
|
-
return;
|
|
3177
|
-
}
|
|
3178
|
-
queuedMessagesRef.current = [...queuedMessagesRef.current, trimmed];
|
|
3179
|
-
setQueuedMessages([...queuedMessagesRef.current]);
|
|
3180
|
-
return;
|
|
3181
|
-
}
|
|
3182
|
-
handleSubmitRef.current(input);
|
|
3183
|
-
}, []); // Stable identity — never changes
|
|
3184
|
-
/**
|
|
3185
|
-
* Handles approval dialog choices
|
|
3186
|
-
*/
|
|
3187
|
-
const handleApprovalChoice = useCallback((choice, userFeedback) => {
|
|
3188
|
-
const approvalManager = getApprovalManager();
|
|
3189
|
-
const request = approvalManager.getPendingRequest();
|
|
3190
|
-
// Update modalActiveRef synchronously BEFORE resolving the approval
|
|
3191
|
-
// This ensures that scheduleRender won't skip renders for tool results
|
|
3192
|
-
// that complete immediately after approval (before useEffect runs)
|
|
3193
|
-
modalActiveRef.current = false;
|
|
3194
|
-
// Handle approve_all_session: enable session auto-approval and treat as 'approve'
|
|
3195
|
-
if (choice === 'approve_all_session') {
|
|
3196
|
-
approvalManager.enableSessionAutoApproval();
|
|
3197
|
-
// Also switch to SMART mode for consistency with /smart switch
|
|
3198
|
-
const sessionManager = getSessionManager();
|
|
3199
|
-
approvalManager.setMode(ApprovalMode.SMART);
|
|
3200
|
-
sessionManager.setApprovalMode(ApprovalMode.SMART);
|
|
3201
|
-
setApprovalMode(ApprovalMode.SMART);
|
|
3202
|
-
approvalManager.resolveRequest('approve');
|
|
3203
|
-
setPendingApproval(null);
|
|
3204
|
-
logger.debug('Operation approved - Auto-approving all changes this session (SMART mode)');
|
|
3205
|
-
return;
|
|
3206
|
-
}
|
|
3207
|
-
// Handle approve_command_type: approve this command prefix for the session
|
|
3208
|
-
if (choice === 'approve_command_type') {
|
|
3209
|
-
const command = request?.details.command;
|
|
3210
|
-
if (command) {
|
|
3211
|
-
approvalManager.approveCommandPrefix(command);
|
|
3212
|
-
const prefix = approvalManager.getCommandPrefix(command);
|
|
3213
|
-
approvalManager.resolveRequest('approve');
|
|
3214
|
-
setPendingApproval(null);
|
|
3215
|
-
logger.debug(`Operation approved - "${prefix}" commands will be auto-approved this session`);
|
|
3216
|
-
return;
|
|
3217
|
-
}
|
|
3218
|
-
}
|
|
3219
|
-
// Pass user feedback as reason if provided
|
|
3220
|
-
const reason = userFeedback && userFeedback.trim()
|
|
3221
|
-
? `User feedback: ${userFeedback}`
|
|
3222
|
-
: undefined;
|
|
3223
|
-
approvalManager.resolveRequest(choice, reason);
|
|
3224
|
-
setPendingApproval(null);
|
|
3225
|
-
if (choice === 'approve') {
|
|
3226
|
-
logger.debug('Operation approved');
|
|
3227
|
-
}
|
|
3228
|
-
else if (choice === 'reject') {
|
|
3229
|
-
// Log rejection details at debug level - user already knows they rejected it
|
|
3230
|
-
const operationType = request?.type || 'operation';
|
|
3231
|
-
const affectedFiles = request?.details.filePaths?.join(', ') || 'unknown';
|
|
3232
|
-
logger.debug(`Operation rejected - type: ${operationType}, files: ${affectedFiles}`);
|
|
3233
|
-
if (userFeedback && userFeedback.trim()) {
|
|
3234
|
-
logger.debug(`User feedback: ${userFeedback}`);
|
|
3235
|
-
}
|
|
3236
|
-
}
|
|
3237
|
-
else if (choice === 'cancel') {
|
|
3238
|
-
logger.debug('Operation cancelled - NO changes were made');
|
|
3239
|
-
}
|
|
3240
|
-
}, []);
|
|
3241
|
-
/**
|
|
3242
|
-
* Handles plan approval dialog choices
|
|
3243
|
-
*/
|
|
3244
|
-
const handlePlanApprovalChoice = useCallback((choice, input) => {
|
|
3245
|
-
const approvalManager = getApprovalManager();
|
|
3246
|
-
// Update modalActiveRef synchronously to allow immediate renders after approval
|
|
3247
|
-
modalActiveRef.current = false;
|
|
3248
|
-
// Helper: build task list preserving completed status for plan updates
|
|
3249
|
-
const buildActivePlan = (request) => {
|
|
3250
|
-
if (!request)
|
|
3251
|
-
return;
|
|
3252
|
-
const completedSet = new Set(request.completedTaskIds || []);
|
|
3253
|
-
setPlanTrackerMode('compact');
|
|
3254
|
-
activePlanRef.current = {
|
|
3255
|
-
planId: request.plan.id,
|
|
3256
|
-
title: request.plan.problemStatement,
|
|
3257
|
-
tasks: request.plan.tasks.map((task) => ({
|
|
3258
|
-
task,
|
|
3259
|
-
status: completedSet.has(task.id) ? 'completed' : 'pending',
|
|
3260
|
-
})),
|
|
3261
|
-
isComplete: false,
|
|
3262
|
-
success: false,
|
|
3263
|
-
};
|
|
3264
|
-
setActivePlan(activePlanRef.current);
|
|
3265
|
-
};
|
|
3266
|
-
if (choice === 'approve') {
|
|
3267
|
-
// Initialize plan execution tracker with the approved plan
|
|
3268
|
-
buildActivePlan(pendingPlanApproval);
|
|
3269
|
-
// Always clear context on plan approval so execution starts fresh with only the plan
|
|
3270
|
-
planApprovalClearContextRef.current = true;
|
|
3271
|
-
approvalManager.resolvePlanApproval('approve');
|
|
3272
|
-
setPendingPlanApproval(null);
|
|
3273
|
-
}
|
|
3274
|
-
else if (choice === 'approve-compact') {
|
|
3275
|
-
buildActivePlan(pendingPlanApproval);
|
|
3276
|
-
planApprovalClearContextRef.current = true;
|
|
3277
|
-
approvalManager.resolvePlanApproval('approve');
|
|
3278
|
-
setPendingPlanApproval(null);
|
|
3279
|
-
}
|
|
3280
|
-
else if (choice === 'approve-auto-edits') {
|
|
3281
|
-
buildActivePlan(pendingPlanApproval);
|
|
3282
|
-
// Always clear context on plan approval so execution starts fresh with only the plan
|
|
3283
|
-
planApprovalClearContextRef.current = true;
|
|
3284
|
-
approvalManager.enableFileEditsAutoApproval();
|
|
3285
|
-
approvalManager.resolvePlanApproval('approve');
|
|
3286
|
-
setPendingPlanApproval(null);
|
|
3287
|
-
}
|
|
3288
|
-
else if (choice === 'clarify') {
|
|
3289
|
-
approvalManager.resolvePlanApproval('clarify', undefined, input);
|
|
3290
|
-
setPendingPlanApproval(null);
|
|
3291
|
-
logger.output('Clarification requested - returning to planning');
|
|
3292
|
-
}
|
|
3293
|
-
else if (choice === 'edit') {
|
|
3294
|
-
approvalManager.resolvePlanApproval('edit', input, undefined);
|
|
3295
|
-
setPendingPlanApproval(null);
|
|
3296
|
-
logger.output('Plan modification requested - updating plan');
|
|
3297
|
-
}
|
|
3298
|
-
else if (choice === 'reject') {
|
|
3299
|
-
approvalManager.resolvePlanApproval('reject');
|
|
3300
|
-
setPendingPlanApproval(null);
|
|
3301
|
-
}
|
|
3302
|
-
}, [pendingPlanApproval]);
|
|
3303
|
-
/**
|
|
3304
|
-
* Handles clarification wizard completion
|
|
3305
|
-
*/
|
|
3306
|
-
const handleClarificationComplete = useCallback((answers) => {
|
|
3307
|
-
const approvalManager = getApprovalManager();
|
|
3308
|
-
// Update modalActiveRef synchronously to allow immediate renders after completion
|
|
3309
|
-
modalActiveRef.current = false;
|
|
3310
|
-
approvalManager.resolveClarificationWizard(answers);
|
|
3311
|
-
setPendingClarification(null);
|
|
3312
|
-
logger.debug('✅ Clarification completed');
|
|
3313
|
-
}, []);
|
|
3314
|
-
/**
|
|
3315
|
-
* Handles clarification wizard cancellation
|
|
3316
|
-
*/
|
|
3317
|
-
const handleClarificationCancel = useCallback(() => {
|
|
3318
|
-
const approvalManager = getApprovalManager();
|
|
3319
|
-
// Update modalActiveRef synchronously to allow immediate renders after cancellation
|
|
3320
|
-
modalActiveRef.current = false;
|
|
3321
|
-
approvalManager.resolveClarificationWizard(null);
|
|
3322
|
-
setPendingClarification(null);
|
|
3323
|
-
abortLoopRef.current = true; // Signal agentic loop to stop
|
|
3324
|
-
logger.output('Clarification cancelled - returning to prompt');
|
|
3325
|
-
}, []);
|
|
3326
|
-
/**
|
|
3327
|
-
* Handles execution mode selection from the mode selector
|
|
3328
|
-
*/
|
|
3329
|
-
const handleModeSelect = useCallback((mode) => {
|
|
3330
|
-
const sessionManager = getSessionManager();
|
|
3331
|
-
setExecutionMode(mode);
|
|
3332
|
-
sessionManager.setExecutionMode(mode);
|
|
3333
|
-
setActiveOverlay('none');
|
|
3334
|
-
// Show confirmation message
|
|
3335
|
-
const modeDescriptions = {
|
|
3336
|
-
hybrid: 'LLM decides when to use planning based on complexity',
|
|
3337
|
-
direct: 'Pure agentic execution, no planning',
|
|
3338
|
-
plan: 'Always forces planning for every task'
|
|
3339
|
-
};
|
|
3340
|
-
const modeAliases = {
|
|
3341
|
-
hybrid: 'h',
|
|
3342
|
-
direct: 'd',
|
|
3343
|
-
plan: 'p'
|
|
3344
|
-
};
|
|
3345
|
-
const color = mode === 'hybrid' ? chalk.cyan : mode === 'direct' ? chalk.yellow : chalk.magenta;
|
|
3346
|
-
logger.output('');
|
|
3347
|
-
logger.output(chalk.green('✓') + ` Switched to ${color.bold(mode.toUpperCase())} ${chalk.gray(`(${modeAliases[mode]})`)}`);
|
|
3348
|
-
logger.output(` ${chalk.gray(modeDescriptions[mode])}`);
|
|
3349
|
-
logger.output('');
|
|
3350
|
-
}, []);
|
|
3351
|
-
/**
|
|
3352
|
-
* Handles mode selector cancellation
|
|
3353
|
-
*/
|
|
3354
|
-
const handleModeCancel = useCallback(() => {
|
|
3355
|
-
setActiveOverlay('none');
|
|
3356
|
-
}, []);
|
|
3357
|
-
/**
|
|
3358
|
-
* Performs the actual logout operation
|
|
3359
|
-
*/
|
|
3360
|
-
const performLogout = useCallback(async () => {
|
|
3361
|
-
try {
|
|
3362
|
-
const { getCredentialStore } = await import('../services/credential-store.js');
|
|
3363
|
-
const credentialStore = await getCredentialStore();
|
|
3364
|
-
logger.output('');
|
|
3365
|
-
logger.output(chalk.gray('Clearing credentials...'));
|
|
3366
|
-
const hasCompassKey = await credentialStore.hasCompassApiKey();
|
|
3367
|
-
const hasAnthropicKey = await credentialStore.hasApiKey();
|
|
3368
|
-
let compassCleared = true;
|
|
3369
|
-
let anthropicCleared = true;
|
|
3370
|
-
if (hasCompassKey) {
|
|
3371
|
-
compassCleared = await credentialStore.deleteCompassApiKey();
|
|
3372
|
-
}
|
|
3373
|
-
if (hasAnthropicKey) {
|
|
3374
|
-
anthropicCleared = await credentialStore.deleteApiKey();
|
|
3375
|
-
}
|
|
3376
|
-
// Clear provider-specific keys
|
|
3377
|
-
const providerNames = ['ollama', 'zai', 'minimax'];
|
|
3378
|
-
const providerResults = {};
|
|
3379
|
-
for (const provider of providerNames) {
|
|
3380
|
-
if (await credentialStore.hasProviderApiKey(provider)) {
|
|
3381
|
-
providerResults[provider] = await credentialStore.deleteProviderApiKey(provider);
|
|
3382
|
-
}
|
|
3383
|
-
}
|
|
3384
|
-
const allProvidersCleared = Object.values(providerResults).every(v => v);
|
|
3385
|
-
const allCleared = compassCleared && anthropicCleared && allProvidersCleared;
|
|
3386
|
-
if (allCleared) {
|
|
3387
|
-
logger.output('');
|
|
3388
|
-
logger.output(chalk.green.bold('✅ Logged out successfully'));
|
|
3389
|
-
logger.output(chalk.gray('All API keys have been cleared from secure storage.'));
|
|
3390
|
-
logger.output('');
|
|
3391
|
-
logger.output(chalk.gray('Exiting Compass...'));
|
|
3392
|
-
logger.output('');
|
|
3393
|
-
logger.info('User logged out - all API keys cleared - exiting');
|
|
3394
|
-
// Exit via the proper Ink exit flow (avoids yoga WASM use-after-free)
|
|
3395
|
-
await onExit();
|
|
3396
|
-
}
|
|
3397
|
-
else {
|
|
3398
|
-
logger.output('');
|
|
3399
|
-
logger.output(chalk.yellow('⚠️ Partial logout'));
|
|
3400
|
-
logger.output(chalk.gray('Some credentials could not be cleared. See logs for details.'));
|
|
3401
|
-
logger.output('');
|
|
3402
|
-
logger.warn('Partial logout - some credentials may not have been cleared', {
|
|
3403
|
-
compassCleared,
|
|
3404
|
-
anthropicCleared,
|
|
3405
|
-
providerResults,
|
|
3406
|
-
});
|
|
3407
|
-
// Still exit even on partial logout
|
|
3408
|
-
logger.output(chalk.gray('Exiting Compass...'));
|
|
3409
|
-
logger.output('');
|
|
3410
|
-
await onExit();
|
|
3411
|
-
}
|
|
3412
|
-
}
|
|
3413
|
-
catch (error) {
|
|
3414
|
-
logger.output('');
|
|
3415
|
-
logger.output(chalk.red('❌ Logout failed'));
|
|
3416
|
-
if (error instanceof Error) {
|
|
3417
|
-
logger.output(chalk.red(` ${error.message}`));
|
|
3418
|
-
}
|
|
3419
|
-
logger.output('');
|
|
3420
|
-
logger.error('Logout failed', error);
|
|
3421
|
-
}
|
|
3422
|
-
}, []);
|
|
3423
|
-
/**
|
|
3424
|
-
* Handles logout confirmation from the selector
|
|
3425
|
-
*/
|
|
3426
|
-
const handleLogoutConfirm = useCallback(() => {
|
|
3427
|
-
setActiveOverlay('none');
|
|
3428
|
-
performLogout();
|
|
3429
|
-
}, [performLogout]);
|
|
3430
|
-
/**
|
|
3431
|
-
* Handles logout cancellation from the selector
|
|
3432
|
-
*/
|
|
3433
|
-
const handleLogoutCancel = useCallback(() => {
|
|
3434
|
-
setActiveOverlay('none');
|
|
3435
|
-
logger.output('');
|
|
3436
|
-
logger.output(chalk.gray('Logout cancelled.'));
|
|
3437
|
-
logger.output('');
|
|
3438
|
-
}, []);
|
|
3439
|
-
/**
|
|
3440
|
-
* Handles rating submission
|
|
3441
|
-
*/
|
|
3442
|
-
const handleRatingSubmit = useCallback(async (rating) => {
|
|
3443
|
-
const ratingService = getRatingService();
|
|
3444
|
-
try {
|
|
3445
|
-
const result = await ratingService.submitRating(rating);
|
|
3446
|
-
if (result.success && ratingStateManagerRef.current) {
|
|
3447
|
-
await ratingStateManagerRef.current.recordRatingSubmitted();
|
|
3448
|
-
}
|
|
3449
|
-
logger.debug('[Rating] Rating submitted', { rating, success: result.success });
|
|
3450
|
-
}
|
|
3451
|
-
catch (error) {
|
|
3452
|
-
logger.debug('[Rating] Failed to submit rating', error);
|
|
3453
|
-
}
|
|
3454
|
-
setShowRatingPanel(false);
|
|
3455
|
-
ratingPanelShownRef.current = false;
|
|
3456
|
-
}, []);
|
|
3457
|
-
/**
|
|
3458
|
-
* Handles rating panel close without submission
|
|
3459
|
-
*/
|
|
3460
|
-
const handleRatingClose = useCallback(() => {
|
|
3461
|
-
setShowRatingPanel(false);
|
|
3462
|
-
ratingPanelShownRef.current = false;
|
|
3463
|
-
// Note: Not recording as "shown" here to allow it to appear again
|
|
3464
|
-
// The rating will appear again after continued usage
|
|
3465
|
-
}, []);
|
|
3466
|
-
/**
|
|
3467
|
-
* Handles keyboard input for overlay navigation and shortcuts
|
|
3468
|
-
*/
|
|
3469
|
-
useInput((input, key) => {
|
|
3470
|
-
// Escape handling
|
|
3471
|
-
if (key.escape) {
|
|
3472
|
-
// During processing (agentic loop), single Esc gracefully breaks the loop
|
|
3473
|
-
if (isProcessing && !pendingClarification && !pendingApproval && !pendingPlanApproval) {
|
|
3474
|
-
// Set abort flag to break the agentic loop
|
|
3475
|
-
abortLoopRef.current = true;
|
|
3476
|
-
// Clear queued messages on abort
|
|
3477
|
-
queuedMessagesRef.current = [];
|
|
3478
|
-
setQueuedMessages([]);
|
|
3479
|
-
// Also abort any in-flight API request immediately
|
|
3480
|
-
if (abortControllerRef.current) {
|
|
3481
|
-
abortControllerRef.current.abort();
|
|
3482
|
-
}
|
|
3483
|
-
// Cancel any pending plan approval or clarification wizards
|
|
3484
|
-
const approvalManager = getApprovalManager();
|
|
3485
|
-
approvalManager.cancelPendingPlanApproval();
|
|
3486
|
-
approvalManager.cancelPendingClarificationWizard();
|
|
3487
|
-
logger.debug('Cancelled by the user');
|
|
3488
|
-
return;
|
|
3489
|
-
}
|
|
3490
|
-
// Close overlays
|
|
3491
|
-
if (activeOverlay !== 'none') {
|
|
3492
|
-
setActiveOverlay('none');
|
|
3493
|
-
return;
|
|
3494
|
-
}
|
|
3495
|
-
if (error) {
|
|
3496
|
-
setError(null);
|
|
3497
|
-
return;
|
|
3498
|
-
}
|
|
3499
|
-
}
|
|
3500
|
-
// Ctrl+X to skip current execute_command (kill subprocess, continue agentic loop)
|
|
3501
|
-
if (key.ctrl && input === 'x' && isProcessing && toolAbortRef.current && currentToolNameRef.current === 'execute_command') {
|
|
3502
|
-
toolAbortRef.current.abort();
|
|
3503
|
-
return;
|
|
3504
|
-
}
|
|
3505
|
-
// Ctrl+C to cancel current operation or exit
|
|
3506
|
-
if (key.ctrl && input === 'c') {
|
|
3507
|
-
if (isProcessing) {
|
|
3508
|
-
queuedMessagesRef.current = [];
|
|
3509
|
-
setQueuedMessages([]);
|
|
3510
|
-
setIsProcessing(false);
|
|
3511
|
-
resetStreamBlocks();
|
|
3512
|
-
setError('Operation cancelled');
|
|
3513
|
-
}
|
|
3514
|
-
return;
|
|
3515
|
-
}
|
|
3516
|
-
// Ctrl+L to clear screen (including Static items in scrollback)
|
|
3517
|
-
if (key.ctrl && input === 'l') {
|
|
3518
|
-
process.stdout.write('\x1B[2J\x1B[H');
|
|
3519
|
-
setConversation([]);
|
|
3520
|
-
setError(null);
|
|
3521
|
-
return;
|
|
3522
|
-
}
|
|
3523
|
-
// Ctrl+T to toggle tool blocks display in history
|
|
3524
|
-
if (key.ctrl && input === 't') {
|
|
3525
|
-
setShowToolBlocks((prev) => {
|
|
3526
|
-
const newValue = !prev;
|
|
3527
|
-
// Persist to session
|
|
3528
|
-
const sessionManager = getSessionManager();
|
|
3529
|
-
sessionManager.setShowToolBlocks(newValue);
|
|
3530
|
-
logger.output(newValue ? 'Tool execution details enabled in history.' : 'Minimal history mode (text only).');
|
|
3531
|
-
return newValue;
|
|
3532
|
-
});
|
|
3533
|
-
return;
|
|
3534
|
-
}
|
|
3535
|
-
// Ctrl+O to toggle user prompt collapse and MCP result expansion in history
|
|
3536
|
-
// Throttled to prevent yoga-layout WASM crash on rapid toggling
|
|
3537
|
-
if (key.ctrl && input === 'o') {
|
|
3538
|
-
const now = Date.now();
|
|
3539
|
-
if (now - lastExpandToggleRef.current < 150) {
|
|
3540
|
-
return; // Throttle rapid toggling
|
|
3541
|
-
}
|
|
3542
|
-
lastExpandToggleRef.current = now;
|
|
3543
|
-
setCollapseUserPrompts((prev) => {
|
|
3544
|
-
const newValue = !prev;
|
|
3545
|
-
// Also toggle MCP results (inverse logic: when prompts collapse, MCP results are trimmed)
|
|
3546
|
-
setExpandMCPResults(!newValue);
|
|
3547
|
-
return newValue;
|
|
3548
|
-
});
|
|
3549
|
-
return;
|
|
3550
|
-
}
|
|
3551
|
-
// Cycle plan tracker mode with 'p' key during plan execution: compact → full → hidden → compact
|
|
3552
|
-
if (input === 'p' && activePlan && !activePlan.isComplete && isProcessing) {
|
|
3553
|
-
setPlanTrackerMode((prev) => prev === 'compact' ? 'full' : prev === 'full' ? 'hidden' : 'compact');
|
|
3554
|
-
return;
|
|
3555
|
-
}
|
|
3556
|
-
}, { isActive: !pendingApproval && !pendingPlanApproval && !pendingClarification });
|
|
3557
|
-
// Conversation entries are split between <Static> (scrollback) and dynamic rendering.
|
|
3558
|
-
// <Static> is FROZEN during active streaming to prevent layout shifts. Adding items to
|
|
3559
|
-
// <Static> mid-stream forces Ink to write permanent content above the dynamic area,
|
|
3560
|
-
// causing reflow/flickering. However, when the user is interacting with a modal
|
|
3561
|
-
// (tool approval, clarification, plan approval), streaming has stopped and the dynamic
|
|
3562
|
-
// area is minimal — safe to settle completed entries into Static. This prevents long
|
|
3563
|
-
// agentic loops from accumulating unbounded dynamic entries that re-render on every update.
|
|
3564
|
-
//
|
|
3565
|
-
// Entries between the Static freeze point and the latest are rendered dynamically
|
|
3566
|
-
// (outside <Static>) so they remain visible without causing scrollback writes.
|
|
3567
|
-
// A ref ensures items are never re-committed to <Static> across re-renders.
|
|
3568
|
-
const staticCommittedRef = useRef([]);
|
|
3569
|
-
const { settledEntries, dynamicEntries } = useMemo(() => {
|
|
3570
|
-
if (conversation.length === 0) {
|
|
3571
|
-
staticCommittedRef.current = [];
|
|
3572
|
-
return { settledEntries: [], dynamicEntries: [] };
|
|
3573
|
-
}
|
|
3574
|
-
// Settle into Static when idle OR when a modal is active (streaming is paused,
|
|
3575
|
-
// user is focused on the modal, and all prior entries are finalized).
|
|
3576
|
-
const inModal = !!(pendingApproval || pendingPlanApproval || pendingClarification);
|
|
3577
|
-
if (!isProcessing || inModal) {
|
|
3578
|
-
if (conversation.length > staticCommittedRef.current.length) {
|
|
3579
|
-
staticCommittedRef.current = conversation;
|
|
3580
|
-
}
|
|
3581
|
-
return { settledEntries: staticCommittedRef.current, dynamicEntries: [] };
|
|
3582
|
-
}
|
|
3583
|
-
// During active streaming, freeze <Static> — don't commit new entries mid-stream.
|
|
3584
|
-
// Any entries added since the freeze point are rendered dynamically.
|
|
3585
|
-
const frozenCount = staticCommittedRef.current.length;
|
|
3586
|
-
return {
|
|
3587
|
-
settledEntries: staticCommittedRef.current,
|
|
3588
|
-
dynamicEntries: frozenCount < conversation.length
|
|
3589
|
-
? conversation.slice(frozenCount)
|
|
3590
|
-
: [],
|
|
3591
|
-
};
|
|
3592
|
-
}, [conversation, isProcessing, pendingApproval, pendingPlanApproval, pendingClarification]);
|
|
3593
|
-
/**
|
|
3594
|
-
* Formats a timestamp for display
|
|
3595
|
-
*/
|
|
3596
|
-
function formatTime(date) {
|
|
3597
|
-
return date.toLocaleTimeString('en-US', {
|
|
3598
|
-
hour: '2-digit',
|
|
3599
|
-
minute: '2-digit',
|
|
3600
|
-
});
|
|
3601
|
-
}
|
|
3602
|
-
// Keep working-dot ANSI offset in sync: 3 fixed lines (top hr, input, bottom hr)
|
|
3603
|
-
// plus one line per queued message. Status line shares the bottom hr line.
|
|
3604
|
-
workingDotOffsetRef.current = 3 + queuedMessages.length;
|
|
3605
|
-
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Static, { items: showHistory ? settledEntries : [], children: (entry) => (_jsx(ConversationEntryComponent, { entry: entry, showToolBlocks: showToolBlocks, collapseUserPrompts: collapseUserPrompts, expandMCPResults: expandMCPResults, formatTime: formatTime }, entry.id)) }), showHistory && dynamicEntries.map((entry) => (_jsx(ConversationEntryComponent, { entry: entry, showToolBlocks: showToolBlocks, collapseUserPrompts: collapseUserPrompts, expandMCPResults: expandMCPResults, formatTime: formatTime }, entry.id))), isProcessing && streamingCommandOutput.length > 0 && (_jsx(Box, { marginBottom: 1, flexDirection: "column", children: streamingCommandOutput.map((line, idx) => (_jsx(Text, { children: line }, idx))) })), !isProcessing && slashCommandOutput && slashCommandOutput.trim().length > 0 && (_jsx(Box, { marginBottom: 1, flexDirection: "column", children: _jsx(Text, { children: slashCommandOutput }) })), sessionPhase === 'executing' && activePlan && !activePlan.isComplete && planTrackerMode !== 'hidden' && !pendingApproval && (_jsx(PlanExecutionTracker, { planId: activePlan.planId, title: activePlan.title, tasks: activePlan.tasks, isComplete: activePlan.isComplete, success: activePlan.success, compact: planTrackerMode === 'compact' })), sessionPhase === 'executing' && activePlan && !activePlan.isComplete && planTrackerMode === 'hidden' && !pendingApproval && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "blue", children: '\u2500'.repeat(Math.max(0, (process.stdout.columns || 80) - 4)) }), _jsx(Text, { dimColor: true, children: "Plan progress hidden (press p to show)" }), _jsx(Text, { color: "blue", children: '\u2500'.repeat(Math.max(0, (process.stdout.columns || 80) - 4)) })] })), _jsx(Box, { flexDirection: "column", children: _jsx(InterleavedStream, { blocks: isProcessing ? streamBlocksRef.current : emptyBlocksRef.current, isStreaming: isProcessing && !pendingApproval && !pendingPlanApproval && !pendingClarification, showThinkingIndicator: isProcessing && !pendingApproval && !pendingPlanApproval && !pendingClarification, expandMCPResults: expandMCPResults, maxVisibleBlocks: MAX_DYNAMIC_STREAM_BLOCKS }) }), error && !isProcessing && (_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: "red", children: ["\u274C ", error] }), _jsx(Text, { dimColor: true, children: " (Press Esc to dismiss)" })] })), pendingApproval && (_jsx(ApprovalDialog, { request: pendingApproval, onChoice: handleApprovalChoice })), pendingPlanApproval && (_jsx(PlanApprovalDialog, { request: pendingPlanApproval, onChoice: handlePlanApprovalChoice, contextTokens: currentContextTokens })), pendingClarification && (_jsx(ClarificationWizard, { questions: pendingClarification.questions, onComplete: handleClarificationComplete, onCancel: handleClarificationCancel })), activeOverlay === 'help' && (_jsx(Box, { flexDirection: "column", marginBottom: 1, children: _jsx(HelpMenu, {}) })), activeOverlay === 'tokens' && (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(TokenUsage, { usage: tokenUsage, cost: cost, contextWindow: contextWindowSize, showDetails: true }), _jsx(Text, { dimColor: true, children: "Press Esc to close" })] })), activeOverlay === 'agents' && (_jsx(Box, { flexDirection: "column", marginBottom: 1, children: _jsx(AgentManager, { onClose: () => setActiveOverlay('none'), isActive: true, projectRoot: projectRoot }) })), activeOverlay === 'agent-wizard' && (_jsx(Box, { flexDirection: "column", marginBottom: 1, children: _jsx(AgentCreationWizard, { onComplete: (agentPath) => {
|
|
3606
|
-
logger.info(`Agent created: ${agentPath}`);
|
|
3607
|
-
setActiveOverlay('none');
|
|
3608
|
-
}, onCancel: () => setActiveOverlay('none'), isActive: true, projectRoot: projectRoot, enabledTools: getToolRegistry().getToolDefinitions().map(t => t.name) }) })), activeOverlay === 'diff' && diffPreview && (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(DiffPreview, { diff: diffPreview }), _jsx(Text, { dimColor: true, children: "Press Esc to close" })] })), activeOverlay === 'mode-selector' && (_jsx(Box, { flexDirection: "column", marginBottom: 1, children: _jsx(ExecutionModeSelector, { currentMode: executionMode, onSelect: handleModeSelect, onCancel: handleModeCancel, isActive: true }) })), activeOverlay === 'logout-confirm' && (_jsx(Box, { flexDirection: "column", marginBottom: 1, children: _jsx(ConfirmationSelector, { title: "\uD83D\uDEAA Logout", question: "Are you sure you want to logout and clear all stored API keys?", details: logoutDetails, onConfirm: handleLogoutConfirm, onCancel: handleLogoutCancel, isActive: true }) })), showRatingPanel && hasCompassApiKey && !pendingApproval && !pendingPlanApproval && !pendingClarification && !isProcessing && activeOverlay === 'none' && (_jsx(RatingPanel, { onClose: handleRatingClose, onSubmit: handleRatingSubmit, isActive: true })), _jsx(CompassSpinner, { isActive: compassSpinner?.active ?? false, message: compassSpinner?.message }), isProcessing && !pendingApproval && !pendingPlanApproval && !pendingClarification && streamBlocksRef.current.length > 0 && (_jsx(WorkingIndicator, { linesBelowRef: workingDotOffsetRef })), queuedMessages.length > 0 && (_jsx(Box, { flexDirection: "column", paddingX: 1, children: queuedMessages.map((msg, i) => (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: " queued: " }), _jsx(Text, { dimColor: true, italic: true, children: msg.length > 60 ? msg.substring(0, 60) + '...' : msg })] }, i))) })), !isContextLoading && !pendingApproval && !pendingPlanApproval && !pendingClarification && activeOverlay !== 'mode-selector' && activeOverlay !== 'logout-confirm' && activeOverlay !== 'agents' && activeOverlay !== 'agent-wizard' && (_jsx(Prompt, { approvalMode: approvalMode, model: currentModel, activeAgentName: activeAgentName, onSubmit: handlePromptSubmit, onPaste: handlePaste, disabled: false, isProcessing: isProcessing, projectRoot: projectRoot, showStatusBar: false, suggestionOffer: suggestionOffer })), _jsxs(Box, { width: "100%", justifyContent: "space-between", children: [_jsxs(Box, { children: [executionMode === 'direct' && _jsx(Text, { color: "yellow", children: "direct mode" }), executionMode === 'plan' && _jsx(Text, { color: "magenta", children: "plan mode" }), activeAgentName && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: " \u2502 " }), _jsxs(Text, { color: "magenta", bold: true, children: ["\uD83E\uDD16 ", activeAgentName] })] }))] }), statusLineVisible && statusLineText && (_jsx(Text, { children: statusLineText }))] })] }));
|
|
3609
|
-
};
|
|
3610
|
-
export default InteractiveSession;
|
|
3611
|
-
//# sourceMappingURL=InteractiveSession.js.map
|