@prestyj/cli 5.8.0 → 5.10.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 +1 -1
- package/assets/skills/evidence-led-ui/LICENSES.md +12 -0
- package/assets/skills/evidence-led-ui/SKILL.md +15 -7
- package/assets/skills/evidence-led-ui/references/anti-defaults.md +9 -1
- package/assets/skills/evidence-led-ui/references/craft-rulings.md +33 -0
- package/assets/skills/evidence-led-ui/references/production-contract.md +41 -21
- package/assets/skills/evidence-led-ui/references/provenance.md +7 -5
- package/assets/skills/evidence-led-ui/references/quality-rubric.md +10 -10
- package/dist/app-sidecar-sessions.d.ts +8 -0
- package/dist/app-sidecar-sessions.d.ts.map +1 -0
- package/dist/app-sidecar-sessions.js +31 -0
- package/dist/app-sidecar-sessions.js.map +1 -0
- package/dist/app-sidecar-sessions.test.d.ts +2 -0
- package/dist/app-sidecar-sessions.test.d.ts.map +1 -0
- package/dist/app-sidecar-sessions.test.js +83 -0
- package/dist/app-sidecar-sessions.test.js.map +1 -0
- package/dist/app-sidecar.js +623 -120
- 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.d.ts.map +1 -1
- package/dist/cli.js +59 -30
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +18 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +50 -2
- package/dist/config.js.map +1 -1
- package/dist/config.test.js +61 -1
- 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-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-compaction.test.js +23 -7
- package/dist/core/agent-session-compaction.test.js.map +1 -1
- 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 +147 -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-queue.test.js +4 -7
- package/dist/core/agent-session-queue.test.js.map +1 -1
- 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 +46 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +177 -19
- 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 +12 -1
- package/dist/core/agents.d.ts.map +1 -1
- package/dist/core/agents.js +25 -1
- 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/auth-storage.d.ts +1 -1
- package/dist/core/auth-storage.d.ts.map +1 -1
- package/dist/core/auth-storage.js +1 -1
- package/dist/core/auth-storage.js.map +1 -1
- package/dist/core/compaction/compactor.d.ts.map +1 -1
- package/dist/core/compaction/compactor.js +20 -11
- package/dist/core/compaction/compactor.js.map +1 -1
- package/dist/core/compaction/compactor.test.js +17 -4
- package/dist/core/compaction/compactor.test.js.map +1 -1
- package/dist/core/compaction/token-estimator.test.js +1 -1
- package/dist/core/compaction/token-estimator.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 +5 -0
- package/dist/core/event-bus.d.ts.map +1 -1
- package/dist/core/event-bus.js +6 -0
- package/dist/core/event-bus.js.map +1 -1
- package/dist/core/goal-controller.test.js +3 -3
- package/dist/core/goal-controller.test.js.map +1 -1
- package/dist/core/ideal-review.d.ts.map +1 -1
- package/dist/core/ideal-review.js +6 -1
- package/dist/core/ideal-review.js.map +1 -1
- package/dist/core/ideal-review.test.js +7 -2
- package/dist/core/ideal-review.test.js.map +1 -1
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +2 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/input-normalization.test.d.ts +2 -0
- package/dist/core/input-normalization.test.d.ts.map +1 -0
- package/dist/core/input-normalization.test.js +75 -0
- package/dist/core/input-normalization.test.js.map +1 -0
- 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 +38 -1
- package/dist/core/loop-breaker.d.ts.map +1 -1
- package/dist/core/loop-breaker.js +84 -2
- package/dist/core/loop-breaker.js.map +1 -1
- package/dist/core/loop-breaker.test.js +108 -1
- package/dist/core/loop-breaker.test.js.map +1 -1
- package/dist/core/lsp/client.d.ts +50 -0
- package/dist/core/lsp/client.d.ts.map +1 -1
- package/dist/core/lsp/client.js +158 -13
- 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.map +1 -1
- package/dist/core/lsp/manager.js +23 -2
- package/dist/core/lsp/manager.js.map +1 -1
- package/dist/core/lsp/manager.test.js +32 -1
- package/dist/core/lsp/manager.test.js.map +1 -1
- 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 +251 -0
- package/dist/core/lsp/windows.test.js.map +1 -0
- 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/message-images.d.ts +12 -0
- package/dist/core/message-images.d.ts.map +1 -0
- package/dist/core/message-images.js +52 -0
- package/dist/core/message-images.js.map +1 -0
- package/dist/core/message-images.test.d.ts +2 -0
- package/dist/core/message-images.test.d.ts.map +1 -0
- package/dist/core/message-images.test.js +62 -0
- package/dist/core/message-images.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/persistent-shell.d.ts +3 -1
- package/dist/core/persistent-shell.d.ts.map +1 -1
- package/dist/core/persistent-shell.js +13 -3
- package/dist/core/persistent-shell.js.map +1 -1
- 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.d.ts +1 -1
- package/dist/core/process-manager.d.ts.map +1 -1
- package/dist/core/process-manager.js +23 -13
- package/dist/core/process-manager.js.map +1 -1
- package/dist/core/project-discovery.d.ts +8 -1
- package/dist/core/project-discovery.d.ts.map +1 -1
- package/dist/core/project-discovery.js +145 -62
- package/dist/core/project-discovery.js.map +1 -1
- package/dist/core/project-discovery.test.js +131 -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/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/session-export.d.ts +98 -0
- package/dist/core/session-export.d.ts.map +1 -0
- package/dist/core/session-export.js +356 -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-manager.d.ts +27 -7
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +257 -124
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/session-storage.d.ts +57 -0
- package/dist/core/session-storage.d.ts.map +1 -0
- package/dist/core/session-storage.js +592 -0
- package/dist/core/session-storage.js.map +1 -0
- package/dist/core/session-storage.test.d.ts +2 -0
- package/dist/core/session-storage.test.d.ts.map +1 -0
- package/dist/core/session-storage.test.js +272 -0
- package/dist/core/session-storage.test.js.map +1 -0
- package/dist/core/settings-manager.d.ts +7 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +17 -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/sidecar-error-reporter.d.ts +12 -0
- package/dist/core/sidecar-error-reporter.d.ts.map +1 -0
- package/dist/core/sidecar-error-reporter.js +57 -0
- package/dist/core/sidecar-error-reporter.js.map +1 -0
- package/dist/core/sidecar-error-reporter.test.d.ts +2 -0
- package/dist/core/sidecar-error-reporter.test.d.ts.map +1 -0
- package/dist/core/sidecar-error-reporter.test.js +61 -0
- package/dist/core/sidecar-error-reporter.test.js.map +1 -0
- package/dist/core/skills-routing.test.js +3 -1
- package/dist/core/skills-routing.test.js.map +1 -1
- package/dist/core/skills.d.ts +1 -1
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +4 -1
- package/dist/core/skills.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/subagent-manager.d.ts +8 -1
- package/dist/core/subagent-manager.d.ts.map +1 -1
- package/dist/core/subagent-manager.js +35 -2
- package/dist/core/subagent-manager.js.map +1 -1
- package/dist/core/subagent-manager.test.js +34 -0
- 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/verify-commands.d.ts.map +1 -1
- package/dist/core/verify-commands.js +1 -6
- package/dist/core/verify-commands.js.map +1 -1
- package/dist/core/verify-commands.test.js +1 -5
- package/dist/core/verify-commands.test.js.map +1 -1
- package/dist/core/workspace-guard.d.ts +40 -0
- package/dist/core/workspace-guard.d.ts.map +1 -0
- package/dist/core/workspace-guard.js +115 -0
- package/dist/core/workspace-guard.js.map +1 -0
- package/dist/core/workspace-guard.test.d.ts +2 -0
- package/dist/core/workspace-guard.test.d.ts.map +1 -0
- package/dist/core/workspace-guard.test.js +107 -0
- package/dist/core/workspace-guard.test.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/modes/json-mode.d.ts +7 -0
- package/dist/modes/json-mode.d.ts.map +1 -1
- package/dist/modes/json-mode.js +10 -0
- package/dist/modes/json-mode.js.map +1 -1
- package/dist/modes/subagent-worker-mode.d.ts +2 -0
- package/dist/modes/subagent-worker-mode.d.ts.map +1 -1
- package/dist/modes/subagent-worker-mode.js +11 -0
- package/dist/modes/subagent-worker-mode.js.map +1 -1
- package/dist/session.d.ts +4 -4
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +38 -101
- package/dist/session.js.map +1 -1
- package/dist/session.test.d.ts +2 -0
- package/dist/session.test.d.ts.map +1 -0
- package/dist/session.test.js +35 -0
- package/dist/session.test.js.map +1 -0
- package/dist/system-prompt.d.ts +23 -1
- package/dist/system-prompt.d.ts.map +1 -1
- package/dist/system-prompt.js +119 -31
- package/dist/system-prompt.js.map +1 -1
- package/dist/system-prompt.test.js +154 -14
- 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 +4 -2
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +57 -9
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/bash.test.js +156 -8
- 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/edit.d.ts +2 -1
- package/dist/tools/edit.d.ts.map +1 -1
- package/dist/tools/edit.js +7 -1
- package/dist/tools/edit.js.map +1 -1
- package/dist/tools/edit.test.js +7 -0
- package/dist/tools/edit.test.js.map +1 -1
- package/dist/tools/exit-plan.d.ts.map +1 -1
- package/dist/tools/exit-plan.js +15 -4
- package/dist/tools/exit-plan.js.map +1 -1
- package/dist/tools/exit-plan.test.d.ts +2 -0
- package/dist/tools/exit-plan.test.d.ts.map +1 -0
- package/dist/tools/exit-plan.test.js +72 -0
- package/dist/tools/exit-plan.test.js.map +1 -0
- 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/grep.d.ts +3 -1
- package/dist/tools/grep.d.ts.map +1 -1
- package/dist/tools/grep.js +143 -14
- package/dist/tools/grep.js.map +1 -1
- package/dist/tools/grep.test.js +59 -0
- package/dist/tools/grep.test.js.map +1 -1
- package/dist/tools/index.d.ts +14 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +6 -5
- package/dist/tools/index.js.map +1 -1
- 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/screenshot.js +1 -1
- package/dist/tools/screenshot.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/skill.d.ts.map +1 -1
- package/dist/tools/skill.js +4 -2
- package/dist/tools/skill.js.map +1 -1
- package/dist/tools/source-path.d.ts.map +1 -1
- package/dist/tools/source-path.js +9 -0
- package/dist/tools/source-path.js.map +1 -1
- package/dist/tools/subagent-shared.d.ts +1 -1
- package/dist/tools/subagent-shared.d.ts.map +1 -1
- package/dist/tools/subagent-shared.js +4 -1
- package/dist/tools/subagent-shared.js.map +1 -1
- package/dist/tools/subagent-shared.test.js +21 -1
- package/dist/tools/subagent-shared.test.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 +8 -0
- package/dist/tools/subagent.js.map +1 -1
- package/dist/tools/web-fetch.d.ts +2 -1
- package/dist/tools/web-fetch.d.ts.map +1 -1
- package/dist/tools/web-fetch.js +17 -6
- 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 +34 -7
- package/dist/tools/web-search.js.map +1 -1
- package/dist/tools/write.d.ts +2 -1
- package/dist/tools/write.d.ts.map +1 -1
- package/dist/tools/write.js +7 -1
- package/dist/tools/write.js.map +1 -1
- package/dist/tools/write.test.js +12 -0
- package/dist/tools/write.test.js.map +1 -1
- package/dist/ui/App.d.ts.map +1 -1
- package/dist/ui/App.js +22 -85
- package/dist/ui/App.js.map +1 -1
- package/dist/ui/app-items.d.ts +11 -12
- package/dist/ui/app-items.d.ts.map +1 -1
- package/dist/ui/app-items.js +10 -0
- package/dist/ui/app-items.js.map +1 -1
- package/dist/ui/components/ActivityIndicator.d.ts.map +1 -1
- package/dist/ui/components/ActivityIndicator.js +1 -0
- package/dist/ui/components/ActivityIndicator.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/components/Footer.test.js +1 -1
- package/dist/ui/hooks/useAgentLoop.d.ts +5 -2
- package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
- package/dist/ui/hooks/useAgentLoop.js +49 -8
- 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/hooks/useTranscriptHistory.d.ts +8 -5
- package/dist/ui/hooks/useTranscriptHistory.d.ts.map +1 -1
- package/dist/ui/hooks/useTranscriptHistory.js +2 -30
- package/dist/ui/hooks/useTranscriptHistory.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/http-body.d.ts +19 -0
- package/dist/utils/http-body.d.ts.map +1 -0
- package/dist/utils/http-body.js +58 -0
- package/dist/utils/http-body.js.map +1 -0
- package/dist/utils/http-body.test.d.ts +2 -0
- package/dist/utils/http-body.test.d.ts.map +1 -0
- package/dist/utils/http-body.test.js +68 -0
- package/dist/utils/http-body.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/dist/utils/text.d.ts +9 -0
- package/dist/utils/text.d.ts.map +1 -0
- package/dist/utils/text.js +11 -0
- package/dist/utils/text.js.map +1 -0
- package/package.json +4 -4
package/dist/app-sidecar.js
CHANGED
|
@@ -21,9 +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";
|
|
26
|
-
import { CHAT_AGENT_IDS, chatAgentSessionsDir, createChatAgent, parseChatAgentId,
|
|
27
|
+
import { CHAT_AGENT_IDS, chatAgentSessionsDir, createChatAgent, parseChatAgentId, switchChatAgent, } from "./chat-agents/index.js";
|
|
27
28
|
import { buildJiwaTools, JiwaStore } from "./chat-agents/jiwa.js";
|
|
28
29
|
import { buildMemoryTools, MemoryStore } from "./chat-agents/memory.js";
|
|
29
30
|
import { buildNolanSystemPrompt, buildNolanAutopilotSystemPrompt } from "./core/nolan-prompt.js";
|
|
@@ -33,9 +34,12 @@ import { isWorkflowCommandText, countAssistantMessages, shouldStartAutopilotCycl
|
|
|
33
34
|
import { driveAutopilotCycle, frameAutopilotInjection } from "./core/autopilot-cycle.js";
|
|
34
35
|
import { validateNolanModelPref, effectiveNolanModel, } from "./core/nolan-model.js";
|
|
35
36
|
import { normalizeAutopilotMarkersForHistory, normalizeAppMarkersForHistory, normalizeNolanTurnsForHistory, restoreUserRow, restoreAssistantTexts, autopilotMarkerCopySeed, } from "./core/session-history.js";
|
|
37
|
+
import { sessionToMarkdown, defaultExportFilename, } from "./core/session-export.js";
|
|
36
38
|
import { AuthStorage } from "./core/auth-storage.js";
|
|
37
39
|
import { cleanupToolOutputs } from "./tools/overflow.js";
|
|
38
|
-
import {
|
|
40
|
+
import { readCappedBody } from "./utils/http-body.js";
|
|
41
|
+
import { fetchSubscriptionUsage, MOONSHOT_OAUTH_KEY, SubscriptionUsageError, XIAOMI_CREDITS_KEY, discoverLocalModels, findProbedModel, formatLocalModelId, parseLocalModelId, probeEndpoint, toModelInfo as localModelInfo, } from "@prestyj/core";
|
|
42
|
+
import { LocalEndpointError, addCustomEndpoint, listAllEndpoints, removeCustomEndpoint, syncEndpointCredentials, } from "./core/local-endpoint-store.js";
|
|
39
43
|
import { loginAnthropic } from "./core/oauth/anthropic.js";
|
|
40
44
|
import { loginOpenAI } from "./core/oauth/openai.js";
|
|
41
45
|
import { loginGemini } from "./core/oauth/gemini.js";
|
|
@@ -43,19 +47,21 @@ import { loginKimi } from "./core/oauth/kimi.js";
|
|
|
43
47
|
import { AUTH_PROVIDERS } from "./core/auth-providers.js";
|
|
44
48
|
import { ensureAppDirs, loadSavedSettings } from "./config.js";
|
|
45
49
|
import { SettingsManager } from "./core/settings-manager.js";
|
|
46
|
-
import { getModel,
|
|
50
|
+
import { getModel, getDefaultThinkingLevel, getContextWindow, getAllModels, clearRuntimeModels, registerRuntimeModels, } from "./core/model-registry.js";
|
|
47
51
|
import { resolveStartOrFallback } from "./core/resolve-start.js";
|
|
48
52
|
import { getGitBranch, getGitDirtyFileCount, isGitRepo } from "./utils/git.js";
|
|
53
|
+
import { getGitHubOpenCounts, getGitHubRepoSlug } from "./utils/github.js";
|
|
49
54
|
import { extractPlanSteps } from "./utils/plan-steps.js";
|
|
50
55
|
import { getNextThinkingLevel, getSupportedThinkingLevels, isThinkingLevelSupported, } from "./core/thinking-level.js";
|
|
51
56
|
import { PROMPT_COMMANDS } from "./core/prompt-commands.js";
|
|
52
57
|
import { loadCustomCommands } from "./core/custom-commands.js";
|
|
53
|
-
import { discoverProjects
|
|
58
|
+
import { discoverProjects } from "./core/project-discovery.js";
|
|
59
|
+
import { listSidecarSessions } from "./app-sidecar-sessions.js";
|
|
54
60
|
import { loadTasksSync, saveTasksSync, pruneDoneTasksSync, getNextPendingTask, markTaskInProgress, } from "./core/task-store.js";
|
|
55
61
|
import { initLogger, log } from "./core/logger.js";
|
|
56
62
|
import { RADIO_STATIONS, getCurrentStation, getRadioVolume, playRadio, setRadioVolume, stopRadio, } from "./core/radio.js";
|
|
57
63
|
import { enrichProcessPath } from "./core/shell-path.js";
|
|
58
|
-
import { downscaleForPreview, validateVisionImage } from "./utils/image.js";
|
|
64
|
+
import { downscaleForPreview, shrinkToFit, validateVisionImage } from "./utils/image.js";
|
|
59
65
|
import { startServeMode } from "./modes/serve-mode.js";
|
|
60
66
|
import { loadTelegramConfig, saveTelegramConfig, verifyBotToken } from "./core/telegram-config.js";
|
|
61
67
|
import { loadServers, addServer, removeServer, getServer, parseMcpAddCommand, MCPClientManager, McpOAuthStore, } from "./core/mcp/index.js";
|
|
@@ -64,6 +70,7 @@ import { loadProgress, peekProgress, updateProgress } from "./core/progress/stor
|
|
|
64
70
|
import { awardPrompt, awardCommits } from "./core/progress/engine.js";
|
|
65
71
|
import { detectNewCommits, repoKey } from "./core/progress/git-xp.js";
|
|
66
72
|
import { rebuildFromSessions } from "./core/progress/rebuild.js";
|
|
73
|
+
import { captureSidecarError, flushSidecarErrors, shouldCaptureToolFailure, shouldCaptureUsagePollingError, wrapSidecarHandler, } from "./core/sidecar-error-reporter.js";
|
|
67
74
|
const ALL_PROVIDERS = [
|
|
68
75
|
// US
|
|
69
76
|
"anthropic",
|
|
@@ -169,6 +176,7 @@ async function persistModelSelection(settingsFile, provider, model) {
|
|
|
169
176
|
await sm.set("defaultModel", model);
|
|
170
177
|
}
|
|
171
178
|
catch (err) {
|
|
179
|
+
captureSidecarError(err, "app-sidecar.settings.persist-model");
|
|
172
180
|
log("WARN", "app-sidecar", "failed to persist model selection", { err: String(err) });
|
|
173
181
|
}
|
|
174
182
|
}
|
|
@@ -185,6 +193,7 @@ async function persistThinkingLevel(settingsFile, level) {
|
|
|
185
193
|
await sm.set("thinkingLevel", level);
|
|
186
194
|
}
|
|
187
195
|
catch (err) {
|
|
196
|
+
captureSidecarError(err, "app-sidecar.settings.persist-thinking");
|
|
188
197
|
log("WARN", "app-sidecar", "failed to persist thinking level", { err: String(err) });
|
|
189
198
|
}
|
|
190
199
|
}
|
|
@@ -202,19 +211,28 @@ async function prepareAttachments(cwd, attachments) {
|
|
|
202
211
|
const fileName = `${Date.now().toString(36)}-${safe}`;
|
|
203
212
|
const filePath = path.join(dir, fileName);
|
|
204
213
|
const buf = Buffer.from(a.data, "base64");
|
|
205
|
-
// Validate image attachments before they become native image content
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
//
|
|
214
|
+
// Validate and cap image attachments before they become native image content
|
|
215
|
+
// blocks. Anthropic applies a 2000 px per-dimension cap once conversation
|
|
216
|
+
// history contains more than 20 images, so every attachment must be safe for
|
|
217
|
+
// later turns too. Keep the original on disk, but send the resized bytes.
|
|
218
|
+
// Corrupt or unsupported images become plain files so they cannot reject the
|
|
219
|
+
// entire provider request.
|
|
211
220
|
let prepared = { ...a };
|
|
212
221
|
if (a.kind === "image") {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
222
|
+
try {
|
|
223
|
+
const resized = await shrinkToFit(buf, a.mediaType);
|
|
224
|
+
const validatedType = await validateVisionImage(resized.buffer);
|
|
225
|
+
prepared = validatedType
|
|
226
|
+
? {
|
|
227
|
+
...a,
|
|
228
|
+
mediaType: validatedType,
|
|
229
|
+
data: resized.buffer.toString("base64"),
|
|
230
|
+
}
|
|
231
|
+
: { ...a, kind: "file" };
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
217
234
|
prepared = { ...a, kind: "file" };
|
|
235
|
+
}
|
|
218
236
|
}
|
|
219
237
|
try {
|
|
220
238
|
await fs.writeFile(filePath, buf);
|
|
@@ -227,6 +245,9 @@ async function prepareAttachments(cwd, attachments) {
|
|
|
227
245
|
return out;
|
|
228
246
|
}
|
|
229
247
|
const FILE_SEARCH_LIMIT = 20;
|
|
248
|
+
// Upper bound on files walked per search (baseline #8 memory cap). Far above the
|
|
249
|
+
// 20-result output limit, so relevance/recency ranking is unaffected in practice.
|
|
250
|
+
const FILE_SEARCH_SCAN_CAP = 50_000;
|
|
230
251
|
/** Score a candidate path against a lowercased query. Higher is better; a
|
|
231
252
|
* negative score means "no match". Basename hits beat path hits; prefix beats
|
|
232
253
|
* substring; shorter paths break ties. */
|
|
@@ -279,8 +300,13 @@ async function searchProjectFiles(cwd, rawQuery) {
|
|
|
279
300
|
}
|
|
280
301
|
const ig = ignore.default().add(gitignore);
|
|
281
302
|
// `stats: true` gives mtime without a second stat pass, so the empty-query
|
|
282
|
-
// "recent files" path is a single walk.
|
|
283
|
-
|
|
303
|
+
// "recent files" path is a single walk. Stream + hard scan cap (baseline #8):
|
|
304
|
+
// collecting the full result array retained ~0.9 MB per 1k files (18.5 MB at
|
|
305
|
+
// 20k), unbounded. Bail after FILE_SEARCH_SCAN_CAP entries so a giant repo
|
|
306
|
+
// can't balloon RSS; the newest/most-relevant matches still surface because
|
|
307
|
+
// the cap is far above the 20-result output limit.
|
|
308
|
+
const entries = [];
|
|
309
|
+
const scanStream = fg.default.stream("**/*", {
|
|
284
310
|
cwd,
|
|
285
311
|
dot: false,
|
|
286
312
|
onlyFiles: true,
|
|
@@ -289,6 +315,17 @@ async function searchProjectFiles(cwd, rawQuery) {
|
|
|
289
315
|
followSymbolicLinks: false,
|
|
290
316
|
stats: true,
|
|
291
317
|
});
|
|
318
|
+
for await (const entry of scanStream) {
|
|
319
|
+
entries.push(entry);
|
|
320
|
+
if (entries.length >= FILE_SEARCH_SCAN_CAP) {
|
|
321
|
+
scanStream.destroy();
|
|
322
|
+
log("DEBUG", "app-sidecar", "file search scan cap hit", {
|
|
323
|
+
cap: String(FILE_SEARCH_SCAN_CAP),
|
|
324
|
+
cwd,
|
|
325
|
+
});
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
292
329
|
const files = entries.filter((e) => !ig.ignores(e.path));
|
|
293
330
|
if (!query) {
|
|
294
331
|
return files
|
|
@@ -311,6 +348,11 @@ async function searchProjectFiles(cwd, rawQuery) {
|
|
|
311
348
|
* hook prompt, by its distinctive opening phrase. Returns the hook kind so the
|
|
312
349
|
* webview can render the short notice line instead of the full prompt body.
|
|
313
350
|
*/
|
|
351
|
+
/** `?tools=` on /export. Anything unrecognised (including absent) falls back to
|
|
352
|
+
* the human-facing `summary` default rather than dumping every payload. */
|
|
353
|
+
function parseToolDetail(value) {
|
|
354
|
+
return value === "none" || value === "full" ? value : "summary";
|
|
355
|
+
}
|
|
314
356
|
function detectHookKind(text) {
|
|
315
357
|
const t = text.trimStart();
|
|
316
358
|
if (t.startsWith("Ideal? Review the actual work"))
|
|
@@ -418,6 +460,7 @@ async function runJsonModeIfRequested() {
|
|
|
418
460
|
"max-turns": { type: "string" },
|
|
419
461
|
"system-prompt": { type: "string" },
|
|
420
462
|
tools: { type: "string" },
|
|
463
|
+
"mcp-servers": { type: "string" },
|
|
421
464
|
"prompt-cache-key": { type: "string" },
|
|
422
465
|
},
|
|
423
466
|
allowPositionals: true,
|
|
@@ -434,16 +477,30 @@ async function runJsonModeIfRequested() {
|
|
|
434
477
|
.filter(Boolean)
|
|
435
478
|
: [];
|
|
436
479
|
const allowedTools = parsedTools.length > 0 ? parsedTools : undefined;
|
|
480
|
+
// MCP servers the agent definition asked for. Without forwarding these, an
|
|
481
|
+
// allow-listed child connects no MCP at all and loses live code search.
|
|
482
|
+
const parsedMcpServers = values["mcp-servers"]
|
|
483
|
+
? values["mcp-servers"]
|
|
484
|
+
.split(",")
|
|
485
|
+
.map((s) => s.trim())
|
|
486
|
+
.filter(Boolean)
|
|
487
|
+
: [];
|
|
488
|
+
const allowedMcpServers = parsedMcpServers.length > 0 ? parsedMcpServers : undefined;
|
|
437
489
|
await runJsonMode({
|
|
438
490
|
message: positionals[0] ?? "",
|
|
439
491
|
provider: (values.provider ?? "anthropic"),
|
|
440
|
-
model: values.model ?? "claude-opus-
|
|
492
|
+
model: values.model ?? "claude-opus-5",
|
|
441
493
|
cwd: process.cwd(),
|
|
442
494
|
systemPrompt: values["system-prompt"],
|
|
443
495
|
maxTurns: maxTurnsRaw ? parseInt(maxTurnsRaw, 10) : undefined,
|
|
444
496
|
allowedTools,
|
|
497
|
+
allowedMcpServers,
|
|
445
498
|
promptCacheKey: values["prompt-cache-key"],
|
|
446
|
-
}).catch((err) => {
|
|
499
|
+
}).catch(async (err) => {
|
|
500
|
+
captureSidecarError(err, "app-sidecar.json-mode", {
|
|
501
|
+
provider: String(values.provider ?? "anthropic"),
|
|
502
|
+
});
|
|
503
|
+
await flushSidecarErrors();
|
|
447
504
|
process.stderr.write((err instanceof Error ? err.message : String(err)) + "\n");
|
|
448
505
|
process.exit(1);
|
|
449
506
|
});
|
|
@@ -452,13 +509,8 @@ async function runJsonModeIfRequested() {
|
|
|
452
509
|
// ── Daemon-level HTTP helpers (shared by the session-management routes) ─────
|
|
453
510
|
// The per-session route table has its own local copies; these serve the
|
|
454
511
|
// daemon's own POST /session / DELETE /session routes.
|
|
455
|
-
function daemonReadBody(req) {
|
|
456
|
-
return
|
|
457
|
-
const chunks = [];
|
|
458
|
-
req.on("data", (c) => chunks.push(c));
|
|
459
|
-
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
|
|
460
|
-
req.on("error", reject);
|
|
461
|
-
});
|
|
512
|
+
function daemonReadBody(req, res) {
|
|
513
|
+
return readCappedBody(req, res);
|
|
462
514
|
}
|
|
463
515
|
function daemonJson(res, status, body) {
|
|
464
516
|
res.writeHead(status, {
|
|
@@ -486,6 +538,31 @@ async function main() {
|
|
|
486
538
|
// ~/.ezcoder/debug.log (initLogger truncates on each start).
|
|
487
539
|
const sidecarLog = path.join(paths.agentDir, "ezcoder-app-sidecar.log");
|
|
488
540
|
initLogger(sidecarLog);
|
|
541
|
+
// The desktop sidecar previously omitted the stream diagnostic hook used by
|
|
542
|
+
// the CLI, leaving device-specific provider stalls impossible to distinguish from
|
|
543
|
+
// event-loop starvation or a broken streaming network path. Keep routine
|
|
544
|
+
// phases lightweight; timeout phases include non-sensitive runtime context.
|
|
545
|
+
setStreamDiagnostic((phase, data) => {
|
|
546
|
+
const includeRuntime = phase === "idle_timeout_fired" ||
|
|
547
|
+
phase === "hard_timeout_fired" ||
|
|
548
|
+
phase === "stall_exhausted";
|
|
549
|
+
log("INFO", "stream", phase, {
|
|
550
|
+
...(data ?? {}),
|
|
551
|
+
...(includeRuntime
|
|
552
|
+
? {
|
|
553
|
+
platform: process.platform,
|
|
554
|
+
arch: process.arch,
|
|
555
|
+
osRelease: os.release(),
|
|
556
|
+
node: process.version,
|
|
557
|
+
logicalCpus: os.cpus().length,
|
|
558
|
+
totalMemoryMb: Math.round(os.totalmem() / 1024 / 1024),
|
|
559
|
+
freeMemoryMb: Math.round(os.freemem() / 1024 / 1024),
|
|
560
|
+
rssMb: Math.round(process.memoryUsage().rss / 1024 / 1024),
|
|
561
|
+
processUptimeSec: Math.round(process.uptime()),
|
|
562
|
+
}
|
|
563
|
+
: {}),
|
|
564
|
+
});
|
|
565
|
+
});
|
|
489
566
|
// Global last-resort guards, installed as early as the logger allows so they
|
|
490
567
|
// cover the WHOLE lifecycle — including startup/initialize, the phase the
|
|
491
568
|
// "sidecar did not start in time" bug lives in. The sidecar is a long-lived
|
|
@@ -544,18 +621,24 @@ async function main() {
|
|
|
544
621
|
});
|
|
545
622
|
const usageCache = new Map();
|
|
546
623
|
const usageRequests = new Map();
|
|
547
|
-
// 429 backoff:
|
|
548
|
-
//
|
|
549
|
-
//
|
|
624
|
+
// 429 backoff: quota endpoints are auxiliary UI data. Honor Retry-After when
|
|
625
|
+
// provided; otherwise retain the unavailable snapshot for 30 minutes. Clamp
|
|
626
|
+
// the provider value so a malformed header can neither hammer the endpoint
|
|
627
|
+
// nor suppress usage data forever.
|
|
550
628
|
const usageRateLimitedUntil = new Map();
|
|
551
|
-
const
|
|
629
|
+
const USAGE_RATE_LIMIT_FALLBACK_BACKOFF_MS = 30 * 60_000;
|
|
630
|
+
const USAGE_RATE_LIMIT_MIN_BACKOFF_MS = 60_000;
|
|
631
|
+
const USAGE_RATE_LIMIT_MAX_BACKOFF_MS = 24 * 60 * 60_000;
|
|
552
632
|
async function fetchUsageProvider(provider) {
|
|
553
|
-
const displayName = provider === "anthropic" ? "Anthropic" : "Codex";
|
|
554
|
-
|
|
633
|
+
const displayName = provider === "anthropic" ? "Anthropic" : provider === "openai" ? "Codex" : "Kimi";
|
|
634
|
+
// Kimi plan usage is tracked on the OAuth credential specifically — the
|
|
635
|
+
// Moonshot platform API key is metered per-token, not per plan window.
|
|
636
|
+
const authKey = provider === "moonshot" ? MOONSHOT_OAUTH_KEY : provider;
|
|
637
|
+
if (!(await auth.hasProviderAuth(authKey))) {
|
|
555
638
|
return { provider, displayName, connected: false, windows: [], fetchedAt: Date.now() };
|
|
556
639
|
}
|
|
557
640
|
try {
|
|
558
|
-
let credentials = await auth.resolveCredentials(
|
|
641
|
+
let credentials = await auth.resolveCredentials(authKey);
|
|
559
642
|
try {
|
|
560
643
|
const snapshot = {
|
|
561
644
|
...(await fetchSubscriptionUsage(provider, credentials)),
|
|
@@ -568,7 +651,7 @@ async function main() {
|
|
|
568
651
|
// A provider can revoke an access token before its stored expiry. Refresh
|
|
569
652
|
// once on 401, matching inference auth recovery, then retry the usage call.
|
|
570
653
|
if (error instanceof SubscriptionUsageError && error.status === 401) {
|
|
571
|
-
credentials = await auth.resolveCredentials(
|
|
654
|
+
credentials = await auth.resolveCredentials(authKey, { forceRefresh: true });
|
|
572
655
|
const snapshot = {
|
|
573
656
|
...(await fetchSubscriptionUsage(provider, credentials)),
|
|
574
657
|
connected: true,
|
|
@@ -581,11 +664,20 @@ async function main() {
|
|
|
581
664
|
}
|
|
582
665
|
catch (error) {
|
|
583
666
|
const message = error instanceof Error ? error.message : String(error);
|
|
584
|
-
|
|
667
|
+
if (shouldCaptureUsagePollingError(error)) {
|
|
668
|
+
captureSidecarError(error, "app-sidecar.usage.fetch", { provider });
|
|
669
|
+
}
|
|
670
|
+
let backoffMs;
|
|
585
671
|
if (error instanceof SubscriptionUsageError && error.status === 429) {
|
|
586
|
-
|
|
672
|
+
backoffMs = Math.min(USAGE_RATE_LIMIT_MAX_BACKOFF_MS, Math.max(USAGE_RATE_LIMIT_MIN_BACKOFF_MS, error.retryAfterMs ?? USAGE_RATE_LIMIT_FALLBACK_BACKOFF_MS));
|
|
673
|
+
usageRateLimitedUntil.set(provider, Date.now() + backoffMs);
|
|
587
674
|
}
|
|
588
|
-
|
|
675
|
+
log("WARN", "app-sidecar", "subscription usage fetch failed", {
|
|
676
|
+
provider,
|
|
677
|
+
message,
|
|
678
|
+
...(backoffMs !== undefined && { backoffMs: String(backoffMs) }),
|
|
679
|
+
});
|
|
680
|
+
const connected = await auth.hasProviderAuth(authKey);
|
|
589
681
|
return {
|
|
590
682
|
provider,
|
|
591
683
|
displayName,
|
|
@@ -640,7 +732,7 @@ async function main() {
|
|
|
640
732
|
return null;
|
|
641
733
|
}
|
|
642
734
|
}
|
|
643
|
-
const server = http.createServer((req, res) => {
|
|
735
|
+
const server = http.createServer(wrapSidecarHandler((req, res) => {
|
|
644
736
|
const url = req.url ?? "/";
|
|
645
737
|
const method = req.method ?? "GET";
|
|
646
738
|
// CORS preflight — the webview origin differs from 127.0.0.1.
|
|
@@ -656,7 +748,9 @@ async function main() {
|
|
|
656
748
|
// ── Daemon-level routes (session lifecycle) ──────────────────────────
|
|
657
749
|
// Create a session for a window: { mode?, cwd, sessionPath? } → { sessionId }.
|
|
658
750
|
if (method === "POST" && url === "/session") {
|
|
659
|
-
void daemonReadBody(req).then(async (raw) => {
|
|
751
|
+
void daemonReadBody(req, res).then(async (raw) => {
|
|
752
|
+
if (raw === null)
|
|
753
|
+
return;
|
|
660
754
|
let body = {};
|
|
661
755
|
try {
|
|
662
756
|
body = raw ? JSON.parse(raw) : {};
|
|
@@ -684,6 +778,7 @@ async function main() {
|
|
|
684
778
|
}
|
|
685
779
|
catch (err) {
|
|
686
780
|
const message = err instanceof Error ? err.message : String(err);
|
|
781
|
+
captureSidecarError(err, "app-sidecar.session.create");
|
|
687
782
|
log("ERROR", "app-sidecar", "session create failed", { message });
|
|
688
783
|
daemonJson(res, 500, { error: message });
|
|
689
784
|
}
|
|
@@ -714,12 +809,13 @@ async function main() {
|
|
|
714
809
|
if (method === "GET" && (url === "/usage" || url.startsWith("/usage?"))) {
|
|
715
810
|
void (async () => {
|
|
716
811
|
const provider = new URL(url, `http://${host}`).searchParams.get("provider");
|
|
717
|
-
if (provider !== "anthropic" && provider !== "openai") {
|
|
812
|
+
if (provider !== "anthropic" && provider !== "openai" && provider !== "moonshot") {
|
|
718
813
|
daemonJson(res, 400, { error: "unsupported usage provider" });
|
|
719
814
|
return;
|
|
720
815
|
}
|
|
721
816
|
daemonJson(res, 200, await subscriptionUsage(provider));
|
|
722
817
|
})().catch((error) => {
|
|
818
|
+
captureSidecarError(error, "app-sidecar.usage.request");
|
|
723
819
|
log("ERROR", "app-sidecar", "subscription usage request failed", {
|
|
724
820
|
message: error instanceof Error ? error.message : String(error),
|
|
725
821
|
});
|
|
@@ -735,7 +831,7 @@ async function main() {
|
|
|
735
831
|
return;
|
|
736
832
|
}
|
|
737
833
|
ctx.handle(req, res, url, method);
|
|
738
|
-
});
|
|
834
|
+
}, "app-sidecar.http"));
|
|
739
835
|
server.listen(port, host, () => {
|
|
740
836
|
const addr = server.address();
|
|
741
837
|
// The Rust shell reads this line to learn the daemon port.
|
|
@@ -752,6 +848,8 @@ async function main() {
|
|
|
752
848
|
// Radio playback is app-wide (one stream across all windows), so it stops
|
|
753
849
|
// at the daemon level, not per session.
|
|
754
850
|
stopRadio();
|
|
851
|
+
// Close the ~/.ezcoder progress fs.watch handle (baseline #8 leak fix).
|
|
852
|
+
progress.dispose();
|
|
755
853
|
await Promise.all([...sessions.values()].map((c) => c.dispose().catch(() => { })));
|
|
756
854
|
server.close();
|
|
757
855
|
process.exit(0);
|
|
@@ -880,6 +978,7 @@ async function createProgressManager(agentDir, broadcastAll) {
|
|
|
880
978
|
broadcastAll(buildSnapshot(updated), originId);
|
|
881
979
|
}
|
|
882
980
|
catch (err) {
|
|
981
|
+
captureSidecarError(err, "app-sidecar.progress.award");
|
|
883
982
|
log("DEBUG", "app-sidecar", "progress award failed", {
|
|
884
983
|
message: err instanceof Error ? err.message : String(err),
|
|
885
984
|
});
|
|
@@ -888,6 +987,7 @@ async function createProgressManager(agentDir, broadcastAll) {
|
|
|
888
987
|
// Watch ~/.ezcoder (dir watch survives the atomic tmp+rename) for progress.json
|
|
889
988
|
// writes from other daemon processes; debounce, reload read-only, dedupe by nonce.
|
|
890
989
|
let watchDebounce = null;
|
|
990
|
+
let progressWatcher = null;
|
|
891
991
|
try {
|
|
892
992
|
const watcher = fsWatch(agentDir, (_event, filename) => {
|
|
893
993
|
if (filename !== "progress.json")
|
|
@@ -909,13 +1009,30 @@ async function createProgressManager(agentDir, broadcastAll) {
|
|
|
909
1009
|
}, 150);
|
|
910
1010
|
});
|
|
911
1011
|
watcher.unref();
|
|
1012
|
+
progressWatcher = watcher;
|
|
912
1013
|
}
|
|
913
1014
|
catch (err) {
|
|
1015
|
+
captureSidecarError(err, "app-sidecar.progress.watch");
|
|
914
1016
|
log("DEBUG", "app-sidecar", "progress watch unavailable", {
|
|
915
1017
|
message: err instanceof Error ? err.message : String(err),
|
|
916
1018
|
});
|
|
917
1019
|
}
|
|
918
|
-
|
|
1020
|
+
// Dispose closes the fs.watch handle (baseline #8: it was previously never
|
|
1021
|
+
// closed — a per-daemon leak) and clears any pending debounce timer.
|
|
1022
|
+
function dispose() {
|
|
1023
|
+
if (watchDebounce) {
|
|
1024
|
+
clearTimeout(watchDebounce);
|
|
1025
|
+
watchDebounce = null;
|
|
1026
|
+
}
|
|
1027
|
+
try {
|
|
1028
|
+
progressWatcher?.close();
|
|
1029
|
+
}
|
|
1030
|
+
catch {
|
|
1031
|
+
// Already closed / never opened — nothing to do.
|
|
1032
|
+
}
|
|
1033
|
+
progressWatcher = null;
|
|
1034
|
+
}
|
|
1035
|
+
return { snapshot, awardRun, dispose };
|
|
919
1036
|
}
|
|
920
1037
|
/**
|
|
921
1038
|
* Build one in-process agent session: its AgentSession, SSE client set, event
|
|
@@ -934,6 +1051,10 @@ async function createSession(deps, opts) {
|
|
|
934
1051
|
// parsing); the daemon owns the real listen host.
|
|
935
1052
|
const host = "127.0.0.1";
|
|
936
1053
|
const saved = loadSavedSettings(paths.settingsFile);
|
|
1054
|
+
// Native login/logout and other live sessions share auth.json. Refresh the
|
|
1055
|
+
// daemon-level snapshot before choosing this session's provider so a project
|
|
1056
|
+
// never boots against credentials that were just replaced or disconnected.
|
|
1057
|
+
await auth.load();
|
|
937
1058
|
// Per-project model/thinking prefs win over the shared global settings.json:
|
|
938
1059
|
// each window (one project cwd) restores its own selection instead of every
|
|
939
1060
|
// window reading the same single global slot that the last writer clobbered
|
|
@@ -952,9 +1073,14 @@ async function createSession(deps, opts) {
|
|
|
952
1073
|
});
|
|
953
1074
|
}
|
|
954
1075
|
// Per-project thinking prefs win over the global settings.json fallback.
|
|
1076
|
+
// With no saved level, the default follows the active credential's endpoint:
|
|
1077
|
+
// Kimi K3 on the OAuth coding endpoint starts at its declared default (high),
|
|
1078
|
+
// matching the official kimi-code CLI's plan-usage profile.
|
|
955
1079
|
const thinkEnabled = projectPrefs?.thinkingEnabled ?? saved.thinkingEnabled;
|
|
956
1080
|
const thinkingLevel = thinkEnabled
|
|
957
|
-
? (projectPrefs?.thinkingLevel ??
|
|
1081
|
+
? (projectPrefs?.thinkingLevel ??
|
|
1082
|
+
saved.thinkingLevel ??
|
|
1083
|
+
getDefaultThinkingLevel(model, { baseUrl: auth.getStoredBaseUrl(provider) }))
|
|
958
1084
|
: undefined;
|
|
959
1085
|
// ── SSE fan-out (declared before the session so plan callbacks can use it) ─
|
|
960
1086
|
const clients = new Set();
|
|
@@ -991,6 +1117,25 @@ async function createSession(deps, opts) {
|
|
|
991
1117
|
// /help
|
|
992
1118
|
.replaceAll(/see \/help/gi, "check the help menu"));
|
|
993
1119
|
}
|
|
1120
|
+
/**
|
|
1121
|
+
* Guidance for a connection failure against the ACTIVE local endpoint, or
|
|
1122
|
+
* undefined when that isn't the situation (so the generic wording stands).
|
|
1123
|
+
* "Disable your VPN / allow us through the firewall" is useless advice for a
|
|
1124
|
+
* server running on this machine — name what the user has to restart.
|
|
1125
|
+
*/
|
|
1126
|
+
function localNetworkGuidance(source) {
|
|
1127
|
+
if (source !== "network")
|
|
1128
|
+
return undefined;
|
|
1129
|
+
const state = session.getState();
|
|
1130
|
+
if (state.provider !== "local")
|
|
1131
|
+
return undefined;
|
|
1132
|
+
const parsed = parseLocalModelId(state.model);
|
|
1133
|
+
const probe = localProbes.find((p) => p.endpoint.id === parsed?.endpointId);
|
|
1134
|
+
if (!probe)
|
|
1135
|
+
return undefined;
|
|
1136
|
+
return (`${probe.endpoint.label} stopped responding at ${probe.endpoint.baseUrl}. ` +
|
|
1137
|
+
"Start it again and retry, or pick another model.");
|
|
1138
|
+
}
|
|
994
1139
|
// Turn any thrown value into the same clear headline/message/guidance shape
|
|
995
1140
|
// the TUI shows (see gg-ai's formatError) instead of a bare `err.message`, log
|
|
996
1141
|
// the full detail, and broadcast it under `type` ("error" or "nolan_error").
|
|
@@ -1000,7 +1145,12 @@ async function createSession(deps, opts) {
|
|
|
1000
1145
|
function broadcastError(type, logLabel, err) {
|
|
1001
1146
|
const f = formatError(err);
|
|
1002
1147
|
const message = f.message ? desktopGuidance(f.message) : undefined;
|
|
1003
|
-
const guidance = desktopGuidance(f.guidance);
|
|
1148
|
+
const guidance = localNetworkGuidance(f.source) ?? desktopGuidance(f.guidance);
|
|
1149
|
+
captureSidecarError(err, `app-sidecar.${logLabel.replaceAll(" ", "-")}`, {
|
|
1150
|
+
scope: type,
|
|
1151
|
+
...(f.provider ? { provider: f.provider } : {}),
|
|
1152
|
+
...(f.statusCode != null ? { status: String(f.statusCode) } : {}),
|
|
1153
|
+
});
|
|
1004
1154
|
log("ERROR", "app-sidecar", logLabel, {
|
|
1005
1155
|
headline: f.headline,
|
|
1006
1156
|
source: f.source,
|
|
@@ -1058,6 +1208,7 @@ async function createSession(deps, opts) {
|
|
|
1058
1208
|
chatAgent = nextAgent;
|
|
1059
1209
|
broadcast("chat_agent_change", { chatAgent: nextAgent });
|
|
1060
1210
|
await session.persistAppMarker("agent_handoff", { chatAgent: nextAgent }).catch((error) => {
|
|
1211
|
+
captureSidecarError(error, "app-sidecar.chat-agent.persist-handoff");
|
|
1061
1212
|
log("WARN", "app-sidecar", "agent handoff marker persist failed", {
|
|
1062
1213
|
message: error instanceof Error ? error.message : String(error),
|
|
1063
1214
|
});
|
|
@@ -1110,17 +1261,116 @@ async function createSession(deps, opts) {
|
|
|
1110
1261
|
}
|
|
1111
1262
|
}
|
|
1112
1263
|
log("INFO", "app-sidecar", "session ready", { provider, model, mode, chatAgent, cwd });
|
|
1264
|
+
// ── Local models (Ollama / LM Studio / llama.cpp / vLLM) ──
|
|
1265
|
+
// Probing four HTTP endpoints must never delay readiness, so this runs in the
|
|
1266
|
+
// background (same shape as backgroundMcpConnect) and pushes a models_change
|
|
1267
|
+
// frame when it lands.
|
|
1268
|
+
let localProbes = [];
|
|
1269
|
+
async function scanLocalModels(force) {
|
|
1270
|
+
const endpoints = await listAllEndpoints();
|
|
1271
|
+
const { probes, models } = await discoverLocalModels(endpoints, { force });
|
|
1272
|
+
localProbes = probes;
|
|
1273
|
+
// Only endpoints that answered get a credential: writing one for a server
|
|
1274
|
+
// that isn't running would make `hasProviderAuth("local")` true forever.
|
|
1275
|
+
await syncEndpointCredentials(probes.filter((probe) => probe.reachable).map((probe) => probe.endpoint));
|
|
1276
|
+
clearRuntimeModels((m) => m.provider === "local");
|
|
1277
|
+
registerRuntimeModels(models);
|
|
1278
|
+
log("INFO", "app-sidecar", "local model scan", {
|
|
1279
|
+
reachable: probes.filter((p) => p.reachable).length + "/" + probes.length,
|
|
1280
|
+
models: String(models.length),
|
|
1281
|
+
});
|
|
1282
|
+
return probes;
|
|
1283
|
+
}
|
|
1284
|
+
/** Endpoint rows + models, in the shape the Local models UI renders. */
|
|
1285
|
+
function localStatePayload() {
|
|
1286
|
+
return {
|
|
1287
|
+
endpoints: localProbes.map((probe) => ({
|
|
1288
|
+
id: probe.endpoint.id,
|
|
1289
|
+
label: probe.endpoint.label,
|
|
1290
|
+
baseUrl: probe.endpoint.baseUrl,
|
|
1291
|
+
kind: probe.endpoint.kind,
|
|
1292
|
+
custom: probe.endpoint.custom === true,
|
|
1293
|
+
reachable: probe.reachable,
|
|
1294
|
+
...(probe.reason ? { reason: probe.reason } : {}),
|
|
1295
|
+
models: probe.models.map((m) => ({
|
|
1296
|
+
id: formatLocalModelId(probe.endpoint.id, m.rawId),
|
|
1297
|
+
rawId: m.rawId,
|
|
1298
|
+
contextWindow: m.contextWindow,
|
|
1299
|
+
contextWindowKnown: m.contextWindowKnown,
|
|
1300
|
+
supportsTools: m.supportsTools,
|
|
1301
|
+
supportsImages: m.supportsImages,
|
|
1302
|
+
supportsThinking: m.supportsThinking,
|
|
1303
|
+
...(m.loaded === undefined ? {} : { loaded: m.loaded }),
|
|
1304
|
+
})),
|
|
1305
|
+
})),
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
/**
|
|
1309
|
+
* Why `modelId` can't be selected right now, or `undefined` when it can.
|
|
1310
|
+
* Two real footguns get a clear answer instead of a mid-run provider error:
|
|
1311
|
+
* a model with no tool calling (can't drive the agent at all), and a server
|
|
1312
|
+
* that has since been shut down.
|
|
1313
|
+
*/
|
|
1314
|
+
async function localModelBlocker(modelId) {
|
|
1315
|
+
const parsed = parseLocalModelId(modelId);
|
|
1316
|
+
if (!parsed)
|
|
1317
|
+
return undefined;
|
|
1318
|
+
const endpoints = await listAllEndpoints();
|
|
1319
|
+
const endpoint = endpoints.find((e) => e.id === parsed.endpointId);
|
|
1320
|
+
if (!endpoint)
|
|
1321
|
+
return `Unknown local endpoint "${parsed.endpointId}" — re-scan for local models.`;
|
|
1322
|
+
const probe = await probeEndpoint(endpoint);
|
|
1323
|
+
// Keep the cached view honest: this probe is fresher than the last scan.
|
|
1324
|
+
localProbes = localProbes.map((p) => (p.endpoint.id === endpoint.id ? probe : p));
|
|
1325
|
+
if (!probe.reachable) {
|
|
1326
|
+
return `${endpoint.label} isn't running at ${endpoint.baseUrl}. Start it and scan again.`;
|
|
1327
|
+
}
|
|
1328
|
+
const model = probe.models.find((m) => m.rawId === parsed.rawId);
|
|
1329
|
+
if (!model) {
|
|
1330
|
+
return `${endpoint.label} no longer serves "${parsed.rawId}".`;
|
|
1331
|
+
}
|
|
1332
|
+
if (!model.supportsTools) {
|
|
1333
|
+
return `${parsed.rawId} has no tool calling, so it can't run the agent. Pick a tool-capable model.`;
|
|
1334
|
+
}
|
|
1335
|
+
registerRuntimeModels(probe.models.map((m) => localModelInfo(m, endpoint)));
|
|
1336
|
+
return undefined;
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* A restored per-project pref can ask for thinking on a local model that
|
|
1340
|
+
* turns out not to reason (capabilities are only known after a probe). Drop
|
|
1341
|
+
* the level once we know, so the first prompt doesn't carry a
|
|
1342
|
+
* `reasoning_effort` the server rejects.
|
|
1343
|
+
*/
|
|
1344
|
+
function clampLocalThinking() {
|
|
1345
|
+
const st = session.getState();
|
|
1346
|
+
const level = session.getThinkingLevel();
|
|
1347
|
+
if (!level || st.provider !== "local")
|
|
1348
|
+
return;
|
|
1349
|
+
if (isThinkingLevelSupported(st.provider, st.model, level))
|
|
1350
|
+
return;
|
|
1351
|
+
session.setThinkingLevel(undefined);
|
|
1352
|
+
broadcast("thinking_change", {
|
|
1353
|
+
thinkingLevel: null,
|
|
1354
|
+
supportedThinkingLevels: getSupportedThinkingLevels(st.provider, st.model),
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1113
1357
|
// Workspace extras (context window, git status, background tasks). Git state
|
|
1114
1358
|
// is resolved once at startup and refreshed after every run; the context
|
|
1115
1359
|
// window follows the active model.
|
|
1116
|
-
const [initialGitBranch, initialGitIsRepo, initialDirtyFileCount] = await Promise.all([
|
|
1360
|
+
const [initialGitBranch, initialGitIsRepo, initialDirtyFileCount, initialGitHubSlug] = await Promise.all([
|
|
1117
1361
|
getGitBranch(cwd).catch(() => null),
|
|
1118
1362
|
isGitRepo(cwd).catch(() => false),
|
|
1119
1363
|
getGitDirtyFileCount(cwd).catch(() => 0),
|
|
1364
|
+
getGitHubRepoSlug(cwd).catch(() => null),
|
|
1120
1365
|
]);
|
|
1121
1366
|
let gitBranch = initialGitBranch;
|
|
1122
1367
|
let gitIsRepo = initialGitIsRepo;
|
|
1123
1368
|
let gitDirtyFileCount = initialDirtyFileCount;
|
|
1369
|
+
// Open issue/PR counts for the origin repo's GitHub slug, via the `gh` CLI's
|
|
1370
|
+
// auth. null = unknown (gh missing/unauthed, non-GitHub origin) → chips hidden.
|
|
1371
|
+
const gitHubSlug = initialGitHubSlug;
|
|
1372
|
+
let gitHubIssues = null;
|
|
1373
|
+
let gitHubPRs = null;
|
|
1124
1374
|
function currentContextWindow() {
|
|
1125
1375
|
const st = session.getState();
|
|
1126
1376
|
return getContextWindow(st.model, { provider: st.provider, accountId: st.accountId });
|
|
@@ -1133,9 +1383,41 @@ async function createSession(deps, opts) {
|
|
|
1133
1383
|
gitBranch,
|
|
1134
1384
|
isGitRepo: gitIsRepo,
|
|
1135
1385
|
gitDirtyFileCount,
|
|
1386
|
+
gitHubIssues,
|
|
1387
|
+
gitHubPRs,
|
|
1388
|
+
gitHubRepoUrl: gitHubSlug ? `https://github.com/${gitHubSlug}` : null,
|
|
1136
1389
|
tasks: session.listBackgroundProcesses(),
|
|
1390
|
+
// Roots added with /add-dir — the header shows a badge when non-empty.
|
|
1391
|
+
additionalRoots: session.getAdditionalRoots(),
|
|
1137
1392
|
};
|
|
1138
1393
|
}
|
|
1394
|
+
void scanLocalModels(false)
|
|
1395
|
+
.then(() => {
|
|
1396
|
+
clampLocalThinking();
|
|
1397
|
+
broadcast("extras", footerExtras());
|
|
1398
|
+
})
|
|
1399
|
+
.then(() => broadcast("models_change", { local: localStatePayload() }))
|
|
1400
|
+
.catch((err) => {
|
|
1401
|
+
// A discovery failure is never fatal — the user simply has no local
|
|
1402
|
+
// models. Log it; don't push an error row into the transcript.
|
|
1403
|
+
log("WARN", "app-sidecar", "local model scan failed", {
|
|
1404
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1405
|
+
});
|
|
1406
|
+
});
|
|
1407
|
+
// Refresh the GitHub counts and broadcast only on change. Transient failures
|
|
1408
|
+
// keep the last-known numbers so the chips don't flicker off on a timeout.
|
|
1409
|
+
async function refreshGitHubCounts() {
|
|
1410
|
+
if (!gitHubSlug)
|
|
1411
|
+
return;
|
|
1412
|
+
const counts = await getGitHubOpenCounts(gitHubSlug);
|
|
1413
|
+
if (!counts)
|
|
1414
|
+
return;
|
|
1415
|
+
if (counts.issues !== gitHubIssues || counts.prs !== gitHubPRs) {
|
|
1416
|
+
gitHubIssues = counts.issues;
|
|
1417
|
+
gitHubPRs = counts.prs;
|
|
1418
|
+
broadcast("extras", footerExtras());
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1139
1421
|
// tool_call_end carries no tool name (only the id), so remember each call's
|
|
1140
1422
|
// name from tool_call_start to log a useful line on completion. Mirrors the
|
|
1141
1423
|
// CLI's logging so the app sidecar's ~/.ezcoder/ezcoder-app-sidecar.log records tool
|
|
@@ -1184,6 +1466,7 @@ async function createSession(deps, opts) {
|
|
|
1184
1466
|
.catch(() => false)
|
|
1185
1467
|
.then(() => syncApprovedPlanProgress(generation))
|
|
1186
1468
|
.catch((error) => {
|
|
1469
|
+
captureSidecarError(error, "app-sidecar.plan.refresh-progress");
|
|
1187
1470
|
log("WARN", "app-sidecar", "plan progress refresh failed", {
|
|
1188
1471
|
message: error instanceof Error ? error.message : String(error),
|
|
1189
1472
|
});
|
|
@@ -1238,6 +1521,11 @@ async function createSession(deps, opts) {
|
|
|
1238
1521
|
session.eventBus.on("tool_call_end", (d) => {
|
|
1239
1522
|
const name = toolCallNames.get(d.toolCallId) ?? "unknown";
|
|
1240
1523
|
toolCallNames.delete(d.toolCallId);
|
|
1524
|
+
if (d.isError && shouldCaptureToolFailure(name, d.result)) {
|
|
1525
|
+
// Expected model-correctable validation failures stay in the local log and
|
|
1526
|
+
// conversation. Unexpected failures are reported without private result data.
|
|
1527
|
+
captureSidecarError(new Error(`Tool ${name} failed`), `tool.${name}`, { tool: name });
|
|
1528
|
+
}
|
|
1241
1529
|
log(d.isError ? "ERROR" : "INFO", "tool", `Tool call ended: ${name}`, {
|
|
1242
1530
|
id: d.toolCallId,
|
|
1243
1531
|
durationMs: String(d.durationMs),
|
|
@@ -1264,6 +1552,9 @@ async function createSession(deps, opts) {
|
|
|
1264
1552
|
session.eventBus.on("server_tool_call", (d) => broadcast("server_tool_call", d));
|
|
1265
1553
|
session.eventBus.on("turn_end", (d) => broadcast("turn_end", d));
|
|
1266
1554
|
session.eventBus.on("agent_done", (d) => broadcast("agent_done", d));
|
|
1555
|
+
// Non-clean stop (max_tokens/refusal/provider error) — info-style frame so
|
|
1556
|
+
// the webview can warn instead of presenting truncated output as complete.
|
|
1557
|
+
session.eventBus.on("truncated", (d) => broadcast("truncated", d));
|
|
1267
1558
|
session.eventBus.on("error", (d) => {
|
|
1268
1559
|
broadcastError("error", "agent error", d.error);
|
|
1269
1560
|
});
|
|
@@ -1568,6 +1859,9 @@ async function createSession(deps, opts) {
|
|
|
1568
1859
|
isGitRepo(cwd).catch(() => gitIsRepo),
|
|
1569
1860
|
getGitDirtyFileCount(cwd).catch(() => gitDirtyFileCount),
|
|
1570
1861
|
]);
|
|
1862
|
+
// A run may have opened/closed issues or PRs — refresh fire-and-forget so
|
|
1863
|
+
// teardown isn't delayed by the network. Broadcasts itself on change.
|
|
1864
|
+
void refreshGitHubCounts();
|
|
1571
1865
|
// Serialize behind any marker/tool-triggered refresh so the terminal
|
|
1572
1866
|
// progress snapshot uses the live plan file. Once every canonical step
|
|
1573
1867
|
// is complete, remove the approved plan from future system prompts and
|
|
@@ -1584,6 +1878,7 @@ async function createSession(deps, opts) {
|
|
|
1584
1878
|
catch (error) {
|
|
1585
1879
|
// Keep tracking when prompt cleanup fails; hiding the widget here
|
|
1586
1880
|
// would claim completion while the approved-plan contract remained.
|
|
1881
|
+
captureSidecarError(error, "app-sidecar.plan.cleanup");
|
|
1587
1882
|
log("WARN", "app-sidecar", "completed plan cleanup failed", {
|
|
1588
1883
|
message: error instanceof Error ? error.message : String(error),
|
|
1589
1884
|
});
|
|
@@ -2039,13 +2334,22 @@ async function createSession(deps, opts) {
|
|
|
2039
2334
|
gitPoll.unref?.();
|
|
2040
2335
|
};
|
|
2041
2336
|
scheduleGitPoll(5000);
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2337
|
+
// GitHub issue/PR counts change outside the app (web UI, teammates), so poll
|
|
2338
|
+
// on a slow cadence. Network-bound, so keep it well under the search API's
|
|
2339
|
+
// rate budget (2 calls per tick). No-op when the origin isn't a GitHub repo.
|
|
2340
|
+
let gitHubPoll;
|
|
2341
|
+
let gitHubPollStopped = false;
|
|
2342
|
+
const scheduleGitHubPoll = (delay) => {
|
|
2343
|
+
if (gitHubPollStopped)
|
|
2344
|
+
return;
|
|
2345
|
+
gitHubPoll = setTimeout(() => {
|
|
2346
|
+
void refreshGitHubCounts().finally(() => scheduleGitHubPoll(60_000));
|
|
2347
|
+
}, delay);
|
|
2348
|
+
gitHubPoll.unref?.();
|
|
2349
|
+
};
|
|
2350
|
+
scheduleGitHubPoll(2000);
|
|
2351
|
+
function readBody(req, res) {
|
|
2352
|
+
return readCappedBody(req, res);
|
|
2049
2353
|
}
|
|
2050
2354
|
function json(res, status, body) {
|
|
2051
2355
|
const payload = JSON.stringify(redactValue(body, { secrets: environmentSecrets(process.env) }));
|
|
@@ -2083,7 +2387,10 @@ async function createSession(deps, opts) {
|
|
|
2083
2387
|
void memoryStore
|
|
2084
2388
|
.snapshot()
|
|
2085
2389
|
.then((snapshot) => json(res, 200, snapshot))
|
|
2086
|
-
.catch((error) =>
|
|
2390
|
+
.catch((error) => {
|
|
2391
|
+
captureSidecarError(error, "app-sidecar.memory.snapshot");
|
|
2392
|
+
json(res, 500, { error: error instanceof Error ? error.message : String(error) });
|
|
2393
|
+
});
|
|
2087
2394
|
return;
|
|
2088
2395
|
}
|
|
2089
2396
|
if (method === "DELETE" && url.startsWith("/memories/")) {
|
|
@@ -2096,14 +2403,20 @@ async function createSession(deps, opts) {
|
|
|
2096
2403
|
.forget(id)
|
|
2097
2404
|
.then(() => memoryStore.snapshot())
|
|
2098
2405
|
.then((snapshot) => json(res, 200, snapshot))
|
|
2099
|
-
.catch((error) =>
|
|
2406
|
+
.catch((error) => {
|
|
2407
|
+
captureSidecarError(error, "app-sidecar.memory.forget");
|
|
2408
|
+
json(res, 500, { error: error instanceof Error ? error.message : String(error) });
|
|
2409
|
+
});
|
|
2100
2410
|
return;
|
|
2101
2411
|
}
|
|
2102
2412
|
if (method === "GET" && url === "/jiwa") {
|
|
2103
2413
|
void jiwaStore
|
|
2104
2414
|
.snapshot()
|
|
2105
2415
|
.then((snapshot) => json(res, 200, snapshot))
|
|
2106
|
-
.catch((error) =>
|
|
2416
|
+
.catch((error) => {
|
|
2417
|
+
captureSidecarError(error, "app-sidecar.jiwa.snapshot");
|
|
2418
|
+
json(res, 500, { error: error instanceof Error ? error.message : String(error) });
|
|
2419
|
+
});
|
|
2107
2420
|
return;
|
|
2108
2421
|
}
|
|
2109
2422
|
if (method === "DELETE" && url.startsWith("/jiwa/")) {
|
|
@@ -2116,7 +2429,10 @@ async function createSession(deps, opts) {
|
|
|
2116
2429
|
.forget(id)
|
|
2117
2430
|
.then(() => jiwaStore.snapshot())
|
|
2118
2431
|
.then((snapshot) => json(res, 200, snapshot))
|
|
2119
|
-
.catch((error) =>
|
|
2432
|
+
.catch((error) => {
|
|
2433
|
+
captureSidecarError(error, "app-sidecar.jiwa.forget");
|
|
2434
|
+
json(res, 500, { error: error instanceof Error ? error.message : String(error) });
|
|
2435
|
+
});
|
|
2120
2436
|
return;
|
|
2121
2437
|
}
|
|
2122
2438
|
if (method === "GET" && (url === "/events" || url.startsWith("/events?"))) {
|
|
@@ -2174,7 +2490,9 @@ async function createSession(deps, opts) {
|
|
|
2174
2490
|
return;
|
|
2175
2491
|
}
|
|
2176
2492
|
if (method === "POST" && url === "/settings") {
|
|
2177
|
-
void readBody(req).then(async (raw) => {
|
|
2493
|
+
void readBody(req, res).then(async (raw) => {
|
|
2494
|
+
if (raw === null)
|
|
2495
|
+
return;
|
|
2178
2496
|
let projectsRoot;
|
|
2179
2497
|
try {
|
|
2180
2498
|
projectsRoot = JSON.parse(raw).projectsRoot ?? "";
|
|
@@ -2197,7 +2515,9 @@ async function createSession(deps, opts) {
|
|
|
2197
2515
|
return;
|
|
2198
2516
|
}
|
|
2199
2517
|
if (method === "POST" && url === "/create-project") {
|
|
2200
|
-
void readBody(req).then(async (raw) => {
|
|
2518
|
+
void readBody(req, res).then(async (raw) => {
|
|
2519
|
+
if (raw === null)
|
|
2520
|
+
return;
|
|
2201
2521
|
let name;
|
|
2202
2522
|
try {
|
|
2203
2523
|
name = JSON.parse(raw).name ?? "";
|
|
@@ -2229,6 +2549,7 @@ async function createSession(deps, opts) {
|
|
|
2229
2549
|
json(res, 200, { path: dir });
|
|
2230
2550
|
}
|
|
2231
2551
|
catch (err) {
|
|
2552
|
+
captureSidecarError(err, "app-sidecar.project.create");
|
|
2232
2553
|
json(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
2233
2554
|
}
|
|
2234
2555
|
});
|
|
@@ -2239,6 +2560,7 @@ async function createSession(deps, opts) {
|
|
|
2239
2560
|
void discoverProjects()
|
|
2240
2561
|
.then((projects) => json(res, 200, { projects }))
|
|
2241
2562
|
.catch((err) => {
|
|
2563
|
+
captureSidecarError(err, "app-sidecar.projects.discover");
|
|
2242
2564
|
log("ERROR", "app-sidecar", "discoverProjects failed", {
|
|
2243
2565
|
message: err instanceof Error ? err.message : String(err),
|
|
2244
2566
|
});
|
|
@@ -2253,37 +2575,14 @@ async function createSession(deps, opts) {
|
|
|
2253
2575
|
return;
|
|
2254
2576
|
}
|
|
2255
2577
|
const requestedAgent = new URL(url, `http://${host}`).searchParams.get("chatAgent");
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
return agentSessions.map((item) => ({ ...item, chatAgent: agentId }));
|
|
2260
|
-
}))
|
|
2261
|
-
.then(async (groups) => {
|
|
2262
|
-
const dated = await Promise.all(groups.flat().map(async (item) => ({
|
|
2263
|
-
item,
|
|
2264
|
-
mtime: await fs
|
|
2265
|
-
.stat(item.path)
|
|
2266
|
-
.then((stat) => stat.mtimeMs)
|
|
2267
|
-
.catch(() => 0),
|
|
2268
|
-
})));
|
|
2269
|
-
const recent = dated
|
|
2270
|
-
.sort((left, right) => right.mtime - left.mtime)
|
|
2271
|
-
.slice(0, 5)
|
|
2272
|
-
.map(({ item }) => item);
|
|
2273
|
-
json(res, 200, { sessions: recent });
|
|
2274
|
-
})
|
|
2275
|
-
.catch(() => json(res, 200, { sessions: [] }));
|
|
2276
|
-
return;
|
|
2277
|
-
}
|
|
2278
|
-
// The picker may be served by a sidecar currently running in Chat mode.
|
|
2279
|
-
// An omitted chatAgent always means coding history; chat callers identify
|
|
2280
|
-
// their namespace explicitly (one agent or "all").
|
|
2281
|
-
const sessionsDir = requestedAgent
|
|
2282
|
-
? sessionsDirForChatAgent(paths.sessionsDir, requestedAgent)
|
|
2283
|
-
: paths.sessionsDir;
|
|
2284
|
-
void listRecentSessions(target, 5, sessionsDir)
|
|
2578
|
+
// An omitted chatAgent means coding history; chat callers identify one
|
|
2579
|
+
// agent or request the combined, recency-sorted "all" listing.
|
|
2580
|
+
void listSidecarSessions(target, requestedAgent, paths.sessionsDir)
|
|
2285
2581
|
.then((sessions) => json(res, 200, { sessions }))
|
|
2286
|
-
.catch(() =>
|
|
2582
|
+
.catch((error) => {
|
|
2583
|
+
captureSidecarError(error, "app-sidecar.sessions.list");
|
|
2584
|
+
json(res, 200, { sessions: [] });
|
|
2585
|
+
});
|
|
2287
2586
|
return;
|
|
2288
2587
|
}
|
|
2289
2588
|
if (method === "GET" && url.startsWith("/files")) {
|
|
@@ -2291,6 +2590,7 @@ async function createSession(deps, opts) {
|
|
|
2291
2590
|
void searchProjectFiles(cwd, q)
|
|
2292
2591
|
.then((files) => json(res, 200, { files }))
|
|
2293
2592
|
.catch((err) => {
|
|
2593
|
+
captureSidecarError(err, "app-sidecar.files.search");
|
|
2294
2594
|
log("ERROR", "app-sidecar", "searchProjectFiles failed", {
|
|
2295
2595
|
message: err instanceof Error ? err.message : String(err),
|
|
2296
2596
|
});
|
|
@@ -2298,6 +2598,30 @@ async function createSession(deps, opts) {
|
|
|
2298
2598
|
});
|
|
2299
2599
|
return;
|
|
2300
2600
|
}
|
|
2601
|
+
// Markdown transcript export for the app's download button. Serialized
|
|
2602
|
+
// here rather than in the webview because the webview's transcript model
|
|
2603
|
+
// deliberately keeps tool activity in the LiveToolPanel — exporting from
|
|
2604
|
+
// there would hand the user a coding session with the coding missing.
|
|
2605
|
+
// `?name=1` asks for the suggested filename only (the save dialog needs it
|
|
2606
|
+
// before there is a path), so the markdown never crosses IPC twice.
|
|
2607
|
+
if (method === "GET" && (url === "/export" || url.startsWith("/export?"))) {
|
|
2608
|
+
const query = new URLSearchParams(url.slice(url.indexOf("?") + 1));
|
|
2609
|
+
const st = session.getState();
|
|
2610
|
+
const filename = defaultExportFilename(mode);
|
|
2611
|
+
if (query.get("name") === "1") {
|
|
2612
|
+
json(res, 200, { filename });
|
|
2613
|
+
return;
|
|
2614
|
+
}
|
|
2615
|
+
const markdown = sessionToMarkdown({
|
|
2616
|
+
mode,
|
|
2617
|
+
cwd,
|
|
2618
|
+
provider: st.provider,
|
|
2619
|
+
model: st.model,
|
|
2620
|
+
...(st.sessionId ? { sessionId: st.sessionId } : {}),
|
|
2621
|
+
}, session.getMessages(), { toolDetail: parseToolDetail(query.get("tools")) });
|
|
2622
|
+
json(res, 200, { filename, markdown });
|
|
2623
|
+
return;
|
|
2624
|
+
}
|
|
2301
2625
|
if (method === "GET" && url === "/history") {
|
|
2302
2626
|
// Reconstruct the transcript from persisted messages so resume is 1:1 with
|
|
2303
2627
|
// the live SSE stream. Walks ALL message types (not just user/assistant):
|
|
@@ -2617,21 +2941,41 @@ async function createSession(deps, opts) {
|
|
|
2617
2941
|
description: c.description,
|
|
2618
2942
|
source: "built-in",
|
|
2619
2943
|
}));
|
|
2944
|
+
// Desktop-safe registry actions belong in the same picker as workflows.
|
|
2945
|
+
// Most registry commands have dedicated app controls or TUI-only flows;
|
|
2946
|
+
// multi-root management has no other affordance, so expose only these.
|
|
2947
|
+
const workspaceActions = [
|
|
2948
|
+
{
|
|
2949
|
+
name: "add-dir",
|
|
2950
|
+
aliases: ["adddir"],
|
|
2951
|
+
description: "Add another project folder to this workspace",
|
|
2952
|
+
source: "built-in",
|
|
2953
|
+
},
|
|
2954
|
+
{
|
|
2955
|
+
name: "remove-dir",
|
|
2956
|
+
aliases: ["removedir"],
|
|
2957
|
+
description: "Remove an added project folder from this workspace",
|
|
2958
|
+
source: "built-in",
|
|
2959
|
+
},
|
|
2960
|
+
];
|
|
2620
2961
|
const custom = (await loadCustomCommands(cwd))
|
|
2621
|
-
// A custom command can't shadow a built-in name.
|
|
2622
|
-
.filter((c) => !PROMPT_COMMANDS.some((b) => b.name === c.name)
|
|
2962
|
+
// A custom command can't shadow a built-in name or app action.
|
|
2963
|
+
.filter((c) => !PROMPT_COMMANDS.some((b) => b.name === c.name) &&
|
|
2964
|
+
!workspaceActions.some((action) => action.name === c.name))
|
|
2623
2965
|
.map((c) => ({
|
|
2624
2966
|
name: c.name,
|
|
2625
2967
|
aliases: [],
|
|
2626
2968
|
description: c.description,
|
|
2627
2969
|
source: "custom",
|
|
2628
2970
|
}));
|
|
2629
|
-
json(res, 200, { commands: [...builtins, ...custom] });
|
|
2971
|
+
json(res, 200, { commands: [...workspaceActions, ...builtins, ...custom] });
|
|
2630
2972
|
})();
|
|
2631
2973
|
return;
|
|
2632
2974
|
}
|
|
2633
2975
|
if (method === "POST" && url === "/prompt") {
|
|
2634
|
-
void readBody(req).then(async (raw) => {
|
|
2976
|
+
void readBody(req, res).then(async (raw) => {
|
|
2977
|
+
if (raw === null)
|
|
2978
|
+
return;
|
|
2635
2979
|
let text;
|
|
2636
2980
|
let attachments;
|
|
2637
2981
|
let meta;
|
|
@@ -2758,7 +3102,9 @@ async function createSession(deps, opts) {
|
|
|
2758
3102
|
json(res, 404, { error: "Nolan is not available in EZ Chat." });
|
|
2759
3103
|
return;
|
|
2760
3104
|
}
|
|
2761
|
-
void readBody(req).then(async (raw) => {
|
|
3105
|
+
void readBody(req, res).then(async (raw) => {
|
|
3106
|
+
if (raw === null)
|
|
3107
|
+
return;
|
|
2762
3108
|
let text;
|
|
2763
3109
|
try {
|
|
2764
3110
|
text = JSON.parse(raw).text ?? "";
|
|
@@ -2817,7 +3163,9 @@ async function createSession(deps, opts) {
|
|
|
2817
3163
|
json(res, 404, { error: "Autopilot is not available in EZ Chat." });
|
|
2818
3164
|
return;
|
|
2819
3165
|
}
|
|
2820
|
-
void readBody(req).then(async (raw) => {
|
|
3166
|
+
void readBody(req, res).then(async (raw) => {
|
|
3167
|
+
if (raw === null)
|
|
3168
|
+
return;
|
|
2821
3169
|
let enabled;
|
|
2822
3170
|
try {
|
|
2823
3171
|
enabled = Boolean(JSON.parse(raw).enabled);
|
|
@@ -2838,7 +3186,9 @@ async function createSession(deps, opts) {
|
|
|
2838
3186
|
return;
|
|
2839
3187
|
}
|
|
2840
3188
|
if (method === "POST" && url === "/enhance") {
|
|
2841
|
-
void readBody(req).then(async (raw) => {
|
|
3189
|
+
void readBody(req, res).then(async (raw) => {
|
|
3190
|
+
if (raw === null)
|
|
3191
|
+
return;
|
|
2842
3192
|
let text;
|
|
2843
3193
|
try {
|
|
2844
3194
|
text = JSON.parse(raw).text ?? "";
|
|
@@ -2859,6 +3209,7 @@ async function createSession(deps, opts) {
|
|
|
2859
3209
|
}
|
|
2860
3210
|
catch (err) {
|
|
2861
3211
|
const message = err instanceof Error ? err.message : String(err);
|
|
3212
|
+
captureSidecarError(err, "app-sidecar.prompt-enhancer");
|
|
2862
3213
|
log("ERROR", "app-sidecar", "enhance failed", { message });
|
|
2863
3214
|
json(res, 500, { error: message });
|
|
2864
3215
|
}
|
|
@@ -2886,7 +3237,9 @@ async function createSession(deps, opts) {
|
|
|
2886
3237
|
return;
|
|
2887
3238
|
}
|
|
2888
3239
|
if (method === "POST" && url === "/radio/volume") {
|
|
2889
|
-
void readBody(req).then((raw) => {
|
|
3240
|
+
void readBody(req, res).then((raw) => {
|
|
3241
|
+
if (raw === null)
|
|
3242
|
+
return;
|
|
2890
3243
|
let volume;
|
|
2891
3244
|
try {
|
|
2892
3245
|
volume = Number(JSON.parse(raw).volume);
|
|
@@ -2909,7 +3262,9 @@ async function createSession(deps, opts) {
|
|
|
2909
3262
|
return;
|
|
2910
3263
|
}
|
|
2911
3264
|
if (method === "POST" && url === "/radio") {
|
|
2912
|
-
void readBody(req).then((raw) => {
|
|
3265
|
+
void readBody(req, res).then((raw) => {
|
|
3266
|
+
if (raw === null)
|
|
3267
|
+
return;
|
|
2913
3268
|
let station;
|
|
2914
3269
|
try {
|
|
2915
3270
|
station = JSON.parse(raw).station ?? "";
|
|
@@ -2933,7 +3288,9 @@ async function createSession(deps, opts) {
|
|
|
2933
3288
|
return;
|
|
2934
3289
|
}
|
|
2935
3290
|
if (method === "POST" && url === "/tasks/run") {
|
|
2936
|
-
void readBody(req).then((raw) => {
|
|
3291
|
+
void readBody(req, res).then((raw) => {
|
|
3292
|
+
if (raw === null)
|
|
3293
|
+
return;
|
|
2937
3294
|
let id;
|
|
2938
3295
|
let all;
|
|
2939
3296
|
try {
|
|
@@ -2955,7 +3312,9 @@ async function createSession(deps, opts) {
|
|
|
2955
3312
|
return;
|
|
2956
3313
|
}
|
|
2957
3314
|
if (method === "POST" && url === "/tasks/add") {
|
|
2958
|
-
void readBody(req).then((raw) => {
|
|
3315
|
+
void readBody(req, res).then((raw) => {
|
|
3316
|
+
if (raw === null)
|
|
3317
|
+
return;
|
|
2959
3318
|
let title;
|
|
2960
3319
|
let prompt;
|
|
2961
3320
|
try {
|
|
@@ -2987,7 +3346,9 @@ async function createSession(deps, opts) {
|
|
|
2987
3346
|
return;
|
|
2988
3347
|
}
|
|
2989
3348
|
if (method === "POST" && url === "/tasks/update") {
|
|
2990
|
-
void readBody(req).then((raw) => {
|
|
3349
|
+
void readBody(req, res).then((raw) => {
|
|
3350
|
+
if (raw === null)
|
|
3351
|
+
return;
|
|
2991
3352
|
let id;
|
|
2992
3353
|
let status;
|
|
2993
3354
|
let title;
|
|
@@ -3032,7 +3393,9 @@ async function createSession(deps, opts) {
|
|
|
3032
3393
|
return;
|
|
3033
3394
|
}
|
|
3034
3395
|
if (method === "POST" && url === "/tasks/delete") {
|
|
3035
|
-
void readBody(req).then((raw) => {
|
|
3396
|
+
void readBody(req, res).then((raw) => {
|
|
3397
|
+
if (raw === null)
|
|
3398
|
+
return;
|
|
3036
3399
|
let id;
|
|
3037
3400
|
try {
|
|
3038
3401
|
id = JSON.parse(raw).id ?? "";
|
|
@@ -3064,18 +3427,38 @@ async function createSession(deps, opts) {
|
|
|
3064
3427
|
loggedIn.push(p);
|
|
3065
3428
|
}
|
|
3066
3429
|
// Just the names, grouped by provider in registry order — the UI shows
|
|
3067
|
-
// a clean multi-column list of model ids.
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
provider
|
|
3072
|
-
|
|
3430
|
+
// a clean multi-column list of model ids. Local models come from the
|
|
3431
|
+
// runtime registry (populated by the background scan) and are always
|
|
3432
|
+
// listed: their "login" is the endpoint answering a probe.
|
|
3433
|
+
const models = getAllModels()
|
|
3434
|
+
.filter((m) => m.provider === "local" || loggedIn.includes(m.provider))
|
|
3435
|
+
.map((m) => {
|
|
3436
|
+
if (m.provider !== "local") {
|
|
3437
|
+
return { id: m.id, name: m.name, provider: m.provider };
|
|
3438
|
+
}
|
|
3439
|
+
const probed = findProbedModel(localProbes, m.id);
|
|
3440
|
+
return {
|
|
3441
|
+
id: m.id,
|
|
3442
|
+
name: m.name,
|
|
3443
|
+
provider: m.provider,
|
|
3444
|
+
local: true,
|
|
3445
|
+
endpoint: probed?.endpoint.label ?? parseLocalModelId(m.id)?.endpointId,
|
|
3446
|
+
// A local model that can't call tools can't run the agent — the UI
|
|
3447
|
+
// renders it disabled rather than hiding it, so the user learns why.
|
|
3448
|
+
supportsTools: probed?.model.supportsTools ?? true,
|
|
3449
|
+
contextWindow: m.contextWindow,
|
|
3450
|
+
contextWindowKnown: probed?.model.contextWindowKnown ?? false,
|
|
3451
|
+
supportsThinking: m.supportsThinking,
|
|
3452
|
+
};
|
|
3453
|
+
});
|
|
3073
3454
|
json(res, 200, { models });
|
|
3074
3455
|
})();
|
|
3075
3456
|
return;
|
|
3076
3457
|
}
|
|
3077
3458
|
if (method === "POST" && url === "/model") {
|
|
3078
|
-
void readBody(req).then(async (raw) => {
|
|
3459
|
+
void readBody(req, res).then(async (raw) => {
|
|
3460
|
+
if (raw === null)
|
|
3461
|
+
return;
|
|
3079
3462
|
let modelId;
|
|
3080
3463
|
try {
|
|
3081
3464
|
modelId = JSON.parse(raw).model ?? "";
|
|
@@ -3096,6 +3479,13 @@ async function createSession(deps, opts) {
|
|
|
3096
3479
|
// Pick up any natively-written keys (desktop app) before resolving the
|
|
3097
3480
|
// new model's credentials, so switching to a just-added provider works.
|
|
3098
3481
|
await auth.reload();
|
|
3482
|
+
if (target.provider === "local") {
|
|
3483
|
+
const problem = await localModelBlocker(target.id);
|
|
3484
|
+
if (problem) {
|
|
3485
|
+
json(res, 409, { error: problem });
|
|
3486
|
+
return;
|
|
3487
|
+
}
|
|
3488
|
+
}
|
|
3099
3489
|
await session.switchModel(target.provider, target.id);
|
|
3100
3490
|
// Nolan follows EZ Coder's model only while un-pinned; a user-set Nolan
|
|
3101
3491
|
// override survives EZ Coder model switches untouched.
|
|
@@ -3147,7 +3537,9 @@ async function createSession(deps, opts) {
|
|
|
3147
3537
|
// to BOTH Nolan sessions (chat + autopilot reviewer); a switch landing while
|
|
3148
3538
|
// either is mid-run defers via the pending-model mechanics.
|
|
3149
3539
|
if (method === "POST" && url === "/ken/model") {
|
|
3150
|
-
void readBody(req).then(async (raw) => {
|
|
3540
|
+
void readBody(req, res).then(async (raw) => {
|
|
3541
|
+
if (raw === null)
|
|
3542
|
+
return;
|
|
3151
3543
|
let modelId;
|
|
3152
3544
|
try {
|
|
3153
3545
|
const parsed = JSON.parse(raw).model;
|
|
@@ -3191,7 +3583,9 @@ async function createSession(deps, opts) {
|
|
|
3191
3583
|
return;
|
|
3192
3584
|
}
|
|
3193
3585
|
if (method === "POST" && url === "/kill") {
|
|
3194
|
-
void readBody(req).then(async (raw) => {
|
|
3586
|
+
void readBody(req, res).then(async (raw) => {
|
|
3587
|
+
if (raw === null)
|
|
3588
|
+
return;
|
|
3195
3589
|
let id;
|
|
3196
3590
|
try {
|
|
3197
3591
|
id = JSON.parse(raw).id ?? "";
|
|
@@ -3269,6 +3663,7 @@ async function createSession(deps, opts) {
|
|
|
3269
3663
|
drained,
|
|
3270
3664
|
});
|
|
3271
3665
|
})().catch((error) => {
|
|
3666
|
+
captureSidecarError(error, "app-sidecar.run.cancel");
|
|
3272
3667
|
broadcast("cancel_failed", { error: "cancel_failed", runState: runLifecycle.state });
|
|
3273
3668
|
json(res, 500, {
|
|
3274
3669
|
error: "cancel_failed",
|
|
@@ -3296,6 +3691,7 @@ async function createSession(deps, opts) {
|
|
|
3296
3691
|
json(res, 200, { ok: true });
|
|
3297
3692
|
})
|
|
3298
3693
|
.catch((err) => {
|
|
3694
|
+
captureSidecarError(err, "app-sidecar.session.new");
|
|
3299
3695
|
json(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
3300
3696
|
});
|
|
3301
3697
|
return;
|
|
@@ -3310,7 +3706,9 @@ async function createSession(deps, opts) {
|
|
|
3310
3706
|
// 3. session_reset tells the webview to clear its transcript; it then runs
|
|
3311
3707
|
// the "implement it now" prompt in the clean session.
|
|
3312
3708
|
if (method === "POST" && url === "/plan/accept") {
|
|
3313
|
-
void readBody(req).then(async (raw) => {
|
|
3709
|
+
void readBody(req, res).then(async (raw) => {
|
|
3710
|
+
if (raw === null)
|
|
3711
|
+
return;
|
|
3314
3712
|
let planPath;
|
|
3315
3713
|
try {
|
|
3316
3714
|
planPath = JSON.parse(raw).planPath || undefined;
|
|
@@ -3350,6 +3748,7 @@ async function createSession(deps, opts) {
|
|
|
3350
3748
|
json(res, 200, { ok: true, planTotal });
|
|
3351
3749
|
}
|
|
3352
3750
|
catch (err) {
|
|
3751
|
+
captureSidecarError(err, "app-sidecar.plan.accept");
|
|
3353
3752
|
json(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
3354
3753
|
}
|
|
3355
3754
|
});
|
|
@@ -3361,7 +3760,9 @@ async function createSession(deps, opts) {
|
|
|
3361
3760
|
return;
|
|
3362
3761
|
}
|
|
3363
3762
|
if (method === "POST" && url === "/auth/apikey") {
|
|
3364
|
-
void readBody(req).then(async (raw) => {
|
|
3763
|
+
void readBody(req, res).then(async (raw) => {
|
|
3764
|
+
if (raw === null)
|
|
3765
|
+
return;
|
|
3365
3766
|
let provider = "";
|
|
3366
3767
|
let key;
|
|
3367
3768
|
let variant;
|
|
@@ -3404,7 +3805,9 @@ async function createSession(deps, opts) {
|
|
|
3404
3805
|
return;
|
|
3405
3806
|
}
|
|
3406
3807
|
if (method === "POST" && url === "/auth/oauth/start") {
|
|
3407
|
-
void readBody(req).then((raw) => {
|
|
3808
|
+
void readBody(req, res).then((raw) => {
|
|
3809
|
+
if (raw === null)
|
|
3810
|
+
return;
|
|
3408
3811
|
let provider = "";
|
|
3409
3812
|
try {
|
|
3410
3813
|
provider = JSON.parse(raw).provider ?? "";
|
|
@@ -3446,6 +3849,7 @@ async function createSession(deps, opts) {
|
|
|
3446
3849
|
broadcast("auth_done", { provider });
|
|
3447
3850
|
}
|
|
3448
3851
|
catch (err) {
|
|
3852
|
+
captureSidecarError(err, "app-sidecar.auth.oauth", { provider });
|
|
3449
3853
|
broadcast("auth_error", {
|
|
3450
3854
|
provider,
|
|
3451
3855
|
message: err instanceof Error ? err.message : String(err),
|
|
@@ -3460,7 +3864,9 @@ async function createSession(deps, opts) {
|
|
|
3460
3864
|
return;
|
|
3461
3865
|
}
|
|
3462
3866
|
if (method === "POST" && url === "/auth/oauth/code") {
|
|
3463
|
-
void readBody(req).then((raw) => {
|
|
3867
|
+
void readBody(req, res).then((raw) => {
|
|
3868
|
+
if (raw === null)
|
|
3869
|
+
return;
|
|
3464
3870
|
let code;
|
|
3465
3871
|
try {
|
|
3466
3872
|
code = JSON.parse(raw).code ?? "";
|
|
@@ -3480,7 +3886,9 @@ async function createSession(deps, opts) {
|
|
|
3480
3886
|
return;
|
|
3481
3887
|
}
|
|
3482
3888
|
if (method === "POST" && url === "/auth/logout") {
|
|
3483
|
-
void readBody(req).then(async (raw) => {
|
|
3889
|
+
void readBody(req, res).then(async (raw) => {
|
|
3890
|
+
if (raw === null)
|
|
3891
|
+
return;
|
|
3484
3892
|
let provider;
|
|
3485
3893
|
try {
|
|
3486
3894
|
provider = JSON.parse(raw).provider ?? "";
|
|
@@ -3503,6 +3911,84 @@ async function createSession(deps, opts) {
|
|
|
3503
3911
|
});
|
|
3504
3912
|
return;
|
|
3505
3913
|
}
|
|
3914
|
+
// ── Local models ──────────────────────────────────────
|
|
3915
|
+
// GET returns the last scan (cheap, no probing) so opening the modal is
|
|
3916
|
+
// instant; POST /local/scan is the explicit refresh.
|
|
3917
|
+
if (method === "GET" && url === "/local") {
|
|
3918
|
+
json(res, 200, localStatePayload());
|
|
3919
|
+
return;
|
|
3920
|
+
}
|
|
3921
|
+
if (method === "POST" && url === "/local/scan") {
|
|
3922
|
+
void scanLocalModels(true)
|
|
3923
|
+
.then(() => {
|
|
3924
|
+
broadcast("models_change", { local: localStatePayload() });
|
|
3925
|
+
json(res, 200, localStatePayload());
|
|
3926
|
+
})
|
|
3927
|
+
.catch((err) => {
|
|
3928
|
+
broadcastError("error", "local model scan failed", err);
|
|
3929
|
+
json(res, 500, { error: "Local model scan failed — see the sidecar log." });
|
|
3930
|
+
});
|
|
3931
|
+
return;
|
|
3932
|
+
}
|
|
3933
|
+
if (method === "POST" && url === "/local/endpoints") {
|
|
3934
|
+
void readBody(req, res).then(async (raw) => {
|
|
3935
|
+
if (raw === null)
|
|
3936
|
+
return;
|
|
3937
|
+
let body;
|
|
3938
|
+
try {
|
|
3939
|
+
body = JSON.parse(raw);
|
|
3940
|
+
}
|
|
3941
|
+
catch {
|
|
3942
|
+
json(res, 400, { error: "invalid JSON body" });
|
|
3943
|
+
return;
|
|
3944
|
+
}
|
|
3945
|
+
try {
|
|
3946
|
+
const endpoint = await addCustomEndpoint({
|
|
3947
|
+
baseUrl: body.baseUrl ?? "",
|
|
3948
|
+
...(body.label ? { label: body.label } : {}),
|
|
3949
|
+
...(body.apiKey ? { apiKey: body.apiKey } : {}),
|
|
3950
|
+
});
|
|
3951
|
+
await scanLocalModels(true);
|
|
3952
|
+
broadcast("models_change", { local: localStatePayload() });
|
|
3953
|
+
json(res, 200, {
|
|
3954
|
+
endpoint: { id: endpoint.id, label: endpoint.label },
|
|
3955
|
+
...localStatePayload(),
|
|
3956
|
+
});
|
|
3957
|
+
}
|
|
3958
|
+
catch (err) {
|
|
3959
|
+
// Validation errors are the user's typo, not a system fault — 400 with
|
|
3960
|
+
// the exact reason, and nothing in the transcript.
|
|
3961
|
+
if (err instanceof LocalEndpointError) {
|
|
3962
|
+
json(res, 400, { error: err.message });
|
|
3963
|
+
return;
|
|
3964
|
+
}
|
|
3965
|
+
broadcastError("error", "add local endpoint failed", err);
|
|
3966
|
+
json(res, 500, { error: "Could not save the endpoint — see the sidecar log." });
|
|
3967
|
+
}
|
|
3968
|
+
});
|
|
3969
|
+
return;
|
|
3970
|
+
}
|
|
3971
|
+
if (method === "DELETE" && url.startsWith("/local/endpoints/")) {
|
|
3972
|
+
const id = decodeURIComponent(url.slice("/local/endpoints/".length));
|
|
3973
|
+
void (async () => {
|
|
3974
|
+
try {
|
|
3975
|
+
await removeCustomEndpoint(id);
|
|
3976
|
+
clearRuntimeModels((m) => m.provider === "local" && parseLocalModelId(m.id)?.endpointId === id);
|
|
3977
|
+
await scanLocalModels(true);
|
|
3978
|
+
broadcast("models_change", { local: localStatePayload() });
|
|
3979
|
+
json(res, 200, localStatePayload());
|
|
3980
|
+
}
|
|
3981
|
+
catch (err) {
|
|
3982
|
+
if (err instanceof LocalEndpointError) {
|
|
3983
|
+
json(res, 400, { error: err.message });
|
|
3984
|
+
return;
|
|
3985
|
+
}
|
|
3986
|
+
broadcastError("error", "remove local endpoint failed", err);
|
|
3987
|
+
json(res, 500, { error: "Could not remove the endpoint — see the sidecar log." });
|
|
3988
|
+
}
|
|
3989
|
+
})();
|
|
3990
|
+
return;
|
|
3991
|
+
}
|
|
3506
3992
|
// ── Telegram config (mirrors `ezcoder telegram`) ─────────
|
|
3507
3993
|
if (method === "GET" && url === "/telegram") {
|
|
3508
3994
|
void loadTelegramConfig().then((cfg) => {
|
|
@@ -3519,7 +4005,9 @@ async function createSession(deps, opts) {
|
|
|
3519
4005
|
return;
|
|
3520
4006
|
}
|
|
3521
4007
|
if (method === "POST" && url === "/telegram") {
|
|
3522
|
-
void readBody(req).then(async (raw) => {
|
|
4008
|
+
void readBody(req, res).then(async (raw) => {
|
|
4009
|
+
if (raw === null)
|
|
4010
|
+
return;
|
|
3523
4011
|
let botTokenInput;
|
|
3524
4012
|
let userIdInput;
|
|
3525
4013
|
try {
|
|
@@ -3587,6 +4075,7 @@ async function createSession(deps, opts) {
|
|
|
3587
4075
|
}
|
|
3588
4076
|
catch (err) {
|
|
3589
4077
|
serveController = null;
|
|
4078
|
+
captureSidecarError(err, "app-sidecar.serve.start", { provider: st.provider });
|
|
3590
4079
|
json(res, 400, { error: err instanceof Error ? err.message : String(err) });
|
|
3591
4080
|
}
|
|
3592
4081
|
})();
|
|
@@ -3613,6 +4102,7 @@ async function createSession(deps, opts) {
|
|
|
3613
4102
|
void buildMcpRows(targetCwd)
|
|
3614
4103
|
.then((servers) => json(res, 200, { servers }))
|
|
3615
4104
|
.catch((err) => {
|
|
4105
|
+
captureSidecarError(err, "app-sidecar.mcp.list");
|
|
3616
4106
|
log("ERROR", "app-sidecar", "buildMcpRows failed", {
|
|
3617
4107
|
message: err instanceof Error ? err.message : String(err),
|
|
3618
4108
|
});
|
|
@@ -3621,7 +4111,9 @@ async function createSession(deps, opts) {
|
|
|
3621
4111
|
return;
|
|
3622
4112
|
}
|
|
3623
4113
|
if (method === "POST" && url === "/mcp/add") {
|
|
3624
|
-
void readBody(req).then(async (raw) => {
|
|
4114
|
+
void readBody(req, res).then(async (raw) => {
|
|
4115
|
+
if (raw === null)
|
|
4116
|
+
return;
|
|
3625
4117
|
let line;
|
|
3626
4118
|
let scopeValue;
|
|
3627
4119
|
let bodyCwd;
|
|
@@ -3669,6 +4161,7 @@ async function createSession(deps, opts) {
|
|
|
3669
4161
|
});
|
|
3670
4162
|
}
|
|
3671
4163
|
catch (err) {
|
|
4164
|
+
captureSidecarError(err, "app-sidecar.mcp.add", { server: config.name });
|
|
3672
4165
|
json(res, 500, {
|
|
3673
4166
|
error: err instanceof Error ? err.message : String(err),
|
|
3674
4167
|
});
|
|
@@ -3677,7 +4170,9 @@ async function createSession(deps, opts) {
|
|
|
3677
4170
|
return;
|
|
3678
4171
|
}
|
|
3679
4172
|
if (method === "POST" && url === "/mcp/remove") {
|
|
3680
|
-
void readBody(req).then(async (raw) => {
|
|
4173
|
+
void readBody(req, res).then(async (raw) => {
|
|
4174
|
+
if (raw === null)
|
|
4175
|
+
return;
|
|
3681
4176
|
let name;
|
|
3682
4177
|
let scopeValue;
|
|
3683
4178
|
let bodyCwd;
|
|
@@ -3714,7 +4209,9 @@ async function createSession(deps, opts) {
|
|
|
3714
4209
|
// `mcp_auth_error`. Responds 202 immediately and runs the flow in the
|
|
3715
4210
|
// background (the browser round-trip can take a while).
|
|
3716
4211
|
if (method === "POST" && url === "/mcp/login") {
|
|
3717
|
-
void readBody(req).then(async (raw) => {
|
|
4212
|
+
void readBody(req, res).then(async (raw) => {
|
|
4213
|
+
if (raw === null)
|
|
4214
|
+
return;
|
|
3718
4215
|
let name;
|
|
3719
4216
|
let scopeValue;
|
|
3720
4217
|
let bodyCwd;
|
|
@@ -3758,6 +4255,7 @@ async function createSession(deps, opts) {
|
|
|
3758
4255
|
}
|
|
3759
4256
|
}
|
|
3760
4257
|
catch (err) {
|
|
4258
|
+
captureSidecarError(err, "app-sidecar.mcp.login", { server: name });
|
|
3761
4259
|
broadcast("mcp_auth_error", {
|
|
3762
4260
|
name,
|
|
3763
4261
|
message: err instanceof Error ? err.message : String(err),
|
|
@@ -3778,6 +4276,9 @@ async function createSession(deps, opts) {
|
|
|
3778
4276
|
gitPollStopped = true;
|
|
3779
4277
|
if (gitPoll)
|
|
3780
4278
|
clearTimeout(gitPoll);
|
|
4279
|
+
gitHubPollStopped = true;
|
|
4280
|
+
if (gitHubPoll)
|
|
4281
|
+
clearTimeout(gitHubPoll);
|
|
3781
4282
|
// Stop the Telegram serve loop + dispose its per-chat sessions.
|
|
3782
4283
|
if (serveController)
|
|
3783
4284
|
await serveController.stop().catch(() => { });
|
|
@@ -3804,7 +4305,9 @@ async function createSession(deps, opts) {
|
|
|
3804
4305
|
dispose,
|
|
3805
4306
|
};
|
|
3806
4307
|
}
|
|
3807
|
-
main().catch((err) => {
|
|
4308
|
+
main().catch(async (err) => {
|
|
4309
|
+
captureSidecarError(err, "app-sidecar.main", { severity: "fatal" });
|
|
4310
|
+
await flushSidecarErrors();
|
|
3808
4311
|
const message = err instanceof Error ? err.message : String(err);
|
|
3809
4312
|
process.stderr.write(`GG_APP_FATAL ${message}\n`);
|
|
3810
4313
|
process.exit(1);
|