@prestyj/cli 5.9.0 → 5.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -5
- package/assets/skills/evidence-led-ui/SKILL.md +1 -1
- package/dist/app-sidecar-auth-broadcast.test.d.ts +2 -0
- package/dist/app-sidecar-auth-broadcast.test.d.ts.map +1 -0
- package/dist/app-sidecar-auth-broadcast.test.js +282 -0
- package/dist/app-sidecar-auth-broadcast.test.js.map +1 -0
- package/dist/app-sidecar-sessions.d.ts +5 -2
- package/dist/app-sidecar-sessions.d.ts.map +1 -1
- package/dist/app-sidecar-sessions.js +34 -8
- package/dist/app-sidecar-sessions.js.map +1 -1
- package/dist/app-sidecar-sessions.test.js +95 -0
- package/dist/app-sidecar-sessions.test.js.map +1 -1
- package/dist/app-sidecar.js +871 -142
- package/dist/app-sidecar.js.map +1 -1
- package/dist/chat-agents/general.test.js +5 -3
- package/dist/chat-agents/general.test.js.map +1 -1
- package/dist/chat-agents/memory.d.ts.map +1 -1
- package/dist/chat-agents/memory.js +14 -5
- package/dist/chat-agents/memory.js.map +1 -1
- package/dist/chat-agents/memory.test.js +11 -1
- package/dist/chat-agents/memory.test.js.map +1 -1
- package/dist/chat-agents/shared.js +1 -1
- package/dist/chat-agents/shared.js.map +1 -1
- package/dist/chat-agents/specialists.test.js +3 -2
- package/dist/chat-agents/specialists.test.js.map +1 -1
- package/dist/cli/command-routing.d.ts +1 -1
- package/dist/cli/command-routing.d.ts.map +1 -1
- package/dist/cli/command-routing.js +1 -0
- package/dist/cli/command-routing.js.map +1 -1
- package/dist/cli/command-routing.test.js +1 -0
- package/dist/cli/command-routing.test.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +183 -51
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +16 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +51 -2
- package/dist/config.js.map +1 -1
- package/dist/config.test.js +71 -2
- package/dist/config.test.js.map +1 -1
- package/dist/core/agent-mcp-passthrough.test.d.ts +2 -0
- package/dist/core/agent-mcp-passthrough.test.d.ts.map +1 -0
- package/dist/core/agent-mcp-passthrough.test.js +61 -0
- package/dist/core/agent-mcp-passthrough.test.js.map +1 -0
- package/dist/core/agent-notifications.d.ts +62 -0
- package/dist/core/agent-notifications.d.ts.map +1 -0
- package/dist/core/agent-notifications.js +92 -0
- package/dist/core/agent-notifications.js.map +1 -0
- package/dist/core/agent-notifications.test.d.ts +2 -0
- package/dist/core/agent-notifications.test.d.ts.map +1 -0
- package/dist/core/agent-notifications.test.js +97 -0
- package/dist/core/agent-notifications.test.js.map +1 -0
- package/dist/core/agent-session-add-dir.test.d.ts +2 -0
- package/dist/core/agent-session-add-dir.test.d.ts.map +1 -0
- package/dist/core/agent-session-add-dir.test.js +110 -0
- package/dist/core/agent-session-add-dir.test.js.map +1 -0
- package/dist/core/agent-session-checkpoint-flush.test.d.ts +2 -0
- package/dist/core/agent-session-checkpoint-flush.test.d.ts.map +1 -0
- package/dist/core/agent-session-checkpoint-flush.test.js +201 -0
- package/dist/core/agent-session-checkpoint-flush.test.js.map +1 -0
- package/dist/core/agent-session-compaction.test.js +327 -13
- package/dist/core/agent-session-compaction.test.js.map +1 -1
- package/dist/core/agent-session-crash-durability.test.d.ts +2 -0
- package/dist/core/agent-session-crash-durability.test.d.ts.map +1 -0
- package/dist/core/agent-session-crash-durability.test.js +102 -0
- package/dist/core/agent-session-crash-durability.test.js.map +1 -0
- package/dist/core/agent-session-marker-anchors.test.d.ts +2 -0
- package/dist/core/agent-session-marker-anchors.test.d.ts.map +1 -0
- package/dist/core/agent-session-marker-anchors.test.js +326 -0
- package/dist/core/agent-session-marker-anchors.test.js.map +1 -0
- package/dist/core/agent-session-memory-tail.test.js +4 -7
- package/dist/core/agent-session-memory-tail.test.js.map +1 -1
- package/dist/core/agent-session-model-switch.test.d.ts +2 -0
- package/dist/core/agent-session-model-switch.test.d.ts.map +1 -0
- package/dist/core/agent-session-model-switch.test.js +161 -0
- package/dist/core/agent-session-model-switch.test.js.map +1 -0
- package/dist/core/agent-session-notifications.test.d.ts +2 -0
- package/dist/core/agent-session-notifications.test.d.ts.map +1 -0
- package/dist/core/agent-session-notifications.test.js +114 -0
- package/dist/core/agent-session-notifications.test.js.map +1 -0
- package/dist/core/agent-session-process-gate.test.d.ts +2 -0
- package/dist/core/agent-session-process-gate.test.d.ts.map +1 -0
- package/dist/core/agent-session-process-gate.test.js +92 -0
- package/dist/core/agent-session-process-gate.test.js.map +1 -0
- package/dist/core/agent-session-queue.test.js +69 -7
- package/dist/core/agent-session-queue.test.js.map +1 -1
- package/dist/core/agent-session-review-coverage.test.js +80 -23
- package/dist/core/agent-session-review-coverage.test.js.map +1 -1
- package/dist/core/agent-session-slash-restore.test.d.ts +2 -0
- package/dist/core/agent-session-slash-restore.test.d.ts.map +1 -0
- package/dist/core/agent-session-slash-restore.test.js +121 -0
- package/dist/core/agent-session-slash-restore.test.js.map +1 -0
- package/dist/core/agent-session-subagent-policy.test.js +7 -3
- package/dist/core/agent-session-subagent-policy.test.js.map +1 -1
- package/dist/core/agent-session.d.ts +256 -10
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +1071 -219
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/agent-tools-allowlist.test.js +5 -3
- package/dist/core/agent-tools-allowlist.test.js.map +1 -1
- package/dist/core/agents.d.ts +11 -0
- package/dist/core/agents.d.ts.map +1 -1
- package/dist/core/agents.js +21 -0
- package/dist/core/agents.js.map +1 -1
- package/dist/core/agents.test.d.ts +2 -0
- package/dist/core/agents.test.d.ts.map +1 -0
- package/dist/core/agents.test.js +67 -0
- package/dist/core/agents.test.js.map +1 -0
- package/dist/core/auth-providers.js +1 -1
- package/dist/core/auth-providers.js.map +1 -1
- package/dist/core/compaction/active-context.test.js +21 -0
- package/dist/core/compaction/active-context.test.js.map +1 -1
- package/dist/core/compaction/compactor.d.ts +108 -8
- package/dist/core/compaction/compactor.d.ts.map +1 -1
- package/dist/core/compaction/compactor.js +423 -119
- package/dist/core/compaction/compactor.js.map +1 -1
- package/dist/core/compaction/compactor.test.js +361 -9
- package/dist/core/compaction/compactor.test.js.map +1 -1
- package/dist/core/compaction/policy.d.ts +17 -0
- package/dist/core/compaction/policy.d.ts.map +1 -0
- package/dist/core/compaction/policy.js +22 -0
- package/dist/core/compaction/policy.js.map +1 -0
- package/dist/core/compaction/policy.test.d.ts +2 -0
- package/dist/core/compaction/policy.test.d.ts.map +1 -0
- package/dist/core/compaction/policy.test.js +35 -0
- package/dist/core/compaction/policy.test.js.map +1 -0
- package/dist/core/compaction/query-aware-selector.d.ts +33 -0
- package/dist/core/compaction/query-aware-selector.d.ts.map +1 -0
- package/dist/core/compaction/query-aware-selector.js +202 -0
- package/dist/core/compaction/query-aware-selector.js.map +1 -0
- package/dist/core/compaction/query-aware-selector.test.d.ts +2 -0
- package/dist/core/compaction/query-aware-selector.test.d.ts.map +1 -0
- package/dist/core/compaction/query-aware-selector.test.js +96 -0
- package/dist/core/compaction/query-aware-selector.test.js.map +1 -0
- package/dist/core/compaction/token-estimator.test.js +1 -1
- package/dist/core/compaction/token-estimator.test.js.map +1 -1
- package/dist/core/compaction/tool-result-pruner.d.ts +24 -11
- package/dist/core/compaction/tool-result-pruner.d.ts.map +1 -1
- package/dist/core/compaction/tool-result-pruner.js +98 -26
- package/dist/core/compaction/tool-result-pruner.js.map +1 -1
- package/dist/core/compaction/tool-result-pruner.test.js +178 -6
- package/dist/core/compaction/tool-result-pruner.test.js.map +1 -1
- package/dist/core/encode-cwd.d.ts +17 -0
- package/dist/core/encode-cwd.d.ts.map +1 -1
- package/dist/core/encode-cwd.js +20 -7
- package/dist/core/encode-cwd.js.map +1 -1
- package/dist/core/encode-cwd.test.js +20 -1
- package/dist/core/encode-cwd.test.js.map +1 -1
- package/dist/core/event-bus.d.ts +23 -0
- package/dist/core/event-bus.d.ts.map +1 -1
- package/dist/core/event-bus.js +10 -0
- package/dist/core/event-bus.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +28 -8
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/plugin-bundles.d.ts +23 -0
- package/dist/core/extensions/plugin-bundles.d.ts.map +1 -0
- package/dist/core/extensions/plugin-bundles.js +228 -0
- package/dist/core/extensions/plugin-bundles.js.map +1 -0
- package/dist/core/extensions/plugin-bundles.test.d.ts +2 -0
- package/dist/core/extensions/plugin-bundles.test.d.ts.map +1 -0
- package/dist/core/extensions/plugin-bundles.test.js +110 -0
- package/dist/core/extensions/plugin-bundles.test.js.map +1 -0
- package/dist/core/foreign-session-import.d.ts +120 -0
- package/dist/core/foreign-session-import.d.ts.map +1 -0
- package/dist/core/foreign-session-import.js +598 -0
- package/dist/core/foreign-session-import.js.map +1 -0
- package/dist/core/foreign-session-import.test.d.ts +2 -0
- package/dist/core/foreign-session-import.test.d.ts.map +1 -0
- package/dist/core/foreign-session-import.test.js +220 -0
- package/dist/core/foreign-session-import.test.js.map +1 -0
- package/dist/core/foreign-transcript-blocks.d.ts +51 -0
- package/dist/core/foreign-transcript-blocks.d.ts.map +1 -0
- package/dist/core/foreign-transcript-blocks.js +151 -0
- package/dist/core/foreign-transcript-blocks.js.map +1 -0
- package/dist/core/ideal-review.d.ts +20 -1
- package/dist/core/ideal-review.d.ts.map +1 -1
- package/dist/core/ideal-review.js +43 -3
- package/dist/core/ideal-review.js.map +1 -1
- package/dist/core/ideal-review.test.js +106 -5
- package/dist/core/ideal-review.test.js.map +1 -1
- package/dist/core/local-endpoint-store.d.ts +50 -0
- package/dist/core/local-endpoint-store.d.ts.map +1 -0
- package/dist/core/local-endpoint-store.js +201 -0
- package/dist/core/local-endpoint-store.js.map +1 -0
- package/dist/core/local-endpoint-store.test.d.ts +2 -0
- package/dist/core/local-endpoint-store.test.d.ts.map +1 -0
- package/dist/core/local-endpoint-store.test.js +131 -0
- package/dist/core/local-endpoint-store.test.js.map +1 -0
- package/dist/core/loop-breaker.d.ts.map +1 -1
- package/dist/core/loop-breaker.js +1 -0
- package/dist/core/loop-breaker.js.map +1 -1
- package/dist/core/lsp/client.d.ts +65 -0
- package/dist/core/lsp/client.d.ts.map +1 -1
- package/dist/core/lsp/client.js +197 -14
- package/dist/core/lsp/client.js.map +1 -1
- package/dist/core/lsp/jsonrpc.d.ts +38 -1
- package/dist/core/lsp/jsonrpc.d.ts.map +1 -1
- package/dist/core/lsp/jsonrpc.js +13 -1
- package/dist/core/lsp/jsonrpc.js.map +1 -1
- package/dist/core/lsp/manager.d.ts +47 -9
- package/dist/core/lsp/manager.d.ts.map +1 -1
- package/dist/core/lsp/manager.js +107 -59
- package/dist/core/lsp/manager.js.map +1 -1
- package/dist/core/lsp/manager.test.js +50 -1
- package/dist/core/lsp/manager.test.js.map +1 -1
- package/dist/core/lsp/pool.d.ts +113 -0
- package/dist/core/lsp/pool.d.ts.map +1 -0
- package/dist/core/lsp/pool.js +255 -0
- package/dist/core/lsp/pool.js.map +1 -0
- package/dist/core/lsp/pool.test.d.ts +2 -0
- package/dist/core/lsp/pool.test.d.ts.map +1 -0
- package/dist/core/lsp/pool.test.js +286 -0
- package/dist/core/lsp/pool.test.js.map +1 -0
- package/dist/core/lsp/servers.d.ts.map +1 -1
- package/dist/core/lsp/servers.js +43 -3
- package/dist/core/lsp/servers.js.map +1 -1
- package/dist/core/lsp/servers.test.d.ts +2 -0
- package/dist/core/lsp/servers.test.d.ts.map +1 -0
- package/dist/core/lsp/servers.test.js +67 -0
- package/dist/core/lsp/servers.test.js.map +1 -0
- package/dist/core/lsp/test-support.d.ts +18 -0
- package/dist/core/lsp/test-support.d.ts.map +1 -0
- package/dist/core/lsp/test-support.js +27 -0
- package/dist/core/lsp/test-support.js.map +1 -0
- package/dist/core/lsp/uri.test.d.ts +2 -0
- package/dist/core/lsp/uri.test.d.ts.map +1 -0
- package/dist/core/lsp/uri.test.js +56 -0
- package/dist/core/lsp/uri.test.js.map +1 -0
- package/dist/core/lsp/windows.test.d.ts +2 -0
- package/dist/core/lsp/windows.test.d.ts.map +1 -0
- package/dist/core/lsp/windows.test.js +239 -0
- package/dist/core/lsp/windows.test.js.map +1 -0
- package/dist/core/mcp/catalog-cache.d.ts +63 -0
- package/dist/core/mcp/catalog-cache.d.ts.map +1 -0
- package/dist/core/mcp/catalog-cache.js +163 -0
- package/dist/core/mcp/catalog-cache.js.map +1 -0
- package/dist/core/mcp/catalog-cache.test.d.ts +2 -0
- package/dist/core/mcp/catalog-cache.test.d.ts.map +1 -0
- package/dist/core/mcp/catalog-cache.test.js +99 -0
- package/dist/core/mcp/catalog-cache.test.js.map +1 -0
- package/dist/core/mcp/client-session-recovery.test.d.ts +2 -0
- package/dist/core/mcp/client-session-recovery.test.d.ts.map +1 -0
- package/dist/core/mcp/client-session-recovery.test.js +231 -0
- package/dist/core/mcp/client-session-recovery.test.js.map +1 -0
- package/dist/core/mcp/client.d.ts +158 -0
- package/dist/core/mcp/client.d.ts.map +1 -1
- package/dist/core/mcp/client.js +476 -35
- package/dist/core/mcp/client.js.map +1 -1
- package/dist/core/mcp/elicitation-bridge.d.ts +41 -0
- package/dist/core/mcp/elicitation-bridge.d.ts.map +1 -0
- package/dist/core/mcp/elicitation-bridge.js +52 -0
- package/dist/core/mcp/elicitation-bridge.js.map +1 -0
- package/dist/core/mcp/elicitation.test.d.ts +2 -0
- package/dist/core/mcp/elicitation.test.d.ts.map +1 -0
- package/dist/core/mcp/elicitation.test.js +158 -0
- package/dist/core/mcp/elicitation.test.js.map +1 -0
- package/dist/core/mcp/index.d.ts +5 -1
- package/dist/core/mcp/index.d.ts.map +1 -1
- package/dist/core/mcp/index.js +2 -0
- package/dist/core/mcp/index.js.map +1 -1
- package/dist/core/mcp/legacy-negotiation.test.d.ts +2 -0
- package/dist/core/mcp/legacy-negotiation.test.d.ts.map +1 -0
- package/dist/core/mcp/legacy-negotiation.test.js +80 -0
- package/dist/core/mcp/legacy-negotiation.test.js.map +1 -0
- package/dist/core/mcp/oauth-provider.d.ts +1 -2
- package/dist/core/mcp/oauth-provider.d.ts.map +1 -1
- package/dist/core/mcp/oauth-provider.js +6 -2
- package/dist/core/mcp/oauth-provider.js.map +1 -1
- package/dist/core/mcp/oauth-store.d.ts +1 -1
- package/dist/core/mcp/oauth-store.d.ts.map +1 -1
- package/dist/core/mcp/oauth-store.js.map +1 -1
- package/dist/core/mcp/resolve-stdio.d.ts +35 -0
- package/dist/core/mcp/resolve-stdio.d.ts.map +1 -1
- package/dist/core/mcp/resolve-stdio.js +80 -1
- package/dist/core/mcp/resolve-stdio.js.map +1 -1
- package/dist/core/mcp/resolve-stdio.test.js +99 -4
- package/dist/core/mcp/resolve-stdio.test.js.map +1 -1
- package/dist/core/mcp/session-elicitation-wiring.test.d.ts +2 -0
- package/dist/core/mcp/session-elicitation-wiring.test.d.ts.map +1 -0
- package/dist/core/mcp/session-elicitation-wiring.test.js +138 -0
- package/dist/core/mcp/session-elicitation-wiring.test.js.map +1 -0
- package/dist/core/mcp/shared-pool.d.ts +158 -0
- package/dist/core/mcp/shared-pool.d.ts.map +1 -0
- package/dist/core/mcp/shared-pool.js +235 -0
- package/dist/core/mcp/shared-pool.js.map +1 -0
- package/dist/core/mcp/shared-pool.test.d.ts +2 -0
- package/dist/core/mcp/shared-pool.test.d.ts.map +1 -0
- package/dist/core/mcp/shared-pool.test.js +425 -0
- package/dist/core/mcp/shared-pool.test.js.map +1 -0
- package/dist/core/mcp/types.d.ts +12 -0
- package/dist/core/mcp/types.d.ts.map +1 -1
- package/dist/core/mcp/when-connected.test.d.ts +2 -0
- package/dist/core/mcp/when-connected.test.d.ts.map +1 -0
- package/dist/core/mcp/when-connected.test.js +46 -0
- package/dist/core/mcp/when-connected.test.js.map +1 -0
- package/dist/core/network-guard.d.ts +47 -0
- package/dist/core/network-guard.d.ts.map +1 -0
- package/dist/core/network-guard.js +208 -0
- package/dist/core/network-guard.js.map +1 -0
- package/dist/core/network-guard.test.d.ts +2 -0
- package/dist/core/network-guard.test.d.ts.map +1 -0
- package/dist/core/network-guard.test.js +72 -0
- package/dist/core/network-guard.test.js.map +1 -0
- package/dist/core/nolan-context.d.ts.map +1 -1
- package/dist/core/nolan-context.js +8 -0
- package/dist/core/nolan-context.js.map +1 -1
- package/dist/core/nolan-context.test.js +40 -0
- package/dist/core/nolan-context.test.js.map +1 -1
- package/dist/core/nolan-prompt.js +3 -0
- package/dist/core/nolan-prompt.js.map +1 -1
- package/dist/core/nolan-prompt.test.js +5 -0
- package/dist/core/nolan-prompt.test.js.map +1 -1
- package/dist/core/persistent-shell.d.ts +4 -1
- package/dist/core/persistent-shell.d.ts.map +1 -1
- package/dist/core/persistent-shell.js +21 -4
- package/dist/core/persistent-shell.js.map +1 -1
- package/dist/core/process-gate.d.ts +45 -0
- package/dist/core/process-gate.d.ts.map +1 -0
- package/dist/core/process-gate.js +79 -0
- package/dist/core/process-gate.js.map +1 -0
- package/dist/core/process-gate.test.d.ts +2 -0
- package/dist/core/process-gate.test.d.ts.map +1 -0
- package/dist/core/process-gate.test.js +87 -0
- package/dist/core/process-gate.test.js.map +1 -0
- package/dist/core/process-manager-dev-server-repro.test.js +34 -12
- package/dist/core/process-manager-dev-server-repro.test.js.map +1 -1
- package/dist/core/process-manager-notifications.test.d.ts +2 -0
- package/dist/core/process-manager-notifications.test.d.ts.map +1 -0
- package/dist/core/process-manager-notifications.test.js +217 -0
- package/dist/core/process-manager-notifications.test.js.map +1 -0
- package/dist/core/process-manager.d.ts +51 -2
- package/dist/core/process-manager.d.ts.map +1 -1
- package/dist/core/process-manager.js +212 -15
- package/dist/core/process-manager.js.map +1 -1
- package/dist/core/project-discovery.d.ts +38 -6
- package/dist/core/project-discovery.d.ts.map +1 -1
- package/dist/core/project-discovery.js +327 -40
- package/dist/core/project-discovery.js.map +1 -1
- package/dist/core/project-discovery.test.js +226 -1
- package/dist/core/project-discovery.test.js.map +1 -1
- package/dist/core/prompt-commands.d.ts.map +1 -1
- package/dist/core/prompt-commands.js +89 -156
- package/dist/core/prompt-commands.js.map +1 -1
- package/dist/core/prompt-commands.test.js +4 -10
- package/dist/core/prompt-commands.test.js.map +1 -1
- package/dist/core/radio.test.js +4 -1
- package/dist/core/radio.test.js.map +1 -1
- package/dist/core/regrounding.d.ts.map +1 -1
- package/dist/core/regrounding.js +1 -0
- package/dist/core/regrounding.js.map +1 -1
- package/dist/core/run-claim.d.ts +37 -0
- package/dist/core/run-claim.d.ts.map +1 -0
- package/dist/core/run-claim.js +46 -0
- package/dist/core/run-claim.js.map +1 -0
- package/dist/core/run-claim.test.d.ts +2 -0
- package/dist/core/run-claim.test.d.ts.map +1 -0
- package/dist/core/run-claim.test.js +107 -0
- package/dist/core/run-claim.test.js.map +1 -0
- package/dist/core/run-journal.test.d.ts +2 -0
- package/dist/core/run-journal.test.d.ts.map +1 -0
- package/dist/core/run-journal.test.js +195 -0
- package/dist/core/run-journal.test.js.map +1 -0
- package/dist/core/run-lifecycle.d.ts +22 -3
- package/dist/core/run-lifecycle.d.ts.map +1 -1
- package/dist/core/run-lifecycle.js +12 -3
- package/dist/core/run-lifecycle.js.map +1 -1
- package/dist/core/runtime-mode.d.ts +2 -0
- package/dist/core/runtime-mode.d.ts.map +1 -1
- package/dist/core/runtime-mode.js +4 -0
- package/dist/core/runtime-mode.js.map +1 -1
- package/dist/core/sandbox-domains.d.ts +14 -0
- package/dist/core/sandbox-domains.d.ts.map +1 -0
- package/dist/core/sandbox-domains.js +76 -0
- package/dist/core/sandbox-domains.js.map +1 -0
- package/dist/core/sandbox-feedback.d.ts +22 -0
- package/dist/core/sandbox-feedback.d.ts.map +1 -0
- package/dist/core/sandbox-feedback.js +69 -0
- package/dist/core/sandbox-feedback.js.map +1 -0
- package/dist/core/sandbox-feedback.test.d.ts +2 -0
- package/dist/core/sandbox-feedback.test.d.ts.map +1 -0
- package/dist/core/sandbox-feedback.test.js +32 -0
- package/dist/core/sandbox-feedback.test.js.map +1 -0
- package/dist/core/sandbox.d.ts +91 -0
- package/dist/core/sandbox.d.ts.map +1 -0
- package/dist/core/sandbox.js +254 -0
- package/dist/core/sandbox.js.map +1 -0
- package/dist/core/sandbox.test.d.ts +2 -0
- package/dist/core/sandbox.test.d.ts.map +1 -0
- package/dist/core/sandbox.test.js +157 -0
- package/dist/core/sandbox.test.js.map +1 -0
- package/dist/core/session-compaction.d.ts +6 -0
- package/dist/core/session-compaction.d.ts.map +1 -1
- package/dist/core/session-compaction.js +11 -1
- package/dist/core/session-compaction.js.map +1 -1
- package/dist/core/session-compaction.test.js +18 -1
- package/dist/core/session-compaction.test.js.map +1 -1
- package/dist/core/session-export.d.ts +98 -0
- package/dist/core/session-export.d.ts.map +1 -0
- package/dist/core/session-export.js +364 -0
- package/dist/core/session-export.js.map +1 -0
- package/dist/core/session-export.test.d.ts +2 -0
- package/dist/core/session-export.test.d.ts.map +1 -0
- package/dist/core/session-export.test.js +353 -0
- package/dist/core/session-export.test.js.map +1 -0
- package/dist/core/session-history.d.ts +95 -6
- package/dist/core/session-history.d.ts.map +1 -1
- package/dist/core/session-history.js +261 -17
- package/dist/core/session-history.js.map +1 -1
- package/dist/core/session-history.test.js +173 -2
- package/dist/core/session-history.test.js.map +1 -1
- package/dist/core/session-manager.d.ts +223 -6
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +515 -26
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/session-manager.test.js +147 -3
- package/dist/core/session-manager.test.js.map +1 -1
- package/dist/core/session-notification-visibility.test.d.ts +2 -0
- package/dist/core/session-notification-visibility.test.d.ts.map +1 -0
- package/dist/core/session-notification-visibility.test.js +113 -0
- package/dist/core/session-notification-visibility.test.js.map +1 -0
- package/dist/core/session-preview.d.ts +5 -4
- package/dist/core/session-preview.d.ts.map +1 -1
- package/dist/core/session-preview.js +12 -5
- package/dist/core/session-preview.js.map +1 -1
- package/dist/core/session-restore-display.test.js +25 -0
- package/dist/core/session-restore-display.test.js.map +1 -1
- package/dist/core/session-storage.d.ts.map +1 -1
- package/dist/core/session-storage.js +64 -4
- package/dist/core/session-storage.js.map +1 -1
- package/dist/core/session-storage.test.js +10 -2
- package/dist/core/session-storage.test.js.map +1 -1
- package/dist/core/settings-manager.d.ts +13 -2
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +36 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/setup-history.d.ts +0 -6
- package/dist/core/setup-history.d.ts.map +1 -1
- package/dist/core/setup-history.js +2 -18
- package/dist/core/setup-history.js.map +1 -1
- package/dist/core/setup-history.test.js +11 -26
- package/dist/core/setup-history.test.js.map +1 -1
- package/dist/core/skills-routing.test.js +3 -1
- package/dist/core/skills-routing.test.js.map +1 -1
- package/dist/core/slash-commands.d.ts +18 -0
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +42 -10
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/slash-commands.test.d.ts +2 -0
- package/dist/core/slash-commands.test.d.ts.map +1 -0
- package/dist/core/slash-commands.test.js +59 -0
- package/dist/core/slash-commands.test.js.map +1 -0
- package/dist/core/speed-benchmark.test.js +9 -3
- package/dist/core/speed-benchmark.test.js.map +1 -1
- package/dist/core/steering.d.ts +12 -0
- package/dist/core/steering.d.ts.map +1 -1
- package/dist/core/steering.js +16 -0
- package/dist/core/steering.js.map +1 -1
- package/dist/core/subagent-manager.d.ts +14 -1
- package/dist/core/subagent-manager.d.ts.map +1 -1
- package/dist/core/subagent-manager.js +48 -1
- package/dist/core/subagent-manager.js.map +1 -1
- package/dist/core/subagent-manager.test.js +43 -7
- package/dist/core/subagent-manager.test.js.map +1 -1
- package/dist/core/subagent-store.test.js +14 -2
- package/dist/core/subagent-store.test.js.map +1 -1
- package/dist/core/verification-evidence.d.ts +17 -0
- package/dist/core/verification-evidence.d.ts.map +1 -0
- package/dist/core/verification-evidence.js +283 -0
- package/dist/core/verification-evidence.js.map +1 -0
- package/dist/core/verification-evidence.test.d.ts +2 -0
- package/dist/core/verification-evidence.test.d.ts.map +1 -0
- package/dist/core/verification-evidence.test.js +103 -0
- package/dist/core/verification-evidence.test.js.map +1 -0
- package/dist/core/workspace-guard.d.ts +2 -0
- package/dist/core/workspace-guard.d.ts.map +1 -1
- package/dist/core/workspace-guard.js +4 -1
- package/dist/core/workspace-guard.js.map +1 -1
- package/dist/core/workspace-guard.test.js +21 -0
- package/dist/core/workspace-guard.test.js.map +1 -1
- package/dist/interactive.d.ts.map +1 -1
- package/dist/interactive.js +17 -3
- package/dist/interactive.js.map +1 -1
- package/dist/modes/acp-mode.d.ts +122 -0
- package/dist/modes/acp-mode.d.ts.map +1 -0
- package/dist/modes/acp-mode.js +1366 -0
- package/dist/modes/acp-mode.js.map +1 -0
- package/dist/modes/acp-mode.test.d.ts +2 -0
- package/dist/modes/acp-mode.test.d.ts.map +1 -0
- package/dist/modes/acp-mode.test.js +1065 -0
- package/dist/modes/acp-mode.test.js.map +1 -0
- package/dist/modes/index.d.ts +1 -0
- package/dist/modes/index.d.ts.map +1 -1
- package/dist/modes/index.js +1 -0
- package/dist/modes/index.js.map +1 -1
- package/dist/modes/json-mode.d.ts +35 -0
- package/dist/modes/json-mode.d.ts.map +1 -1
- package/dist/modes/json-mode.js +38 -0
- package/dist/modes/json-mode.js.map +1 -1
- package/dist/modes/json-mode.test.d.ts +2 -0
- package/dist/modes/json-mode.test.d.ts.map +1 -0
- package/dist/modes/json-mode.test.js +84 -0
- package/dist/modes/json-mode.test.js.map +1 -0
- package/dist/modes/subagent-worker-mode.d.ts +5 -0
- package/dist/modes/subagent-worker-mode.d.ts.map +1 -1
- package/dist/modes/subagent-worker-mode.js +91 -11
- package/dist/modes/subagent-worker-mode.js.map +1 -1
- package/dist/modes/subagent-worker-mode.test.d.ts +2 -0
- package/dist/modes/subagent-worker-mode.test.d.ts.map +1 -0
- package/dist/modes/subagent-worker-mode.test.js +52 -0
- package/dist/modes/subagent-worker-mode.test.js.map +1 -0
- package/dist/session.d.ts +29 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +41 -0
- package/dist/session.js.map +1 -1
- package/dist/system-prompt.d.ts +11 -1
- package/dist/system-prompt.d.ts.map +1 -1
- package/dist/system-prompt.js +39 -18
- package/dist/system-prompt.js.map +1 -1
- package/dist/system-prompt.test.js +33 -17
- package/dist/system-prompt.test.js.map +1 -1
- package/dist/test-support/fake-home.d.ts +20 -0
- package/dist/test-support/fake-home.d.ts.map +1 -0
- package/dist/test-support/fake-home.js +37 -0
- package/dist/test-support/fake-home.js.map +1 -0
- package/dist/tools/bash.d.ts +3 -1
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +70 -14
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/bash.test.js +122 -7
- package/dist/tools/bash.test.js.map +1 -1
- package/dist/tools/edit-robustness.test.js +3 -1
- package/dist/tools/edit-robustness.test.js.map +1 -1
- package/dist/tools/find.d.ts.map +1 -1
- package/dist/tools/find.js +4 -1
- package/dist/tools/find.js.map +1 -1
- package/dist/tools/goals.d.ts +2 -2
- package/dist/tools/grep.d.ts +3 -1
- package/dist/tools/grep.d.ts.map +1 -1
- package/dist/tools/grep.js +137 -12
- package/dist/tools/grep.js.map +1 -1
- package/dist/tools/grep.test.js +53 -0
- package/dist/tools/grep.test.js.map +1 -1
- package/dist/tools/index.d.ts +19 -2
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +22 -19
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/index.test.d.ts +2 -0
- package/dist/tools/index.test.d.ts.map +1 -0
- package/dist/tools/index.test.js +55 -0
- package/dist/tools/index.test.js.map +1 -0
- package/dist/tools/overflow.test.js +4 -7
- package/dist/tools/overflow.test.js.map +1 -1
- package/dist/tools/path-utils.d.ts +15 -0
- package/dist/tools/path-utils.d.ts.map +1 -1
- package/dist/tools/path-utils.js +17 -0
- package/dist/tools/path-utils.js.map +1 -1
- package/dist/tools/path-utils.test.js +7 -2
- package/dist/tools/path-utils.test.js.map +1 -1
- package/dist/tools/prompt-hints.js +1 -1
- package/dist/tools/prompt-hints.js.map +1 -1
- package/dist/tools/read-only-bash.d.ts +3 -0
- package/dist/tools/read-only-bash.d.ts.map +1 -1
- package/dist/tools/read-only-bash.js +7 -9
- package/dist/tools/read-only-bash.js.map +1 -1
- package/dist/tools/search-code.d.ts.map +1 -1
- package/dist/tools/search-code.js +6 -4
- package/dist/tools/search-code.js.map +1 -1
- package/dist/tools/subagent-control.d.ts.map +1 -1
- package/dist/tools/subagent-control.js +6 -2
- package/dist/tools/subagent-control.js.map +1 -1
- package/dist/tools/subagent-shared.d.ts +6 -0
- package/dist/tools/subagent-shared.d.ts.map +1 -1
- package/dist/tools/subagent-shared.js +6 -0
- package/dist/tools/subagent-shared.js.map +1 -1
- package/dist/tools/subagent.d.ts +1 -1
- package/dist/tools/subagent.d.ts.map +1 -1
- package/dist/tools/subagent.js +51 -8
- package/dist/tools/subagent.js.map +1 -1
- package/dist/tools/subagent.test.js +65 -3
- package/dist/tools/subagent.test.js.map +1 -1
- package/dist/tools/task-output.d.ts.map +1 -1
- package/dist/tools/task-output.js +3 -1
- package/dist/tools/task-output.js.map +1 -1
- package/dist/tools/tool-search.d.ts +12 -1
- package/dist/tools/tool-search.d.ts.map +1 -1
- package/dist/tools/tool-search.js +22 -3
- package/dist/tools/tool-search.js.map +1 -1
- package/dist/tools/tool-search.test.d.ts +2 -0
- package/dist/tools/tool-search.test.d.ts.map +1 -0
- package/dist/tools/tool-search.test.js +69 -0
- package/dist/tools/tool-search.test.js.map +1 -0
- package/dist/tools/web-fetch.d.ts +3 -2
- package/dist/tools/web-fetch.d.ts.map +1 -1
- package/dist/tools/web-fetch.js +22 -8
- package/dist/tools/web-fetch.js.map +1 -1
- package/dist/tools/web-fetch.test.js +34 -0
- package/dist/tools/web-fetch.test.js.map +1 -1
- package/dist/tools/web-search.d.ts +2 -1
- package/dist/tools/web-search.d.ts.map +1 -1
- package/dist/tools/web-search.js +21 -5
- package/dist/tools/web-search.js.map +1 -1
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/App.d.ts.map +1 -1
- package/dist/ui/App.js +32 -81
- package/dist/ui/App.js.map +1 -1
- package/dist/ui/app-items.d.ts +1 -12
- package/dist/ui/app-items.d.ts.map +1 -1
- package/dist/ui/app-items.js.map +1 -1
- package/dist/ui/components/Footer.d.ts +1 -1
- package/dist/ui/components/Footer.js +2 -2
- package/dist/ui/components/Footer.js.map +1 -1
- package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
- package/dist/ui/hooks/useAgentLoop.js +18 -4
- package/dist/ui/hooks/useAgentLoop.js.map +1 -1
- package/dist/ui/hooks/usePixelFixFlow.d.ts +1 -2
- package/dist/ui/hooks/usePixelFixFlow.d.ts.map +1 -1
- package/dist/ui/hooks/usePixelFixFlow.js +2 -5
- package/dist/ui/hooks/usePixelFixFlow.js.map +1 -1
- package/dist/ui/login.js +1 -1
- package/dist/ui/login.js.map +1 -1
- package/dist/ui/terminal-history-status-renderers.d.ts +1 -2
- package/dist/ui/terminal-history-status-renderers.d.ts.map +1 -1
- package/dist/ui/terminal-history-status-renderers.js +2 -15
- package/dist/ui/terminal-history-status-renderers.js.map +1 -1
- package/dist/ui/terminal-history.d.ts.map +1 -1
- package/dist/ui/terminal-history.js +2 -4
- package/dist/ui/terminal-history.js.map +1 -1
- package/dist/ui/thinking-level-cycle.test.js +8 -8
- package/dist/ui/thinking-level-cycle.test.js.map +1 -1
- package/dist/ui/transcript/MiscRows.d.ts +1 -4
- package/dist/ui/transcript/MiscRows.d.ts.map +1 -1
- package/dist/ui/transcript/MiscRows.js +2 -7
- package/dist/ui/transcript/MiscRows.js.map +1 -1
- package/dist/ui/transcript/TranscriptRenderer.d.ts.map +1 -1
- package/dist/ui/transcript/TranscriptRenderer.js +1 -3
- package/dist/ui/transcript/TranscriptRenderer.js.map +1 -1
- package/dist/ui/transcript/presentation.d.ts +1 -9
- package/dist/ui/transcript/presentation.d.ts.map +1 -1
- package/dist/ui/transcript/presentation.js +0 -9
- package/dist/ui/transcript/presentation.js.map +1 -1
- package/dist/ui/transcript/spacing.d.ts +2 -2
- package/dist/ui/transcript/spacing.d.ts.map +1 -1
- package/dist/ui/transcript/spacing.js +0 -1
- package/dist/ui/transcript/spacing.js.map +1 -1
- package/dist/ui/transcript/spacing.test.js +1 -3
- package/dist/ui/transcript/spacing.test.js.map +1 -1
- package/dist/ui/tui-history-parity.test.js +2 -9
- package/dist/ui/tui-history-parity.test.js.map +1 -1
- package/dist/utils/github.d.ts +20 -0
- package/dist/utils/github.d.ts.map +1 -0
- package/dist/utils/github.js +65 -0
- package/dist/utils/github.js.map +1 -0
- package/dist/utils/github.test.d.ts +2 -0
- package/dist/utils/github.test.d.ts.map +1 -0
- package/dist/utils/github.test.js +23 -0
- package/dist/utils/github.test.js.map +1 -0
- package/dist/utils/image.d.ts +26 -3
- package/dist/utils/image.d.ts.map +1 -1
- package/dist/utils/image.js +83 -14
- package/dist/utils/image.js.map +1 -1
- package/dist/utils/image.test.d.ts +2 -0
- package/dist/utils/image.test.d.ts.map +1 -0
- package/dist/utils/image.test.js +139 -0
- package/dist/utils/image.test.js.map +1 -0
- package/dist/utils/process.d.ts +26 -3
- package/dist/utils/process.d.ts.map +1 -1
- package/dist/utils/process.js +68 -10
- package/dist/utils/process.js.map +1 -1
- package/dist/utils/process.test.d.ts +2 -0
- package/dist/utils/process.test.d.ts.map +1 -0
- package/dist/utils/process.test.js +42 -0
- package/dist/utils/process.test.js.map +1 -0
- package/package.json +6 -5
package/dist/app-sidecar.js
CHANGED
|
@@ -21,8 +21,10 @@ import { parseArgs } from "node:util";
|
|
|
21
21
|
import { environmentSecrets, formatError, redactValue } from "@prestyj/ai";
|
|
22
22
|
import { runJsonMode } from "./modes/json-mode.js";
|
|
23
23
|
import { runSubagentWorkerMode } from "./modes/subagent-worker-mode.js";
|
|
24
|
+
import { setStreamDiagnostic } from "@prestyj/agent";
|
|
24
25
|
import { AgentSession } from "./core/agent-session.js";
|
|
25
26
|
import { RunLifecycle } from "./core/run-lifecycle.js";
|
|
27
|
+
import { RunClaim } from "./core/run-claim.js";
|
|
26
28
|
import { CHAT_AGENT_IDS, chatAgentSessionsDir, createChatAgent, parseChatAgentId, switchChatAgent, } from "./chat-agents/index.js";
|
|
27
29
|
import { buildJiwaTools, JiwaStore } from "./chat-agents/jiwa.js";
|
|
28
30
|
import { buildMemoryTools, MemoryStore } from "./chat-agents/memory.js";
|
|
@@ -32,11 +34,13 @@ import { parseAutopilotVerdict } from "./core/autopilot-verdict.js";
|
|
|
32
34
|
import { isWorkflowCommandText, countAssistantMessages, shouldStartAutopilotCycle, extractTurnToolCalls, isMechanicalOnlyTurn, } from "./core/autopilot-gate.js";
|
|
33
35
|
import { driveAutopilotCycle, frameAutopilotInjection } from "./core/autopilot-cycle.js";
|
|
34
36
|
import { validateNolanModelPref, effectiveNolanModel, } from "./core/nolan-model.js";
|
|
35
|
-
import { normalizeAutopilotMarkersForHistory, normalizeAppMarkersForHistory, normalizeNolanTurnsForHistory, restoreUserRow, restoreAssistantTexts, autopilotMarkerCopySeed, } from "./core/session-history.js";
|
|
37
|
+
import { normalizeAutopilotMarkersForHistory, normalizeAppMarkersForHistory, normalizeNolanTurnsForHistory, getHistoryMessageVisibility, replayMessagesInOrder, restoreUserRow, restoreAssistantTexts, resolveRestoredCommand, autopilotMarkerCopySeed, } from "./core/session-history.js";
|
|
38
|
+
import { sessionToMarkdown, defaultExportFilename, } from "./core/session-export.js";
|
|
36
39
|
import { AuthStorage } from "./core/auth-storage.js";
|
|
37
40
|
import { cleanupToolOutputs } from "./tools/overflow.js";
|
|
38
41
|
import { readCappedBody } from "./utils/http-body.js";
|
|
39
|
-
import { fetchSubscriptionUsage, MOONSHOT_OAUTH_KEY, SubscriptionUsageError, XIAOMI_CREDITS_KEY, } from "@prestyj/core";
|
|
42
|
+
import { fetchSubscriptionUsage, MOONSHOT_OAUTH_KEY, SubscriptionUsageError, XIAOMI_CREDITS_KEY, discoverLocalModels, findProbedModel, formatLocalModelId, parseLocalModelId, probeEndpoint, toModelInfo as localModelInfo, } from "@prestyj/core";
|
|
43
|
+
import { LocalEndpointError, addCustomEndpoint, listAllEndpoints, removeCustomEndpoint, syncEndpointCredentials, } from "./core/local-endpoint-store.js";
|
|
40
44
|
import { loginAnthropic } from "./core/oauth/anthropic.js";
|
|
41
45
|
import { loginOpenAI } from "./core/oauth/openai.js";
|
|
42
46
|
import { loginGemini } from "./core/oauth/gemini.js";
|
|
@@ -44,9 +48,11 @@ import { loginKimi } from "./core/oauth/kimi.js";
|
|
|
44
48
|
import { AUTH_PROVIDERS } from "./core/auth-providers.js";
|
|
45
49
|
import { ensureAppDirs, loadSavedSettings } from "./config.js";
|
|
46
50
|
import { SettingsManager } from "./core/settings-manager.js";
|
|
47
|
-
import {
|
|
51
|
+
import { installPlugin, listInstalledPlugins, removePlugin, } from "./core/extensions/plugin-bundles.js";
|
|
52
|
+
import { getModel, getDefaultThinkingLevel, getContextWindow, getAllModels, clearRuntimeModels, registerRuntimeModels, } from "./core/model-registry.js";
|
|
48
53
|
import { resolveStartOrFallback } from "./core/resolve-start.js";
|
|
49
54
|
import { getGitBranch, getGitDirtyFileCount, isGitRepo } from "./utils/git.js";
|
|
55
|
+
import { getGitHubOpenCounts, getGitHubRepoSlug } from "./utils/github.js";
|
|
50
56
|
import { extractPlanSteps } from "./utils/plan-steps.js";
|
|
51
57
|
import { getNextThinkingLevel, getSupportedThinkingLevels, isThinkingLevelSupported, } from "./core/thinking-level.js";
|
|
52
58
|
import { PROMPT_COMMANDS } from "./core/prompt-commands.js";
|
|
@@ -60,13 +66,18 @@ import { enrichProcessPath } from "./core/shell-path.js";
|
|
|
60
66
|
import { downscaleForPreview, shrinkToFit, validateVisionImage } from "./utils/image.js";
|
|
61
67
|
import { startServeMode } from "./modes/serve-mode.js";
|
|
62
68
|
import { loadTelegramConfig, saveTelegramConfig, verifyBotToken } from "./core/telegram-config.js";
|
|
63
|
-
import { loadServers, addServer, removeServer, getServer, parseMcpAddCommand, MCPClientManager, McpOAuthStore, } from "./core/mcp/index.js";
|
|
69
|
+
import { loadServers, addServer, removeServer, getServer, parseMcpAddCommand, MCPClientManager, McpOAuthStore, createElicitationBridge, } from "./core/mcp/index.js";
|
|
64
70
|
import { buildSnapshot, levelForXp, rankForLevel } from "./core/progress/ranks.js";
|
|
65
71
|
import { loadProgress, peekProgress, updateProgress } from "./core/progress/store.js";
|
|
66
72
|
import { awardPrompt, awardCommits } from "./core/progress/engine.js";
|
|
67
73
|
import { detectNewCommits, repoKey } from "./core/progress/git-xp.js";
|
|
68
74
|
import { rebuildFromSessions } from "./core/progress/rebuild.js";
|
|
69
75
|
import { captureSidecarError, flushSidecarErrors, shouldCaptureToolFailure, shouldCaptureUsagePollingError, wrapSidecarHandler, } from "./core/sidecar-error-reporter.js";
|
|
76
|
+
const AUTOMATION_PROVENANCE = {
|
|
77
|
+
source: "runtime",
|
|
78
|
+
kind: "automation",
|
|
79
|
+
visibility: "hidden",
|
|
80
|
+
};
|
|
70
81
|
const ALL_PROVIDERS = [
|
|
71
82
|
// US
|
|
72
83
|
"anthropic",
|
|
@@ -83,6 +94,12 @@ const ALL_PROVIDERS = [
|
|
|
83
94
|
"sakana",
|
|
84
95
|
"openrouter",
|
|
85
96
|
];
|
|
97
|
+
function stringArray(value) {
|
|
98
|
+
if (!Array.isArray(value))
|
|
99
|
+
return undefined;
|
|
100
|
+
const out = value.filter((v) => typeof v === "string" && v.trim().length > 0);
|
|
101
|
+
return out.length > 0 ? out : undefined;
|
|
102
|
+
}
|
|
86
103
|
function appSettingsFile() {
|
|
87
104
|
return path.join(os.homedir(), ".ezcoder", "ezcoder-app.json");
|
|
88
105
|
}
|
|
@@ -106,6 +123,8 @@ async function loadAppSettings() {
|
|
|
106
123
|
projectModels: raw.projectModels && typeof raw.projectModels === "object" ? raw.projectModels : undefined,
|
|
107
124
|
autopilot: raw.autopilot && typeof raw.autopilot === "object" ? raw.autopilot : undefined,
|
|
108
125
|
nolanModels: raw.nolanModels && typeof raw.nolanModels === "object" ? raw.nolanModels : undefined,
|
|
126
|
+
projectRoots: stringArray(raw.projectRoots),
|
|
127
|
+
hiddenProjects: stringArray(raw.hiddenProjects),
|
|
109
128
|
};
|
|
110
129
|
}
|
|
111
130
|
catch {
|
|
@@ -344,6 +363,11 @@ async function searchProjectFiles(cwd, rawQuery) {
|
|
|
344
363
|
* hook prompt, by its distinctive opening phrase. Returns the hook kind so the
|
|
345
364
|
* webview can render the short notice line instead of the full prompt body.
|
|
346
365
|
*/
|
|
366
|
+
/** `?tools=` on /export. Anything unrecognised (including absent) falls back to
|
|
367
|
+
* the human-facing `summary` default rather than dumping every payload. */
|
|
368
|
+
function parseToolDetail(value) {
|
|
369
|
+
return value === "none" || value === "full" ? value : "summary";
|
|
370
|
+
}
|
|
347
371
|
function detectHookKind(text) {
|
|
348
372
|
const t = text.trimStart();
|
|
349
373
|
if (t.startsWith("Ideal? Review the actual work"))
|
|
@@ -354,30 +378,6 @@ function detectHookKind(text) {
|
|
|
354
378
|
return "regrounding";
|
|
355
379
|
return null;
|
|
356
380
|
}
|
|
357
|
-
// Separator AgentSession.prompt() inserts between a command's prompt body and
|
|
358
|
-
// the user's trailing args. Must stay in sync with the expansion there.
|
|
359
|
-
const COMMAND_ARGS_SEP = "\n\n## User Instructions\n\n";
|
|
360
|
-
/**
|
|
361
|
-
* Reverse a prompt-template command's expansion. When a `/name` command runs,
|
|
362
|
-
* the agent persists the FULL expanded prompt body as the user message — so on
|
|
363
|
-
* resume the raw body would render instead of the short `/name` chip the user
|
|
364
|
-
* saw live. Given the candidate commands (built-in + custom) and a restored
|
|
365
|
-
* message body, recover the original `/name [args]` invocation. Returns null
|
|
366
|
-
* when the text isn't a known command body (an ordinary user message).
|
|
367
|
-
*/
|
|
368
|
-
function detectPromptCommand(text, candidates) {
|
|
369
|
-
for (const c of candidates) {
|
|
370
|
-
if (!c.prompt)
|
|
371
|
-
continue;
|
|
372
|
-
if (text === c.prompt)
|
|
373
|
-
return `/${c.name}`;
|
|
374
|
-
if (text.startsWith(c.prompt + COMMAND_ARGS_SEP)) {
|
|
375
|
-
const args = text.slice(c.prompt.length + COMMAND_ARGS_SEP.length).trim();
|
|
376
|
-
return args ? `/${c.name} ${args}` : `/${c.name}`;
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
return null;
|
|
380
|
-
}
|
|
381
381
|
/** A short transport summary for display (URL for http/sse, command+args for stdio). */
|
|
382
382
|
function mcpRowSummary(config) {
|
|
383
383
|
if (config.url)
|
|
@@ -451,6 +451,7 @@ async function runJsonModeIfRequested() {
|
|
|
451
451
|
"max-turns": { type: "string" },
|
|
452
452
|
"system-prompt": { type: "string" },
|
|
453
453
|
tools: { type: "string" },
|
|
454
|
+
"mcp-servers": { type: "string" },
|
|
454
455
|
"prompt-cache-key": { type: "string" },
|
|
455
456
|
},
|
|
456
457
|
allowPositionals: true,
|
|
@@ -467,14 +468,24 @@ async function runJsonModeIfRequested() {
|
|
|
467
468
|
.filter(Boolean)
|
|
468
469
|
: [];
|
|
469
470
|
const allowedTools = parsedTools.length > 0 ? parsedTools : undefined;
|
|
471
|
+
// MCP servers the agent definition asked for. Without forwarding these, an
|
|
472
|
+
// allow-listed child connects no MCP at all and loses live code search.
|
|
473
|
+
const parsedMcpServers = values["mcp-servers"]
|
|
474
|
+
? values["mcp-servers"]
|
|
475
|
+
.split(",")
|
|
476
|
+
.map((s) => s.trim())
|
|
477
|
+
.filter(Boolean)
|
|
478
|
+
: [];
|
|
479
|
+
const allowedMcpServers = parsedMcpServers.length > 0 ? parsedMcpServers : undefined;
|
|
470
480
|
await runJsonMode({
|
|
471
481
|
message: positionals[0] ?? "",
|
|
472
482
|
provider: (values.provider ?? "anthropic"),
|
|
473
|
-
model: values.model ?? "claude-opus-
|
|
483
|
+
model: values.model ?? "claude-opus-5",
|
|
474
484
|
cwd: process.cwd(),
|
|
475
485
|
systemPrompt: values["system-prompt"],
|
|
476
486
|
maxTurns: maxTurnsRaw ? parseInt(maxTurnsRaw, 10) : undefined,
|
|
477
487
|
allowedTools,
|
|
488
|
+
allowedMcpServers,
|
|
478
489
|
promptCacheKey: values["prompt-cache-key"],
|
|
479
490
|
}).catch(async (err) => {
|
|
480
491
|
captureSidecarError(err, "app-sidecar.json-mode", {
|
|
@@ -518,6 +529,31 @@ async function main() {
|
|
|
518
529
|
// ~/.ezcoder/debug.log (initLogger truncates on each start).
|
|
519
530
|
const sidecarLog = path.join(paths.agentDir, "ezcoder-app-sidecar.log");
|
|
520
531
|
initLogger(sidecarLog);
|
|
532
|
+
// The desktop sidecar previously omitted the stream diagnostic hook used by
|
|
533
|
+
// the CLI, leaving device-specific provider stalls impossible to distinguish from
|
|
534
|
+
// event-loop starvation or a broken streaming network path. Keep routine
|
|
535
|
+
// phases lightweight; timeout phases include non-sensitive runtime context.
|
|
536
|
+
setStreamDiagnostic((phase, data) => {
|
|
537
|
+
const includeRuntime = phase === "idle_timeout_fired" ||
|
|
538
|
+
phase === "hard_timeout_fired" ||
|
|
539
|
+
phase === "stall_exhausted";
|
|
540
|
+
log("INFO", "stream", phase, {
|
|
541
|
+
...(data ?? {}),
|
|
542
|
+
...(includeRuntime
|
|
543
|
+
? {
|
|
544
|
+
platform: process.platform,
|
|
545
|
+
arch: process.arch,
|
|
546
|
+
osRelease: os.release(),
|
|
547
|
+
node: process.version,
|
|
548
|
+
logicalCpus: os.cpus().length,
|
|
549
|
+
totalMemoryMb: Math.round(os.totalmem() / 1024 / 1024),
|
|
550
|
+
freeMemoryMb: Math.round(os.freemem() / 1024 / 1024),
|
|
551
|
+
rssMb: Math.round(process.memoryUsage().rss / 1024 / 1024),
|
|
552
|
+
processUptimeSec: Math.round(process.uptime()),
|
|
553
|
+
}
|
|
554
|
+
: {}),
|
|
555
|
+
});
|
|
556
|
+
});
|
|
521
557
|
// Global last-resort guards, installed as early as the logger allows so they
|
|
522
558
|
// cover the WHOLE lifecycle — including startup/initialize, the phase the
|
|
523
559
|
// "sidecar did not start in time" bug lives in. The sidecar is a long-lived
|
|
@@ -553,6 +589,20 @@ async function main() {
|
|
|
553
589
|
// request to its window's session via the `x-gg-session` header (and the
|
|
554
590
|
// `?session=` query for the SSE /events stream).
|
|
555
591
|
const sessions = new Map();
|
|
592
|
+
/**
|
|
593
|
+
* Fan one frame out to every window.
|
|
594
|
+
*
|
|
595
|
+
* For state that is genuinely global rather than per-session — `~/.ezcoder/auth.json`
|
|
596
|
+
* is shared by all windows, so connecting a provider in one must refresh the
|
|
597
|
+
* model picker in all of them. Mirrors the memory/jiwa/progress fan-outs.
|
|
598
|
+
*/
|
|
599
|
+
const broadcastAll = (type, data) => {
|
|
600
|
+
for (const ctx of sessions.values())
|
|
601
|
+
ctx.broadcast(type, data);
|
|
602
|
+
};
|
|
603
|
+
// Providers currently mid-OAuth in some window. Daemon-level so two windows
|
|
604
|
+
// cannot race two browser flows for the same provider into one auth file.
|
|
605
|
+
const oauthInFlightProviders = new Set();
|
|
556
606
|
const memoryStore = new MemoryStore({
|
|
557
607
|
onChange: ({ memories }) => {
|
|
558
608
|
for (const ctx of sessions.values()) {
|
|
@@ -576,6 +626,13 @@ async function main() {
|
|
|
576
626
|
});
|
|
577
627
|
const usageCache = new Map();
|
|
578
628
|
const usageRequests = new Map();
|
|
629
|
+
// Last snapshot that actually carried windows, per provider. Replayed while a
|
|
630
|
+
// fetch is failing so the title meter never blinks out of existence. Bounded
|
|
631
|
+
// by USAGE_LAST_GOOD_MAX_AGE_MS — a provider that never recovers must stop
|
|
632
|
+
// reporting rather than freeze a percentage (and a long-past reset time) on
|
|
633
|
+
// screen forever. The 429 backoff alone runs to 24h, far past any usefulness.
|
|
634
|
+
const usageLastGood = new Map();
|
|
635
|
+
const USAGE_LAST_GOOD_MAX_AGE_MS = 30 * 60_000;
|
|
579
636
|
// 429 backoff: quota endpoints are auxiliary UI data. Honor Retry-After when
|
|
580
637
|
// provided; otherwise retain the unavailable snapshot for 30 minutes. Clamp
|
|
581
638
|
// the provider value so a malformed header can neither hammer the endpoint
|
|
@@ -590,6 +647,9 @@ async function main() {
|
|
|
590
647
|
// Moonshot platform API key is metered per-token, not per plan window.
|
|
591
648
|
const authKey = provider === "moonshot" ? MOONSHOT_OAUTH_KEY : provider;
|
|
592
649
|
if (!(await auth.hasProviderAuth(authKey))) {
|
|
650
|
+
// Logged out: drop the replay cache so a later login on a DIFFERENT
|
|
651
|
+
// account can never inherit the previous one's numbers.
|
|
652
|
+
usageLastGood.delete(provider);
|
|
593
653
|
return { provider, displayName, connected: false, windows: [], fetchedAt: Date.now() };
|
|
594
654
|
}
|
|
595
655
|
try {
|
|
@@ -633,6 +693,18 @@ async function main() {
|
|
|
633
693
|
...(backoffMs !== undefined && { backoffMs: String(backoffMs) }),
|
|
634
694
|
});
|
|
635
695
|
const connected = await auth.hasProviderAuth(authKey);
|
|
696
|
+
// Transient failures (notably the 429s these auxiliary quota endpoints
|
|
697
|
+
// hand out) must not blank the meter. Keep serving the last good
|
|
698
|
+
// snapshot, flagged `stale`, so the bar stays put instead of flickering
|
|
699
|
+
// out for the whole backoff window and back in on the next success.
|
|
700
|
+
// Past the max age it's dropped — no data beats confidently wrong data.
|
|
701
|
+
const lastGood = usageLastGood.get(provider);
|
|
702
|
+
if (lastGood && Date.now() - lastGood.fetchedAt >= USAGE_LAST_GOOD_MAX_AGE_MS) {
|
|
703
|
+
usageLastGood.delete(provider);
|
|
704
|
+
}
|
|
705
|
+
else if (connected && lastGood) {
|
|
706
|
+
return { ...lastGood, stale: true };
|
|
707
|
+
}
|
|
636
708
|
return {
|
|
637
709
|
provider,
|
|
638
710
|
displayName,
|
|
@@ -654,6 +726,11 @@ async function main() {
|
|
|
654
726
|
usageRequests.set(provider, request);
|
|
655
727
|
try {
|
|
656
728
|
const result = await request;
|
|
729
|
+
// Never re-store a replay — it would keep its original `fetchedAt`, but
|
|
730
|
+
// writing it back muddies the "last GOOD" contract for no gain.
|
|
731
|
+
if (result.connected && !result.error && !result.stale && result.windows.length > 0) {
|
|
732
|
+
usageLastGood.set(provider, result);
|
|
733
|
+
}
|
|
657
734
|
// Anthropic can return utilization before it assigns reset timestamps
|
|
658
735
|
// (notably before the account's first active request). Retry that partial
|
|
659
736
|
// snapshot quickly; complete snapshots keep the normal one-minute cache.
|
|
@@ -721,7 +798,15 @@ async function main() {
|
|
|
721
798
|
const sessionPath = typeof body.sessionPath === "string" && body.sessionPath ? body.sessionPath : undefined;
|
|
722
799
|
const id = randomUUID();
|
|
723
800
|
try {
|
|
724
|
-
const ctx = await createSession({
|
|
801
|
+
const ctx = await createSession({
|
|
802
|
+
auth,
|
|
803
|
+
paths,
|
|
804
|
+
progress,
|
|
805
|
+
memoryStore,
|
|
806
|
+
jiwaStore,
|
|
807
|
+
broadcastAll,
|
|
808
|
+
oauthInFlightProviders,
|
|
809
|
+
}, { id, mode, chatAgent, cwd: sessionCwd, sessionPath });
|
|
725
810
|
sessions.set(id, ctx);
|
|
726
811
|
log("INFO", "app-sidecar", "session created", {
|
|
727
812
|
id,
|
|
@@ -997,7 +1082,7 @@ async function createProgressManager(agentDir, broadcastAll) {
|
|
|
997
1082
|
* logger, PATH, shared auth file, and radio live at the daemon level.
|
|
998
1083
|
*/
|
|
999
1084
|
async function createSession(deps, opts) {
|
|
1000
|
-
const { auth, progress, memoryStore, jiwaStore } = deps;
|
|
1085
|
+
const { auth, progress, memoryStore, jiwaStore, broadcastAll, oauthInFlightProviders } = deps;
|
|
1001
1086
|
const paths = deps.paths;
|
|
1002
1087
|
const mode = opts.mode;
|
|
1003
1088
|
let chatAgent = opts.chatAgent;
|
|
@@ -1072,6 +1157,25 @@ async function createSession(deps, opts) {
|
|
|
1072
1157
|
// /help
|
|
1073
1158
|
.replaceAll(/see \/help/gi, "check the help menu"));
|
|
1074
1159
|
}
|
|
1160
|
+
/**
|
|
1161
|
+
* Guidance for a connection failure against the ACTIVE local endpoint, or
|
|
1162
|
+
* undefined when that isn't the situation (so the generic wording stands).
|
|
1163
|
+
* "Disable your VPN / allow us through the firewall" is useless advice for a
|
|
1164
|
+
* server running on this machine — name what the user has to restart.
|
|
1165
|
+
*/
|
|
1166
|
+
function localNetworkGuidance(source) {
|
|
1167
|
+
if (source !== "network")
|
|
1168
|
+
return undefined;
|
|
1169
|
+
const state = session.getState();
|
|
1170
|
+
if (state.provider !== "local")
|
|
1171
|
+
return undefined;
|
|
1172
|
+
const parsed = parseLocalModelId(state.model);
|
|
1173
|
+
const probe = localProbes.find((p) => p.endpoint.id === parsed?.endpointId);
|
|
1174
|
+
if (!probe)
|
|
1175
|
+
return undefined;
|
|
1176
|
+
return (`${probe.endpoint.label} stopped responding at ${probe.endpoint.baseUrl}. ` +
|
|
1177
|
+
"Start it again and retry, or pick another model.");
|
|
1178
|
+
}
|
|
1075
1179
|
// Turn any thrown value into the same clear headline/message/guidance shape
|
|
1076
1180
|
// the TUI shows (see gg-ai's formatError) instead of a bare `err.message`, log
|
|
1077
1181
|
// the full detail, and broadcast it under `type` ("error" or "nolan_error").
|
|
@@ -1081,7 +1185,7 @@ async function createSession(deps, opts) {
|
|
|
1081
1185
|
function broadcastError(type, logLabel, err) {
|
|
1082
1186
|
const f = formatError(err);
|
|
1083
1187
|
const message = f.message ? desktopGuidance(f.message) : undefined;
|
|
1084
|
-
const guidance = desktopGuidance(f.guidance);
|
|
1188
|
+
const guidance = localNetworkGuidance(f.source) ?? desktopGuidance(f.guidance);
|
|
1085
1189
|
captureSidecarError(err, `app-sidecar.${logLabel.replaceAll(" ", "-")}`, {
|
|
1086
1190
|
scope: type,
|
|
1087
1191
|
...(f.provider ? { provider: f.provider } : {}),
|
|
@@ -1114,6 +1218,17 @@ async function createSession(deps, opts) {
|
|
|
1114
1218
|
})
|
|
1115
1219
|
.catch(() => { });
|
|
1116
1220
|
}
|
|
1221
|
+
// ── MCP elicitation bridge ─────────────────────────────────
|
|
1222
|
+
// An MCP server can ask for user input in the middle of a tool call. The
|
|
1223
|
+
// bridge parks the promise; we broadcast the prompt over SSE and resolve it
|
|
1224
|
+
// when the webview POSTs /mcp/elicit/:id.
|
|
1225
|
+
const elicitations = createElicitationBridge({
|
|
1226
|
+
broadcast: (prompt) => broadcast("mcp_elicit", prompt),
|
|
1227
|
+
onTimeout: (prompt) => log("WARN", "app-sidecar", "MCP elicitation timed out", {
|
|
1228
|
+
id: prompt.id,
|
|
1229
|
+
server: prompt.server,
|
|
1230
|
+
}),
|
|
1231
|
+
});
|
|
1117
1232
|
// The session file path to resume (passed by the daemon's POST /session);
|
|
1118
1233
|
// empty/unset starts a fresh session.
|
|
1119
1234
|
const resumeSessionPath = opts.sessionPath;
|
|
@@ -1127,6 +1242,7 @@ async function createSession(deps, opts) {
|
|
|
1127
1242
|
signal: abort.signal,
|
|
1128
1243
|
// Keep MCP startup off the readiness path in both modes.
|
|
1129
1244
|
backgroundMcpConnect: true,
|
|
1245
|
+
onMcpElicit: elicitations.onElicit,
|
|
1130
1246
|
// Keep restore-time auto-compaction off the readiness path too: its summary
|
|
1131
1247
|
// LLM call (30s timeout) used to freeze waitForReady — and with it the whole
|
|
1132
1248
|
// window (project picker, session list) — whenever a resumed session was
|
|
@@ -1197,17 +1313,116 @@ async function createSession(deps, opts) {
|
|
|
1197
1313
|
}
|
|
1198
1314
|
}
|
|
1199
1315
|
log("INFO", "app-sidecar", "session ready", { provider, model, mode, chatAgent, cwd });
|
|
1316
|
+
// ── Local models (Ollama / LM Studio / llama.cpp / vLLM) ──
|
|
1317
|
+
// Probing four HTTP endpoints must never delay readiness, so this runs in the
|
|
1318
|
+
// background (same shape as backgroundMcpConnect) and pushes a models_change
|
|
1319
|
+
// frame when it lands.
|
|
1320
|
+
let localProbes = [];
|
|
1321
|
+
async function scanLocalModels(force) {
|
|
1322
|
+
const endpoints = await listAllEndpoints();
|
|
1323
|
+
const { probes, models } = await discoverLocalModels(endpoints, { force });
|
|
1324
|
+
localProbes = probes;
|
|
1325
|
+
// Only endpoints that answered get a credential: writing one for a server
|
|
1326
|
+
// that isn't running would make `hasProviderAuth("local")` true forever.
|
|
1327
|
+
await syncEndpointCredentials(probes.filter((probe) => probe.reachable).map((probe) => probe.endpoint));
|
|
1328
|
+
clearRuntimeModels((m) => m.provider === "local");
|
|
1329
|
+
registerRuntimeModels(models);
|
|
1330
|
+
log("INFO", "app-sidecar", "local model scan", {
|
|
1331
|
+
reachable: probes.filter((p) => p.reachable).length + "/" + probes.length,
|
|
1332
|
+
models: String(models.length),
|
|
1333
|
+
});
|
|
1334
|
+
return probes;
|
|
1335
|
+
}
|
|
1336
|
+
/** Endpoint rows + models, in the shape the Local models UI renders. */
|
|
1337
|
+
function localStatePayload() {
|
|
1338
|
+
return {
|
|
1339
|
+
endpoints: localProbes.map((probe) => ({
|
|
1340
|
+
id: probe.endpoint.id,
|
|
1341
|
+
label: probe.endpoint.label,
|
|
1342
|
+
baseUrl: probe.endpoint.baseUrl,
|
|
1343
|
+
kind: probe.endpoint.kind,
|
|
1344
|
+
custom: probe.endpoint.custom === true,
|
|
1345
|
+
reachable: probe.reachable,
|
|
1346
|
+
...(probe.reason ? { reason: probe.reason } : {}),
|
|
1347
|
+
models: probe.models.map((m) => ({
|
|
1348
|
+
id: formatLocalModelId(probe.endpoint.id, m.rawId),
|
|
1349
|
+
rawId: m.rawId,
|
|
1350
|
+
contextWindow: m.contextWindow,
|
|
1351
|
+
contextWindowKnown: m.contextWindowKnown,
|
|
1352
|
+
supportsTools: m.supportsTools,
|
|
1353
|
+
supportsImages: m.supportsImages,
|
|
1354
|
+
supportsThinking: m.supportsThinking,
|
|
1355
|
+
...(m.loaded === undefined ? {} : { loaded: m.loaded }),
|
|
1356
|
+
})),
|
|
1357
|
+
})),
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
/**
|
|
1361
|
+
* Why `modelId` can't be selected right now, or `undefined` when it can.
|
|
1362
|
+
* Two real footguns get a clear answer instead of a mid-run provider error:
|
|
1363
|
+
* a model with no tool calling (can't drive the agent at all), and a server
|
|
1364
|
+
* that has since been shut down.
|
|
1365
|
+
*/
|
|
1366
|
+
async function localModelBlocker(modelId) {
|
|
1367
|
+
const parsed = parseLocalModelId(modelId);
|
|
1368
|
+
if (!parsed)
|
|
1369
|
+
return undefined;
|
|
1370
|
+
const endpoints = await listAllEndpoints();
|
|
1371
|
+
const endpoint = endpoints.find((e) => e.id === parsed.endpointId);
|
|
1372
|
+
if (!endpoint)
|
|
1373
|
+
return `Unknown local endpoint "${parsed.endpointId}" — re-scan for local models.`;
|
|
1374
|
+
const probe = await probeEndpoint(endpoint);
|
|
1375
|
+
// Keep the cached view honest: this probe is fresher than the last scan.
|
|
1376
|
+
localProbes = localProbes.map((p) => (p.endpoint.id === endpoint.id ? probe : p));
|
|
1377
|
+
if (!probe.reachable) {
|
|
1378
|
+
return `${endpoint.label} isn't running at ${endpoint.baseUrl}. Start it and scan again.`;
|
|
1379
|
+
}
|
|
1380
|
+
const model = probe.models.find((m) => m.rawId === parsed.rawId);
|
|
1381
|
+
if (!model) {
|
|
1382
|
+
return `${endpoint.label} no longer serves "${parsed.rawId}".`;
|
|
1383
|
+
}
|
|
1384
|
+
if (!model.supportsTools) {
|
|
1385
|
+
return `${parsed.rawId} has no tool calling, so it can't run the agent. Pick a tool-capable model.`;
|
|
1386
|
+
}
|
|
1387
|
+
registerRuntimeModels(probe.models.map((m) => localModelInfo(m, endpoint)));
|
|
1388
|
+
return undefined;
|
|
1389
|
+
}
|
|
1390
|
+
/**
|
|
1391
|
+
* A restored per-project pref can ask for thinking on a local model that
|
|
1392
|
+
* turns out not to reason (capabilities are only known after a probe). Drop
|
|
1393
|
+
* the level once we know, so the first prompt doesn't carry a
|
|
1394
|
+
* `reasoning_effort` the server rejects.
|
|
1395
|
+
*/
|
|
1396
|
+
function clampLocalThinking() {
|
|
1397
|
+
const st = session.getState();
|
|
1398
|
+
const level = session.getThinkingLevel();
|
|
1399
|
+
if (!level || st.provider !== "local")
|
|
1400
|
+
return;
|
|
1401
|
+
if (isThinkingLevelSupported(st.provider, st.model, level))
|
|
1402
|
+
return;
|
|
1403
|
+
session.setThinkingLevel(undefined);
|
|
1404
|
+
broadcast("thinking_change", {
|
|
1405
|
+
thinkingLevel: null,
|
|
1406
|
+
supportedThinkingLevels: getSupportedThinkingLevels(st.provider, st.model),
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1200
1409
|
// Workspace extras (context window, git status, background tasks). Git state
|
|
1201
1410
|
// is resolved once at startup and refreshed after every run; the context
|
|
1202
1411
|
// window follows the active model.
|
|
1203
|
-
const [initialGitBranch, initialGitIsRepo, initialDirtyFileCount] = await Promise.all([
|
|
1412
|
+
const [initialGitBranch, initialGitIsRepo, initialDirtyFileCount, initialGitHubSlug] = await Promise.all([
|
|
1204
1413
|
getGitBranch(cwd).catch(() => null),
|
|
1205
1414
|
isGitRepo(cwd).catch(() => false),
|
|
1206
1415
|
getGitDirtyFileCount(cwd).catch(() => 0),
|
|
1416
|
+
getGitHubRepoSlug(cwd).catch(() => null),
|
|
1207
1417
|
]);
|
|
1208
1418
|
let gitBranch = initialGitBranch;
|
|
1209
1419
|
let gitIsRepo = initialGitIsRepo;
|
|
1210
1420
|
let gitDirtyFileCount = initialDirtyFileCount;
|
|
1421
|
+
// Open issue/PR counts for the origin repo's GitHub slug, via the `gh` CLI's
|
|
1422
|
+
// auth. null = unknown (gh missing/unauthed, non-GitHub origin) → chips hidden.
|
|
1423
|
+
const gitHubSlug = initialGitHubSlug;
|
|
1424
|
+
let gitHubIssues = null;
|
|
1425
|
+
let gitHubPRs = null;
|
|
1211
1426
|
function currentContextWindow() {
|
|
1212
1427
|
const st = session.getState();
|
|
1213
1428
|
return getContextWindow(st.model, { provider: st.provider, accountId: st.accountId });
|
|
@@ -1220,9 +1435,41 @@ async function createSession(deps, opts) {
|
|
|
1220
1435
|
gitBranch,
|
|
1221
1436
|
isGitRepo: gitIsRepo,
|
|
1222
1437
|
gitDirtyFileCount,
|
|
1438
|
+
gitHubIssues,
|
|
1439
|
+
gitHubPRs,
|
|
1440
|
+
gitHubRepoUrl: gitHubSlug ? `https://github.com/${gitHubSlug}` : null,
|
|
1223
1441
|
tasks: session.listBackgroundProcesses(),
|
|
1442
|
+
// Roots added with /add-dir — the header shows a badge when non-empty.
|
|
1443
|
+
additionalRoots: session.getAdditionalRoots(),
|
|
1224
1444
|
};
|
|
1225
1445
|
}
|
|
1446
|
+
void scanLocalModels(false)
|
|
1447
|
+
.then(() => {
|
|
1448
|
+
clampLocalThinking();
|
|
1449
|
+
broadcast("extras", footerExtras());
|
|
1450
|
+
})
|
|
1451
|
+
.then(() => broadcast("models_change", { local: localStatePayload() }))
|
|
1452
|
+
.catch((err) => {
|
|
1453
|
+
// A discovery failure is never fatal — the user simply has no local
|
|
1454
|
+
// models. Log it; don't push an error row into the transcript.
|
|
1455
|
+
log("WARN", "app-sidecar", "local model scan failed", {
|
|
1456
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1457
|
+
});
|
|
1458
|
+
});
|
|
1459
|
+
// Refresh the GitHub counts and broadcast only on change. Transient failures
|
|
1460
|
+
// keep the last-known numbers so the chips don't flicker off on a timeout.
|
|
1461
|
+
async function refreshGitHubCounts() {
|
|
1462
|
+
if (!gitHubSlug)
|
|
1463
|
+
return;
|
|
1464
|
+
const counts = await getGitHubOpenCounts(gitHubSlug);
|
|
1465
|
+
if (!counts)
|
|
1466
|
+
return;
|
|
1467
|
+
if (counts.issues !== gitHubIssues || counts.prs !== gitHubPRs) {
|
|
1468
|
+
gitHubIssues = counts.issues;
|
|
1469
|
+
gitHubPRs = counts.prs;
|
|
1470
|
+
broadcast("extras", footerExtras());
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1226
1473
|
// tool_call_end carries no tool name (only the id), so remember each call's
|
|
1227
1474
|
// name from tool_call_start to log a useful line on completion. Mirrors the
|
|
1228
1475
|
// CLI's logging so the app sidecar's ~/.ezcoder/ezcoder-app-sidecar.log records tool
|
|
@@ -1318,6 +1565,10 @@ async function createSession(deps, opts) {
|
|
|
1318
1565
|
recordApprovedPlanMarkers(d.text);
|
|
1319
1566
|
});
|
|
1320
1567
|
session.eventBus.on("thinking_delta", (d) => broadcast("thinking_delta", d));
|
|
1568
|
+
// The agent consumed queued steering at a turn boundary. Re-broadcast as the
|
|
1569
|
+
// usual `queued` depth update so the webview drops the pending affordance the
|
|
1570
|
+
// moment the message lands in the loop, not at run_end.
|
|
1571
|
+
session.eventBus.on("queue_drained", (d) => broadcast("queued", { count: d.count, messages: session.listQueuedMessages() }));
|
|
1321
1572
|
session.eventBus.on("tool_call_start", (d) => {
|
|
1322
1573
|
toolCallNames.set(d.toolCallId, d.name);
|
|
1323
1574
|
broadcast("tool_call_start", d);
|
|
@@ -1369,10 +1620,29 @@ async function createSession(deps, opts) {
|
|
|
1369
1620
|
session.eventBus.on("compaction_start", (d) => broadcast("compaction_start", d));
|
|
1370
1621
|
session.eventBus.on("compaction_end", (d) => broadcast("compaction_end", d));
|
|
1371
1622
|
let running = false;
|
|
1623
|
+
// Closes the window between `/prompt` deciding to start a run and `runAgent`
|
|
1624
|
+
// flipping `running` — that stretch awaits, so Node yields inside it. See
|
|
1625
|
+
// RunClaim.
|
|
1626
|
+
const runClaim = new RunClaim();
|
|
1372
1627
|
const runLifecycle = new RunLifecycle((runState) => {
|
|
1373
1628
|
running = runState !== "idle";
|
|
1374
1629
|
if (runState === "cancelling")
|
|
1375
1630
|
broadcast("run_cancelling", { runState });
|
|
1631
|
+
},
|
|
1632
|
+
// Durable run journal. Fire-and-forget on purpose: an unwritten journal
|
|
1633
|
+
// entry is a missed crash hint, while a journal write that throws inside
|
|
1634
|
+
// begin()/settle() would break run ownership itself.
|
|
1635
|
+
{
|
|
1636
|
+
started: (generation) => void session.persistRunStarted(generation).catch((err) => {
|
|
1637
|
+
log("WARN", "app-sidecar", "failed to journal run start", {
|
|
1638
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1639
|
+
});
|
|
1640
|
+
}),
|
|
1641
|
+
finished: (generation, outcome) => void session.persistRunFinished(generation, outcome).catch((err) => {
|
|
1642
|
+
log("WARN", "app-sidecar", "failed to journal run finish", {
|
|
1643
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1644
|
+
});
|
|
1645
|
+
}),
|
|
1376
1646
|
});
|
|
1377
1647
|
const cancelledRunEndGenerations = new Set();
|
|
1378
1648
|
let pendingCancelDrain = null;
|
|
@@ -1448,7 +1718,7 @@ async function createSession(deps, opts) {
|
|
|
1448
1718
|
// with `@Nolan …`; he reads EZ Coder's transcript (one-way — EZ Coder never sees
|
|
1449
1719
|
// Nolan's) and hands back runnable prompts + mentorship. Created lazily on the
|
|
1450
1720
|
// first `@Nolan` so windows that never use Nolan pay zero cost. His events ride the
|
|
1451
|
-
// SAME SSE stream with `
|
|
1721
|
+
// SAME SSE stream with `nolan_`-prefixed types, routed to the Nolan bubble.
|
|
1452
1722
|
let nolanSession = null;
|
|
1453
1723
|
let nolanAbort = new AbortController();
|
|
1454
1724
|
let nolanRunning = false;
|
|
@@ -1478,7 +1748,7 @@ async function createSession(deps, opts) {
|
|
|
1478
1748
|
return { provider: st.provider, model: st.model };
|
|
1479
1749
|
}
|
|
1480
1750
|
/** Footer payload: Nolan's effective model + whether it's a pin. Merged into
|
|
1481
|
-
* /state, the SSE ready frame, and every
|
|
1751
|
+
* /state, the SSE ready frame, and every nolan_model_change broadcast. */
|
|
1482
1752
|
function nolanStatePayload() {
|
|
1483
1753
|
const st = session.getState();
|
|
1484
1754
|
return effectiveNolanModel(nolanModelOverride, { provider: st.provider, model: st.model });
|
|
@@ -1494,13 +1764,13 @@ async function createSession(deps, opts) {
|
|
|
1494
1764
|
if (st.provider === provider && st.model === model)
|
|
1495
1765
|
return;
|
|
1496
1766
|
await nolanSession.switchModel(provider, model);
|
|
1497
|
-
log("INFO", "app-sidecar", "
|
|
1767
|
+
log("INFO", "app-sidecar", "nolan session model synced", { provider, model });
|
|
1498
1768
|
}
|
|
1499
1769
|
async function ensureNolanSession() {
|
|
1500
1770
|
if (nolanSession)
|
|
1501
1771
|
return nolanSession;
|
|
1502
1772
|
const target = nolanCurrentModel();
|
|
1503
|
-
const
|
|
1773
|
+
const nolanAgent = new AgentSession({
|
|
1504
1774
|
provider: target.provider,
|
|
1505
1775
|
model: target.model,
|
|
1506
1776
|
cwd,
|
|
@@ -1509,38 +1779,41 @@ async function createSession(deps, opts) {
|
|
|
1509
1779
|
allowedMcpServers: NOLAN_ALLOWED_MCP_SERVERS,
|
|
1510
1780
|
transient: true,
|
|
1511
1781
|
signal: nolanAbort.signal,
|
|
1512
|
-
// Nolan
|
|
1782
|
+
// Nolan belongs to this window, so route MCP elicitation prompts there.
|
|
1783
|
+
// Keeping daemon sessions interactive also lets them share one pooled MCP connection.
|
|
1784
|
+
onMcpElicit: elicitations.onElicit,
|
|
1785
|
+
// Nolan's bursty, spread-out turns outlast the default cache
|
|
1513
1786
|
// TTL regardless of the user's global speedProfile pick.
|
|
1514
1787
|
forceLongCacheRetention: true,
|
|
1515
1788
|
});
|
|
1516
|
-
await
|
|
1517
|
-
// Bridge Nolan's bus to the shared SSE fan-out with
|
|
1789
|
+
await nolanAgent.initialize();
|
|
1790
|
+
// Bridge Nolan's bus to the shared SSE fan-out with nolan_-prefixed types so the
|
|
1518
1791
|
// webview routes them to the Nolan bubble, never EZ Coder's.
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1792
|
+
nolanAgent.eventBus.on("text_delta", (d) => broadcast("nolan_text_delta", d));
|
|
1793
|
+
nolanAgent.eventBus.on("thinking_delta", (d) => broadcast("nolan_thinking_delta", d));
|
|
1794
|
+
nolanAgent.eventBus.on("tool_call_start", (d) => {
|
|
1522
1795
|
nolanToolCallNames.set(d.toolCallId, d.name);
|
|
1523
1796
|
broadcast("nolan_tool_call_start", d);
|
|
1524
1797
|
});
|
|
1525
|
-
|
|
1526
|
-
|
|
1798
|
+
nolanAgent.eventBus.on("tool_call_update", (d) => broadcast("nolan_tool_call_update", d));
|
|
1799
|
+
nolanAgent.eventBus.on("tool_call_end", (d) => {
|
|
1527
1800
|
nolanToolCallNames.delete(d.toolCallId);
|
|
1528
1801
|
broadcast("nolan_tool_call_end", d);
|
|
1529
1802
|
});
|
|
1530
1803
|
// Native server tools (Anthropic web_search) stream text both before AND
|
|
1531
1804
|
// after them in the same turn; forward so the webview can break the bubble
|
|
1532
1805
|
// (otherwise "...work.Local tools..." glues together). Mirrors the build bus.
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
broadcastError("nolan_error", "
|
|
1806
|
+
nolanAgent.eventBus.on("server_tool_call", (d) => broadcast("nolan_server_tool_call", d));
|
|
1807
|
+
nolanAgent.eventBus.on("turn_end", (d) => broadcast("nolan_turn_end", d));
|
|
1808
|
+
nolanAgent.eventBus.on("error", (d) => {
|
|
1809
|
+
broadcastError("nolan_error", "nolan error", d.error);
|
|
1537
1810
|
});
|
|
1538
|
-
nolanSession =
|
|
1539
|
-
log("INFO", "app-sidecar", "
|
|
1811
|
+
nolanSession = nolanAgent;
|
|
1812
|
+
log("INFO", "app-sidecar", "nolan session ready", {
|
|
1540
1813
|
provider: target.provider,
|
|
1541
1814
|
model: target.model,
|
|
1542
1815
|
});
|
|
1543
|
-
return
|
|
1816
|
+
return nolanAgent;
|
|
1544
1817
|
}
|
|
1545
1818
|
// ── Autopilot Nolan (auto-reviewer) ──────────────────────────
|
|
1546
1819
|
// A THIRD read-only AgentSession, separate from chat Nolan. In autopilot mode
|
|
@@ -1563,13 +1836,13 @@ async function createSession(deps, opts) {
|
|
|
1563
1836
|
if (st.provider === provider && st.model === model)
|
|
1564
1837
|
return;
|
|
1565
1838
|
await nolanAutoSession.switchModel(provider, model);
|
|
1566
|
-
log("INFO", "app-sidecar", "
|
|
1839
|
+
log("INFO", "app-sidecar", "nolan autopilot session model synced", { provider, model });
|
|
1567
1840
|
}
|
|
1568
1841
|
async function ensureNolanAutoSession() {
|
|
1569
1842
|
if (nolanAutoSession)
|
|
1570
1843
|
return nolanAutoSession;
|
|
1571
1844
|
const target = nolanCurrentModel();
|
|
1572
|
-
const
|
|
1845
|
+
const nolanAgent = new AgentSession({
|
|
1573
1846
|
provider: target.provider,
|
|
1574
1847
|
model: target.model,
|
|
1575
1848
|
cwd,
|
|
@@ -1578,26 +1851,32 @@ async function createSession(deps, opts) {
|
|
|
1578
1851
|
allowedMcpServers: NOLAN_ALLOWED_MCP_SERVERS,
|
|
1579
1852
|
transient: true,
|
|
1580
1853
|
signal: nolanAutoAbort.signal,
|
|
1854
|
+
// Route this window's MCP elicitation prompts through the shared bridge.
|
|
1855
|
+
onMcpElicit: elicitations.onElicit,
|
|
1581
1856
|
// Autopilot review rounds routinely span the injected EZ Coder run
|
|
1582
1857
|
// (often >5 min) regardless of the user's global speedProfile pick.
|
|
1583
1858
|
forceLongCacheRetention: true,
|
|
1584
1859
|
});
|
|
1585
1860
|
// Nolan is already the independent autopilot reviewer; recursively running
|
|
1586
1861
|
// his own Ideal self-review adds latency and can corrupt the verdict shape.
|
|
1587
|
-
|
|
1588
|
-
await
|
|
1862
|
+
nolanAgent.setIdealReviewSuppressed(true);
|
|
1863
|
+
await nolanAgent.initialize();
|
|
1589
1864
|
// Deliberately no bus bridge: the review is silent. Errors surface via the
|
|
1590
1865
|
// runAutopilotReview try/catch as autopilot_error frames.
|
|
1591
|
-
nolanAutoSession =
|
|
1592
|
-
log("INFO", "app-sidecar", "
|
|
1866
|
+
nolanAutoSession = nolanAgent;
|
|
1867
|
+
log("INFO", "app-sidecar", "nolan autopilot session ready", {
|
|
1593
1868
|
provider: target.provider,
|
|
1594
1869
|
model: target.model,
|
|
1595
1870
|
});
|
|
1596
|
-
return
|
|
1871
|
+
return nolanAgent;
|
|
1597
1872
|
}
|
|
1598
1873
|
function abortOwnedWork() {
|
|
1599
1874
|
cancelGeneration++;
|
|
1600
1875
|
abort.abort();
|
|
1876
|
+
// An MCP tool call parked on user input is not cancelled by the signal —
|
|
1877
|
+
// the promise lives in the bridge. Release it, or the aborted turn's tool
|
|
1878
|
+
// call never returns.
|
|
1879
|
+
elicitations.cancelAll();
|
|
1601
1880
|
// Stop a run-all sweep and every async child through AgentSession's signal.
|
|
1602
1881
|
taskRunAll = false;
|
|
1603
1882
|
autopilotCancelled = true;
|
|
@@ -1609,9 +1888,9 @@ async function createSession(deps, opts) {
|
|
|
1609
1888
|
nolanAutoAbort = new AbortController();
|
|
1610
1889
|
nolanAutoSession?.setSignal(nolanAutoAbort.signal);
|
|
1611
1890
|
}
|
|
1612
|
-
function finishOwnedGeneration(generation, emitCancelledFallback) {
|
|
1891
|
+
function finishOwnedGeneration(generation, emitCancelledFallback, outcome = "completed") {
|
|
1613
1892
|
const cancelled = runLifecycle.isCancellationRequested(generation);
|
|
1614
|
-
const settlement = runLifecycle.settle(generation);
|
|
1893
|
+
const settlement = runLifecycle.settle(generation, outcome);
|
|
1615
1894
|
if (!settlement.settled)
|
|
1616
1895
|
return cancelled;
|
|
1617
1896
|
// A replacement signal is safe only after the provider-backed owner settled.
|
|
@@ -1664,6 +1943,9 @@ async function createSession(deps, opts) {
|
|
|
1664
1943
|
isGitRepo(cwd).catch(() => gitIsRepo),
|
|
1665
1944
|
getGitDirtyFileCount(cwd).catch(() => gitDirtyFileCount),
|
|
1666
1945
|
]);
|
|
1946
|
+
// A run may have opened/closed issues or PRs — refresh fire-and-forget so
|
|
1947
|
+
// teardown isn't delayed by the network. Broadcasts itself on change.
|
|
1948
|
+
void refreshGitHubCounts();
|
|
1667
1949
|
// Serialize behind any marker/tool-triggered refresh so the terminal
|
|
1668
1950
|
// progress snapshot uses the live plan file. Once every canonical step
|
|
1669
1951
|
// is complete, remove the approved plan from future system prompts and
|
|
@@ -1686,8 +1968,9 @@ async function createSession(deps, opts) {
|
|
|
1686
1968
|
});
|
|
1687
1969
|
}
|
|
1688
1970
|
}
|
|
1689
|
-
if (ownsGeneration)
|
|
1690
|
-
finishOwnedGeneration(generation, false);
|
|
1971
|
+
if (ownsGeneration) {
|
|
1972
|
+
finishOwnedGeneration(generation, false, runSucceeded ? "completed" : "failed");
|
|
1973
|
+
}
|
|
1691
1974
|
// A cancelled injected run is still owned by the surrounding autopilot
|
|
1692
1975
|
// cycle; its outer finalizer emits the one terminal cancelled run_end.
|
|
1693
1976
|
if (!(cancelled && !ownsGeneration)) {
|
|
@@ -1706,7 +1989,10 @@ async function createSession(deps, opts) {
|
|
|
1706
1989
|
// completed ones so they drop out of the Tasks modal automatically (users
|
|
1707
1990
|
// never have to delete finished tasks by hand).
|
|
1708
1991
|
broadcast("tasks_list", { tasks: pruneDoneTasksSync(cwd) });
|
|
1709
|
-
broadcast("queued", {
|
|
1992
|
+
broadcast("queued", {
|
|
1993
|
+
count: session.getQueuedCount(),
|
|
1994
|
+
messages: session.listQueuedMessages(),
|
|
1995
|
+
});
|
|
1710
1996
|
broadcast("extras", footerExtras());
|
|
1711
1997
|
}
|
|
1712
1998
|
}
|
|
@@ -1720,7 +2006,7 @@ async function createSession(deps, opts) {
|
|
|
1720
2006
|
autopilotReviewing = true;
|
|
1721
2007
|
broadcast("autopilot_review_start", {});
|
|
1722
2008
|
try {
|
|
1723
|
-
const
|
|
2009
|
+
const nolanAgent = await ensureNolanAutoSession();
|
|
1724
2010
|
const digest = buildNolanAutopilotContext({
|
|
1725
2011
|
cwd,
|
|
1726
2012
|
gitBranch,
|
|
@@ -1729,8 +2015,8 @@ async function createSession(deps, opts) {
|
|
|
1729
2015
|
injectedPrompts: [...injectedAutopilotPrompts],
|
|
1730
2016
|
workflowCommands: await loadWorkflowCommandSpecs(),
|
|
1731
2017
|
});
|
|
1732
|
-
await
|
|
1733
|
-
return parseAutopilotVerdict(lastAssistantText(
|
|
2018
|
+
await nolanAgent.prompt(digest);
|
|
2019
|
+
return parseAutopilotVerdict(lastAssistantText(nolanAgent.getMessages()));
|
|
1734
2020
|
}
|
|
1735
2021
|
catch (err) {
|
|
1736
2022
|
if (!autopilotCancelled)
|
|
@@ -1760,7 +2046,7 @@ async function createSession(deps, opts) {
|
|
|
1760
2046
|
autopilotReviewing = true;
|
|
1761
2047
|
broadcast("autopilot_review_start", {});
|
|
1762
2048
|
try {
|
|
1763
|
-
const
|
|
2049
|
+
const nolanAgent = await ensureNolanAutoSession();
|
|
1764
2050
|
// Re-read the plan file (the run may have revised it in place); fall
|
|
1765
2051
|
// back to the content captured at exit_plan time.
|
|
1766
2052
|
const planContent = await fs.readFile(planPath, "utf-8").catch(() => pendingPlanContent);
|
|
@@ -1773,10 +2059,10 @@ async function createSession(deps, opts) {
|
|
|
1773
2059
|
workflowCommands: await loadWorkflowCommandSpecs(),
|
|
1774
2060
|
planContent,
|
|
1775
2061
|
});
|
|
1776
|
-
await
|
|
2062
|
+
await nolanAgent.prompt(digest);
|
|
1777
2063
|
if (autopilotCancelled || planGeneration !== genAtStart)
|
|
1778
2064
|
return null;
|
|
1779
|
-
return parseAutopilotVerdict(lastAssistantText(
|
|
2065
|
+
return parseAutopilotVerdict(lastAssistantText(nolanAgent.getMessages()));
|
|
1780
2066
|
}
|
|
1781
2067
|
catch (err) {
|
|
1782
2068
|
// User action mid-review (manual Accept aborts the nolanAuto run): drop
|
|
@@ -1802,7 +2088,7 @@ async function createSession(deps, opts) {
|
|
|
1802
2088
|
const IMPLEMENT_PLAN_PROMPT = "The plan has been approved. Implement it now, following each step in order.";
|
|
1803
2089
|
// Drive the review→prompt→review loop for one finished user turn. Only ever
|
|
1804
2090
|
// called after shouldStartAutopilotCycle approves the turn (POST /prompt or
|
|
1805
|
-
// the stranded-queue drain) — never from the task runner, resume, /
|
|
2091
|
+
// the stranded-queue drain) — never from the task runner, resume, /nolan, or
|
|
1806
2092
|
// error paths, so there's no recursion and no guard tangle. The loop's
|
|
1807
2093
|
// control flow lives in driveAutopilotCycle (core/autopilot-cycle.ts) so
|
|
1808
2094
|
// every exit path is unit-tested; this only wires the real dependencies.
|
|
@@ -1866,7 +2152,7 @@ async function createSession(deps, opts) {
|
|
|
1866
2152
|
// label stays the clean prompt.
|
|
1867
2153
|
const framed = frameAutopilotInjection(IMPLEMENT_PLAN_PROMPT);
|
|
1868
2154
|
injectedAutopilotPrompts.push(framed);
|
|
1869
|
-
return runAgent(IMPLEMENT_PLAN_PROMPT, () => session.prompt(framed));
|
|
2155
|
+
return runAgent(IMPLEMENT_PLAN_PROMPT, () => session.prompt(framed, AUTOMATION_PROVENANCE));
|
|
1870
2156
|
},
|
|
1871
2157
|
// Lean context per user turn: wipe prior review history so each new
|
|
1872
2158
|
// turn starts cheap, while within this cycle the few review messages
|
|
@@ -1895,19 +2181,21 @@ async function createSession(deps, opts) {
|
|
|
1895
2181
|
},
|
|
1896
2182
|
// Autopilot-injected run: EZ Coder receives the framed prompt (no human
|
|
1897
2183
|
// is watching this turn) while run_start keeps the clean label.
|
|
1898
|
-
runPrompt: (body) => runAgent(body, () => session.prompt(frameAutopilotInjection(body))),
|
|
2184
|
+
runPrompt: (body) => runAgent(body, () => session.prompt(frameAutopilotInjection(body), AUTOMATION_PROVENANCE)),
|
|
1899
2185
|
emit: (event) => {
|
|
1900
2186
|
// Persist the terminal verdict marker so a resumed session renders the
|
|
1901
2187
|
// same Nolan bubble the live run showed instead of dropping it or
|
|
1902
2188
|
// falling back to the raw verdict text (e.g. ALL_CLEAR).
|
|
1903
2189
|
if (event.type === "autopilot_done") {
|
|
1904
2190
|
// Broadcast the SAME copySeed the persisted marker will produce on
|
|
1905
|
-
// resume, so the live all-clear wording matches the resumed one
|
|
1906
|
-
//
|
|
2191
|
+
// resume, so the live all-clear wording matches the resumed one.
|
|
2192
|
+
// Must use the PERSISTED count — that's what persistAutopilotMarker
|
|
2193
|
+
// anchors against, and it trails the in-memory list after a run
|
|
2194
|
+
// whose messages never made it to disk.
|
|
1907
2195
|
const seed = autopilotMarkerCopySeed({
|
|
1908
2196
|
version: 1,
|
|
1909
2197
|
phase: "done",
|
|
1910
|
-
afterMessageCount: session.
|
|
2198
|
+
afterMessageCount: session.getPersistedTranscriptCount(),
|
|
1911
2199
|
});
|
|
1912
2200
|
broadcast(event.type, { ...event.data, copySeed: seed });
|
|
1913
2201
|
void session.persistAutopilotMarker("done");
|
|
@@ -1952,7 +2240,10 @@ async function createSession(deps, opts) {
|
|
|
1952
2240
|
const next = session.takeNextQueuedMessage();
|
|
1953
2241
|
if (!next)
|
|
1954
2242
|
return;
|
|
1955
|
-
broadcast("queued", {
|
|
2243
|
+
broadcast("queued", {
|
|
2244
|
+
count: session.getQueuedCount(),
|
|
2245
|
+
messages: session.listQueuedMessages(),
|
|
2246
|
+
});
|
|
1956
2247
|
if (!next.text.trim() && next.attachments.length === 0)
|
|
1957
2248
|
continue;
|
|
1958
2249
|
// A queued message draining as a fresh turn supersedes any pending
|
|
@@ -2038,7 +2329,7 @@ async function createSession(deps, opts) {
|
|
|
2038
2329
|
const shortId = task.id.slice(0, 8);
|
|
2039
2330
|
const completionHint = `\n\n---\nWhen you have fully completed this task, call the tasks tool to mark it done:\n` +
|
|
2040
2331
|
`tasks({ action: "done", id: "${shortId}" })`;
|
|
2041
|
-
await runAgent(task.title, () => session.prompt(task.prompt + completionHint));
|
|
2332
|
+
await runAgent(task.title, () => session.prompt(task.prompt + completionHint, AUTOMATION_PROVENANCE));
|
|
2042
2333
|
// The agent typically marks the task done via the tasks tool during the run;
|
|
2043
2334
|
// prune completed tasks and push the refreshed list so the modal drops them.
|
|
2044
2335
|
broadcast("tasks_list", { tasks: pruneDoneTasksSync(cwd) });
|
|
@@ -2136,6 +2427,20 @@ async function createSession(deps, opts) {
|
|
|
2136
2427
|
gitPoll.unref?.();
|
|
2137
2428
|
};
|
|
2138
2429
|
scheduleGitPoll(5000);
|
|
2430
|
+
// GitHub issue/PR counts change outside the app (web UI, teammates), so poll
|
|
2431
|
+
// on a slow cadence. Network-bound, so keep it well under the search API's
|
|
2432
|
+
// rate budget (2 calls per tick). No-op when the origin isn't a GitHub repo.
|
|
2433
|
+
let gitHubPoll;
|
|
2434
|
+
let gitHubPollStopped = false;
|
|
2435
|
+
const scheduleGitHubPoll = (delay) => {
|
|
2436
|
+
if (gitHubPollStopped)
|
|
2437
|
+
return;
|
|
2438
|
+
gitHubPoll = setTimeout(() => {
|
|
2439
|
+
void refreshGitHubCounts().finally(() => scheduleGitHubPoll(60_000));
|
|
2440
|
+
}, delay);
|
|
2441
|
+
gitHubPoll.unref?.();
|
|
2442
|
+
};
|
|
2443
|
+
scheduleGitHubPoll(2000);
|
|
2139
2444
|
function readBody(req, res) {
|
|
2140
2445
|
return readCappedBody(req, res);
|
|
2141
2446
|
}
|
|
@@ -2257,6 +2562,45 @@ async function createSession(deps, opts) {
|
|
|
2257
2562
|
});
|
|
2258
2563
|
return;
|
|
2259
2564
|
}
|
|
2565
|
+
if (method === "GET" && url === "/plugins") {
|
|
2566
|
+
void listInstalledPlugins(paths.extensionsDir)
|
|
2567
|
+
.then((plugins) => json(res, 200, { plugins }))
|
|
2568
|
+
.catch((error) => {
|
|
2569
|
+
captureSidecarError(error, "app-sidecar.plugins.list");
|
|
2570
|
+
json(res, 500, { error: error instanceof Error ? error.message : String(error) });
|
|
2571
|
+
});
|
|
2572
|
+
return;
|
|
2573
|
+
}
|
|
2574
|
+
if (method === "POST" && url === "/plugins/install") {
|
|
2575
|
+
void readBody(req, res).then(async (raw) => {
|
|
2576
|
+
if (raw === null)
|
|
2577
|
+
return;
|
|
2578
|
+
try {
|
|
2579
|
+
const bundlePath = JSON.parse(raw).bundlePath;
|
|
2580
|
+
if (typeof bundlePath !== "string" || !path.isAbsolute(bundlePath)) {
|
|
2581
|
+
json(res, 400, { error: "bundlePath must be an absolute path" });
|
|
2582
|
+
return;
|
|
2583
|
+
}
|
|
2584
|
+
const plugin = await installPlugin(bundlePath, paths.extensionsDir);
|
|
2585
|
+
json(res, 200, { plugin, restartRequired: true });
|
|
2586
|
+
}
|
|
2587
|
+
catch (error) {
|
|
2588
|
+
captureSidecarError(error, "app-sidecar.plugins.install");
|
|
2589
|
+
json(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
2590
|
+
}
|
|
2591
|
+
});
|
|
2592
|
+
return;
|
|
2593
|
+
}
|
|
2594
|
+
if (method === "DELETE" && url.startsWith("/plugins/")) {
|
|
2595
|
+
const pluginId = decodeURIComponent(url.slice("/plugins/".length));
|
|
2596
|
+
void removePlugin(pluginId, paths.extensionsDir)
|
|
2597
|
+
.then(() => json(res, 200, { removed: pluginId, restartRequired: true }))
|
|
2598
|
+
.catch((error) => {
|
|
2599
|
+
captureSidecarError(error, "app-sidecar.plugins.remove");
|
|
2600
|
+
json(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
2601
|
+
});
|
|
2602
|
+
return;
|
|
2603
|
+
}
|
|
2260
2604
|
if (method === "GET" && url === "/settings") {
|
|
2261
2605
|
// `configured` is true only when the user explicitly saved a projects root
|
|
2262
2606
|
// (the ezcoder-app.json file exists with a value) — not when we fall back to the
|
|
@@ -2343,9 +2687,53 @@ async function createSession(deps, opts) {
|
|
|
2343
2687
|
});
|
|
2344
2688
|
return;
|
|
2345
2689
|
}
|
|
2690
|
+
// Dismiss a project from the picker (or restore it with hidden:false). The
|
|
2691
|
+
// path is kept rather than the row: sessions in scratch dirs keep
|
|
2692
|
+
// re-surfacing, so the user's decision has to outlive any one scan.
|
|
2693
|
+
if (method === "POST" && url === "/projects/hidden") {
|
|
2694
|
+
void readBody(req, res).then(async (raw) => {
|
|
2695
|
+
if (raw === null)
|
|
2696
|
+
return;
|
|
2697
|
+
let body;
|
|
2698
|
+
try {
|
|
2699
|
+
body = JSON.parse(raw);
|
|
2700
|
+
}
|
|
2701
|
+
catch {
|
|
2702
|
+
json(res, 400, { error: "invalid JSON body" });
|
|
2703
|
+
return;
|
|
2704
|
+
}
|
|
2705
|
+
const target = body.path?.trim();
|
|
2706
|
+
if (!target) {
|
|
2707
|
+
json(res, 400, { error: "path is required" });
|
|
2708
|
+
return;
|
|
2709
|
+
}
|
|
2710
|
+
const key = path.resolve(target);
|
|
2711
|
+
try {
|
|
2712
|
+
const settings = await loadAppSettings();
|
|
2713
|
+
const current = new Set((settings.hiddenProjects ?? []).map((p) => path.resolve(p)));
|
|
2714
|
+
if (body.hidden === false)
|
|
2715
|
+
current.delete(key);
|
|
2716
|
+
else
|
|
2717
|
+
current.add(key);
|
|
2718
|
+
settings.hiddenProjects = current.size > 0 ? Array.from(current) : undefined;
|
|
2719
|
+
await saveAppSettings(settings);
|
|
2720
|
+
json(res, 200, { hidden: Array.from(current) });
|
|
2721
|
+
}
|
|
2722
|
+
catch (err) {
|
|
2723
|
+
captureSidecarError(err, "app-sidecar.projects.hidden");
|
|
2724
|
+
json(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
2725
|
+
}
|
|
2726
|
+
});
|
|
2727
|
+
return;
|
|
2728
|
+
}
|
|
2346
2729
|
if (method === "GET" && url === "/projects") {
|
|
2347
|
-
//
|
|
2348
|
-
void
|
|
2730
|
+
// Session stores plus configured project folders, including unopened projects.
|
|
2731
|
+
void loadAppSettings()
|
|
2732
|
+
.then(({ projectsRoot, projectRoots, hiddenProjects }) => discoverProjects({
|
|
2733
|
+
projectsRoot,
|
|
2734
|
+
extraRoots: projectRoots,
|
|
2735
|
+
hiddenPaths: hiddenProjects,
|
|
2736
|
+
}))
|
|
2349
2737
|
.then((projects) => json(res, 200, { projects }))
|
|
2350
2738
|
.catch((err) => {
|
|
2351
2739
|
captureSidecarError(err, "app-sidecar.projects.discover");
|
|
@@ -2386,6 +2774,30 @@ async function createSession(deps, opts) {
|
|
|
2386
2774
|
});
|
|
2387
2775
|
return;
|
|
2388
2776
|
}
|
|
2777
|
+
// Markdown transcript export for the app's download button. Serialized
|
|
2778
|
+
// here rather than in the webview because the webview's transcript model
|
|
2779
|
+
// deliberately keeps tool activity in the LiveToolPanel — exporting from
|
|
2780
|
+
// there would hand the user a coding session with the coding missing.
|
|
2781
|
+
// `?name=1` asks for the suggested filename only (the save dialog needs it
|
|
2782
|
+
// before there is a path), so the markdown never crosses IPC twice.
|
|
2783
|
+
if (method === "GET" && (url === "/export" || url.startsWith("/export?"))) {
|
|
2784
|
+
const query = new URLSearchParams(url.slice(url.indexOf("?") + 1));
|
|
2785
|
+
const st = session.getState();
|
|
2786
|
+
const filename = defaultExportFilename(mode);
|
|
2787
|
+
if (query.get("name") === "1") {
|
|
2788
|
+
json(res, 200, { filename });
|
|
2789
|
+
return;
|
|
2790
|
+
}
|
|
2791
|
+
const markdown = sessionToMarkdown({
|
|
2792
|
+
mode,
|
|
2793
|
+
cwd,
|
|
2794
|
+
provider: st.provider,
|
|
2795
|
+
model: st.model,
|
|
2796
|
+
...(st.sessionId ? { sessionId: st.sessionId } : {}),
|
|
2797
|
+
}, session.getMessages(), { toolDetail: parseToolDetail(query.get("tools")) });
|
|
2798
|
+
json(res, 200, { filename, markdown });
|
|
2799
|
+
return;
|
|
2800
|
+
}
|
|
2389
2801
|
if (method === "GET" && url === "/history") {
|
|
2390
2802
|
// Reconstruct the transcript from persisted messages so resume is 1:1 with
|
|
2391
2803
|
// the live SSE stream. Walks ALL message types (not just user/assistant):
|
|
@@ -2431,7 +2843,7 @@ async function createSession(deps, opts) {
|
|
|
2431
2843
|
// Nolan (mentor) turns to interleave: group by the non-system message count
|
|
2432
2844
|
// they were recorded after, so each lands right after that message. A
|
|
2433
2845
|
// turn becomes two wire rows: the `@Nolan` question (user) + Nolan's reply
|
|
2434
|
-
// (assistant), both flagged `
|
|
2846
|
+
// (assistant), both flagged `nolan` so the webview tints them.
|
|
2435
2847
|
// Deduped; stale anchors are clamped to the last message (Nolan turns
|
|
2436
2848
|
// carry real conversation, so they render at the end instead of
|
|
2437
2849
|
// vanishing).
|
|
@@ -2447,16 +2859,16 @@ async function createSession(deps, opts) {
|
|
|
2447
2859
|
return;
|
|
2448
2860
|
nolanByCount.delete(count);
|
|
2449
2861
|
for (const turn of turns) {
|
|
2450
|
-
history.push({ role: "user", text: `@Nolan ${turn.question}`,
|
|
2451
|
-
history.push({ role: "assistant", text: turn.reply,
|
|
2862
|
+
history.push({ role: "user", text: `@Nolan ${turn.question}`, nolan: true });
|
|
2863
|
+
history.push({ role: "assistant", text: turn.reply, nolan: true });
|
|
2452
2864
|
}
|
|
2453
2865
|
};
|
|
2454
2866
|
// Autopilot verdict markers to interleave, same anchor scheme as Nolan
|
|
2455
2867
|
// turns — each becomes a single assistant row the webview renders
|
|
2456
2868
|
// exactly like the live `autopilot` item (never a raw verdict string).
|
|
2457
|
-
//
|
|
2458
|
-
//
|
|
2459
|
-
//
|
|
2869
|
+
// Normalization pulls anchors left over from an unrebased compaction
|
|
2870
|
+
// back to where the marker was actually written, so stale all-clear
|
|
2871
|
+
// bubbles no longer bunch at the bottom of a reopened session.
|
|
2460
2872
|
const restoredMessageCount = messages.filter((m) => m.role !== "system").length;
|
|
2461
2873
|
const autopilotByCount = new Map();
|
|
2462
2874
|
for (const marker of normalizeAutopilotMarkersForHistory(session.getAutopilotMarkers(), restoredMessageCount)) {
|
|
@@ -2540,6 +2952,21 @@ async function createSession(deps, opts) {
|
|
|
2540
2952
|
},
|
|
2541
2953
|
});
|
|
2542
2954
|
}
|
|
2955
|
+
else if (marker.kind === "interrupted_run") {
|
|
2956
|
+
// Rendered as an error row: the run's tools already changed the
|
|
2957
|
+
// repo, so the user needs to see it and decide what to do. We
|
|
2958
|
+
// never replay it — that would duplicate those changes.
|
|
2959
|
+
history.push({
|
|
2960
|
+
role: "assistant",
|
|
2961
|
+
text: "",
|
|
2962
|
+
error: {
|
|
2963
|
+
scope: "interrupted_run",
|
|
2964
|
+
headline: "A run was interrupted",
|
|
2965
|
+
message: "EZ Coder stopped mid-run, so this turn is incomplete. Any files its tools already changed are still on disk.",
|
|
2966
|
+
guidance: "Review the working tree, then re-send the request if you still want it.",
|
|
2967
|
+
},
|
|
2968
|
+
});
|
|
2969
|
+
}
|
|
2543
2970
|
}
|
|
2544
2971
|
};
|
|
2545
2972
|
let nonSystemCount = 0;
|
|
@@ -2548,10 +2975,11 @@ async function createSession(deps, opts) {
|
|
|
2548
2975
|
flushNolan(0);
|
|
2549
2976
|
flushAutopilot(0);
|
|
2550
2977
|
flushAppMarkers(0);
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2978
|
+
await replayMessagesInOrder(messages, async (msg, count) => {
|
|
2979
|
+
nonSystemCount = count;
|
|
2980
|
+
const visibility = getHistoryMessageVisibility(msg);
|
|
2981
|
+
if (visibility === "hidden")
|
|
2982
|
+
return;
|
|
2555
2983
|
if (msg.role === "tool") {
|
|
2556
2984
|
// Tool result messages: check for ImageContent blocks (screenshots,
|
|
2557
2985
|
// generated images) and emit a toolImages entry.
|
|
@@ -2595,20 +3023,39 @@ async function createSession(deps, opts) {
|
|
|
2595
3023
|
});
|
|
2596
3024
|
}
|
|
2597
3025
|
}
|
|
2598
|
-
|
|
3026
|
+
return;
|
|
2599
3027
|
}
|
|
2600
3028
|
// User or assistant message — text/hook/command/compacted extraction,
|
|
2601
3029
|
// plus sub-agent group detection for assistant tool_calls.
|
|
2602
3030
|
if (msg.role === "user") {
|
|
2603
3031
|
// Rebuild the live bubble: strip the steering wrapper, drop
|
|
2604
3032
|
// attachment/file notes the model saw but the bubble never showed.
|
|
2605
|
-
const restored = restoreUserRow(msg.content);
|
|
3033
|
+
const restored = restoreUserRow(msg.content, msg.provenance);
|
|
2606
3034
|
const text = restored.text;
|
|
2607
|
-
const hook = detectHookKind(text);
|
|
2608
|
-
const compacted =
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
3035
|
+
const hook = msg.provenance ? null : detectHookKind(text);
|
|
3036
|
+
const compacted = visibility === "summary" ||
|
|
3037
|
+
(!msg.provenance && !hook && text.startsWith("[Previous conversation summary]"));
|
|
3038
|
+
const hint = userHintByCount.get(nonSystemCount);
|
|
3039
|
+
// The typed invocation persisted alongside the prompt is
|
|
3040
|
+
// authoritative. Reversing the expanded body only works while the
|
|
3041
|
+
// template is byte-identical, and templates drift (edited
|
|
3042
|
+
// `.ezcoder/commands/*.md`, reworded built-ins, app-vs-CLI phrasing) —
|
|
3043
|
+
// after which the resumed session dumped the raw multi-KB body
|
|
3044
|
+
// instead of the `/name` chip. Older sessions have no hint, so the
|
|
3045
|
+
// body match stays as the fallback.
|
|
3046
|
+
const command = !hook && !compacted
|
|
3047
|
+
? resolveRestoredCommand(typeof hint?.command === "string" ? hint.command : null, text, commandCandidates)
|
|
3048
|
+
: null;
|
|
3049
|
+
// Autopilot injected this turn — live showed only the Nolan-tinted
|
|
3050
|
+
// marker for it, never a user bubble. Emitting one here would print
|
|
3051
|
+
// the injected instruction a second time, unstyled.
|
|
3052
|
+
//
|
|
3053
|
+
// Pushed background-status updates are skipped for the same reason:
|
|
3054
|
+
// the live run rendered no bubble for them, so showing them here
|
|
3055
|
+
// would fill a reopened session with machine-facing status lines
|
|
3056
|
+
// the user never saw while working.
|
|
3057
|
+
if ((!msg.provenance || (!restored.autopilotInjected && !restored.notification)) &&
|
|
3058
|
+
(text.trim() || restored.images.length > 0)) {
|
|
2612
3059
|
history.push({
|
|
2613
3060
|
role: "user",
|
|
2614
3061
|
text: command ?? text,
|
|
@@ -2671,12 +3118,13 @@ async function createSession(deps, opts) {
|
|
|
2671
3118
|
});
|
|
2672
3119
|
}
|
|
2673
3120
|
}
|
|
2674
|
-
|
|
2675
|
-
// after
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
3121
|
+
}, (count) => {
|
|
3122
|
+
// Markers flush after every physical message, including tools and
|
|
3123
|
+
// provenance-hidden runtime context.
|
|
3124
|
+
flushNolan(count);
|
|
3125
|
+
flushAutopilot(count);
|
|
3126
|
+
flushAppMarkers(count);
|
|
3127
|
+
});
|
|
2680
3128
|
// Flush remaining Nolan turns whose anchor is at/after the message count so
|
|
2681
3129
|
// none are dropped. Autopilot/app markers beyond the restored message
|
|
2682
3130
|
// count were already filtered above; any remaining marker here is valid.
|
|
@@ -2705,21 +3153,44 @@ async function createSession(deps, opts) {
|
|
|
2705
3153
|
description: c.description,
|
|
2706
3154
|
source: "built-in",
|
|
2707
3155
|
}));
|
|
3156
|
+
// Desktop-safe registry actions belong in the same picker as workflows.
|
|
3157
|
+
// Most registry commands have dedicated app controls or TUI-only flows;
|
|
3158
|
+
// multi-root management has no other affordance, so expose only these.
|
|
3159
|
+
const workspaceActions = [
|
|
3160
|
+
{
|
|
3161
|
+
name: "add-dir",
|
|
3162
|
+
aliases: ["adddir"],
|
|
3163
|
+
description: "Add another project folder to this workspace",
|
|
3164
|
+
source: "built-in",
|
|
3165
|
+
},
|
|
3166
|
+
{
|
|
3167
|
+
name: "remove-dir",
|
|
3168
|
+
aliases: ["removedir"],
|
|
3169
|
+
description: "Remove an added project folder from this workspace",
|
|
3170
|
+
source: "built-in",
|
|
3171
|
+
},
|
|
3172
|
+
];
|
|
2708
3173
|
const custom = (await loadCustomCommands(cwd))
|
|
2709
|
-
// A custom command can't shadow a built-in name.
|
|
2710
|
-
.filter((c) => !PROMPT_COMMANDS.some((b) => b.name === c.name)
|
|
3174
|
+
// A custom command can't shadow a built-in name or app action.
|
|
3175
|
+
.filter((c) => !PROMPT_COMMANDS.some((b) => b.name === c.name) &&
|
|
3176
|
+
!workspaceActions.some((action) => action.name === c.name))
|
|
2711
3177
|
.map((c) => ({
|
|
2712
3178
|
name: c.name,
|
|
2713
3179
|
aliases: [],
|
|
2714
3180
|
description: c.description,
|
|
2715
3181
|
source: "custom",
|
|
2716
3182
|
}));
|
|
2717
|
-
json(res, 200, { commands: [...builtins, ...custom] });
|
|
3183
|
+
json(res, 200, { commands: [...workspaceActions, ...builtins, ...custom] });
|
|
2718
3184
|
})();
|
|
2719
3185
|
return;
|
|
2720
3186
|
}
|
|
2721
3187
|
if (method === "POST" && url === "/prompt") {
|
|
2722
|
-
|
|
3188
|
+
// Per-request: only the prompt that actually claimed the start may release
|
|
3189
|
+
// it. A bare release would let an early-returning request (bad JSON, or a
|
|
3190
|
+
// prompt that queued) clear a claim another request is still holding.
|
|
3191
|
+
let claimedStart = false;
|
|
3192
|
+
void readBody(req, res)
|
|
3193
|
+
.then(async (raw) => {
|
|
2723
3194
|
if (raw === null)
|
|
2724
3195
|
return;
|
|
2725
3196
|
let text;
|
|
@@ -2739,14 +3210,17 @@ async function createSession(deps, opts) {
|
|
|
2739
3210
|
json(res, 400, { error: "empty prompt" });
|
|
2740
3211
|
return;
|
|
2741
3212
|
}
|
|
2742
|
-
if (runLifecycle.running &&
|
|
3213
|
+
if (runLifecycle.running &&
|
|
3214
|
+
runLifecycle.isCancellationRequested(runLifecycle.generation)) {
|
|
2743
3215
|
json(res, 409, {
|
|
2744
3216
|
error: runLifecycle.state === "cancelling" ? "run_cancelling" : "cancel_failed",
|
|
2745
3217
|
runState: runLifecycle.state,
|
|
2746
3218
|
});
|
|
2747
3219
|
return;
|
|
2748
3220
|
}
|
|
2749
|
-
|
|
3221
|
+
// `runClaim` covers the gap before `runAgent` flips `running`: a
|
|
3222
|
+
// prompt arriving in that window must queue, not start a second run.
|
|
3223
|
+
if (running || runClaim.active || autopilotActive) {
|
|
2750
3224
|
// Queue prompts as mid-run steering (mirrors the CLI). Also queue while
|
|
2751
3225
|
// an autopilot cycle is active but between injected runs (build idle,
|
|
2752
3226
|
// Nolan reviewing) so the message never starts a run that collides with
|
|
@@ -2755,20 +3229,47 @@ async function createSession(deps, opts) {
|
|
|
2755
3229
|
// path as a non-queued attachment prompt when it drains.
|
|
2756
3230
|
const prepared = attachments.length > 0 ? await prepareAttachments(cwd, attachments) : [];
|
|
2757
3231
|
const count = session.queueMessage(text, prepared);
|
|
2758
|
-
broadcast("queued", { count });
|
|
3232
|
+
broadcast("queued", { count, messages: session.listQueuedMessages() });
|
|
2759
3233
|
json(res, 202, { queued: true, count });
|
|
2760
3234
|
return;
|
|
2761
3235
|
}
|
|
3236
|
+
// Claim the run NOW, synchronously. Everything below this line may
|
|
3237
|
+
// yield, and `running` does not flip until runAgent begins.
|
|
3238
|
+
claimedStart = runClaim.claim();
|
|
2762
3239
|
json(res, 202, { accepted: true });
|
|
3240
|
+
// Gate inputs captured around the run: whether this turn is a workflow
|
|
3241
|
+
// slash command (attachment prompts skip slash expansion entirely), and
|
|
3242
|
+
// how many assistant messages the run actually adds. Computed even when
|
|
3243
|
+
// autopilot is currently off — the toggle can flip ON mid-run, and the
|
|
3244
|
+
// gate reads the post-run value.
|
|
3245
|
+
const workflowCommand = attachments.length === 0 &&
|
|
3246
|
+
isWorkflowCommandText(text, await loadWorkflowCommandSpecs());
|
|
3247
|
+
// Does this input actually expand into a persisted user message? Asked
|
|
3248
|
+
// of the session itself, because only it knows whether the command
|
|
3249
|
+
// resolves here (name/alias casing, custom `.ezcoder/commands`, non-coder
|
|
3250
|
+
// agents that don't expand at all). A looser guess would anchor the
|
|
3251
|
+
// hint at +1 with no message to land on — decorating an unrelated
|
|
3252
|
+
// later bubble with the wrong `/name`.
|
|
3253
|
+
const expandsToTemplate = attachments.length === 0 && (await session.willExpandPromptTemplate(text));
|
|
2763
3254
|
// Webview display hint for this prompt's user bubble (nolanSent shimmer
|
|
2764
|
-
// label / enhancer highlight segments
|
|
2765
|
-
// the user message the prompt below
|
|
2766
|
-
// skip this (their position in the run
|
|
2767
|
-
|
|
3255
|
+
// label / enhancer highlight segments / the `/name` a command was typed
|
|
3256
|
+
// as). Anchored +1 so it attaches to the user message the prompt below
|
|
3257
|
+
// is about to push. Queued prompts skip this (their position in the run
|
|
3258
|
+
// is unpredictable).
|
|
3259
|
+
//
|
|
3260
|
+
// Recording the invocation matters because the agent persists the
|
|
3261
|
+
// EXPANDED template as the user message. Resume used to recover
|
|
3262
|
+
// `/name` by matching that body against the current templates, which
|
|
3263
|
+
// silently fails the moment a template is edited or reworded — the
|
|
3264
|
+
// reopened session then rendered the raw multi-KB prompt instead of
|
|
3265
|
+
// the command chip.
|
|
3266
|
+
if (expandsToTemplate ||
|
|
3267
|
+
(meta && (meta.nolanSent === true || Array.isArray(meta.enhancements)))) {
|
|
2768
3268
|
void session
|
|
2769
3269
|
.persistAppMarker("user_hint", {
|
|
2770
|
-
...(
|
|
2771
|
-
...(
|
|
3270
|
+
...(expandsToTemplate ? { command: text.trim() } : {}),
|
|
3271
|
+
...(meta?.nolanSent === true ? { nolanSent: true } : {}),
|
|
3272
|
+
...(Array.isArray(meta?.enhancements) ? { enhancements: meta.enhancements } : {}),
|
|
2772
3273
|
}, 1)
|
|
2773
3274
|
.catch(() => { });
|
|
2774
3275
|
}
|
|
@@ -2779,12 +3280,6 @@ async function createSession(deps, opts) {
|
|
|
2779
3280
|
// feedback, anything) supersedes the pending plan — the bump also
|
|
2780
3281
|
// invalidates any in-flight Nolan plan review.
|
|
2781
3282
|
clearPendingPlan();
|
|
2782
|
-
// Gate inputs captured around the run: whether this turn is a workflow
|
|
2783
|
-
// slash command (attachment prompts skip slash expansion entirely), and
|
|
2784
|
-
// how many assistant messages the run actually adds. Computed even when
|
|
2785
|
-
// autopilot is currently off — the toggle can flip ON mid-run, and the
|
|
2786
|
-
// gate reads the post-run value.
|
|
2787
|
-
const workflowCommand = attachments.length === 0 && isWorkflowCommandText(text, await loadWorkflowCommandSpecs());
|
|
2788
3283
|
const assistantsBefore = countAssistantMessages(session.getMessages());
|
|
2789
3284
|
const messagesBefore = session.getMessages().length;
|
|
2790
3285
|
await runAgent(text, async () => {
|
|
@@ -2836,14 +3331,18 @@ async function createSession(deps, opts) {
|
|
|
2836
3331
|
// A prompt sent while Nolan was reviewing (build idle) queued but had no
|
|
2837
3332
|
// run to steer into — run it now as a fresh turn so it never strands.
|
|
2838
3333
|
await runStrandedQueue();
|
|
3334
|
+
})
|
|
3335
|
+
.finally(() => {
|
|
3336
|
+
if (claimedStart)
|
|
3337
|
+
runClaim.release();
|
|
2839
3338
|
});
|
|
2840
3339
|
return;
|
|
2841
3340
|
}
|
|
2842
3341
|
// Nolan Grout (mentor): an independent read-only advisory run on the nolanSession.
|
|
2843
|
-
// Runs concurrently with a build run — its events are
|
|
3342
|
+
// Runs concurrently with a build run — its events are nolan_-prefixed so the
|
|
2844
3343
|
// webview keeps the bubbles separate. The context digest is assembled fresh
|
|
2845
3344
|
// from the BUILD session's transcript each turn (one-way mirror).
|
|
2846
|
-
if (method === "POST" && url === "/
|
|
3345
|
+
if (method === "POST" && url === "/nolan/prompt") {
|
|
2847
3346
|
if (mode === "chat") {
|
|
2848
3347
|
json(res, 404, { error: "Nolan is not available in EZ Chat." });
|
|
2849
3348
|
return;
|
|
@@ -2871,18 +3370,18 @@ async function createSession(deps, opts) {
|
|
|
2871
3370
|
nolanRunning = true;
|
|
2872
3371
|
broadcast("nolan_run_start", { text });
|
|
2873
3372
|
try {
|
|
2874
|
-
const
|
|
3373
|
+
const nolanAgent = await ensureNolanSession();
|
|
2875
3374
|
const digest = await buildNolanContext(session, cwd, gitBranch, text, await loadWorkflowCommandSpecs(), injectedAutopilotPrompts);
|
|
2876
|
-
await
|
|
3375
|
+
await nolanAgent.prompt(digest);
|
|
2877
3376
|
// Record the turn against the BUILD session so it persists + survives
|
|
2878
3377
|
// resume (advisory custom entry, never an LLM message). Reply is Nolan's
|
|
2879
3378
|
// last assistant message; skip persistence if he produced nothing.
|
|
2880
|
-
const reply = lastAssistantText(
|
|
3379
|
+
const reply = lastAssistantText(nolanAgent.getMessages());
|
|
2881
3380
|
if (reply.trim())
|
|
2882
3381
|
await session.persistNolanTurn(text, reply);
|
|
2883
3382
|
}
|
|
2884
3383
|
catch (err) {
|
|
2885
|
-
broadcastError("nolan_error", "
|
|
3384
|
+
broadcastError("nolan_error", "nolan run failed", err);
|
|
2886
3385
|
}
|
|
2887
3386
|
finally {
|
|
2888
3387
|
nolanRunning = false;
|
|
@@ -2895,7 +3394,7 @@ async function createSession(deps, opts) {
|
|
|
2895
3394
|
});
|
|
2896
3395
|
return;
|
|
2897
3396
|
}
|
|
2898
|
-
if (method === "POST" && url === "/
|
|
3397
|
+
if (method === "POST" && url === "/nolan/cancel") {
|
|
2899
3398
|
nolanAbort.abort();
|
|
2900
3399
|
nolanAbort = new AbortController();
|
|
2901
3400
|
nolanSession?.setSignal(nolanAbort.signal);
|
|
@@ -3173,12 +3672,30 @@ async function createSession(deps, opts) {
|
|
|
3173
3672
|
loggedIn.push(p);
|
|
3174
3673
|
}
|
|
3175
3674
|
// Just the names, grouped by provider in registry order — the UI shows
|
|
3176
|
-
// a clean multi-column list of model ids.
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
provider
|
|
3181
|
-
|
|
3675
|
+
// a clean multi-column list of model ids. Local models come from the
|
|
3676
|
+
// runtime registry (populated by the background scan) and are always
|
|
3677
|
+
// listed: their "login" is the endpoint answering a probe.
|
|
3678
|
+
const models = getAllModels()
|
|
3679
|
+
.filter((m) => m.provider === "local" || loggedIn.includes(m.provider))
|
|
3680
|
+
.map((m) => {
|
|
3681
|
+
if (m.provider !== "local") {
|
|
3682
|
+
return { id: m.id, name: m.name, provider: m.provider };
|
|
3683
|
+
}
|
|
3684
|
+
const probed = findProbedModel(localProbes, m.id);
|
|
3685
|
+
return {
|
|
3686
|
+
id: m.id,
|
|
3687
|
+
name: m.name,
|
|
3688
|
+
provider: m.provider,
|
|
3689
|
+
local: true,
|
|
3690
|
+
endpoint: probed?.endpoint.label ?? parseLocalModelId(m.id)?.endpointId,
|
|
3691
|
+
// A local model that can't call tools can't run the agent — the UI
|
|
3692
|
+
// renders it disabled rather than hiding it, so the user learns why.
|
|
3693
|
+
supportsTools: probed?.model.supportsTools ?? true,
|
|
3694
|
+
contextWindow: m.contextWindow,
|
|
3695
|
+
contextWindowKnown: probed?.model.contextWindowKnown ?? false,
|
|
3696
|
+
supportsThinking: m.supportsThinking,
|
|
3697
|
+
};
|
|
3698
|
+
});
|
|
3182
3699
|
json(res, 200, { models });
|
|
3183
3700
|
})();
|
|
3184
3701
|
return;
|
|
@@ -3207,6 +3724,13 @@ async function createSession(deps, opts) {
|
|
|
3207
3724
|
// Pick up any natively-written keys (desktop app) before resolving the
|
|
3208
3725
|
// new model's credentials, so switching to a just-added provider works.
|
|
3209
3726
|
await auth.reload();
|
|
3727
|
+
if (target.provider === "local") {
|
|
3728
|
+
const problem = await localModelBlocker(target.id);
|
|
3729
|
+
if (problem) {
|
|
3730
|
+
json(res, 409, { error: problem });
|
|
3731
|
+
return;
|
|
3732
|
+
}
|
|
3733
|
+
}
|
|
3210
3734
|
await session.switchModel(target.provider, target.id);
|
|
3211
3735
|
// Nolan follows EZ Coder's model only while un-pinned; a user-set Nolan
|
|
3212
3736
|
// override survives EZ Coder model switches untouched.
|
|
@@ -3245,7 +3769,7 @@ async function createSession(deps, opts) {
|
|
|
3245
3769
|
// no-op event (keeps footer/event tests from treating an EZ Coder
|
|
3246
3770
|
// switch as a Nolan switch).
|
|
3247
3771
|
if (!nolanModelOverride)
|
|
3248
|
-
broadcast("
|
|
3772
|
+
broadcast("nolan_model_change", nolanStatePayload());
|
|
3249
3773
|
// The new model usually has a different context window — push extras so
|
|
3250
3774
|
// the footer's context meter rescales immediately.
|
|
3251
3775
|
broadcast("extras", footerExtras());
|
|
@@ -3257,7 +3781,7 @@ async function createSession(deps, opts) {
|
|
|
3257
3781
|
// { model: null } / "" to clear (Nolan resumes following EZ Coder). Applies
|
|
3258
3782
|
// to BOTH Nolan sessions (chat + autopilot reviewer); a switch landing while
|
|
3259
3783
|
// either is mid-run defers via the pending-model mechanics.
|
|
3260
|
-
if (method === "POST" && url === "/
|
|
3784
|
+
if (method === "POST" && url === "/nolan/model") {
|
|
3261
3785
|
void readBody(req, res).then(async (raw) => {
|
|
3262
3786
|
if (raw === null)
|
|
3263
3787
|
return;
|
|
@@ -3292,17 +3816,49 @@ async function createSession(deps, opts) {
|
|
|
3292
3816
|
await saveNolanModelPref(cwd, nolanModelOverride);
|
|
3293
3817
|
await syncNolanModel(target.provider, target.id);
|
|
3294
3818
|
await syncNolanAutoModel(target.provider, target.id);
|
|
3295
|
-
log("INFO", "app-sidecar", "
|
|
3819
|
+
log("INFO", "app-sidecar", "nolan model pinned", {
|
|
3296
3820
|
provider: target.provider,
|
|
3297
3821
|
model: target.id,
|
|
3298
3822
|
});
|
|
3299
3823
|
}
|
|
3300
3824
|
const payload = nolanStatePayload();
|
|
3301
|
-
broadcast("
|
|
3825
|
+
broadcast("nolan_model_change", payload);
|
|
3302
3826
|
json(res, 200, payload);
|
|
3303
3827
|
});
|
|
3304
3828
|
return;
|
|
3305
3829
|
}
|
|
3830
|
+
// Pending queued steering, for the composer's cancel affordance.
|
|
3831
|
+
if (method === "GET" && url === "/queued") {
|
|
3832
|
+
json(res, 200, { queued: session.listQueuedMessages() });
|
|
3833
|
+
return;
|
|
3834
|
+
}
|
|
3835
|
+
// Cancel one pending queued message by id.
|
|
3836
|
+
if (method === "POST" && url === "/queued/cancel") {
|
|
3837
|
+
void readBody(req, res).then((raw) => {
|
|
3838
|
+
if (raw === null)
|
|
3839
|
+
return;
|
|
3840
|
+
let id;
|
|
3841
|
+
try {
|
|
3842
|
+
id = JSON.parse(raw).id ?? "";
|
|
3843
|
+
}
|
|
3844
|
+
catch {
|
|
3845
|
+
json(res, 400, { error: "invalid JSON body" });
|
|
3846
|
+
return;
|
|
3847
|
+
}
|
|
3848
|
+
if (!id.trim()) {
|
|
3849
|
+
json(res, 400, { error: "missing queued message id" });
|
|
3850
|
+
return;
|
|
3851
|
+
}
|
|
3852
|
+
// `false` means it already drained into the run between render and
|
|
3853
|
+
// click. That is a race, not an error, so report it as a normal result
|
|
3854
|
+
// and let the client reconcile from the fresh list.
|
|
3855
|
+
const cancelled = session.cancelQueuedMessage(id);
|
|
3856
|
+
const queued = session.listQueuedMessages();
|
|
3857
|
+
broadcast("queued", { count: queued.length, messages: queued });
|
|
3858
|
+
json(res, 200, { cancelled, queued });
|
|
3859
|
+
});
|
|
3860
|
+
return;
|
|
3861
|
+
}
|
|
3306
3862
|
if (method === "POST" && url === "/kill") {
|
|
3307
3863
|
void readBody(req, res).then(async (raw) => {
|
|
3308
3864
|
if (raw === null)
|
|
@@ -3326,6 +3882,33 @@ async function createSession(deps, opts) {
|
|
|
3326
3882
|
});
|
|
3327
3883
|
return;
|
|
3328
3884
|
}
|
|
3885
|
+
// Import a Claude Code / Codex / Cursor transcript into a resumable GG
|
|
3886
|
+
// Coder session. The importer never throws — it returns a typed failure so
|
|
3887
|
+
// the app can show the reason verbatim.
|
|
3888
|
+
if (method === "POST" && url === "/import-transcript") {
|
|
3889
|
+
void readBody(req, res).then(async (raw) => {
|
|
3890
|
+
if (raw === null)
|
|
3891
|
+
return;
|
|
3892
|
+
let body;
|
|
3893
|
+
try {
|
|
3894
|
+
body = JSON.parse(raw);
|
|
3895
|
+
}
|
|
3896
|
+
catch {
|
|
3897
|
+
json(res, 400, { error: "invalid JSON body" });
|
|
3898
|
+
return;
|
|
3899
|
+
}
|
|
3900
|
+
const filePath = body.path?.trim();
|
|
3901
|
+
if (!filePath) {
|
|
3902
|
+
json(res, 400, { error: "missing transcript path" });
|
|
3903
|
+
return;
|
|
3904
|
+
}
|
|
3905
|
+
const result = await session.importForeignTranscript(filePath, {
|
|
3906
|
+
...(body.cwd ? { cwd: body.cwd } : {}),
|
|
3907
|
+
});
|
|
3908
|
+
json(res, result.ok ? 200 : 400, result);
|
|
3909
|
+
});
|
|
3910
|
+
return;
|
|
3911
|
+
}
|
|
3329
3912
|
if (method === "POST" && url === "/thinking") {
|
|
3330
3913
|
const st = session.getState();
|
|
3331
3914
|
const next = getNextThinkingLevel(st.provider, st.model, session.getThinkingLevel());
|
|
@@ -3360,7 +3943,7 @@ async function createSession(deps, opts) {
|
|
|
3360
3943
|
const generation = runLifecycle.generation;
|
|
3361
3944
|
if (!pendingCancelDrain || pendingCancelDrain.generation !== generation) {
|
|
3362
3945
|
pendingCancelDrain = { generation, text: session.drainQueue() };
|
|
3363
|
-
broadcast("queued", { count: 0 });
|
|
3946
|
+
broadcast("queued", { count: 0, messages: [] });
|
|
3364
3947
|
}
|
|
3365
3948
|
const result = await runLifecycle.cancel(CANCEL_TIMEOUT_MS);
|
|
3366
3949
|
const drained = pendingCancelDrain.text;
|
|
@@ -3520,7 +4103,17 @@ async function createSession(deps, opts) {
|
|
|
3520
4103
|
...(baseUrl ? { baseUrl } : {}),
|
|
3521
4104
|
};
|
|
3522
4105
|
await auth.setCredentials(storageKey, creds);
|
|
3523
|
-
|
|
4106
|
+
// auth.json is shared by every window, so this is a global change:
|
|
4107
|
+
// close their login modals and refresh their provider lists too.
|
|
4108
|
+
broadcastAll("auth_done", { provider });
|
|
4109
|
+
// `auth_done` means "a login succeeded" (modals close on it).
|
|
4110
|
+
// `auth_change` means "auth.json changed" — which a DISCONNECT also is,
|
|
4111
|
+
// so connection state has one signal that covers both directions.
|
|
4112
|
+
broadcastAll("auth_change", { provider });
|
|
4113
|
+
// A newly connected provider unlocks its models. `/models` filters on
|
|
4114
|
+
// who is logged in, so every window's picker is now stale — without
|
|
4115
|
+
// this the new models don't appear until the session is reopened.
|
|
4116
|
+
broadcastAll("models_change", {});
|
|
3524
4117
|
json(res, 200, { ok: true });
|
|
3525
4118
|
});
|
|
3526
4119
|
return;
|
|
@@ -3546,7 +4139,18 @@ async function createSession(deps, opts) {
|
|
|
3546
4139
|
json(res, 409, { error: "a login is already in progress" });
|
|
3547
4140
|
return;
|
|
3548
4141
|
}
|
|
4142
|
+
// A login writes the shared ~/.ezcoder/auth.json, so two windows racing the
|
|
4143
|
+
// same provider means two browser tabs and two token exchanges whose
|
|
4144
|
+
// writes clobber each other. The per-session flag above cannot see
|
|
4145
|
+
// that — guard the provider daemon-wide as well.
|
|
4146
|
+
if (oauthInFlightProviders.has(provider)) {
|
|
4147
|
+
json(res, 409, {
|
|
4148
|
+
error: `a ${meta.label} login is already in progress in another window`,
|
|
4149
|
+
});
|
|
4150
|
+
return;
|
|
4151
|
+
}
|
|
3549
4152
|
oauthInFlight = true;
|
|
4153
|
+
oauthInFlightProviders.add(provider);
|
|
3550
4154
|
json(res, 202, { accepted: true });
|
|
3551
4155
|
void (async () => {
|
|
3552
4156
|
const cb = authCallbacks();
|
|
@@ -3567,10 +4171,19 @@ async function createSession(deps, opts) {
|
|
|
3567
4171
|
throw new Error(`OAuth not implemented for ${provider}`);
|
|
3568
4172
|
}
|
|
3569
4173
|
await auth.setCredentials(storageKey, creds);
|
|
3570
|
-
|
|
4174
|
+
// Terminal outcome of a GLOBAL change: every window's login modal
|
|
4175
|
+
// should close and its provider list refresh, not just the one
|
|
4176
|
+
// that started the flow.
|
|
4177
|
+
broadcastAll("auth_done", { provider });
|
|
4178
|
+
broadcastAll("auth_change", { provider });
|
|
4179
|
+
// The OAuth provider's models just became selectable everywhere.
|
|
4180
|
+
broadcastAll("models_change", {});
|
|
3571
4181
|
}
|
|
3572
4182
|
catch (err) {
|
|
3573
4183
|
captureSidecarError(err, "app-sidecar.auth.oauth", { provider });
|
|
4184
|
+
// Deliberately session-scoped: this is the outcome of ONE window's
|
|
4185
|
+
// attempt. Another window that never pressed Connect has nothing to
|
|
4186
|
+
// show an error about, and its modal correctly still offers login.
|
|
3574
4187
|
broadcast("auth_error", {
|
|
3575
4188
|
provider,
|
|
3576
4189
|
message: err instanceof Error ? err.message : String(err),
|
|
@@ -3578,6 +4191,7 @@ async function createSession(deps, opts) {
|
|
|
3578
4191
|
}
|
|
3579
4192
|
finally {
|
|
3580
4193
|
oauthInFlight = false;
|
|
4194
|
+
oauthInFlightProviders.delete(provider);
|
|
3581
4195
|
pendingCode = null;
|
|
3582
4196
|
}
|
|
3583
4197
|
})();
|
|
@@ -3606,6 +4220,39 @@ async function createSession(deps, opts) {
|
|
|
3606
4220
|
});
|
|
3607
4221
|
return;
|
|
3608
4222
|
}
|
|
4223
|
+
if (method === "POST" && url.startsWith("/mcp/elicit/")) {
|
|
4224
|
+
const id = decodeURIComponent(url.slice("/mcp/elicit/".length));
|
|
4225
|
+
void readBody(req, res).then((raw) => {
|
|
4226
|
+
if (raw === null)
|
|
4227
|
+
return;
|
|
4228
|
+
let result;
|
|
4229
|
+
try {
|
|
4230
|
+
const parsed = JSON.parse(raw);
|
|
4231
|
+
if (parsed.action !== "accept" &&
|
|
4232
|
+
parsed.action !== "decline" &&
|
|
4233
|
+
parsed.action !== "cancel") {
|
|
4234
|
+
json(res, 400, { error: "action must be accept, decline, or cancel" });
|
|
4235
|
+
return;
|
|
4236
|
+
}
|
|
4237
|
+
result =
|
|
4238
|
+
parsed.action === "accept"
|
|
4239
|
+
? { action: "accept", content: parsed.content ?? {} }
|
|
4240
|
+
: { action: parsed.action };
|
|
4241
|
+
}
|
|
4242
|
+
catch {
|
|
4243
|
+
json(res, 400, { error: "invalid JSON body" });
|
|
4244
|
+
return;
|
|
4245
|
+
}
|
|
4246
|
+
// Unknown id means it already timed out or was cancelled by an abort —
|
|
4247
|
+
// the tool call has moved on, so the answer has nowhere to go.
|
|
4248
|
+
if (!elicitations.settle(id, result)) {
|
|
4249
|
+
json(res, 409, { error: "no elicitation is awaiting a response" });
|
|
4250
|
+
return;
|
|
4251
|
+
}
|
|
4252
|
+
json(res, 200, { ok: true });
|
|
4253
|
+
});
|
|
4254
|
+
return;
|
|
4255
|
+
}
|
|
3609
4256
|
if (method === "POST" && url === "/auth/logout") {
|
|
3610
4257
|
void readBody(req, res).then(async (raw) => {
|
|
3611
4258
|
if (raw === null)
|
|
@@ -3632,6 +4279,84 @@ async function createSession(deps, opts) {
|
|
|
3632
4279
|
});
|
|
3633
4280
|
return;
|
|
3634
4281
|
}
|
|
4282
|
+
// ── Local models ──────────────────────────────────────
|
|
4283
|
+
// GET returns the last scan (cheap, no probing) so opening the modal is
|
|
4284
|
+
// instant; POST /local/scan is the explicit refresh.
|
|
4285
|
+
if (method === "GET" && url === "/local") {
|
|
4286
|
+
json(res, 200, localStatePayload());
|
|
4287
|
+
return;
|
|
4288
|
+
}
|
|
4289
|
+
if (method === "POST" && url === "/local/scan") {
|
|
4290
|
+
void scanLocalModels(true)
|
|
4291
|
+
.then(() => {
|
|
4292
|
+
broadcast("models_change", { local: localStatePayload() });
|
|
4293
|
+
json(res, 200, localStatePayload());
|
|
4294
|
+
})
|
|
4295
|
+
.catch((err) => {
|
|
4296
|
+
broadcastError("error", "local model scan failed", err);
|
|
4297
|
+
json(res, 500, { error: "Local model scan failed — see the sidecar log." });
|
|
4298
|
+
});
|
|
4299
|
+
return;
|
|
4300
|
+
}
|
|
4301
|
+
if (method === "POST" && url === "/local/endpoints") {
|
|
4302
|
+
void readBody(req, res).then(async (raw) => {
|
|
4303
|
+
if (raw === null)
|
|
4304
|
+
return;
|
|
4305
|
+
let body;
|
|
4306
|
+
try {
|
|
4307
|
+
body = JSON.parse(raw);
|
|
4308
|
+
}
|
|
4309
|
+
catch {
|
|
4310
|
+
json(res, 400, { error: "invalid JSON body" });
|
|
4311
|
+
return;
|
|
4312
|
+
}
|
|
4313
|
+
try {
|
|
4314
|
+
const endpoint = await addCustomEndpoint({
|
|
4315
|
+
baseUrl: body.baseUrl ?? "",
|
|
4316
|
+
...(body.label ? { label: body.label } : {}),
|
|
4317
|
+
...(body.apiKey ? { apiKey: body.apiKey } : {}),
|
|
4318
|
+
});
|
|
4319
|
+
await scanLocalModels(true);
|
|
4320
|
+
broadcast("models_change", { local: localStatePayload() });
|
|
4321
|
+
json(res, 200, {
|
|
4322
|
+
endpoint: { id: endpoint.id, label: endpoint.label },
|
|
4323
|
+
...localStatePayload(),
|
|
4324
|
+
});
|
|
4325
|
+
}
|
|
4326
|
+
catch (err) {
|
|
4327
|
+
// Validation errors are the user's typo, not a system fault — 400 with
|
|
4328
|
+
// the exact reason, and nothing in the transcript.
|
|
4329
|
+
if (err instanceof LocalEndpointError) {
|
|
4330
|
+
json(res, 400, { error: err.message });
|
|
4331
|
+
return;
|
|
4332
|
+
}
|
|
4333
|
+
broadcastError("error", "add local endpoint failed", err);
|
|
4334
|
+
json(res, 500, { error: "Could not save the endpoint — see the sidecar log." });
|
|
4335
|
+
}
|
|
4336
|
+
});
|
|
4337
|
+
return;
|
|
4338
|
+
}
|
|
4339
|
+
if (method === "DELETE" && url.startsWith("/local/endpoints/")) {
|
|
4340
|
+
const id = decodeURIComponent(url.slice("/local/endpoints/".length));
|
|
4341
|
+
void (async () => {
|
|
4342
|
+
try {
|
|
4343
|
+
await removeCustomEndpoint(id);
|
|
4344
|
+
clearRuntimeModels((m) => m.provider === "local" && parseLocalModelId(m.id)?.endpointId === id);
|
|
4345
|
+
await scanLocalModels(true);
|
|
4346
|
+
broadcast("models_change", { local: localStatePayload() });
|
|
4347
|
+
json(res, 200, localStatePayload());
|
|
4348
|
+
}
|
|
4349
|
+
catch (err) {
|
|
4350
|
+
if (err instanceof LocalEndpointError) {
|
|
4351
|
+
json(res, 400, { error: err.message });
|
|
4352
|
+
return;
|
|
4353
|
+
}
|
|
4354
|
+
broadcastError("error", "remove local endpoint failed", err);
|
|
4355
|
+
json(res, 500, { error: "Could not remove the endpoint — see the sidecar log." });
|
|
4356
|
+
}
|
|
4357
|
+
})();
|
|
4358
|
+
return;
|
|
4359
|
+
}
|
|
3635
4360
|
// ── Telegram config (mirrors `ezcoder telegram`) ─────────
|
|
3636
4361
|
if (method === "GET" && url === "/telegram") {
|
|
3637
4362
|
void loadTelegramConfig().then((cfg) => {
|
|
@@ -3913,12 +4638,16 @@ async function createSession(deps, opts) {
|
|
|
3913
4638
|
json(res, 404, { error: "not found" });
|
|
3914
4639
|
}
|
|
3915
4640
|
async function dispose() {
|
|
4641
|
+
elicitations.cancelAll();
|
|
3916
4642
|
tasksPollStopped = true;
|
|
3917
4643
|
if (tasksPoll)
|
|
3918
4644
|
clearTimeout(tasksPoll);
|
|
3919
4645
|
gitPollStopped = true;
|
|
3920
4646
|
if (gitPoll)
|
|
3921
4647
|
clearTimeout(gitPoll);
|
|
4648
|
+
gitHubPollStopped = true;
|
|
4649
|
+
if (gitHubPoll)
|
|
4650
|
+
clearTimeout(gitHubPoll);
|
|
3922
4651
|
// Stop the Telegram serve loop + dispose its per-chat sessions.
|
|
3923
4652
|
if (serveController)
|
|
3924
4653
|
await serveController.stop().catch(() => { });
|