synabun 1.3.0 → 2.0.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/CHANGELOG.md +205 -662
- package/LICENSE-COMMERCIAL.md +33 -0
- package/NOTICE +17 -0
- package/README.md +175 -853
- package/THIRD-PARTY-LICENSES.md +136 -0
- package/benchmarks/browser-live-results.json +79 -0
- package/benchmarks/browser-v2-results.json +258 -0
- package/benchmarks/fixtures/browser/README.md +57 -0
- package/benchmarks/fixtures/browser/page-state.json +132 -0
- package/benchmarks/fixtures/browser/social.json +92 -0
- package/benchmarks/fixtures/browser/target.json +319 -0
- package/benchmarks/fixtures/desktop/README.md +58 -0
- package/benchmarks/fixtures/desktop/abstention.json +59 -0
- package/benchmarks/fixtures/desktop/adversarial.json +22 -0
- package/benchmarks/fixtures/desktop/forbidden.json +93 -0
- package/benchmarks/fixtures/desktop/generate.mjs +1023 -0
- package/benchmarks/fixtures/desktop/target.json +136 -0
- package/bin/synabun.js +30 -0
- package/docs/whatsapp.md +325 -0
- package/hooks/claude-code/post-plan.mjs +613 -391
- package/hooks/claude-code/post-remember.mjs +246 -122
- package/hooks/claude-code/pre-compact.mjs +171 -110
- package/hooks/claude-code/pre-task.mjs +133 -0
- package/hooks/claude-code/pre-websearch.mjs +15 -4
- package/hooks/claude-code/prompt-origin.mjs +94 -0
- package/hooks/claude-code/prompt-submit.mjs +1244 -861
- package/hooks/claude-code/session-start.mjs +114 -156
- package/hooks/claude-code/shared.mjs +1228 -368
- package/hooks/claude-code/state.mjs +167 -0
- package/hooks/claude-code/stop.mjs +515 -140
- package/hooks/claude-code/subagent-start.mjs +154 -0
- package/hooks/claude-code/subagent-stop.mjs +102 -0
- package/hooks/claude-code/task-gate.mjs +150 -0
- package/launcher.mjs +40 -0
- package/lib/claude-hooks.js +137 -0
- package/lib/claude-usage-audit.js +141 -0
- package/lib/client-config-repair.js +145 -0
- package/lib/data-home-migration.js +502 -0
- package/lib/desktop-pwa.js +206 -0
- package/lib/external-tools.js +221 -0
- package/lib/first-launch-protection.js +245 -0
- package/lib/packaged-runtime.js +110 -0
- package/lib/paths.d.ts +21 -0
- package/lib/paths.js +103 -0
- package/lib/start-launcher.js +1309 -0
- package/mcp-server/dist/config.d.ts +22 -0
- package/mcp-server/dist/config.d.ts.map +1 -0
- package/mcp-server/dist/config.js +109 -0
- package/mcp-server/dist/config.js.map +1 -0
- package/mcp-server/dist/http.d.ts +30 -0
- package/mcp-server/dist/http.d.ts.map +1 -0
- package/mcp-server/dist/http.js +276 -0
- package/mcp-server/dist/http.js.map +1 -0
- package/mcp-server/dist/index.d.ts +48 -0
- package/mcp-server/dist/index.d.ts.map +1 -0
- package/mcp-server/dist/index.js +538 -0
- package/mcp-server/dist/index.js.map +1 -0
- package/mcp-server/dist/preload-http.d.ts +3 -0
- package/mcp-server/dist/preload-http.d.ts.map +1 -0
- package/mcp-server/dist/preload-http.js +28 -0
- package/mcp-server/dist/preload-http.js.map +1 -0
- package/mcp-server/dist/preload.d.ts +3 -0
- package/mcp-server/dist/preload.d.ts.map +1 -0
- package/mcp-server/dist/preload.js +32 -0
- package/mcp-server/dist/preload.js.map +1 -0
- package/mcp-server/dist/scripts/migrate-qdrant-to-sqlite.d.ts +19 -0
- package/mcp-server/dist/scripts/migrate-qdrant-to-sqlite.d.ts.map +1 -0
- package/mcp-server/dist/scripts/migrate-qdrant-to-sqlite.js +403 -0
- package/mcp-server/dist/scripts/migrate-qdrant-to-sqlite.js.map +1 -0
- package/mcp-server/dist/scripts/setup-embeddings.d.ts +3 -0
- package/mcp-server/dist/scripts/setup-embeddings.d.ts.map +1 -0
- package/mcp-server/dist/scripts/setup-embeddings.js +15 -0
- package/mcp-server/dist/scripts/setup-embeddings.js.map +1 -0
- package/mcp-server/dist/services/assistant-judgments.d.ts +79 -0
- package/mcp-server/dist/services/assistant-judgments.d.ts.map +1 -0
- package/mcp-server/dist/services/assistant-judgments.js +128 -0
- package/mcp-server/dist/services/assistant-judgments.js.map +1 -0
- package/mcp-server/dist/services/browser-assist-gate.d.ts +314 -0
- package/mcp-server/dist/services/browser-assist-gate.d.ts.map +1 -0
- package/mcp-server/dist/services/browser-assist-gate.js +344 -0
- package/mcp-server/dist/services/browser-assist-gate.js.map +1 -0
- package/mcp-server/dist/services/browser-assist.d.ts +104 -0
- package/mcp-server/dist/services/browser-assist.d.ts.map +1 -0
- package/mcp-server/dist/services/browser-assist.js +282 -0
- package/mcp-server/dist/services/browser-assist.js.map +1 -0
- package/mcp-server/dist/services/browser-bench.d.ts +772 -0
- package/mcp-server/dist/services/browser-bench.d.ts.map +1 -0
- package/mcp-server/dist/services/browser-bench.js +424 -0
- package/mcp-server/dist/services/browser-bench.js.map +1 -0
- package/mcp-server/dist/services/browser-judgments.d.ts +196 -0
- package/mcp-server/dist/services/browser-judgments.d.ts.map +1 -0
- package/mcp-server/dist/services/browser-judgments.js +363 -0
- package/mcp-server/dist/services/browser-judgments.js.map +1 -0
- package/mcp-server/dist/services/browser-risk.d.ts +166 -0
- package/mcp-server/dist/services/browser-risk.d.ts.map +1 -0
- package/mcp-server/dist/services/browser-risk.js +458 -0
- package/mcp-server/dist/services/browser-risk.js.map +1 -0
- package/mcp-server/dist/services/categories.d.ts +61 -0
- package/mcp-server/dist/services/categories.d.ts.map +1 -0
- package/mcp-server/dist/services/categories.js +377 -0
- package/mcp-server/dist/services/categories.js.map +1 -0
- package/mcp-server/dist/services/codex-config-heal.d.ts +49 -0
- package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -0
- package/mcp-server/dist/services/codex-config-heal.js +346 -0
- package/mcp-server/dist/services/codex-config-heal.js.map +1 -0
- package/mcp-server/dist/services/desktop-assist-gate.d.ts +198 -0
- package/mcp-server/dist/services/desktop-assist-gate.d.ts.map +1 -0
- package/mcp-server/dist/services/desktop-assist-gate.js +202 -0
- package/mcp-server/dist/services/desktop-assist-gate.js.map +1 -0
- package/mcp-server/dist/services/desktop-assist.d.ts +104 -0
- package/mcp-server/dist/services/desktop-assist.d.ts.map +1 -0
- package/mcp-server/dist/services/desktop-assist.js +266 -0
- package/mcp-server/dist/services/desktop-assist.js.map +1 -0
- package/mcp-server/dist/services/desktop-bench.d.ts +553 -0
- package/mcp-server/dist/services/desktop-bench.d.ts.map +1 -0
- package/mcp-server/dist/services/desktop-bench.js +311 -0
- package/mcp-server/dist/services/desktop-bench.js.map +1 -0
- package/mcp-server/dist/services/desktop-client.d.ts +23 -0
- package/mcp-server/dist/services/desktop-client.d.ts.map +1 -0
- package/mcp-server/dist/services/desktop-client.js +79 -0
- package/mcp-server/dist/services/desktop-client.js.map +1 -0
- package/mcp-server/dist/services/desktop-judgments.d.ts +125 -0
- package/mcp-server/dist/services/desktop-judgments.d.ts.map +1 -0
- package/mcp-server/dist/services/desktop-judgments.js +177 -0
- package/mcp-server/dist/services/desktop-judgments.js.map +1 -0
- package/mcp-server/dist/services/desktop-risk.d.ts +283 -0
- package/mcp-server/dist/services/desktop-risk.d.ts.map +1 -0
- package/mcp-server/dist/services/desktop-risk.js +605 -0
- package/mcp-server/dist/services/desktop-risk.js.map +1 -0
- package/mcp-server/dist/services/discord.d.ts +112 -0
- package/mcp-server/dist/services/discord.d.ts.map +1 -0
- package/mcp-server/dist/services/discord.js +477 -0
- package/mcp-server/dist/services/discord.js.map +1 -0
- package/mcp-server/dist/services/edit-stale.d.ts +81 -0
- package/mcp-server/dist/services/edit-stale.d.ts.map +1 -0
- package/mcp-server/dist/services/edit-stale.js +358 -0
- package/mcp-server/dist/services/edit-stale.js.map +1 -0
- package/mcp-server/dist/services/embedding-service.d.ts +16 -0
- package/mcp-server/dist/services/embedding-service.d.ts.map +1 -0
- package/mcp-server/dist/services/embedding-service.js +126 -0
- package/mcp-server/dist/services/embedding-service.js.map +1 -0
- package/mcp-server/dist/services/embedding-worker.d.ts +2 -0
- package/mcp-server/dist/services/embedding-worker.d.ts.map +1 -0
- package/mcp-server/dist/services/embedding-worker.js +52 -0
- package/mcp-server/dist/services/embedding-worker.js.map +1 -0
- package/mcp-server/dist/services/fb-regions.d.ts +71 -0
- package/mcp-server/dist/services/fb-regions.d.ts.map +1 -0
- package/mcp-server/dist/services/fb-regions.js +211 -0
- package/mcp-server/dist/services/fb-regions.js.map +1 -0
- package/mcp-server/dist/services/file-checksums.d.ts +22 -0
- package/mcp-server/dist/services/file-checksums.d.ts.map +1 -0
- package/mcp-server/dist/services/file-checksums.js +55 -0
- package/mcp-server/dist/services/file-checksums.js.map +1 -0
- package/mcp-server/dist/services/file-excerpt.d.ts +24 -0
- package/mcp-server/dist/services/file-excerpt.d.ts.map +1 -0
- package/mcp-server/dist/services/file-excerpt.js +65 -0
- package/mcp-server/dist/services/file-excerpt.js.map +1 -0
- package/mcp-server/dist/services/identity.d.ts +124 -0
- package/mcp-server/dist/services/identity.d.ts.map +1 -0
- package/mcp-server/dist/services/identity.js +241 -0
- package/mcp-server/dist/services/identity.js.map +1 -0
- package/mcp-server/dist/services/local-embeddings.d.ts +2 -0
- package/mcp-server/dist/services/local-embeddings.d.ts.map +1 -0
- package/mcp-server/dist/services/local-embeddings.js +3 -0
- package/mcp-server/dist/services/local-embeddings.js.map +1 -0
- package/mcp-server/dist/services/memory-client.d.ts +4 -0
- package/mcp-server/dist/services/memory-client.d.ts.map +1 -0
- package/mcp-server/dist/services/memory-client.js +27 -0
- package/mcp-server/dist/services/memory-client.js.map +1 -0
- package/mcp-server/dist/services/memory-judge-backfill.d.ts +135 -0
- package/mcp-server/dist/services/memory-judge-backfill.d.ts.map +1 -0
- package/mcp-server/dist/services/memory-judge-backfill.js +510 -0
- package/mcp-server/dist/services/memory-judge-backfill.js.map +1 -0
- package/mcp-server/dist/services/memory-judgments.d.ts +362 -0
- package/mcp-server/dist/services/memory-judgments.d.ts.map +1 -0
- package/mcp-server/dist/services/memory-judgments.js +674 -0
- package/mcp-server/dist/services/memory-judgments.js.map +1 -0
- package/mcp-server/dist/services/memory-maintenance.d.ts +108 -0
- package/mcp-server/dist/services/memory-maintenance.d.ts.map +1 -0
- package/mcp-server/dist/services/memory-maintenance.js +403 -0
- package/mcp-server/dist/services/memory-maintenance.js.map +1 -0
- package/mcp-server/dist/services/memory-retrieval.d.ts +66 -0
- package/mcp-server/dist/services/memory-retrieval.d.ts.map +1 -0
- package/mcp-server/dist/services/memory-retrieval.js +412 -0
- package/mcp-server/dist/services/memory-retrieval.js.map +1 -0
- package/mcp-server/dist/services/memory-schema.d.ts +16 -0
- package/mcp-server/dist/services/memory-schema.d.ts.map +1 -0
- package/mcp-server/dist/services/memory-schema.js +282 -0
- package/mcp-server/dist/services/memory-schema.js.map +1 -0
- package/mcp-server/dist/services/memory-sessions.d.ts +24 -0
- package/mcp-server/dist/services/memory-sessions.d.ts.map +1 -0
- package/mcp-server/dist/services/memory-sessions.js +110 -0
- package/mcp-server/dist/services/memory-sessions.js.map +1 -0
- package/mcp-server/dist/services/memory-triage.d.ts +60 -0
- package/mcp-server/dist/services/memory-triage.d.ts.map +1 -0
- package/mcp-server/dist/services/memory-triage.js +189 -0
- package/mcp-server/dist/services/memory-triage.js.map +1 -0
- package/mcp-server/dist/services/memory-verification.d.ts +24 -0
- package/mcp-server/dist/services/memory-verification.d.ts.map +1 -0
- package/mcp-server/dist/services/memory-verification.js +92 -0
- package/mcp-server/dist/services/memory-verification.js.map +1 -0
- package/mcp-server/dist/services/memory-writer.d.ts +26 -0
- package/mcp-server/dist/services/memory-writer.d.ts.map +1 -0
- package/mcp-server/dist/services/memory-writer.js +126 -0
- package/mcp-server/dist/services/memory-writer.js.map +1 -0
- package/mcp-server/dist/services/neural-interface.d.ts +439 -0
- package/mcp-server/dist/services/neural-interface.d.ts.map +1 -0
- package/mcp-server/dist/services/neural-interface.js +1135 -0
- package/mcp-server/dist/services/neural-interface.js.map +1 -0
- package/mcp-server/dist/services/profiles.d.ts +120 -0
- package/mcp-server/dist/services/profiles.d.ts.map +1 -0
- package/mcp-server/dist/services/profiles.js +387 -0
- package/mcp-server/dist/services/profiles.js.map +1 -0
- package/mcp-server/dist/services/secret-gate.d.ts +29 -0
- package/mcp-server/dist/services/secret-gate.d.ts.map +1 -0
- package/mcp-server/dist/services/secret-gate.js +43 -0
- package/mcp-server/dist/services/secret-gate.js.map +1 -0
- package/mcp-server/dist/services/server-instructions.d.ts +10 -0
- package/mcp-server/dist/services/server-instructions.d.ts.map +1 -0
- package/mcp-server/dist/services/server-instructions.js +101 -0
- package/mcp-server/dist/services/server-instructions.js.map +1 -0
- package/mcp-server/dist/services/sqlite.d.ts +235 -0
- package/mcp-server/dist/services/sqlite.d.ts.map +1 -0
- package/mcp-server/dist/services/sqlite.js +1185 -0
- package/mcp-server/dist/services/sqlite.js.map +1 -0
- package/mcp-server/dist/services/typesafe-bench.d.ts +99 -0
- package/mcp-server/dist/services/typesafe-bench.d.ts.map +1 -0
- package/mcp-server/dist/services/typesafe-bench.js +218 -0
- package/mcp-server/dist/services/typesafe-bench.js.map +1 -0
- package/mcp-server/dist/services/typesafe-config.d.ts +342 -0
- package/mcp-server/dist/services/typesafe-config.d.ts.map +1 -0
- package/mcp-server/dist/services/typesafe-config.js +746 -0
- package/mcp-server/dist/services/typesafe-config.js.map +1 -0
- package/mcp-server/dist/services/typesafe.d.ts +213 -0
- package/mcp-server/dist/services/typesafe.d.ts.map +1 -0
- package/mcp-server/dist/services/typesafe.js +330 -0
- package/mcp-server/dist/services/typesafe.js.map +1 -0
- package/mcp-server/dist/services/vector-cache.d.ts +68 -0
- package/mcp-server/dist/services/vector-cache.d.ts.map +1 -0
- package/mcp-server/dist/services/vector-cache.js +271 -0
- package/mcp-server/dist/services/vector-cache.js.map +1 -0
- package/mcp-server/dist/services/whiteboard-geometry.d.ts +305 -0
- package/mcp-server/dist/services/whiteboard-geometry.d.ts.map +1 -0
- package/mcp-server/dist/services/whiteboard-geometry.js +666 -0
- package/mcp-server/dist/services/whiteboard-geometry.js.map +1 -0
- package/mcp-server/dist/tools/agents.d.ts +212 -0
- package/mcp-server/dist/tools/agents.d.ts.map +1 -0
- package/mcp-server/dist/tools/agents.js +407 -0
- package/mcp-server/dist/tools/agents.js.map +1 -0
- package/mcp-server/dist/tools/bluesky.d.ts +39 -0
- package/mcp-server/dist/tools/bluesky.d.ts.map +1 -0
- package/mcp-server/dist/tools/bluesky.js +1144 -0
- package/mcp-server/dist/tools/bluesky.js.map +1 -0
- package/mcp-server/dist/tools/browser-advanced.d.ts +58 -0
- package/mcp-server/dist/tools/browser-advanced.d.ts.map +1 -0
- package/mcp-server/dist/tools/browser-advanced.js +106 -0
- package/mcp-server/dist/tools/browser-advanced.js.map +1 -0
- package/mcp-server/dist/tools/browser-batch.d.ts +99 -0
- package/mcp-server/dist/tools/browser-batch.d.ts.map +1 -0
- package/mcp-server/dist/tools/browser-batch.js +187 -0
- package/mcp-server/dist/tools/browser-batch.js.map +1 -0
- package/mcp-server/dist/tools/browser-cheatsheet.d.ts +21 -0
- package/mcp-server/dist/tools/browser-cheatsheet.d.ts.map +1 -0
- package/mcp-server/dist/tools/browser-cheatsheet.js +144 -0
- package/mcp-server/dist/tools/browser-cheatsheet.js.map +1 -0
- package/mcp-server/dist/tools/browser-interact.d.ts +239 -0
- package/mcp-server/dist/tools/browser-interact.d.ts.map +1 -0
- package/mcp-server/dist/tools/browser-interact.js +300 -0
- package/mcp-server/dist/tools/browser-interact.js.map +1 -0
- package/mcp-server/dist/tools/browser-navigate.d.ts +90 -0
- package/mcp-server/dist/tools/browser-navigate.d.ts.map +1 -0
- package/mcp-server/dist/tools/browser-navigate.js +142 -0
- package/mcp-server/dist/tools/browser-navigate.js.map +1 -0
- package/mcp-server/dist/tools/browser-observe.d.ts +561 -0
- package/mcp-server/dist/tools/browser-observe.d.ts.map +1 -0
- package/mcp-server/dist/tools/browser-observe.js +2009 -0
- package/mcp-server/dist/tools/browser-observe.js.map +1 -0
- package/mcp-server/dist/tools/browser.d.ts +34 -0
- package/mcp-server/dist/tools/browser.d.ts.map +1 -0
- package/mcp-server/dist/tools/browser.js +112 -0
- package/mcp-server/dist/tools/browser.js.map +1 -0
- package/mcp-server/dist/tools/card-tools.d.ts +82 -0
- package/mcp-server/dist/tools/card-tools.d.ts.map +1 -0
- package/mcp-server/dist/tools/card-tools.js +137 -0
- package/mcp-server/dist/tools/card-tools.js.map +1 -0
- package/mcp-server/dist/tools/card.d.ts +8 -0
- package/mcp-server/dist/tools/card.d.ts.map +1 -0
- package/mcp-server/dist/tools/card.js +16 -0
- package/mcp-server/dist/tools/card.js.map +1 -0
- package/mcp-server/dist/tools/category.d.ts +32 -0
- package/mcp-server/dist/tools/category.d.ts.map +1 -0
- package/mcp-server/dist/tools/category.js +290 -0
- package/mcp-server/dist/tools/category.js.map +1 -0
- package/mcp-server/dist/tools/choice.d.ts +160 -0
- package/mcp-server/dist/tools/choice.d.ts.map +1 -0
- package/mcp-server/dist/tools/choice.js +80 -0
- package/mcp-server/dist/tools/choice.js.map +1 -0
- package/mcp-server/dist/tools/computer.d.ts +71 -0
- package/mcp-server/dist/tools/computer.d.ts.map +1 -0
- package/mcp-server/dist/tools/computer.js +174 -0
- package/mcp-server/dist/tools/computer.js.map +1 -0
- package/mcp-server/dist/tools/discord-channel.d.ts +44 -0
- package/mcp-server/dist/tools/discord-channel.d.ts.map +1 -0
- package/mcp-server/dist/tools/discord-channel.js +236 -0
- package/mcp-server/dist/tools/discord-channel.js.map +1 -0
- package/mcp-server/dist/tools/discord-guild.d.ts +20 -0
- package/mcp-server/dist/tools/discord-guild.d.ts.map +1 -0
- package/mcp-server/dist/tools/discord-guild.js +138 -0
- package/mcp-server/dist/tools/discord-guild.js.map +1 -0
- package/mcp-server/dist/tools/discord-member.d.ts +26 -0
- package/mcp-server/dist/tools/discord-member.d.ts.map +1 -0
- package/mcp-server/dist/tools/discord-member.js +122 -0
- package/mcp-server/dist/tools/discord-member.js.map +1 -0
- package/mcp-server/dist/tools/discord-message.d.ts +36 -0
- package/mcp-server/dist/tools/discord-message.d.ts.map +1 -0
- package/mcp-server/dist/tools/discord-message.js +184 -0
- package/mcp-server/dist/tools/discord-message.js.map +1 -0
- package/mcp-server/dist/tools/discord-onboarding.d.ts +32 -0
- package/mcp-server/dist/tools/discord-onboarding.d.ts.map +1 -0
- package/mcp-server/dist/tools/discord-onboarding.js +179 -0
- package/mcp-server/dist/tools/discord-onboarding.js.map +1 -0
- package/mcp-server/dist/tools/discord-role.d.ts +34 -0
- package/mcp-server/dist/tools/discord-role.d.ts.map +1 -0
- package/mcp-server/dist/tools/discord-role.js +178 -0
- package/mcp-server/dist/tools/discord-role.js.map +1 -0
- package/mcp-server/dist/tools/discord-thread.d.ts +28 -0
- package/mcp-server/dist/tools/discord-thread.d.ts.map +1 -0
- package/mcp-server/dist/tools/discord-thread.js +88 -0
- package/mcp-server/dist/tools/discord-thread.js.map +1 -0
- package/mcp-server/dist/tools/discord-webhook.d.ts +32 -0
- package/mcp-server/dist/tools/discord-webhook.d.ts.map +1 -0
- package/mcp-server/dist/tools/discord-webhook.js +154 -0
- package/mcp-server/dist/tools/discord-webhook.js.map +1 -0
- package/mcp-server/dist/tools/discord.d.ts +7 -0
- package/mcp-server/dist/tools/discord.d.ts.map +1 -0
- package/mcp-server/dist/tools/discord.js +25 -0
- package/mcp-server/dist/tools/discord.js.map +1 -0
- package/mcp-server/dist/tools/fb-groups.d.ts +98 -0
- package/mcp-server/dist/tools/fb-groups.d.ts.map +1 -0
- package/mcp-server/dist/tools/fb-groups.js +369 -0
- package/mcp-server/dist/tools/fb-groups.js.map +1 -0
- package/mcp-server/dist/tools/forget.d.ts +14 -0
- package/mcp-server/dist/tools/forget.d.ts.map +1 -0
- package/mcp-server/dist/tools/forget.js +26 -0
- package/mcp-server/dist/tools/forget.js.map +1 -0
- package/mcp-server/dist/tools/git-tools.d.ts +24 -0
- package/mcp-server/dist/tools/git-tools.d.ts.map +1 -0
- package/mcp-server/dist/tools/git-tools.js +107 -0
- package/mcp-server/dist/tools/git-tools.js.map +1 -0
- package/mcp-server/dist/tools/git.d.ts +7 -0
- package/mcp-server/dist/tools/git.d.ts.map +1 -0
- package/mcp-server/dist/tools/git.js +11 -0
- package/mcp-server/dist/tools/git.js.map +1 -0
- package/mcp-server/dist/tools/gsc/experience.d.ts +135 -0
- package/mcp-server/dist/tools/gsc/experience.d.ts.map +1 -0
- package/mcp-server/dist/tools/gsc/experience.js +351 -0
- package/mcp-server/dist/tools/gsc/experience.js.map +1 -0
- package/mcp-server/dist/tools/gsc/helpers.d.ts +78 -0
- package/mcp-server/dist/tools/gsc/helpers.d.ts.map +1 -0
- package/mcp-server/dist/tools/gsc/helpers.js +258 -0
- package/mcp-server/dist/tools/gsc/helpers.js.map +1 -0
- package/mcp-server/dist/tools/gsc/indexing.d.ts +121 -0
- package/mcp-server/dist/tools/gsc/indexing.d.ts.map +1 -0
- package/mcp-server/dist/tools/gsc/indexing.js +357 -0
- package/mcp-server/dist/tools/gsc/indexing.js.map +1 -0
- package/mcp-server/dist/tools/gsc/inspect.d.ts +78 -0
- package/mcp-server/dist/tools/gsc/inspect.d.ts.map +1 -0
- package/mcp-server/dist/tools/gsc/inspect.js +321 -0
- package/mcp-server/dist/tools/gsc/inspect.js.map +1 -0
- package/mcp-server/dist/tools/gsc/navigate.d.ts +44 -0
- package/mcp-server/dist/tools/gsc/navigate.d.ts.map +1 -0
- package/mcp-server/dist/tools/gsc/navigate.js +197 -0
- package/mcp-server/dist/tools/gsc/navigate.js.map +1 -0
- package/mcp-server/dist/tools/gsc/performance.d.ts +84 -0
- package/mcp-server/dist/tools/gsc/performance.d.ts.map +1 -0
- package/mcp-server/dist/tools/gsc/performance.js +274 -0
- package/mcp-server/dist/tools/gsc/performance.js.map +1 -0
- package/mcp-server/dist/tools/gsc/settings.d.ts +153 -0
- package/mcp-server/dist/tools/gsc/settings.d.ts.map +1 -0
- package/mcp-server/dist/tools/gsc/settings.js +425 -0
- package/mcp-server/dist/tools/gsc/settings.js.map +1 -0
- package/mcp-server/dist/tools/gsc.d.ts +7 -0
- package/mcp-server/dist/tools/gsc.d.ts.map +1 -0
- package/mcp-server/dist/tools/gsc.js +51 -0
- package/mcp-server/dist/tools/gsc.js.map +1 -0
- package/mcp-server/dist/tools/image-tools.d.ts +24 -0
- package/mcp-server/dist/tools/image-tools.d.ts.map +1 -0
- package/mcp-server/dist/tools/image-tools.js +80 -0
- package/mcp-server/dist/tools/image-tools.js.map +1 -0
- package/mcp-server/dist/tools/image.d.ts +10 -0
- package/mcp-server/dist/tools/image.d.ts.map +1 -0
- package/mcp-server/dist/tools/image.js +14 -0
- package/mcp-server/dist/tools/image.js.map +1 -0
- package/mcp-server/dist/tools/leonardo-browser-tools.d.ts +97 -0
- package/mcp-server/dist/tools/leonardo-browser-tools.d.ts.map +1 -0
- package/mcp-server/dist/tools/leonardo-browser-tools.js +228 -0
- package/mcp-server/dist/tools/leonardo-browser-tools.js.map +1 -0
- package/mcp-server/dist/tools/leonardo.d.ts +13 -0
- package/mcp-server/dist/tools/leonardo.d.ts.map +1 -0
- package/mcp-server/dist/tools/leonardo.js +21 -0
- package/mcp-server/dist/tools/leonardo.js.map +1 -0
- package/mcp-server/dist/tools/loop.d.ts +30 -0
- package/mcp-server/dist/tools/loop.d.ts.map +1 -0
- package/mcp-server/dist/tools/loop.js +401 -0
- package/mcp-server/dist/tools/loop.js.map +1 -0
- package/mcp-server/dist/tools/memories.d.ts +56 -0
- package/mcp-server/dist/tools/memories.d.ts.map +1 -0
- package/mcp-server/dist/tools/memories.js +171 -0
- package/mcp-server/dist/tools/memories.js.map +1 -0
- package/mcp-server/dist/tools/morelogin.d.ts +24 -0
- package/mcp-server/dist/tools/morelogin.d.ts.map +1 -0
- package/mcp-server/dist/tools/morelogin.js +79 -0
- package/mcp-server/dist/tools/morelogin.js.map +1 -0
- package/mcp-server/dist/tools/profile.d.ts +17 -0
- package/mcp-server/dist/tools/profile.d.ts.map +1 -0
- package/mcp-server/dist/tools/profile.js +114 -0
- package/mcp-server/dist/tools/profile.js.map +1 -0
- package/mcp-server/dist/tools/recall.d.ts +71 -0
- package/mcp-server/dist/tools/recall.d.ts.map +1 -0
- package/mcp-server/dist/tools/recall.js +147 -0
- package/mcp-server/dist/tools/recall.js.map +1 -0
- package/mcp-server/dist/tools/reflect.d.ts +56 -0
- package/mcp-server/dist/tools/reflect.d.ts.map +1 -0
- package/mcp-server/dist/tools/reflect.js +157 -0
- package/mcp-server/dist/tools/reflect.js.map +1 -0
- package/mcp-server/dist/tools/remember.d.ts +62 -0
- package/mcp-server/dist/tools/remember.d.ts.map +1 -0
- package/mcp-server/dist/tools/remember.js +201 -0
- package/mcp-server/dist/tools/remember.js.map +1 -0
- package/mcp-server/dist/tools/response.d.ts +23 -0
- package/mcp-server/dist/tools/response.d.ts.map +1 -0
- package/mcp-server/dist/tools/response.js +24 -0
- package/mcp-server/dist/tools/response.js.map +1 -0
- package/mcp-server/dist/tools/restore.d.ts +14 -0
- package/mcp-server/dist/tools/restore.d.ts.map +1 -0
- package/mcp-server/dist/tools/restore.js +23 -0
- package/mcp-server/dist/tools/restore.js.map +1 -0
- package/mcp-server/dist/tools/style-guide-tools.d.ts +51 -0
- package/mcp-server/dist/tools/style-guide-tools.d.ts.map +1 -0
- package/mcp-server/dist/tools/style-guide-tools.js +231 -0
- package/mcp-server/dist/tools/style-guide-tools.js.map +1 -0
- package/mcp-server/dist/tools/style-guide.d.ts +8 -0
- package/mcp-server/dist/tools/style-guide.d.ts.map +1 -0
- package/mcp-server/dist/tools/style-guide.js +12 -0
- package/mcp-server/dist/tools/style-guide.js.map +1 -0
- package/mcp-server/dist/tools/sync.d.ts +20 -0
- package/mcp-server/dist/tools/sync.d.ts.map +1 -0
- package/mcp-server/dist/tools/sync.js +156 -0
- package/mcp-server/dist/tools/sync.js.map +1 -0
- package/mcp-server/dist/tools/tictactoe-tools.d.ts +18 -0
- package/mcp-server/dist/tools/tictactoe-tools.d.ts.map +1 -0
- package/mcp-server/dist/tools/tictactoe-tools.js +78 -0
- package/mcp-server/dist/tools/tictactoe-tools.js.map +1 -0
- package/mcp-server/dist/tools/tictactoe.d.ts +7 -0
- package/mcp-server/dist/tools/tictactoe.d.ts.map +1 -0
- package/mcp-server/dist/tools/tictactoe.js +11 -0
- package/mcp-server/dist/tools/tictactoe.js.map +1 -0
- package/mcp-server/dist/tools/utils.d.ts +19 -0
- package/mcp-server/dist/tools/utils.d.ts.map +1 -0
- package/mcp-server/dist/tools/utils.js +54 -0
- package/mcp-server/dist/tools/utils.js.map +1 -0
- package/mcp-server/dist/tools/whiteboard-format.d.ts +88 -0
- package/mcp-server/dist/tools/whiteboard-format.d.ts.map +1 -0
- package/mcp-server/dist/tools/whiteboard-format.js +337 -0
- package/mcp-server/dist/tools/whiteboard-format.js.map +1 -0
- package/mcp-server/dist/tools/whiteboard-tools.d.ts +326 -0
- package/mcp-server/dist/tools/whiteboard-tools.d.ts.map +1 -0
- package/mcp-server/dist/tools/whiteboard-tools.js +222 -0
- package/mcp-server/dist/tools/whiteboard-tools.js.map +1 -0
- package/mcp-server/dist/tools/whiteboard.d.ts +8 -0
- package/mcp-server/dist/tools/whiteboard.d.ts.map +1 -0
- package/mcp-server/dist/tools/whiteboard.js +16 -0
- package/mcp-server/dist/tools/whiteboard.js.map +1 -0
- package/mcp-server/dist/tools/youtube/analytics.d.ts +46 -0
- package/mcp-server/dist/tools/youtube/analytics.d.ts.map +1 -0
- package/mcp-server/dist/tools/youtube/analytics.js +83 -0
- package/mcp-server/dist/tools/youtube/analytics.js.map +1 -0
- package/mcp-server/dist/tools/youtube/dedup.d.ts +35 -0
- package/mcp-server/dist/tools/youtube/dedup.d.ts.map +1 -0
- package/mcp-server/dist/tools/youtube/dedup.js +56 -0
- package/mcp-server/dist/tools/youtube/dedup.js.map +1 -0
- package/mcp-server/dist/tools/youtube/download.d.ts +31 -0
- package/mcp-server/dist/tools/youtube/download.d.ts.map +1 -0
- package/mcp-server/dist/tools/youtube/download.js +30 -0
- package/mcp-server/dist/tools/youtube/download.js.map +1 -0
- package/mcp-server/dist/tools/youtube/helpers.d.ts +66 -0
- package/mcp-server/dist/tools/youtube/helpers.d.ts.map +1 -0
- package/mcp-server/dist/tools/youtube/helpers.js +143 -0
- package/mcp-server/dist/tools/youtube/helpers.js.map +1 -0
- package/mcp-server/dist/tools/youtube/media.d.ts +43 -0
- package/mcp-server/dist/tools/youtube/media.d.ts.map +1 -0
- package/mcp-server/dist/tools/youtube/media.js +97 -0
- package/mcp-server/dist/tools/youtube/media.js.map +1 -0
- package/mcp-server/dist/tools/youtube/metadata.d.ts +33 -0
- package/mcp-server/dist/tools/youtube/metadata.d.ts.map +1 -0
- package/mcp-server/dist/tools/youtube/metadata.js +106 -0
- package/mcp-server/dist/tools/youtube/metadata.js.map +1 -0
- package/mcp-server/dist/tools/youtube/navigate.d.ts +30 -0
- package/mcp-server/dist/tools/youtube/navigate.d.ts.map +1 -0
- package/mcp-server/dist/tools/youtube/navigate.js +71 -0
- package/mcp-server/dist/tools/youtube/navigate.js.map +1 -0
- package/mcp-server/dist/tools/youtube/playlist.d.ts +29 -0
- package/mcp-server/dist/tools/youtube/playlist.d.ts.map +1 -0
- package/mcp-server/dist/tools/youtube/playlist.js +128 -0
- package/mcp-server/dist/tools/youtube/playlist.js.map +1 -0
- package/mcp-server/dist/tools/youtube/schedule.d.ts +31 -0
- package/mcp-server/dist/tools/youtube/schedule.d.ts.map +1 -0
- package/mcp-server/dist/tools/youtube/schedule.js +147 -0
- package/mcp-server/dist/tools/youtube/schedule.js.map +1 -0
- package/mcp-server/dist/tools/youtube/source.d.ts +36 -0
- package/mcp-server/dist/tools/youtube/source.d.ts.map +1 -0
- package/mcp-server/dist/tools/youtube/source.js +85 -0
- package/mcp-server/dist/tools/youtube/source.js.map +1 -0
- package/mcp-server/dist/tools/youtube/upload.d.ts +53 -0
- package/mcp-server/dist/tools/youtube/upload.d.ts.map +1 -0
- package/mcp-server/dist/tools/youtube/upload.js +255 -0
- package/mcp-server/dist/tools/youtube/upload.js.map +1 -0
- package/mcp-server/dist/tools/youtube.d.ts +12 -0
- package/mcp-server/dist/tools/youtube.d.ts.map +1 -0
- package/mcp-server/dist/tools/youtube.js +38 -0
- package/mcp-server/dist/tools/youtube.js.map +1 -0
- package/mcp-server/dist/tui.d.ts +3 -0
- package/mcp-server/dist/tui.d.ts.map +1 -0
- package/mcp-server/dist/tui.js +806 -0
- package/mcp-server/dist/tui.js.map +1 -0
- package/mcp-server/dist/types.d.ts +77 -0
- package/mcp-server/dist/types.d.ts.map +1 -0
- package/mcp-server/dist/types.js +2 -0
- package/mcp-server/dist/types.js.map +1 -0
- package/mcp-server/package-lock.json +4798 -0
- package/mcp-server/package.json +55 -38
- package/mcp-server/run.mjs +12 -2
- package/mcp-server/src/config.ts +48 -3
- package/mcp-server/src/http.ts +296 -93
- package/mcp-server/src/index.ts +546 -203
- package/mcp-server/src/preload-http.ts +19 -18
- package/mcp-server/src/preload.ts +19 -18
- package/mcp-server/src/scripts/setup-embeddings.ts +9 -0
- package/mcp-server/src/services/assistant-judgments.ts +157 -0
- package/mcp-server/src/services/browser-assist-gate.ts +383 -0
- package/mcp-server/src/services/browser-assist.ts +267 -0
- package/mcp-server/src/services/browser-bench.ts +477 -0
- package/mcp-server/src/services/browser-judgments.ts +414 -0
- package/mcp-server/src/services/browser-risk.ts +484 -0
- package/mcp-server/src/services/categories.ts +36 -19
- package/mcp-server/src/services/codex-config-heal.ts +403 -0
- package/mcp-server/src/services/desktop-assist-gate.ts +251 -0
- package/mcp-server/src/services/desktop-assist.ts +308 -0
- package/mcp-server/src/services/desktop-bench.ts +372 -0
- package/mcp-server/src/services/desktop-client.ts +76 -0
- package/mcp-server/src/services/desktop-judgments.ts +218 -0
- package/mcp-server/src/services/desktop-risk.ts +743 -0
- package/mcp-server/src/services/edit-stale.ts +299 -0
- package/mcp-server/src/services/embedding-service.ts +108 -0
- package/mcp-server/src/services/embedding-worker.ts +46 -0
- package/mcp-server/src/services/fb-regions.ts +221 -0
- package/mcp-server/src/services/file-checksums.ts +26 -9
- package/mcp-server/src/services/file-excerpt.ts +59 -0
- package/mcp-server/src/services/identity.ts +291 -0
- package/mcp-server/src/services/local-embeddings.ts +2 -77
- package/mcp-server/src/services/memory-client.ts +23 -0
- package/mcp-server/src/services/memory-judge-backfill.ts +462 -0
- package/mcp-server/src/services/memory-judgments.ts +901 -0
- package/mcp-server/src/services/memory-maintenance.ts +360 -0
- package/mcp-server/src/services/memory-retrieval.ts +349 -0
- package/mcp-server/src/services/memory-schema.ts +280 -0
- package/mcp-server/src/services/memory-sessions.ts +87 -0
- package/mcp-server/src/services/memory-triage.ts +182 -0
- package/mcp-server/src/services/memory-verification.ts +68 -0
- package/mcp-server/src/services/memory-writer.ts +125 -0
- package/mcp-server/src/services/neural-interface.ts +1217 -73
- package/mcp-server/src/services/profiles.ts +428 -0
- package/mcp-server/src/services/secret-gate.ts +52 -0
- package/mcp-server/src/services/server-instructions.ts +122 -0
- package/mcp-server/src/services/sqlite.ts +1456 -823
- package/mcp-server/src/services/typesafe-bench.ts +247 -0
- package/mcp-server/src/services/typesafe-config.ts +790 -0
- package/mcp-server/src/services/typesafe.ts +390 -0
- package/mcp-server/src/services/vector-cache.ts +255 -0
- package/mcp-server/src/services/whiteboard-geometry.ts +711 -0
- package/mcp-server/src/tools/agents.ts +539 -0
- package/mcp-server/src/tools/bluesky.ts +1186 -0
- package/mcp-server/src/tools/browser-advanced.ts +15 -6
- package/mcp-server/src/tools/browser-batch.ts +178 -0
- package/mcp-server/src/tools/browser-cheatsheet.ts +155 -0
- package/mcp-server/src/tools/browser-interact.ts +362 -255
- package/mcp-server/src/tools/browser-navigate.ts +164 -77
- package/mcp-server/src/tools/browser-observe.ts +2222 -1330
- package/mcp-server/src/tools/browser.ts +181 -107
- package/mcp-server/src/tools/card.ts +7 -5
- package/mcp-server/src/tools/choice.ts +103 -0
- package/mcp-server/src/tools/computer.ts +190 -0
- package/mcp-server/src/tools/discord.ts +10 -23
- package/mcp-server/src/tools/fb-groups.ts +387 -0
- package/mcp-server/src/tools/forget.ts +1 -1
- package/mcp-server/src/tools/git.ts +3 -1
- package/mcp-server/src/tools/gsc/experience.ts +440 -0
- package/mcp-server/src/tools/gsc/helpers.ts +288 -0
- package/mcp-server/src/tools/gsc/indexing.ts +495 -0
- package/mcp-server/src/tools/gsc/inspect.ts +391 -0
- package/mcp-server/src/tools/gsc/navigate.ts +236 -0
- package/mcp-server/src/tools/gsc/performance.ts +352 -0
- package/mcp-server/src/tools/gsc/settings.ts +564 -0
- package/mcp-server/src/tools/gsc.ts +95 -0
- package/mcp-server/src/tools/image.ts +3 -1
- package/mcp-server/src/tools/leonardo-browser-tools.ts +47 -37
- package/mcp-server/src/tools/leonardo.ts +7 -5
- package/mcp-server/src/tools/loop.ts +118 -25
- package/mcp-server/src/tools/memories.ts +65 -5
- package/mcp-server/src/tools/morelogin.ts +81 -0
- package/mcp-server/src/tools/profile.ts +122 -0
- package/mcp-server/src/tools/recall.ts +105 -261
- package/mcp-server/src/tools/reflect.ts +171 -145
- package/mcp-server/src/tools/remember.ts +223 -115
- package/mcp-server/src/tools/style-guide-tools.ts +250 -0
- package/mcp-server/src/tools/style-guide.ts +16 -0
- package/mcp-server/src/tools/sync.ts +66 -9
- package/mcp-server/src/tools/tictactoe.ts +3 -1
- package/mcp-server/src/tools/utils.ts +22 -0
- package/mcp-server/src/tools/whiteboard-format.ts +394 -0
- package/mcp-server/src/tools/whiteboard-tools.ts +191 -183
- package/mcp-server/src/tools/whiteboard.ts +7 -5
- package/mcp-server/src/tools/youtube/analytics.ts +106 -0
- package/mcp-server/src/tools/youtube/dedup.ts +57 -0
- package/mcp-server/src/tools/youtube/download.ts +41 -0
- package/mcp-server/src/tools/youtube/helpers.ts +183 -0
- package/mcp-server/src/tools/youtube/media.ts +109 -0
- package/mcp-server/src/tools/youtube/metadata.ts +112 -0
- package/mcp-server/src/tools/youtube/navigate.ts +81 -0
- package/mcp-server/src/tools/youtube/playlist.ts +134 -0
- package/mcp-server/src/tools/youtube/schedule.ts +158 -0
- package/mcp-server/src/tools/youtube/source.ts +101 -0
- package/mcp-server/src/tools/youtube/upload.ts +296 -0
- package/mcp-server/src/tools/youtube.ts +47 -0
- package/mcp-server/src/tui.ts +8 -9
- package/neural-interface/games/TicTacToe/1.svg +1 -0
- package/neural-interface/games/TicTacToe/2.svg +1 -0
- package/neural-interface/games/TicTacToe/3.svg +1 -0
- package/neural-interface/games/TicTacToe/4.svg +1 -0
- package/neural-interface/games/TicTacToe/5.svg +1 -0
- package/neural-interface/games/TicTacToe/6.svg +1 -0
- package/neural-interface/games/TicTacToe/7.svg +1 -0
- package/neural-interface/games/TicTacToe/8.svg +1 -0
- package/neural-interface/games/TicTacToe/9.svg +1 -0
- package/neural-interface/games/TicTacToe/Board.svg +1 -0
- package/neural-interface/games/TicTacToe/Circle.svg +1 -0
- package/neural-interface/games/TicTacToe/Cross.svg +1 -0
- package/neural-interface/i18n/en.json +7022 -0
- package/neural-interface/i18n/pt-BR.json +7026 -0
- package/neural-interface/lib/assistant-api.js +1005 -0
- package/neural-interface/lib/assistant-attachments.js +121 -0
- package/neural-interface/lib/assistant-brains/claude.js +207 -0
- package/neural-interface/lib/assistant-brains/codex-browser-policy-hook.mjs +43 -0
- package/neural-interface/lib/assistant-brains/codex-route-gate-hook.mjs +56 -0
- package/neural-interface/lib/assistant-brains/codex.js +213 -0
- package/neural-interface/lib/assistant-brains/opencode-browser-policy.js +47 -0
- package/neural-interface/lib/assistant-brains/opencode-route-gate.js +62 -0
- package/neural-interface/lib/assistant-brains/opencode.js +320 -0
- package/neural-interface/lib/assistant-budget.js +318 -0
- package/neural-interface/lib/assistant-catalog.js +731 -0
- package/neural-interface/lib/assistant-clarify.js +664 -0
- package/neural-interface/lib/assistant-config.js +232 -0
- package/neural-interface/lib/assistant-dispatch.js +2744 -0
- package/neural-interface/lib/assistant-envelope.js +582 -0
- package/neural-interface/lib/assistant-evidence.js +236 -0
- package/neural-interface/lib/assistant-hidden-models.js +272 -0
- package/neural-interface/lib/assistant-jev.js +117 -0
- package/neural-interface/lib/assistant-judgments.js +111 -0
- package/neural-interface/lib/assistant-media.js +335 -0
- package/neural-interface/lib/assistant-memory.js +398 -0
- package/neural-interface/lib/assistant-persona.js +444 -0
- package/neural-interface/lib/assistant-plan-permissions.js +448 -0
- package/neural-interface/lib/assistant-playbooks/design.md +87 -0
- package/neural-interface/lib/assistant-playbooks.js +72 -0
- package/neural-interface/lib/assistant-pricing.js +159 -0
- package/neural-interface/lib/assistant-route-gate-codex.js +163 -0
- package/neural-interface/lib/assistant-route-gate.js +212 -0
- package/neural-interface/lib/assistant-router.js +1324 -0
- package/neural-interface/lib/assistant-runtime.js +3337 -0
- package/neural-interface/lib/assistant-task-prompt.js +472 -0
- package/neural-interface/lib/assistant-usage.js +1688 -0
- package/neural-interface/lib/augmented-path.js +70 -0
- package/neural-interface/lib/automation-storage-migration.js +54 -0
- package/neural-interface/lib/backup-service.js +851 -0
- package/neural-interface/lib/backup-zip-reader.js +262 -0
- package/neural-interface/lib/browser-assist-api.js +41 -0
- package/neural-interface/lib/browser-console.js +176 -0
- package/neural-interface/lib/browser-execution.js +241 -0
- package/neural-interface/lib/browser-extraction.js +191 -0
- package/neural-interface/lib/browser-language.js +83 -0
- package/neural-interface/lib/browser-screenshot.js +399 -0
- package/neural-interface/lib/browser-semantic-context.js +639 -0
- package/neural-interface/lib/browser-snapshots.js +192 -0
- package/neural-interface/lib/browser-tab-recovery.js +119 -0
- package/neural-interface/lib/browser-targets.js +70 -0
- package/neural-interface/lib/browser-tool-policy.js +926 -0
- package/neural-interface/lib/claude-accounts.js +230 -0
- package/neural-interface/lib/claude-agent-bridge.js +3203 -0
- package/neural-interface/lib/claude-bypass-policy.js +99 -0
- package/neural-interface/lib/claude-executable.js +241 -0
- package/neural-interface/lib/claude-history.js +429 -0
- package/neural-interface/lib/claude-model-catalog.js +444 -0
- package/neural-interface/lib/claude-panel-requests.js +165 -0
- package/neural-interface/lib/claude-panel-session.js +272 -0
- package/neural-interface/lib/claude-permission-rules.js +153 -0
- package/neural-interface/lib/claude-session-cleanup.js +71 -0
- package/neural-interface/lib/claude-session-list.js +82 -0
- package/neural-interface/lib/claude-session-ops.js +140 -0
- package/neural-interface/lib/claude-settings-view.js +51 -0
- package/neural-interface/lib/claude-skin-connect.js +79 -0
- package/neural-interface/lib/claude-stall-policy.js +7 -0
- package/neural-interface/lib/claude-temporary.js +435 -0
- package/neural-interface/lib/cli-update-planner.js +312 -0
- package/neural-interface/lib/codex-capabilities.d.ts +41 -0
- package/neural-interface/lib/codex-capabilities.js +148 -0
- package/neural-interface/lib/codex-collaboration-mode.js +17 -0
- package/neural-interface/lib/codex-compat-rpc.js +241 -0
- package/neural-interface/lib/codex-mcp-approvals.js +56 -0
- package/neural-interface/lib/codex-mcp-apps.js +47 -0
- package/neural-interface/lib/codex-model-context.js +202 -0
- package/neural-interface/lib/codex-plan-store.js +71 -0
- package/neural-interface/lib/codex-request-journal.js +242 -0
- package/neural-interface/lib/codex-runtime-path.js +213 -0
- package/neural-interface/lib/codex-session-catalog.js +231 -0
- package/neural-interface/lib/codex-session-settings.js +46 -0
- package/neural-interface/lib/codex-sidepanel-permissions.js +110 -0
- package/neural-interface/lib/codex-thread-history.js +165 -0
- package/neural-interface/lib/codex-user-verification.js +34 -0
- package/neural-interface/lib/codex-writer-lifecycle.js +152 -0
- package/neural-interface/lib/confined-fs.js +256 -0
- package/neural-interface/lib/db.js +922 -0
- package/neural-interface/lib/design-md.js +9 -0
- package/neural-interface/lib/desktop/api.js +92 -0
- package/neural-interface/lib/desktop/audit.js +62 -0
- package/neural-interface/lib/desktop/build.js +423 -0
- package/neural-interface/lib/desktop/config.js +606 -0
- package/neural-interface/lib/desktop/coords.js +76 -0
- package/neural-interface/lib/desktop/fake-helper.js +1192 -0
- package/neural-interface/lib/desktop/frames.js +77 -0
- package/neural-interface/lib/desktop/grants.js +84 -0
- package/neural-interface/lib/desktop/guards.js +170 -0
- package/neural-interface/lib/desktop/helper/SynabunDesktop.swift +3056 -0
- package/neural-interface/lib/desktop/lease.js +51 -0
- package/neural-interface/lib/desktop/manager.js +545 -0
- package/neural-interface/lib/desktop/press-contexts.js +103 -0
- package/neural-interface/lib/desktop/protocol.js +495 -0
- package/neural-interface/lib/desktop/service.js +1202 -0
- package/neural-interface/lib/effort-levels.js +148 -0
- package/neural-interface/lib/event-loop-monitor.js +67 -0
- package/neural-interface/lib/exec-loop-command.js +209 -0
- package/neural-interface/lib/folder-picker.ps1 +77 -0
- package/neural-interface/lib/http-guards.js +287 -0
- package/neural-interface/lib/jsonl-lines.js +94 -0
- package/neural-interface/lib/loop-logger.js +127 -0
- package/neural-interface/lib/mcp-client-config.js +133 -0
- package/neural-interface/lib/mcp-installer.js +487 -0
- package/neural-interface/lib/mcp-profile-refresh-coordinator.js +78 -0
- package/neural-interface/lib/mcp-profile-servers.js +120 -0
- package/neural-interface/lib/memo-cache.js +84 -0
- package/neural-interface/lib/memory-api.js +758 -0
- package/neural-interface/lib/memory-map-api.js +1150 -0
- package/neural-interface/lib/memory-map-layout.js +872 -0
- package/neural-interface/lib/memory-map-worker.js +165 -0
- package/neural-interface/lib/memory-session-capture.js +81 -0
- package/neural-interface/lib/memory-timers.js +62 -0
- package/neural-interface/lib/morelogin.js +301 -0
- package/neural-interface/lib/native-binary-runtime.js +382 -0
- package/neural-interface/lib/native-loop-goal.js +33 -0
- package/neural-interface/lib/native-loop-providers.js +1002 -0
- package/neural-interface/lib/native-loop-runtime.js +747 -0
- package/neural-interface/lib/offline-page.js +54 -0
- package/neural-interface/lib/opencode-hidden-models.js +113 -0
- package/neural-interface/lib/opencode-history.js +253 -0
- package/neural-interface/lib/opencode-runtime-config.js +58 -0
- package/neural-interface/lib/opencode-v2-client.js +486 -0
- package/neural-interface/lib/opencode-v2-ws-requests.js +955 -0
- package/neural-interface/lib/path-confine.js +213 -0
- package/neural-interface/lib/plugin-installer.js +264 -0
- package/neural-interface/lib/project-storage.js +766 -0
- package/neural-interface/lib/pty-host/core.js +603 -0
- package/neural-interface/lib/pty-host/flow.js +43 -0
- package/neural-interface/lib/pty-host/host.js +47 -0
- package/neural-interface/lib/pty-host/manager.js +360 -0
- package/neural-interface/lib/pty-host/protocol.js +81 -0
- package/neural-interface/lib/pty-host/snapshot.js +133 -0
- package/neural-interface/lib/remote-policy.js +582 -0
- package/neural-interface/lib/rulesets/api.js +175 -0
- package/neural-interface/lib/rulesets/fs-safe.js +245 -0
- package/neural-interface/lib/rulesets/installer.js +1453 -0
- package/neural-interface/lib/rulesets/json-edit.js +186 -0
- package/neural-interface/lib/rulesets/lock.js +170 -0
- package/neural-interface/lib/rulesets/managed-text.js +314 -0
- package/neural-interface/lib/rulesets/normalise.js +24 -0
- package/neural-interface/lib/rulesets/render.js +84 -0
- package/neural-interface/lib/schedule-launch-intent.js +40 -0
- package/neural-interface/lib/session-cache-builder.js +524 -0
- package/neural-interface/lib/session-chunker.js +344 -0
- package/neural-interface/lib/session-indexer.js +327 -0
- package/neural-interface/lib/session-title-generator.js +232 -0
- package/neural-interface/lib/skill-prompt-bridge.js +11 -0
- package/neural-interface/lib/style-guide/api.js +375 -0
- package/neural-interface/lib/style-guide/color.js +352 -0
- package/neural-interface/lib/style-guide/export-css.js +99 -0
- package/neural-interface/lib/style-guide/export-dtcg.js +209 -0
- package/neural-interface/lib/style-guide/export-tailwind.js +102 -0
- package/neural-interface/lib/style-guide/fonts.json +227 -0
- package/neural-interface/lib/style-guide/import.js +1268 -0
- package/neural-interface/lib/style-guide/pointers.js +103 -0
- package/neural-interface/lib/style-guide/presets.js +232 -0
- package/neural-interface/lib/style-guide/render-design-md.js +319 -0
- package/neural-interface/lib/style-guide/schema.js +848 -0
- package/neural-interface/lib/style-guide/security.js +43 -0
- package/neural-interface/lib/style-guide/store.js +491 -0
- package/neural-interface/lib/style-guide/summary.js +106 -0
- package/neural-interface/lib/style-guide/tokens.js +170 -0
- package/neural-interface/lib/synabun-update-plan.js +324 -0
- package/neural-interface/lib/synabun-version.js +179 -0
- package/neural-interface/lib/system-restore.js +99 -0
- package/neural-interface/lib/terminal-env.js +86 -0
- package/neural-interface/lib/ui-blob-store.js +106 -0
- package/neural-interface/lib/vector-cache.js +2 -0
- package/neural-interface/lib/virtual-socket.js +60 -0
- package/neural-interface/lib/whatsapp/api.js +146 -0
- package/neural-interface/lib/whatsapp/auth-store.js +389 -0
- package/neural-interface/lib/whatsapp/baileys-adapter.js +457 -0
- package/neural-interface/lib/whatsapp/brain.js +128 -0
- package/neural-interface/lib/whatsapp/bridge.js +1499 -0
- package/neural-interface/lib/whatsapp/cards.js +519 -0
- package/neural-interface/lib/whatsapp/commands.js +92 -0
- package/neural-interface/lib/whatsapp/config.js +291 -0
- package/neural-interface/lib/whatsapp/connector/entry.mjs +25 -0
- package/neural-interface/lib/whatsapp/connector/manifest.json +6 -0
- package/neural-interface/lib/whatsapp/connector/npm-shrinkwrap.json +717 -0
- package/neural-interface/lib/whatsapp/connector/package.json +8 -0
- package/neural-interface/lib/whatsapp/fake-baileys.js +444 -0
- package/neural-interface/lib/whatsapp/format.js +552 -0
- package/neural-interface/lib/whatsapp/host-core.js +1225 -0
- package/neural-interface/lib/whatsapp/host.js +119 -0
- package/neural-interface/lib/whatsapp/identity.js +288 -0
- package/neural-interface/lib/whatsapp/inbound.js +149 -0
- package/neural-interface/lib/whatsapp/installer.js +562 -0
- package/neural-interface/lib/whatsapp/manager.js +638 -0
- package/neural-interface/lib/whatsapp/paths.js +81 -0
- package/neural-interface/lib/whatsapp/protocol.js +327 -0
- package/neural-interface/lib/whatsapp/qr-svg.js +57 -0
- package/neural-interface/lib/whatsapp/redact.js +130 -0
- package/neural-interface/lib/whatsapp/service.js +1305 -0
- package/neural-interface/lib/whatsapp/vendor/qrcodegen.js +848 -0
- package/neural-interface/package-lock.json +4431 -0
- package/neural-interface/package.json +58 -27
- package/neural-interface/public/apple-touch-icon.png +0 -0
- package/neural-interface/public/category-logos/synabun.png +0 -0
- package/neural-interface/public/claude.svg +1 -0
- package/neural-interface/public/favicon-16x16.png +0 -0
- package/neural-interface/public/favicon-32x32.png +0 -0
- package/neural-interface/public/icon-192.png +0 -0
- package/neural-interface/public/icon-512.png +0 -0
- package/neural-interface/public/index.html +32 -182
- package/neural-interface/public/index2d.html +2 -2
- package/neural-interface/public/install-app.html +31 -0
- package/neural-interface/public/logo-synabun.png +0 -0
- package/neural-interface/public/logoHD.png +0 -0
- package/neural-interface/public/manifest.json +2 -0
- package/neural-interface/public/mcp.svg +4 -0
- package/neural-interface/public/offline.html +670 -299
- package/neural-interface/public/onboarding.html +804 -161
- package/neural-interface/public/opencode-logo-dark-square.svg +1 -0
- package/neural-interface/public/shared/agent-runtime-options.js +596 -0
- package/neural-interface/public/shared/api.js +339 -28
- package/neural-interface/public/shared/assistant/asst-accounts.js +318 -0
- package/neural-interface/public/shared/assistant/asst-agents-dock.js +296 -0
- package/neural-interface/public/shared/assistant/asst-api.js +270 -0
- package/neural-interface/public/shared/assistant/asst-brain-picker.js +744 -0
- package/neural-interface/public/shared/assistant/asst-budget.js +333 -0
- package/neural-interface/public/shared/assistant/asst-composer.js +656 -0
- package/neural-interface/public/shared/assistant/asst-computer.js +803 -0
- package/neural-interface/public/shared/assistant/asst-control.js +1018 -0
- package/neural-interface/public/shared/assistant/asst-hosts.js +262 -0
- package/neural-interface/public/shared/assistant/asst-mascot.js +313 -0
- package/neural-interface/public/shared/assistant/asst-menu.js +375 -0
- package/neural-interface/public/shared/assistant/asst-panel.js +1845 -0
- package/neural-interface/public/shared/assistant/asst-rack-styles.js +586 -0
- package/neural-interface/public/shared/assistant/asst-render.js +3305 -0
- package/neural-interface/public/shared/assistant/asst-route.js +1653 -0
- package/neural-interface/public/shared/assistant/asst-roving.js +75 -0
- package/neural-interface/public/shared/assistant/asst-sanitize.js +117 -0
- package/neural-interface/public/shared/assistant/asst-sidepanel-styles.js +488 -0
- package/neural-interface/public/shared/assistant/asst-sidepanel.js +675 -0
- package/neural-interface/public/shared/assistant/asst-slash.js +258 -0
- package/neural-interface/public/shared/assistant/asst-state.js +727 -0
- package/neural-interface/public/shared/assistant/asst-styles.js +1686 -0
- package/neural-interface/public/shared/assistant/asst-tool-kinds.js +1017 -0
- package/neural-interface/public/shared/assistant/asst-usage.js +444 -0
- package/neural-interface/public/shared/assistant/asst-ws.js +175 -0
- package/neural-interface/public/shared/automation-context.js +75 -0
- package/neural-interface/public/shared/blob-store.js +99 -0
- package/neural-interface/public/shared/cdx/cdx-capabilities.js +118 -0
- package/neural-interface/public/shared/cdx/cdx-cards.js +37 -0
- package/neural-interface/public/shared/cdx/cdx-commands.js +171 -0
- package/neural-interface/public/shared/cdx/cdx-context-menu.js +402 -0
- package/neural-interface/public/shared/cdx/cdx-context-model.js +220 -0
- package/neural-interface/public/shared/cdx/cdx-features.js +408 -0
- package/neural-interface/public/shared/cdx/cdx-icons.js +89 -0
- package/neural-interface/public/shared/cdx/cdx-mcp-app-host.js +414 -0
- package/neural-interface/public/shared/cdx/cdx-mcp-app-proxy.html +6 -0
- package/neural-interface/public/shared/cdx/cdx-mcp-app-proxy.js +56 -0
- package/neural-interface/public/shared/cdx/cdx-mcp-app.js +178 -0
- package/neural-interface/public/shared/cdx/cdx-model-catalog.js +120 -0
- package/neural-interface/public/shared/cdx/cdx-output-model.js +261 -0
- package/neural-interface/public/shared/cdx/cdx-output.js +382 -0
- package/neural-interface/public/shared/cdx/cdx-panel.js +1372 -0
- package/neural-interface/public/shared/cdx/cdx-plan.js +266 -0
- package/neural-interface/public/shared/cdx/cdx-protocol.js +682 -0
- package/neural-interface/public/shared/cdx/cdx-render.js +3381 -0
- package/neural-interface/public/shared/cdx/cdx-requests.js +1136 -0
- package/neural-interface/public/shared/cdx/cdx-styles.js +3251 -0
- package/neural-interface/public/shared/cdx/cdx-tabs.js +7992 -0
- package/neural-interface/public/shared/cdx/cdx-ws.js +363 -0
- package/neural-interface/public/shared/cli-status.js +169 -0
- package/neural-interface/public/shared/command-runner-model.js +30 -0
- package/neural-interface/public/shared/constants.js +22 -5
- package/neural-interface/public/shared/cp/cp-agents.js +186 -0
- package/neural-interface/public/shared/cp/cp-bash.js +252 -0
- package/neural-interface/public/shared/cp/cp-compose.js +159 -0
- package/neural-interface/public/shared/cp/cp-context-menu.js +309 -0
- package/neural-interface/public/shared/cp/cp-context-model.js +157 -0
- package/neural-interface/public/shared/cp/cp-context-window.js +80 -0
- package/neural-interface/public/shared/cp/cp-ctx.js +11 -0
- package/neural-interface/public/shared/cp/cp-diff.js +255 -0
- package/neural-interface/public/shared/cp/cp-event-rows.js +637 -0
- package/neural-interface/public/shared/cp/cp-events.js +846 -0
- package/neural-interface/public/shared/cp/cp-markdown.js +104 -0
- package/neural-interface/public/shared/cp/cp-permission-model.js +905 -0
- package/neural-interface/public/shared/cp/cp-permissions.js +626 -0
- package/neural-interface/public/shared/cp/cp-rehydrate.js +113 -0
- package/neural-interface/public/shared/cp/cp-restore.js +224 -0
- package/neural-interface/public/shared/cp/cp-session-model.js +338 -0
- package/neural-interface/public/shared/cp/cp-session-settings.js +254 -0
- package/neural-interface/public/shared/cp/cp-sessions.js +227 -0
- package/neural-interface/public/shared/cp/cp-statusline.js +153 -0
- package/neural-interface/public/shared/cp/cp-styles.js +349 -0
- package/neural-interface/public/shared/cp/cp-tasks-model.js +282 -0
- package/neural-interface/public/shared/cp/cp-tasks.js +107 -0
- package/neural-interface/public/shared/cp/cp-temporary-view.js +56 -0
- package/neural-interface/public/shared/cp/cp-temporary.js +231 -0
- package/neural-interface/public/shared/cp/cp-tool-cards.js +326 -0
- package/neural-interface/public/shared/cp/cp-tool-results.js +618 -0
- package/neural-interface/public/shared/cp/cp-usage-model.js +124 -0
- package/neural-interface/public/shared/graph-activity.js +68 -0
- package/neural-interface/public/shared/html-shell.js +116 -17
- package/neural-interface/public/shared/i18n.js +132 -34
- package/neural-interface/public/shared/node-positions-store.js +44 -0
- package/neural-interface/public/shared/ocp-hidden-models.js +116 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-approvals.js +355 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-autoaccept-button.js +35 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-automation-ownership.js +72 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-binding.js +280 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-caps.js +67 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-changes-logic.js +179 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-changes.js +164 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +725 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-compact-button.js +75 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-composer-logic.js +1204 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-confirm-logic.js +117 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-confirm.js +59 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-context-gauge.js +252 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-context-menu.js +478 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-context-model.js +179 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-events.js +233 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-icons.js +30 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-manager.js +1065 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-mention-hints.js +182 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-modelpicker.js +655 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +2636 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-plan.js +601 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-profile-inheritance.js +78 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-projectbar.js +649 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-providers.js +35 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-rehydrate.js +357 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-render-logic.js +189 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +1869 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-select.js +42 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-send-logic.js +117 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +1714 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-session-actions.js +468 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-session-widgets.js +63 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-sessions-logic.js +366 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-settings-link.js +19 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-slash-hints.js +343 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-state.js +746 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-status-logic.js +347 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-status.js +338 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +3543 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-subagents-logic.js +482 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-tools-logic.js +351 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-tools.js +168 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-variantpicker.js +315 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +545 -0
- package/neural-interface/public/shared/provider-icons.js +30 -0
- package/neural-interface/public/shared/pwa-install-page.js +34 -0
- package/neural-interface/public/shared/pwa-install.css +12 -0
- package/neural-interface/public/shared/pwa-install.js +28 -0
- package/neural-interface/public/shared/session-title.js +100 -0
- package/neural-interface/public/shared/settings/settings-ia.js +389 -0
- package/neural-interface/public/shared/settings/settings-kit.js +234 -0
- package/neural-interface/public/shared/settings/settings-rules.js +160 -0
- package/neural-interface/public/shared/settings/settings-search.js +463 -0
- package/neural-interface/public/shared/settings/settings-shell.js +570 -0
- package/neural-interface/public/shared/start-bridge.js +583 -0
- package/neural-interface/public/shared/state.js +5 -0
- package/neural-interface/public/shared/storage.js +50 -7
- package/neural-interface/public/shared/styleguide/sg-agents.js +42 -0
- package/neural-interface/public/shared/styleguide/sg-brand.js +4 -0
- package/neural-interface/public/shared/styleguide/sg-color-client.js +388 -0
- package/neural-interface/public/shared/styleguide/sg-colors.js +151 -0
- package/neural-interface/public/shared/styleguide/sg-components.js +35 -0
- package/neural-interface/public/shared/styleguide/sg-history.js +41 -0
- package/neural-interface/public/shared/styleguide/sg-icons-logo.js +38 -0
- package/neural-interface/public/shared/styleguide/sg-imagery.js +4 -0
- package/neural-interface/public/shared/styleguide/sg-import-export.js +48 -0
- package/neural-interface/public/shared/styleguide/sg-kit.js +284 -0
- package/neural-interface/public/shared/styleguide/sg-labels.js +318 -0
- package/neural-interface/public/shared/styleguide/sg-layout.js +4 -0
- package/neural-interface/public/shared/styleguide/sg-motion.js +5 -0
- package/neural-interface/public/shared/styleguide/sg-panel.js +237 -0
- package/neural-interface/public/shared/styleguide/sg-preview.js +138 -0
- package/neural-interface/public/shared/styleguide/sg-rules.js +9 -0
- package/neural-interface/public/shared/styleguide/sg-shape.js +24 -0
- package/neural-interface/public/shared/styleguide/sg-state.js +132 -0
- package/neural-interface/public/shared/styleguide/sg-typography.js +125 -0
- package/neural-interface/public/shared/styles.css +8614 -1477
- package/neural-interface/public/shared/synabun-mascot.js +1625 -0
- package/neural-interface/public/shared/synabun-ticker.js +376 -0
- package/neural-interface/public/shared/term-ready.js +307 -0
- package/neural-interface/public/shared/term-renderer.js +343 -0
- package/neural-interface/public/shared/ui-assistant-panel.js +10 -0
- package/neural-interface/public/shared/ui-assistant.js +77 -0
- package/neural-interface/public/shared/ui-automation-studio.js +5105 -3573
- package/neural-interface/public/shared/ui-claude-panel.js +11685 -6333
- package/neural-interface/public/shared/ui-codex-panel.js +12 -0
- package/neural-interface/public/shared/ui-command-runner.js +251 -468
- package/neural-interface/public/shared/ui-cost-widget.js +55 -34
- package/neural-interface/public/shared/ui-detail.js +77 -14
- package/neural-interface/public/shared/ui-explorer.js +360 -181
- package/neural-interface/public/shared/ui-file-explorer.js +485 -107
- package/neural-interface/public/shared/ui-help.js +2 -2
- package/neural-interface/public/shared/ui-image-gallery.js +45 -21
- package/neural-interface/public/shared/ui-judgments.js +1346 -0
- package/neural-interface/public/shared/ui-keybinds.js +3 -2
- package/neural-interface/public/shared/ui-link.js +105 -51
- package/neural-interface/public/shared/ui-loading.js +222 -11
- package/neural-interface/public/shared/ui-memory-maintenance.js +111 -0
- package/neural-interface/public/shared/ui-menubar.js +100 -31
- package/neural-interface/public/shared/ui-native-loop-router-state.js +94 -0
- package/neural-interface/public/shared/ui-native-loop-router.js +493 -0
- package/neural-interface/public/shared/ui-native-window-id.js +7 -0
- package/neural-interface/public/shared/ui-navbar.js +357 -222
- package/neural-interface/public/shared/ui-notifications.js +58 -12
- package/neural-interface/public/shared/ui-opencode-panel-v2.js +17 -0
- package/neural-interface/public/shared/ui-panels.js +15 -7
- package/neural-interface/public/shared/ui-resume.js +823 -653
- package/neural-interface/public/shared/ui-schedule-queue.js +172 -0
- package/neural-interface/public/shared/ui-schedules-studio.js +1882 -0
- package/neural-interface/public/shared/ui-scroll-follow.js +194 -0
- package/neural-interface/public/shared/ui-sessions.js +789 -34
- package/neural-interface/public/shared/ui-settings.js +5774 -2194
- package/neural-interface/public/shared/ui-sidepanel-layout.js +109 -0
- package/neural-interface/public/shared/ui-sidepanel-runtime.js +19 -0
- package/neural-interface/public/shared/ui-sidepanel-session-entry.js +40 -0
- package/neural-interface/public/shared/ui-sidepanel-sessions.js +221 -0
- package/neural-interface/public/shared/ui-sidepanel-state.js +131 -0
- package/neural-interface/public/shared/ui-sidepanel-tray.js +547 -0
- package/neural-interface/public/shared/ui-sidepanel-windows.js +387 -0
- package/neural-interface/public/shared/ui-skills.js +18 -32
- package/neural-interface/public/shared/ui-styleguide.js +35 -0
- package/neural-interface/public/shared/ui-sync.js +87 -0
- package/neural-interface/public/shared/ui-terminal.js +7863 -6385
- package/neural-interface/public/shared/ui-tool-updates.js +292 -0
- package/neural-interface/public/shared/ui-tooltip.js +156 -15
- package/neural-interface/public/shared/ui-tutorial-draw.js +119 -683
- package/neural-interface/public/shared/ui-tutorial-layout.js +135 -0
- package/neural-interface/public/shared/ui-tutorial-steps.js +59 -2209
- package/neural-interface/public/shared/ui-tutorial.css +102 -0
- package/neural-interface/public/shared/ui-tutorial.js +557 -1076
- package/neural-interface/public/shared/ui-update.js +420 -68
- package/neural-interface/public/shared/ui-whatsapp.js +941 -0
- package/neural-interface/public/shared/ui-whiteboard.js +1076 -233
- package/neural-interface/public/shared/ui-workspaces.js +2 -1
- package/neural-interface/public/shared/utils.js +58 -0
- package/neural-interface/public/shared/vterm-buffer.js +15 -5
- package/neural-interface/public/shared/whatsapp/wa-phone.js +65 -0
- package/neural-interface/public/shared/whatsapp/wa-view.js +411 -0
- package/neural-interface/public/sidepanel-session.html +18 -0
- package/neural-interface/public/sw.js +10 -2
- package/neural-interface/public/synabun.png +0 -0
- package/neural-interface/public/variant/2d/graph.js +1707 -1635
- package/neural-interface/public/variant/2d/lasso.js +2 -1
- package/neural-interface/public/variant/2d/layout.js +595 -594
- package/neural-interface/public/variant/2d/main.js +557 -521
- package/neural-interface/public/variant/2d/settings-gfx.js +18 -24
- package/neural-interface/public/variant/3d/main.js +517 -500
- package/neural-interface/public/variant/3d/map-controls.js +144 -0
- package/neural-interface/public/variant/3d/map-data.js +251 -0
- package/neural-interface/public/variant/3d/map-labels.js +241 -0
- package/neural-interface/public/variant/3d/map-pick.js +90 -0
- package/neural-interface/public/variant/3d/map-renderer.js +521 -0
- package/neural-interface/public/variant/3d/map-shaders.js +158 -0
- package/neural-interface/public/variant/3d/map-view.js +618 -0
- package/neural-interface/public/vendor/three/manifest.json +3 -0
- package/neural-interface/public/vendor/three/three-0.169.0.min.js +3844 -0
- package/neural-interface/public/vendor/xterm/manifest.json +3 -0
- package/neural-interface/public/vendor/xterm/xterm-6.0.0.css +285 -0
- package/neural-interface/public/vendor/xterm/xterm-6.0.0.min.js +192 -0
- package/neural-interface/scripts/build-vendor.mjs +64 -0
- package/neural-interface/scripts/codex-protocol-fixture.mjs +83 -0
- package/neural-interface/scripts/rebuild-pty.js +39 -22
- package/neural-interface/scripts/vendor-three-entry.js +24 -0
- package/neural-interface/scripts/vendor-xterm-entry.js +8 -0
- package/neural-interface/server.js +33983 -15000
- package/neural-interface/skins/default/skin.css +90 -0
- package/neural-interface/skins/default/skin.json +8 -0
- package/neural-interface/templates/rulesets/README.md +9 -0
- package/neural-interface/templates/rulesets/claude.md +3 -0
- package/neural-interface/templates/rulesets/codex.md +2 -0
- package/neural-interface/templates/rulesets/coexistence.md +6 -0
- package/neural-interface/templates/rulesets/core.md +26 -0
- package/neural-interface/templates/rulesets/cursor.md +2 -0
- package/neural-interface/templates/rulesets/gemini.md +2 -0
- package/neural-interface/templates/rulesets/legacy-hashes.json +227 -0
- package/neural-interface/templates/rulesets/manifest.json +12 -0
- package/neural-interface/templates/rulesets/opencode.md +2 -0
- package/package.json +131 -53
- package/postinstall.js +55 -26
- package/preuninstall.js +187 -0
- package/scripts/restore-data-home-from-backup.mjs +239 -0
- package/setup.js +872 -260
- package/skills/leonardo/SKILL.md +19 -10
- package/skills/leonardo/modules/image-prompter.md +18 -12
- package/skills/leonardo/modules/video-prompter.md +19 -13
- package/skills/synabun/SKILL.md +209 -185
- package/skills/synabun/icon.png +0 -0
- package/skills/synabun/modules/audit.md +380 -374
- package/skills/synabun/modules/changelog.md +15 -6
- package/skills/synabun/modules/idea.md +148 -144
- package/skills/synabun/modules/memorize.md +13 -4
- package/skills/synabun/modules/schedule.md +198 -0
- package/skills/synabun/synabunicon.png +0 -0
- package/updater.cmd +31 -0
- package/updater.mjs +382 -0
- package/updater.sh +29 -0
- package/hooks/claude-code/test-user-learning.mjs +0 -531
- package/neural-interface/public/claude-chat.css +0 -878
- package/neural-interface/public/claude-chat.html +0 -92
- package/neural-interface/public/claude-chat.js +0 -1168
- package/neural-interface/public/index-legacy.html +0 -14677
- package/neural-interface/public/index2d-legacy.html +0 -8193
- package/neural-interface/public/shared/html-term-renderer.js +0 -1022
- package/neural-interface/public/variant/3d/background.js +0 -884
- package/neural-interface/public/variant/3d/camera.js +0 -661
- package/neural-interface/public/variant/3d/gfx.js +0 -116
- package/neural-interface/public/variant/3d/graph.js +0 -2399
- package/neural-interface/public/variant/3d/settings-gfx.js +0 -228
- package/neural-interface/templates/CLAUDE-template.md +0 -211
|
@@ -0,0 +1,2636 @@
|
|
|
1
|
+
import { hostedSidepanelId, sidepanelHost } from '../ui-sidepanel-runtime.js';
|
|
2
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
3
|
+
// OpenCode V2 — Panel container
|
|
4
|
+
// Mounts a slide-in sidepanel on the right edge. Owns the lifecycle:
|
|
5
|
+
// first toggle → inject styles, build DOM, connect WS, ensure session
|
|
6
|
+
// subsequent → just slide in/out; state persists
|
|
7
|
+
// Exposes toggleOpencodePanel / isOpencodePanelOpen / openOpencodeWithPrompt
|
|
8
|
+
// / attachPathToOpencode to match the surface shared UI modules bind against.
|
|
9
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
import { state, emit, on } from '../state.js';
|
|
12
|
+
import { storage } from '../storage.js';
|
|
13
|
+
import { fetchProjects } from '../api.js';
|
|
14
|
+
import { injectStyles } from './ocp-v2-styles.js';
|
|
15
|
+
import { connect, api, hasSessionStore, onSessionCompacted, onEvent, supports, isDescendantSession } from './ocp-v2-ws.js';
|
|
16
|
+
import {
|
|
17
|
+
menuSessions, pickReusableEmptySession, shareView, SHARE_CONFIRM_TEXT, createPendingRenames, createStoppedShares,
|
|
18
|
+
} from './ocp-v2-sessions-logic.js';
|
|
19
|
+
import {
|
|
20
|
+
forkSession, shareSession, unshareSession, undoLastTurn, restoreReverted,
|
|
21
|
+
loadSelectedSession, applySessionSelections, restoreDraftAttachments,
|
|
22
|
+
readTranscriptExport, stopForNavigation, createSessionForNavigation, renameSession,
|
|
23
|
+
navigateAfter, adoptReusableSession, landWithoutTabs, shouldNameCreatedSession, renameBox,
|
|
24
|
+
} from './ocp-v2-session-actions.js';
|
|
25
|
+
import { captureBinding, latestOnly, latestFor, createNavigation } from './ocp-v2-binding.js';
|
|
26
|
+
import { createConfirmations } from './ocp-v2-confirm-logic.js';
|
|
27
|
+
import { syncConfirmRow } from './ocp-v2-confirm.js';
|
|
28
|
+
import { closeWithPromptsConfirm, prefilledDraft, samePrompts } from './ocp-v2-composer-logic.js';
|
|
29
|
+
import { setSelectOptions, projectOptionRows } from './ocp-v2-select.js';
|
|
30
|
+
import { replyFailed, replyError } from './ocp-v2-caps.js';
|
|
31
|
+
import { rehydratePanelSession, recoveredErrorSweep } from './ocp-v2-rehydrate.js';
|
|
32
|
+
import { headerStatus } from './ocp-v2-render-logic.js';
|
|
33
|
+
import {
|
|
34
|
+
getState, subscribe, setSession, setServerStatus, setCwd, clearMessages, pushError, clearErrors, setHealing,
|
|
35
|
+
addAttachedImage, getDefaultStore, setRunning,
|
|
36
|
+
} from './ocp-v2-state.js';
|
|
37
|
+
import { trackContextGauge } from './ocp-v2-context-gauge.js';
|
|
38
|
+
import { mountContextMenu } from './ocp-v2-context-menu.js';
|
|
39
|
+
import { createCompactControl } from './ocp-v2-compact-button.js';
|
|
40
|
+
import { createAutoAcceptControl } from './ocp-v2-autoaccept-button.js';
|
|
41
|
+
import { mountTodoWidget } from './ocp-v2-session-widgets.js';
|
|
42
|
+
import { mountChangesBar } from './ocp-v2-changes.js';
|
|
43
|
+
import { mountEnvironmentPopover } from './ocp-v2-status.js';
|
|
44
|
+
import { watchWorktree, newSessionPlan, checkoutOutcome } from './ocp-v2-changes-logic.js';
|
|
45
|
+
import { mountRenderer } from './ocp-v2-render.js';
|
|
46
|
+
import { mountCompose, focusCompose, appendPathToCompose } from './ocp-v2-send.js';
|
|
47
|
+
import { mountProjectBar, unmountProjectBar } from './ocp-v2-projectbar.js';
|
|
48
|
+
import { registerSidepanel, syncSidepanelLayout, focusSidepanel, openAnotherSidepanelSession, isHostedSessionFocused } from '../ui-sidepanel-windows.js';
|
|
49
|
+
import { nativeLoopWindowId } from '../ui-native-window-id.js';
|
|
50
|
+
import { requestGeneratedSessionTitle } from '../session-title.js';
|
|
51
|
+
import {
|
|
52
|
+
registerAutomationSession, unregisterAutomationSession, stopAutomationRun,
|
|
53
|
+
} from './ocp-v2-automation-ownership.js';
|
|
54
|
+
import {
|
|
55
|
+
registerPrimaryPanel, bindPrimarySession, isChildPanelOpen, getVisibleChildPanel, openChildSession,
|
|
56
|
+
registerMainView, requestOpencodeView, MAIN_VIEW, releaseSubagentPanels, subagentPromptsWaiting, syncSubagentTray,
|
|
57
|
+
} from './ocp-v2-manager.js';
|
|
58
|
+
|
|
59
|
+
const PANEL_ID = 'ocp-v2-panel';
|
|
60
|
+
const PANEL_OWNER = 'ocp-v2-panel';
|
|
61
|
+
|
|
62
|
+
const ICON_PLUS = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>';
|
|
63
|
+
const ICON_MINIMIZE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/></svg>';
|
|
64
|
+
const ICON_SLIDE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>';
|
|
65
|
+
const ICON_X = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18M6 6l12 12"/></svg>';
|
|
66
|
+
const ICON_X_SMALL = '<svg viewBox="0 0 24 24"><path d="M18 6 6 18M6 6l12 12"/></svg>';
|
|
67
|
+
const ICON_EDIT = '<svg viewBox="0 0 24 24"><path d="M17 3a2.83 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg>';
|
|
68
|
+
const OPENCODE_ICON = '<svg viewBox="0 0 24 30" fill="currentColor"><path d="M18 6H6V24H18V6ZM24 30H0V0H24V30Z"/></svg>';
|
|
69
|
+
|
|
70
|
+
const STOR_PROJECT = 'opencode-v2-project';
|
|
71
|
+
let OCP_WINDOW_ID = (() => {
|
|
72
|
+
if (hostedSidepanelId) return hostedSidepanelId;
|
|
73
|
+
let value = sessionStorage.getItem('ocp-window-id');
|
|
74
|
+
if (!value) { value = crypto.randomUUID(); sessionStorage.setItem('ocp-window-id', value); }
|
|
75
|
+
return value;
|
|
76
|
+
})();
|
|
77
|
+
let STOR_TABS = `opencode-v2-tabs-${OCP_WINDOW_ID}`;
|
|
78
|
+
let STOR_AUTOMATIONS = `opencode-v2-automations-${OCP_WINDOW_ID}`;
|
|
79
|
+
let STOR_TITLE_STATES = `opencode-v2-title-states-${OCP_WINDOW_ID}`;
|
|
80
|
+
const STOR_WINDOW_REGISTRY = 'opencode-v2-windows';
|
|
81
|
+
const HEAL_DEBOUNCE_MS = 800;
|
|
82
|
+
const HEAL_RETRY_MAX_MS = 30000;
|
|
83
|
+
|
|
84
|
+
let _panelEl = null;
|
|
85
|
+
let _statusEl = null;
|
|
86
|
+
let _titleEl = null;
|
|
87
|
+
let _tabSessionIds = []; // ordered session IDs that own a tray pill
|
|
88
|
+
let _tabPills = new Map(); // sessionId -> pill element
|
|
89
|
+
let _tabInfoCache = new Map(); // sessionId -> last-known session info (title, etc.)
|
|
90
|
+
const _automationRunIds = new Map(); // sessionId -> native loop runId
|
|
91
|
+
const _automationRunning = new Set();
|
|
92
|
+
const _automationContinuedManually = new Set();
|
|
93
|
+
const _automationManualSendInFlight = new Set();
|
|
94
|
+
const _foreignActiveAutomationSessions = new Set();
|
|
95
|
+
let _visible = false;
|
|
96
|
+
let _booted = false;
|
|
97
|
+
let _bootPromise = null;
|
|
98
|
+
let _pendingAutomationAttach = null;
|
|
99
|
+
let _unsubHeader = null;
|
|
100
|
+
let _projects = [];
|
|
101
|
+
let _projectsLoaded = false;
|
|
102
|
+
let _projectsLoadPromise = null;
|
|
103
|
+
let _sessionMenuEl = null;
|
|
104
|
+
let _contextMenu = null;
|
|
105
|
+
let _sessionBtnEl = null;
|
|
106
|
+
let _sessionLabelEl = null;
|
|
107
|
+
let _renameBtnEl = null;
|
|
108
|
+
let _docClickWired = false;
|
|
109
|
+
let _healTimer = null;
|
|
110
|
+
let _healingPromise = null;
|
|
111
|
+
let _healRetryCount = 0;
|
|
112
|
+
const _sessionTitleState = new Map(); // sessionId -> { state, title, requestId, controller }
|
|
113
|
+
const _titleReassertTimers = new Map();
|
|
114
|
+
// Renames whose request is out. Until the server has answered, the title a
|
|
115
|
+
// rename carries is not the session's: nothing shows it or pushes it again.
|
|
116
|
+
const _pendingRenames = createPendingRenames();
|
|
117
|
+
let _composer = null; // the primary panel's composer (mountCompose)
|
|
118
|
+
// Where the panel goes. Every choice of the user (a tab, a session in the
|
|
119
|
+
// menu, New session, a project, a worktree) begins an intent, and a navigation
|
|
120
|
+
// binds the panel only while its intent is the latest one: a later choice wins
|
|
121
|
+
// over an older one that is still in flight (ocp-v2-binding.js). What the
|
|
122
|
+
// panel starts by itself (boot, the recovery, an automation asking for focus)
|
|
123
|
+
// only observes: it never overtakes a choice of the user.
|
|
124
|
+
const _nav = createNavigation(getDefaultStore());
|
|
125
|
+
// What publishes or destroys is asked in the panel, in two steps, never with a
|
|
126
|
+
// native dialog (ocp-v2-confirm-logic.js). `surface` says where the question
|
|
127
|
+
// is drawn: 'menu' under its row of the session menu, 'panel' above the
|
|
128
|
+
// compose box, 'dialog' in the naming dialog.
|
|
129
|
+
const _confirms = createConfirmations({ store: getDefaultStore() });
|
|
130
|
+
let _confirmSlotEl = null;
|
|
131
|
+
const _menuConfirmAnchors = new Map(); // question key -> the menu row it is asked under
|
|
132
|
+
_confirms.onChange(() => { syncSessionMenuConfirm(); syncPanelConfirm(); });
|
|
133
|
+
// Share links whose sharing this panel stopped. OpenCode goes on reporting
|
|
134
|
+
// them (createStoppedShares in ocp-v2-sessions-logic.js), so the store takes
|
|
135
|
+
// such a link out of any session info it is handed, and so do the session
|
|
136
|
+
// menu and the tab cache. One list for every window, kept across reloads.
|
|
137
|
+
const STOR_STOPPED_SHARES = 'opencode-v2-stopped-shares';
|
|
138
|
+
const _stoppedShares = createStoppedShares({
|
|
139
|
+
load: () => JSON.parse(storage.getItem(STOR_STOPPED_SHARES) || '{}'),
|
|
140
|
+
save: (entries) => storage.setItem(STOR_STOPPED_SHARES, JSON.stringify(entries)),
|
|
141
|
+
});
|
|
142
|
+
getDefaultStore().setSessionInfoFilter?.((info) => _stoppedShares.clean(info));
|
|
143
|
+
|
|
144
|
+
function forkClonedOpenCodePersistence() {
|
|
145
|
+
try {
|
|
146
|
+
const rows = JSON.parse(storage.getItem(STOR_AUTOMATIONS) || '[]');
|
|
147
|
+
const foreignActiveIds = new Set((Array.isArray(rows) ? rows : [])
|
|
148
|
+
.filter((row) => row?.running && row?.sessionId && row.ownerId !== nativeLoopWindowId)
|
|
149
|
+
.map((row) => row.sessionId));
|
|
150
|
+
if (!foreignActiveIds.size) return;
|
|
151
|
+
const savedTabs = JSON.parse(storage.getItem(STOR_TABS) || '[]');
|
|
152
|
+
const transferableTabs = (Array.isArray(savedTabs) ? savedTabs : [])
|
|
153
|
+
.filter((sessionId) => !foreignActiveIds.has(sessionId));
|
|
154
|
+
const transferableRows = (Array.isArray(rows) ? rows : [])
|
|
155
|
+
.filter((row) => !foreignActiveIds.has(row?.sessionId));
|
|
156
|
+
let transferableTitleStates = {};
|
|
157
|
+
try {
|
|
158
|
+
const savedTitleStates = JSON.parse(storage.getItem(STOR_TITLE_STATES) || '{}');
|
|
159
|
+
transferableTitleStates = Object.fromEntries(Object.entries(savedTitleStates)
|
|
160
|
+
.filter(([sessionId]) => transferableTabs.includes(sessionId)));
|
|
161
|
+
} catch {}
|
|
162
|
+
OCP_WINDOW_ID = crypto.randomUUID();
|
|
163
|
+
sessionStorage.setItem('ocp-window-id', OCP_WINDOW_ID);
|
|
164
|
+
STOR_TABS = `opencode-v2-tabs-${OCP_WINDOW_ID}`;
|
|
165
|
+
STOR_AUTOMATIONS = `opencode-v2-automations-${OCP_WINDOW_ID}`;
|
|
166
|
+
STOR_TITLE_STATES = `opencode-v2-title-states-${OCP_WINDOW_ID}`;
|
|
167
|
+
if (transferableTabs.length) storage.setItem(STOR_TABS, JSON.stringify(transferableTabs));
|
|
168
|
+
if (transferableRows.length) storage.setItem(STOR_AUTOMATIONS, JSON.stringify(transferableRows));
|
|
169
|
+
if (Object.keys(transferableTitleStates).length) storage.setItem(STOR_TITLE_STATES, JSON.stringify(transferableTitleStates));
|
|
170
|
+
} catch {}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function cleanStaleOpenCodeWindows() {
|
|
174
|
+
try {
|
|
175
|
+
const registry = JSON.parse(storage.getItem(STOR_WINDOW_REGISTRY) || '{}');
|
|
176
|
+
const now = Date.now();
|
|
177
|
+
for (const [windowId, lastSeen] of Object.entries(registry)) {
|
|
178
|
+
if (windowId === OCP_WINDOW_ID || now - Number(lastSeen) <= 24 * 60 * 60 * 1000) continue;
|
|
179
|
+
storage.removeItem(`opencode-v2-tabs-${windowId}`);
|
|
180
|
+
storage.removeItem(`opencode-v2-automations-${windowId}`);
|
|
181
|
+
storage.removeItem(`opencode-v2-title-states-${windowId}`);
|
|
182
|
+
delete registry[windowId];
|
|
183
|
+
}
|
|
184
|
+
registry[OCP_WINDOW_ID] = now;
|
|
185
|
+
storage.setItem(STOR_WINDOW_REGISTRY, JSON.stringify(registry));
|
|
186
|
+
} catch {}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function touchOpenCodeWindow() {
|
|
190
|
+
try {
|
|
191
|
+
const registry = JSON.parse(storage.getItem(STOR_WINDOW_REGISTRY) || '{}');
|
|
192
|
+
registry[OCP_WINDOW_ID] = Date.now();
|
|
193
|
+
storage.setItem(STOR_WINDOW_REGISTRY, JSON.stringify(registry));
|
|
194
|
+
} catch {}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
forkClonedOpenCodePersistence();
|
|
198
|
+
cleanStaleOpenCodeWindows();
|
|
199
|
+
|
|
200
|
+
function loadTitleStates() {
|
|
201
|
+
try {
|
|
202
|
+
const raw = JSON.parse(storage.getItem(STOR_TITLE_STATES) || '{}');
|
|
203
|
+
for (const [sid, entry] of Object.entries(raw)) {
|
|
204
|
+
if (!sid || !entry || typeof entry !== 'object') continue;
|
|
205
|
+
_sessionTitleState.set(sid, {
|
|
206
|
+
state: entry.state === 'generating' ? 'default' : (entry.state || 'manual'),
|
|
207
|
+
title: entry.title || '',
|
|
208
|
+
requestId: null,
|
|
209
|
+
controller: null,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
} catch {}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function saveTitleStates() {
|
|
216
|
+
try {
|
|
217
|
+
const rows = {};
|
|
218
|
+
for (const [sid, entry] of _sessionTitleState) {
|
|
219
|
+
rows[sid] = {
|
|
220
|
+
state: entry.state === 'generating' ? 'default' : (entry.state || 'default'),
|
|
221
|
+
title: entry.title || '',
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
if (Object.keys(rows).length) storage.setItem(STOR_TITLE_STATES, JSON.stringify(rows));
|
|
225
|
+
else storage.removeItem(STOR_TITLE_STATES);
|
|
226
|
+
} catch {}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
loadTitleStates();
|
|
230
|
+
|
|
231
|
+
function saveAutomationTabs() {
|
|
232
|
+
try {
|
|
233
|
+
const localRows = [..._automationRunIds].map(([sessionId, runId]) => ({
|
|
234
|
+
sessionId,
|
|
235
|
+
runId,
|
|
236
|
+
running: _automationRunning.has(sessionId),
|
|
237
|
+
ownerId: nativeLoopWindowId,
|
|
238
|
+
continuedManually: _automationContinuedManually.has(sessionId),
|
|
239
|
+
}));
|
|
240
|
+
const localSessionIds = new Set(localRows.map((row) => row.sessionId));
|
|
241
|
+
let foreignRows = [];
|
|
242
|
+
try {
|
|
243
|
+
const existing = JSON.parse(storage.getItem(STOR_AUTOMATIONS) || '[]');
|
|
244
|
+
foreignRows = (Array.isArray(existing) ? existing : []).filter((row) => (
|
|
245
|
+
row?.sessionId
|
|
246
|
+
&& row?.runId
|
|
247
|
+
&& row.ownerId !== nativeLoopWindowId
|
|
248
|
+
&& !localSessionIds.has(row.sessionId)
|
|
249
|
+
));
|
|
250
|
+
} catch {}
|
|
251
|
+
const rows = [...localRows, ...foreignRows];
|
|
252
|
+
if (rows.length) storage.setItem(STOR_AUTOMATIONS, JSON.stringify(rows));
|
|
253
|
+
else storage.removeItem(STOR_AUTOMATIONS);
|
|
254
|
+
touchOpenCodeWindow();
|
|
255
|
+
} catch {}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
const rows = JSON.parse(storage.getItem(STOR_AUTOMATIONS) || '[]');
|
|
260
|
+
for (const row of Array.isArray(rows) ? rows : []) {
|
|
261
|
+
if (!row?.sessionId || !row?.runId) continue;
|
|
262
|
+
if (row.running && row.ownerId !== nativeLoopWindowId) {
|
|
263
|
+
_foreignActiveAutomationSessions.add(row.sessionId);
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
_automationRunIds.set(row.sessionId, row.runId);
|
|
267
|
+
registerAutomationSession(row.sessionId, row.runId);
|
|
268
|
+
if (row.running) _automationRunning.add(row.sessionId);
|
|
269
|
+
if (row.continuedManually) _automationContinuedManually.add(row.sessionId);
|
|
270
|
+
}
|
|
271
|
+
} catch {}
|
|
272
|
+
|
|
273
|
+
function buildPanel() {
|
|
274
|
+
injectStyles();
|
|
275
|
+
|
|
276
|
+
const panel = document.createElement('div');
|
|
277
|
+
panel.id = PANEL_ID;
|
|
278
|
+
panel.className = 'ocpv2-panel';
|
|
279
|
+
|
|
280
|
+
// ── Resize handle (left edge) ─────────────────────────────────────────────
|
|
281
|
+
const resizeHandle = document.createElement('div');
|
|
282
|
+
resizeHandle.className = 'ocpv2-resize-handle';
|
|
283
|
+
panel.appendChild(resizeHandle);
|
|
284
|
+
|
|
285
|
+
// ── Header ────────────────────────────────────────────────────────────────
|
|
286
|
+
// Same order as the other side panels: the session button, the rename
|
|
287
|
+
// pencil joined to it, the action buttons on the right edge, the menu.
|
|
288
|
+
const header = document.createElement('div');
|
|
289
|
+
header.className = 'ocpv2-header';
|
|
290
|
+
|
|
291
|
+
// Session button + rename group (acts as title + dropdown trigger)
|
|
292
|
+
const sessionBtn = document.createElement('button');
|
|
293
|
+
sessionBtn.className = 'ocpv2-session-btn';
|
|
294
|
+
sessionBtn.type = 'button';
|
|
295
|
+
const sessionLabel = document.createElement('span');
|
|
296
|
+
sessionLabel.className = 'ocpv2-session-label';
|
|
297
|
+
sessionLabel.textContent = 'OpenCode';
|
|
298
|
+
const arrow = document.createElement('span');
|
|
299
|
+
arrow.className = 'ocpv2-dd-arrow';
|
|
300
|
+
arrow.innerHTML = '▾';
|
|
301
|
+
sessionBtn.appendChild(sessionLabel);
|
|
302
|
+
sessionBtn.appendChild(arrow);
|
|
303
|
+
sessionBtn.addEventListener('click', (event) => {
|
|
304
|
+
if (event.target.closest('.ocpv2-rename-input')) return;
|
|
305
|
+
toggleSessionMenu();
|
|
306
|
+
});
|
|
307
|
+
header.appendChild(sessionBtn);
|
|
308
|
+
_sessionBtnEl = sessionBtn;
|
|
309
|
+
_sessionLabelEl = sessionLabel;
|
|
310
|
+
_titleEl = sessionLabel;
|
|
311
|
+
|
|
312
|
+
const renameBtn = document.createElement('button');
|
|
313
|
+
renameBtn.className = 'ocpv2-header-rename';
|
|
314
|
+
renameBtn.type = 'button';
|
|
315
|
+
renameBtn.title = 'Rename';
|
|
316
|
+
renameBtn.setAttribute('data-tooltip', 'Rename');
|
|
317
|
+
renameBtn.innerHTML = ICON_EDIT;
|
|
318
|
+
renameBtn.addEventListener('mousedown', (event) => {
|
|
319
|
+
if (sessionLabel.querySelector('.ocpv2-rename-input')) event.preventDefault();
|
|
320
|
+
});
|
|
321
|
+
renameBtn.addEventListener('click', (event) => {
|
|
322
|
+
event.stopPropagation();
|
|
323
|
+
closeSessionMenu();
|
|
324
|
+
beginRenameSession();
|
|
325
|
+
});
|
|
326
|
+
header.appendChild(renameBtn);
|
|
327
|
+
_renameBtnEl = renameBtn;
|
|
328
|
+
|
|
329
|
+
// The session's state. Not a header piece: it is mounted in the footer
|
|
330
|
+
// toolbar below, once the composer has built that row.
|
|
331
|
+
const status = document.createElement('div');
|
|
332
|
+
status.className = 'ocpv2-footer-status';
|
|
333
|
+
status.textContent = '…';
|
|
334
|
+
_statusEl = status;
|
|
335
|
+
|
|
336
|
+
const actions = document.createElement('div');
|
|
337
|
+
actions.className = 'ocpv2-actions';
|
|
338
|
+
|
|
339
|
+
const newBtn = document.createElement('button');
|
|
340
|
+
newBtn.className = 'ocpv2-btn ocpv2-btn-new';
|
|
341
|
+
newBtn.type = 'button';
|
|
342
|
+
newBtn.title = 'New session';
|
|
343
|
+
newBtn.setAttribute('data-tooltip', 'New session');
|
|
344
|
+
newBtn.setAttribute('data-tooltip-pos', 'bottom');
|
|
345
|
+
newBtn.innerHTML = ICON_PLUS;
|
|
346
|
+
newBtn.addEventListener('click', (event) => {
|
|
347
|
+
event.stopPropagation();
|
|
348
|
+
beginNewSession();
|
|
349
|
+
});
|
|
350
|
+
actions.appendChild(newBtn);
|
|
351
|
+
|
|
352
|
+
const minimizeBtn = document.createElement('button');
|
|
353
|
+
minimizeBtn.className = 'ocpv2-btn ocpv2-btn-minimize';
|
|
354
|
+
minimizeBtn.type = 'button';
|
|
355
|
+
minimizeBtn.title = 'Minimize';
|
|
356
|
+
minimizeBtn.setAttribute('data-tooltip', 'Minimize');
|
|
357
|
+
minimizeBtn.setAttribute('data-tooltip-pos', 'bottom');
|
|
358
|
+
minimizeBtn.innerHTML = ICON_MINIMIZE;
|
|
359
|
+
minimizeBtn.addEventListener('click', (event) => {
|
|
360
|
+
event.stopPropagation();
|
|
361
|
+
setVisible(false);
|
|
362
|
+
});
|
|
363
|
+
actions.appendChild(minimizeBtn);
|
|
364
|
+
|
|
365
|
+
const closeBtn = document.createElement('button');
|
|
366
|
+
closeBtn.className = 'ocpv2-btn ocpv2-btn-danger';
|
|
367
|
+
closeBtn.type = 'button';
|
|
368
|
+
closeBtn.title = 'Close session';
|
|
369
|
+
closeBtn.setAttribute('data-tooltip', 'Close session');
|
|
370
|
+
closeBtn.setAttribute('data-tooltip-pos', 'bottom');
|
|
371
|
+
closeBtn.innerHTML = ICON_X;
|
|
372
|
+
closeBtn.addEventListener('click', (event) => {
|
|
373
|
+
event.stopPropagation();
|
|
374
|
+
closeCurrentSession();
|
|
375
|
+
});
|
|
376
|
+
actions.appendChild(closeBtn);
|
|
377
|
+
|
|
378
|
+
const slideBtn = document.createElement('button');
|
|
379
|
+
slideBtn.className = 'ocpv2-btn ocpv2-btn-slide';
|
|
380
|
+
slideBtn.type = 'button';
|
|
381
|
+
slideBtn.title = 'Slide';
|
|
382
|
+
slideBtn.setAttribute('data-tooltip', 'Slide');
|
|
383
|
+
slideBtn.setAttribute('data-tooltip-pos', 'bottom');
|
|
384
|
+
slideBtn.innerHTML = ICON_SLIDE;
|
|
385
|
+
slideBtn.addEventListener('click', (event) => {
|
|
386
|
+
event.stopPropagation();
|
|
387
|
+
setVisible(false);
|
|
388
|
+
});
|
|
389
|
+
actions.appendChild(slideBtn);
|
|
390
|
+
|
|
391
|
+
header.appendChild(actions);
|
|
392
|
+
|
|
393
|
+
// Session menu (anchored to header via absolute positioning)
|
|
394
|
+
const sessionMenu = document.createElement('div');
|
|
395
|
+
sessionMenu.className = 'ocpv2-session-menu';
|
|
396
|
+
header.appendChild(sessionMenu);
|
|
397
|
+
_sessionMenuEl = sessionMenu;
|
|
398
|
+
|
|
399
|
+
panel.appendChild(header);
|
|
400
|
+
|
|
401
|
+
// Context settings: the cog in the header and its popover (the context
|
|
402
|
+
// window and Compact, the connected tools, the versions, the session and
|
|
403
|
+
// its auto-accept switch). The gauge only keeps the store's reading current.
|
|
404
|
+
trackContextGauge(getDefaultStore());
|
|
405
|
+
const environment = mountEnvironmentPopover(panel, getDefaultStore(), {
|
|
406
|
+
// A reference's "Attach": a referenced path on the prompt being written.
|
|
407
|
+
onAttachPath: (path) => appendPathToCompose(path),
|
|
408
|
+
});
|
|
409
|
+
_contextMenu = mountContextMenu(actions, panel, getDefaultStore(), {
|
|
410
|
+
compact: createCompactControl(getDefaultStore()),
|
|
411
|
+
autoAccept: createAutoAcceptControl(getDefaultStore()),
|
|
412
|
+
onManage: () => environment.open(),
|
|
413
|
+
onOpen: () => { closeSessionMenu(); environment.close(); },
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
// ── Messages scroll area (card wrapper + absolute scroll list) ──────────
|
|
417
|
+
const messagesContainer = document.createElement('div');
|
|
418
|
+
messagesContainer.className = 'ocpv2-messages-container';
|
|
419
|
+
const messages = document.createElement('div');
|
|
420
|
+
messages.className = 'ocpv2-messages';
|
|
421
|
+
messagesContainer.appendChild(messages);
|
|
422
|
+
panel.appendChild(messagesContainer);
|
|
423
|
+
|
|
424
|
+
// ── What the session changed on disk (session.diff) ─────────────────────
|
|
425
|
+
mountChangesBar(panel, getDefaultStore());
|
|
426
|
+
|
|
427
|
+
// ── Todo list of the running work (session.todo / todo.updated) ─────────
|
|
428
|
+
mountTodoWidget(panel, getDefaultStore());
|
|
429
|
+
|
|
430
|
+
// ── The question before a tab is closed or a message is deleted ─────────
|
|
431
|
+
_confirmSlotEl = document.createElement('div');
|
|
432
|
+
_confirmSlotEl.className = 'ocpv2-confirm-slot';
|
|
433
|
+
panel.appendChild(_confirmSlotEl);
|
|
434
|
+
|
|
435
|
+
// ── Compose footer ───────────────────────────────────────────────────────
|
|
436
|
+
const compose = document.createElement('div');
|
|
437
|
+
compose.className = 'ocpv2-compose';
|
|
438
|
+
panel.appendChild(compose);
|
|
439
|
+
|
|
440
|
+
document.body.appendChild(panel);
|
|
441
|
+
_panelEl = panel;
|
|
442
|
+
registerSidepanel({
|
|
443
|
+
owner: PANEL_OWNER, provider: 'opencode', element: panel,
|
|
444
|
+
header, actions, buttonClass: 'ocpv2-btn', dockHandle: resizeHandle, applyVisibility,
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
let composer = null;
|
|
448
|
+
const renderer = mountRenderer(messages, getDefaultStore(), {
|
|
449
|
+
onOpenSession: (session, extra) => openCreatedSession(session, extra),
|
|
450
|
+
// Fork is a navigation: its intent begins at the click, and the fork takes
|
|
451
|
+
// the panel only while that click is still the user's latest choice.
|
|
452
|
+
onNavigate: () => _nav.begin(),
|
|
453
|
+
onOpenChild: (childSessionId) => openChildSession(getState().sessionId, childSessionId),
|
|
454
|
+
// Undo hands the prompt back: its text, and what was attached to it.
|
|
455
|
+
onComposeText: (text, extra) => putPromptInComposer(composer, text, extra),
|
|
456
|
+
// Retry sends the original prompt with its own file parts; `images: []`
|
|
457
|
+
// marks the attachments as the prompt's own, so the draft in the strips
|
|
458
|
+
// is neither sent nor cleared.
|
|
459
|
+
// `binding` is the one Retry was clicked on: the composer refuses the
|
|
460
|
+
// prompt once the panel has left it.
|
|
461
|
+
onSendText: (text, extra) => composer?.sendTextMessage(text, {
|
|
462
|
+
images: [], paths: [], mentions: extra?.files || [], allowEmptyText: true, binding: extra?.binding,
|
|
463
|
+
}),
|
|
464
|
+
// Deleting a message is asked above the compose box; the second click deletes.
|
|
465
|
+
confirm: (question) => _confirms.ask({ surface: 'panel', ...question }),
|
|
466
|
+
});
|
|
467
|
+
// Slash commands that act on the session itself (ocp-v2-composer-logic.js
|
|
468
|
+
// names them; the composer calls these).
|
|
469
|
+
// `moved`: the panel was bound to another session while the action was out
|
|
470
|
+
// (ocp-v2-session-actions.js). Its result is dropped, without a banner.
|
|
471
|
+
const failIfRefused = (result, fallback) => {
|
|
472
|
+
if (result && result.ok === false && !result.cancelled && !result.moved) pushError({ message: result.error || fallback });
|
|
473
|
+
return result;
|
|
474
|
+
};
|
|
475
|
+
const slashActions = {
|
|
476
|
+
new: () => beginNewSession(),
|
|
477
|
+
sessions: () => openSessionMenu(),
|
|
478
|
+
export: () => exportActiveTranscript(),
|
|
479
|
+
undo: async () => {
|
|
480
|
+
const result = failIfRefused(await undoLastTurn(getDefaultStore(), api), 'Undo failed');
|
|
481
|
+
if (result?.ok) putPromptInComposer(composer, result.text, result);
|
|
482
|
+
},
|
|
483
|
+
redo: async () => { failIfRefused(await restoreReverted(getDefaultStore(), api), 'Restore failed'); },
|
|
484
|
+
fork: async () => {
|
|
485
|
+
const nav = _nav.begin();
|
|
486
|
+
const result = failIfRefused(await forkSession(getDefaultStore(), api), 'Fork failed');
|
|
487
|
+
if (result?.ok) await openCreatedSession(result.session, { activate: !result.moved, nav });
|
|
488
|
+
},
|
|
489
|
+
// Asked every time, in the session menu: /share only opens the question
|
|
490
|
+
// there, and its button publishes the transcript at a public URL.
|
|
491
|
+
share: () => shareActiveSession(),
|
|
492
|
+
unshare: () => stopSharingActiveSession(),
|
|
493
|
+
};
|
|
494
|
+
composer = _composer = mountCompose(compose, getDefaultStore(), {
|
|
495
|
+
slashActions,
|
|
496
|
+
// Nothing is parked for a session that has no tab to come back to.
|
|
497
|
+
hasTab: (sessionId) => _tabSessionIds.includes(sessionId),
|
|
498
|
+
onBeforeSend: () => renderer.scrollToBottom({ force: true }),
|
|
499
|
+
// `session` in these three callbacks is the composer's binding snapshot
|
|
500
|
+
// (ocp-v2-binding.js): the session the prompt or the Stop was for, frozen
|
|
501
|
+
// before the first await. It is never the live state.
|
|
502
|
+
canSend: (session) => !_automationRunning.has(session?.sessionId),
|
|
503
|
+
onManualSend: (session, phase, meta) => {
|
|
504
|
+
const sessionId = session?.sessionId;
|
|
505
|
+
if (phase === 'started') requestOpenCodeSessionTitle(session, meta);
|
|
506
|
+
if (!sessionId || !_automationRunIds.has(sessionId) || _automationRunning.has(sessionId)) return;
|
|
507
|
+
if (phase === 'started') _automationManualSendInFlight.add(sessionId);
|
|
508
|
+
if (phase === 'failed') _automationManualSendInFlight.delete(sessionId);
|
|
509
|
+
if (phase === 'succeeded') {
|
|
510
|
+
_automationManualSendInFlight.delete(sessionId);
|
|
511
|
+
_automationContinuedManually.add(sessionId);
|
|
512
|
+
saveAutomationTabs();
|
|
513
|
+
}
|
|
514
|
+
},
|
|
515
|
+
onAbort: async (session) => {
|
|
516
|
+
// The session Stop was pressed in, by value: its bookkeeping is what the
|
|
517
|
+
// stop request updates, also when another session is selected meanwhile.
|
|
518
|
+
const stopped = await stopAutomationRun(
|
|
519
|
+
session?.sessionId,
|
|
520
|
+
{ runIds: _automationRunIds, running: _automationRunning },
|
|
521
|
+
(runId) => fetch('/api/loop/stop', {
|
|
522
|
+
method: 'POST',
|
|
523
|
+
headers: { 'Content-Type': 'application/json' },
|
|
524
|
+
body: JSON.stringify({ runId }),
|
|
525
|
+
}).catch(() => {}),
|
|
526
|
+
);
|
|
527
|
+
if (stopped) saveAutomationTabs();
|
|
528
|
+
return stopped;
|
|
529
|
+
},
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
// Project + branch dropdowns — mounted INSIDE the compose card, above input-area.
|
|
533
|
+
mountProjectBar(compose, {
|
|
534
|
+
getProjects: () => _projects,
|
|
535
|
+
ensureProjectsLoaded: () => loadProjectsOnce(),
|
|
536
|
+
setActiveProject,
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
// The session's state reads in the footer toolbar's left group, after the
|
|
540
|
+
// agent buttons (the compose card itself when that row is not there).
|
|
541
|
+
(compose.querySelector('.ocpv2-footer-left') || compose).appendChild(status);
|
|
542
|
+
|
|
543
|
+
// Header reflects state
|
|
544
|
+
_unsubHeader = subscribe(handleStoreEvent);
|
|
545
|
+
syncHeader();
|
|
546
|
+
|
|
547
|
+
// Sessions change outside this panel too (another window, the CLI, a title
|
|
548
|
+
// OpenCode generated): keep the open menu and the tab list in step.
|
|
549
|
+
onEvent((eventType, ev) => {
|
|
550
|
+
if (eventType !== 'session.updated' && eventType !== 'session.deleted' && eventType !== 'session.created') return;
|
|
551
|
+
if (eventType === 'session.deleted') {
|
|
552
|
+
const sid = ev?.info?.id || ev?.sessionID || ev?.sessionId;
|
|
553
|
+
if (sid) _stoppedShares.resume(sid);
|
|
554
|
+
if (sid && _tabSessionIds.includes(sid)) {
|
|
555
|
+
handlePillClose(sid).catch((err) => console.warn('[ocp-v2-panel] close of a deleted session failed', err));
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
scheduleSessionMenuRefresh();
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
// A compaction rewrites what the model sees: re-read the transcript so the
|
|
562
|
+
// marker and the summary show up even when it ran without this panel asking.
|
|
563
|
+
onSessionCompacted((store) => {
|
|
564
|
+
if (store !== getDefaultStore()) return;
|
|
565
|
+
const sid = getState().sessionId;
|
|
566
|
+
if (sid) refreshSessionMessages(sid).catch((err) => console.warn('[ocp-v2-panel] post-compaction refresh failed', err));
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// A reverted prompt goes back into the composer with what it carried, each
|
|
571
|
+
// thing as what it was: pasted files in the strip, referenced paths as chips,
|
|
572
|
+
// @ mentions (a symbol's line range, an MCP resource) as mentions again. A
|
|
573
|
+
// draft the user is already writing is left alone, attachments included.
|
|
574
|
+
function putPromptInComposer(composer, text, prompt = {}) {
|
|
575
|
+
if (!composer) return false;
|
|
576
|
+
const files = Array.isArray(prompt?.files) ? prompt.files : [];
|
|
577
|
+
const resources = Array.isArray(prompt?.resources) ? prompt.resources : [];
|
|
578
|
+
if (!text && !files.length) return false;
|
|
579
|
+
if (text && !composer.setText(text)) return false;
|
|
580
|
+
if (files.length || resources.length) {
|
|
581
|
+
const { lost } = restoreDraftAttachments(
|
|
582
|
+
getDefaultStore(),
|
|
583
|
+
{ text, files, resources, resourceList: prompt?.resourceList },
|
|
584
|
+
{ addMentions: (items) => composer.restoreMentions(items) },
|
|
585
|
+
);
|
|
586
|
+
if (lost.length) {
|
|
587
|
+
pushError({ message: `Could not put back ${lost.join(', ')}. Attach ${lost.length === 1 ? 'it' : 'them'} again before you send.` });
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return true;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function handleStoreEvent(event, stateSnapshot) {
|
|
594
|
+
const nextState = stateSnapshot || getState();
|
|
595
|
+
if (event?.type === 'running:set' && !nextState.running && nextState.sessionId) {
|
|
596
|
+
_titleNeedsReassert(nextState.sessionId);
|
|
597
|
+
}
|
|
598
|
+
if (event?.type === 'session:info') keepOwnSessionTitle(nextState);
|
|
599
|
+
syncHeader();
|
|
600
|
+
maybeScheduleAutoHeal(event, nextState);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function syncHeader() {
|
|
604
|
+
if (!_statusEl) return;
|
|
605
|
+
const s = getState();
|
|
606
|
+
_statusEl.className = 'ocpv2-footer-status';
|
|
607
|
+
const status = headerStatus(s);
|
|
608
|
+
if (status.cls) _statusEl.classList.add(`ocpv2-status-${status.cls}`);
|
|
609
|
+
_statusEl.textContent = status.text;
|
|
610
|
+
// The footer row gives the text only the room that is left: the tooltip
|
|
611
|
+
// says it in full when just the dot fits.
|
|
612
|
+
_statusEl.setAttribute('data-tooltip', status.text);
|
|
613
|
+
// Skip label update while user is mid-rename (input is mounted in the label).
|
|
614
|
+
if (_sessionLabelEl && !_sessionLabelEl.querySelector('.ocpv2-rename-input')) {
|
|
615
|
+
_sessionLabelEl.textContent = sessionTitleFor(s);
|
|
616
|
+
}
|
|
617
|
+
if (_renameBtnEl) {
|
|
618
|
+
_renameBtnEl.disabled = !s.sessionId;
|
|
619
|
+
}
|
|
620
|
+
renderPill();
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function maybeScheduleAutoHeal(event, s) {
|
|
624
|
+
if (!event || !_panelEl) return;
|
|
625
|
+
if (event.type === 'server:status') {
|
|
626
|
+
if (s.serverStatus === 'offline' || s.serverStatus === 'reconnecting' || s.serverStatus === 'error') {
|
|
627
|
+
scheduleAutoHeal(`server:${s.serverStatus}`);
|
|
628
|
+
}
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
if (event.type === 'error:push') {
|
|
632
|
+
const lastError = s.errors[s.errors.length - 1];
|
|
633
|
+
if (isRecoverablePanelError(lastError)) scheduleAutoHeal('recoverable-error');
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function scheduleAutoHeal(reason, delayMs = HEAL_DEBOUNCE_MS) {
|
|
638
|
+
if (_healingPromise || _healTimer) return;
|
|
639
|
+
_healTimer = setTimeout(() => {
|
|
640
|
+
_healTimer = null;
|
|
641
|
+
runAutoHeal(reason).catch((err) => {
|
|
642
|
+
console.warn('[ocp-v2-panel] auto-heal failed', err);
|
|
643
|
+
});
|
|
644
|
+
}, Math.max(0, delayMs));
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
async function runAutoHeal(reason) {
|
|
648
|
+
if (_healingPromise) return _healingPromise;
|
|
649
|
+
_healingPromise = (async () => {
|
|
650
|
+
let retryDelay = 0;
|
|
651
|
+
setHealing(true);
|
|
652
|
+
// The errors this recovery may clear are those of the session it starts
|
|
653
|
+
// on. When the user is on another session by the time it is done, that
|
|
654
|
+
// session's errors are its own and stay.
|
|
655
|
+
const recovered = recoveredErrorSweep(getDefaultStore());
|
|
656
|
+
try {
|
|
657
|
+
await connect();
|
|
658
|
+
const init = await api.init();
|
|
659
|
+
if (!init?.ready) throw new Error('OpenCode server is not ready');
|
|
660
|
+
setServerStatus({
|
|
661
|
+
status: 'ready',
|
|
662
|
+
port: init.port,
|
|
663
|
+
version: init.version,
|
|
664
|
+
managed: init.managed,
|
|
665
|
+
});
|
|
666
|
+
await rehydrateActiveSession();
|
|
667
|
+
recovered.clear();
|
|
668
|
+
_booted = true;
|
|
669
|
+
_healRetryCount = 0;
|
|
670
|
+
} catch (err) {
|
|
671
|
+
console.warn('[ocp-v2-panel] auto-heal attempt failed', { reason, message: err?.message || String(err) });
|
|
672
|
+
setServerStatus({ status: 'error' });
|
|
673
|
+
_healRetryCount += 1;
|
|
674
|
+
retryDelay = Math.min(HEAL_RETRY_MAX_MS, 1000 * Math.pow(2, Math.min(_healRetryCount, 5)));
|
|
675
|
+
} finally {
|
|
676
|
+
setHealing(false);
|
|
677
|
+
_healingPromise = null;
|
|
678
|
+
if (retryDelay) scheduleAutoHeal('retry', retryDelay);
|
|
679
|
+
}
|
|
680
|
+
})();
|
|
681
|
+
return _healingPromise;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
async function rehydrateActiveSession() {
|
|
685
|
+
const sid = getState().sessionId;
|
|
686
|
+
if (sid) {
|
|
687
|
+
const out = await hydrateBoundSession(sid, { refreshInfo: true, requireInfo: true });
|
|
688
|
+
// The user switched session while this was out: that switch reads its own
|
|
689
|
+
// session, and this one must not be put back on screen.
|
|
690
|
+
if (out.moved) return;
|
|
691
|
+
if (!out.unavailable) {
|
|
692
|
+
ensureTab(sid, getState().sessionInfo);
|
|
693
|
+
if (!out.transcript) console.warn('[ocp-v2-panel] auto-heal messages refresh failed', out.error);
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
console.warn('[ocp-v2-panel] auto-heal session refresh failed', out.error);
|
|
697
|
+
removeTab(sid);
|
|
698
|
+
}
|
|
699
|
+
// The session is gone (or there was none) and the panel is still on it: a
|
|
700
|
+
// new one takes its place, unless the panel is taken somewhere else while
|
|
701
|
+
// it is created (then it is kept as a tab). This is the panel's own
|
|
702
|
+
// rebinding, not a choice of the user (repair): it overtakes nothing, and a
|
|
703
|
+
// session the user asked for meanwhile still takes the panel when it arrives.
|
|
704
|
+
const cwd = getState().cwd || storage.getItem(STOR_PROJECT) || undefined;
|
|
705
|
+
await startFreshSession({ cwd, nav: _nav.repair() });
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// Every transcript read of the primary panel (boot, tab switch, auto-heal,
|
|
709
|
+
// after a compaction) goes through the guarded recovery the sub-agent panel
|
|
710
|
+
// uses: the answer is applied only while the panel is still on the session it
|
|
711
|
+
// was asked for, a snapshot never ends a turn an event has just reported
|
|
712
|
+
// running, and run state, approvals and todos follow with their own guards.
|
|
713
|
+
function hydrateBoundSession(sessionId, { refreshInfo = false, requireInfo = false } = {}) {
|
|
714
|
+
return rehydratePanelSession(getDefaultStore(), api, {
|
|
715
|
+
sessionId,
|
|
716
|
+
refreshInfo,
|
|
717
|
+
requireInfo,
|
|
718
|
+
hasPanel: hasSessionStore,
|
|
719
|
+
isDescendant: isDescendantSession,
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
async function refreshSessionMessages(sessionId) {
|
|
724
|
+
const out = await hydrateBoundSession(sessionId);
|
|
725
|
+
if (!out.moved && !out.transcript) throw new Error(out.error || 'Session messages unavailable');
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function isRecoverablePanelError(err) {
|
|
729
|
+
// A notice tells the user something; there is nothing to recover from.
|
|
730
|
+
if (err?.notice) return false;
|
|
731
|
+
const msg = String(err?.message || err?.error || err || '').trim();
|
|
732
|
+
if (!msg) return true;
|
|
733
|
+
if (/choose continue|question reply failed|plan action failed|could not create plan file|could not read the plan|rename failed|could not delete|could not unarchive|not queued/i.test(msg)) return false;
|
|
734
|
+
return /websocket|closed|disconnect|offline|reconnect|request timeout|timeout|network|failed|server|session|opencode|send|boot|no session|not connected/i.test(msg);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// ── Session picker + rename ─────────────────────────────────────────────────
|
|
738
|
+
|
|
739
|
+
function sessionTitleFor(stateOrSession) {
|
|
740
|
+
const info = stateOrSession?.sessionInfo || stateOrSession?.info || stateOrSession || {};
|
|
741
|
+
const id = stateOrSession?.sessionId || info?.id || info?.sessionID || '';
|
|
742
|
+
const candidates = [info?.title, info?.description, info?.slug];
|
|
743
|
+
for (const c of candidates) {
|
|
744
|
+
if (typeof c === 'string' && c.trim()) return c.trim();
|
|
745
|
+
}
|
|
746
|
+
return id ? id.slice(0, 8) : 'New session';
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function ensureOpenCodeTitleState(sessionId, info = null, { fresh = false, manual = false } = {}) {
|
|
750
|
+
if (!sessionId) return null;
|
|
751
|
+
let entry = _sessionTitleState.get(sessionId);
|
|
752
|
+
if (!entry) {
|
|
753
|
+
entry = {
|
|
754
|
+
state: fresh && !manual ? 'default' : 'manual',
|
|
755
|
+
title: manual ? sessionTitleFor({ sessionId, sessionInfo: info }) : '',
|
|
756
|
+
requestId: null,
|
|
757
|
+
controller: null,
|
|
758
|
+
};
|
|
759
|
+
_sessionTitleState.set(sessionId, entry);
|
|
760
|
+
saveTitleStates();
|
|
761
|
+
}
|
|
762
|
+
return entry;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function updateOpenCodeTitleLocally(sessionId, title) {
|
|
766
|
+
if (!sessionId || !title) return;
|
|
767
|
+
const active = getState();
|
|
768
|
+
const current = sessionId === active.sessionId
|
|
769
|
+
? active.sessionInfo
|
|
770
|
+
: _tabInfoCache.get(sessionId);
|
|
771
|
+
const updated = { ...(current || {}), id: sessionId, title };
|
|
772
|
+
_tabInfoCache.set(sessionId, _stoppedShares.clean(updated));
|
|
773
|
+
if (sessionId === active.sessionId) setSession(sessionId, updated);
|
|
774
|
+
ensureTab(sessionId, updated);
|
|
775
|
+
renderPill();
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function markOpenCodeTitleManual(sessionId, title = '') {
|
|
779
|
+
if (!sessionId) return;
|
|
780
|
+
const entry = ensureOpenCodeTitleState(sessionId);
|
|
781
|
+
entry.controller?.abort();
|
|
782
|
+
entry.controller = null;
|
|
783
|
+
entry.requestId = null;
|
|
784
|
+
entry.state = 'manual';
|
|
785
|
+
entry.title = title || sessionTitleFor({ sessionId, sessionInfo: _tabInfoCache.get(sessionId) });
|
|
786
|
+
saveTitleStates();
|
|
787
|
+
_titleNeedsReassert(sessionId);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// For a rename that is on its way: the rename itself carries the title, and
|
|
791
|
+
// a second request for it must not succeed where the rename was refused.
|
|
792
|
+
function cancelTitleReassert(sessionId) {
|
|
793
|
+
const timer = _titleReassertTimers.get(sessionId);
|
|
794
|
+
if (timer) clearTimeout(timer);
|
|
795
|
+
_titleReassertTimers.delete(sessionId);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// A rename marks its title as the panel's own before the request leaves (so
|
|
799
|
+
// no generated title lands over it meanwhile). When the server refuses the
|
|
800
|
+
// rename, that mark is taken back: the panel must not go on showing, and
|
|
801
|
+
// pushing, a title the session never got. `before` is titleStateBefore().
|
|
802
|
+
function titleStateBefore(sessionId) {
|
|
803
|
+
const entry = _sessionTitleState.get(sessionId);
|
|
804
|
+
return entry ? { state: entry.state, title: entry.title } : null;
|
|
805
|
+
}
|
|
806
|
+
function takeBackRenamedTitle(sessionId, before, attempted) {
|
|
807
|
+
const entry = _sessionTitleState.get(sessionId);
|
|
808
|
+
// Only what this rename wrote: a later rename's title is not this one's.
|
|
809
|
+
if (!entry || entry.state !== 'manual' || entry.title !== attempted) return;
|
|
810
|
+
cancelTitleReassert(sessionId);
|
|
811
|
+
if (before) {
|
|
812
|
+
// (A title that was being generated was cancelled by the rename: it may be asked for again.)
|
|
813
|
+
entry.state = before.state === 'generating' ? 'default' : before.state;
|
|
814
|
+
entry.title = before.title;
|
|
815
|
+
} else {
|
|
816
|
+
_sessionTitleState.delete(sessionId);
|
|
817
|
+
}
|
|
818
|
+
saveTitleStates();
|
|
819
|
+
// The title the panel held before is its own again, on screen too (what
|
|
820
|
+
// OpenCode reported while the rename was out was shown as it came).
|
|
821
|
+
if (sessionId === getState().sessionId) { keepOwnSessionTitle(getState()); syncHeader(); }
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// A rename was answered (`rename` is what _pendingRenames.begin returned
|
|
825
|
+
// before its request left). A refused one takes its title back.
|
|
826
|
+
function settleRename(rename, ok) {
|
|
827
|
+
if (!_pendingRenames.end(rename, ok) || ok) return;
|
|
828
|
+
takeBackRenamedTitle(rename.sessionId, rename.before, rename.title);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// session.updated now reaches the header live. While this panel has its own
|
|
832
|
+
// title for the session (generated by SynaBun or typed by the user) that one
|
|
833
|
+
// stays on screen and is pushed back; until then OpenCode's title shows.
|
|
834
|
+
function keepOwnSessionTitle(s) {
|
|
835
|
+
// A rename of this session is out: its title is not the session's yet.
|
|
836
|
+
if (_pendingRenames.isPending(s.sessionId)) return;
|
|
837
|
+
const entry = _sessionTitleState.get(s.sessionId);
|
|
838
|
+
if (!entry || !['auto', 'manual'].includes(entry.state) || !entry.title) return;
|
|
839
|
+
if (!s.sessionInfo || s.sessionInfo.title === entry.title) return;
|
|
840
|
+
s.sessionInfo.title = entry.title;
|
|
841
|
+
_titleNeedsReassert(s.sessionId);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function _titleNeedsReassert(sessionId) {
|
|
845
|
+
// A rename of this session is out: it carries the title itself.
|
|
846
|
+
if (_pendingRenames.isPending(sessionId)) return;
|
|
847
|
+
const entry = _sessionTitleState.get(sessionId);
|
|
848
|
+
if (!entry || !['auto', 'manual'].includes(entry.state) || !entry.title) return;
|
|
849
|
+
const existing = _titleReassertTimers.get(sessionId);
|
|
850
|
+
if (existing) clearTimeout(existing);
|
|
851
|
+
const timer = setTimeout(() => {
|
|
852
|
+
_titleReassertTimers.delete(sessionId);
|
|
853
|
+
const latest = _sessionTitleState.get(sessionId);
|
|
854
|
+
if (!latest || !['auto', 'manual'].includes(latest.state) || !latest.title) return;
|
|
855
|
+
api.sessionUpdate(sessionId, { title: latest.title }).then((res) => {
|
|
856
|
+
// A push the server refused relabels nothing; neither does one for a
|
|
857
|
+
// title that is no longer the panel's own for this session.
|
|
858
|
+
if (replyFailed(res) || _sessionTitleState.get(sessionId)?.title !== latest.title) return;
|
|
859
|
+
updateOpenCodeTitleLocally(sessionId, latest.title);
|
|
860
|
+
}).catch(() => {});
|
|
861
|
+
}, 150);
|
|
862
|
+
_titleReassertTimers.set(sessionId, timer);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function requestOpenCodeSessionTitle(session, meta = {}) {
|
|
866
|
+
const sessionId = session?.sessionId;
|
|
867
|
+
if (!sessionId || _automationRunIds.has(sessionId)) return;
|
|
868
|
+
const entry = ensureOpenCodeTitleState(sessionId);
|
|
869
|
+
if (!entry || entry.state !== 'default') return;
|
|
870
|
+
const requestId = crypto.randomUUID();
|
|
871
|
+
const controller = new AbortController();
|
|
872
|
+
entry.state = 'generating';
|
|
873
|
+
entry.requestId = requestId;
|
|
874
|
+
entry.controller = controller;
|
|
875
|
+
saveTitleStates();
|
|
876
|
+
|
|
877
|
+
requestGeneratedSessionTitle({
|
|
878
|
+
provider: 'opencode',
|
|
879
|
+
prompt: meta.prompt || '',
|
|
880
|
+
paths: meta.paths || [],
|
|
881
|
+
hasImages: !!meta.hasImages,
|
|
882
|
+
cwd: session.cwd || undefined,
|
|
883
|
+
model: session.model || undefined,
|
|
884
|
+
mode: meta.mode || session.mode || 'build',
|
|
885
|
+
agent: meta.agent || session.agent || undefined,
|
|
886
|
+
variant: session.variant || undefined,
|
|
887
|
+
}, { signal: controller.signal }).then(({ title }) => {
|
|
888
|
+
const current = _sessionTitleState.get(sessionId);
|
|
889
|
+
if (!title || !current || current.state !== 'generating' || current.requestId !== requestId) return;
|
|
890
|
+
current.state = 'auto';
|
|
891
|
+
current.title = title;
|
|
892
|
+
current.requestId = null;
|
|
893
|
+
current.controller = null;
|
|
894
|
+
saveTitleStates();
|
|
895
|
+
updateOpenCodeTitleLocally(sessionId, title);
|
|
896
|
+
_titleNeedsReassert(sessionId);
|
|
897
|
+
}).finally(() => {
|
|
898
|
+
const current = _sessionTitleState.get(sessionId);
|
|
899
|
+
if (!current || current.requestId !== requestId) return;
|
|
900
|
+
current.requestId = null;
|
|
901
|
+
current.controller = null;
|
|
902
|
+
if (current.state === 'generating') current.state = 'default';
|
|
903
|
+
saveTitleStates();
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
function sessionMatchesProject(session, projectPath) {
|
|
908
|
+
if (!projectPath) return true;
|
|
909
|
+
const dir = session?.directory || '';
|
|
910
|
+
if (!dir) return false;
|
|
911
|
+
const trimmed = projectPath.replace(/\/+$/, '');
|
|
912
|
+
return dir === trimmed || dir.startsWith(trimmed + '/');
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function toggleSessionMenu() {
|
|
916
|
+
if (!_sessionMenuEl) return;
|
|
917
|
+
if (_sessionMenuEl.classList.contains('open')) {
|
|
918
|
+
closeSessionMenu();
|
|
919
|
+
} else {
|
|
920
|
+
openSessionMenu();
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function openSessionMenu() {
|
|
925
|
+
if (!_sessionMenuEl) return;
|
|
926
|
+
_sessionMenuEl.classList.add('open');
|
|
927
|
+
wireOutsideClick();
|
|
928
|
+
renderSessionMenu();
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function closeSessionMenu() {
|
|
932
|
+
_sessionMenuEl?.classList.remove('open');
|
|
933
|
+
// A question that was asked in the menu has nowhere to show: it is off.
|
|
934
|
+
_confirms.cancel((view) => view.surface === 'menu');
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// The question of the session menu, under the row it was asked on (or on top
|
|
938
|
+
// of the list while that row is not there), also after the list was refilled.
|
|
939
|
+
// (A question is asked in the menu only while it is open, and closing it takes
|
|
940
|
+
// the question, and its row, away.)
|
|
941
|
+
function syncSessionMenuConfirm() {
|
|
942
|
+
syncConfirmRow(_confirms, _sessionMenuBodyEl, 'menu', (key) => _menuConfirmAnchors.get(key));
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// The question above the compose box (closing a tab, deleting a message).
|
|
946
|
+
function syncPanelConfirm() {
|
|
947
|
+
syncConfirmRow(_confirms, _confirmSlotEl, 'panel');
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function wireOutsideClick() {
|
|
951
|
+
if (_docClickWired) return;
|
|
952
|
+
_docClickWired = true;
|
|
953
|
+
document.addEventListener('mousedown', (event) => {
|
|
954
|
+
if (!_sessionMenuEl?.classList.contains('open')) return;
|
|
955
|
+
if (event.target.closest('.ocpv2-session-menu')) return;
|
|
956
|
+
if (event.target.closest('.ocpv2-session-btn')) return;
|
|
957
|
+
closeSessionMenu();
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// Menu state that outlives one render: the search text and which list shows.
|
|
962
|
+
let _sessionMenuSearch = '';
|
|
963
|
+
let _sessionMenuArchived = false;
|
|
964
|
+
let _sessionMenuBodyEl = null;
|
|
965
|
+
let _sessionMenuRefreshTimer = null;
|
|
966
|
+
let _sharePolicy = null; // OpenCode's `share` setting, read once per menu open
|
|
967
|
+
const _sharePolicyReads = latestOnly();
|
|
968
|
+
// The list under the search box: the newest read wins, and only for the
|
|
969
|
+
// filters (search text, archived or not) the menu still shows.
|
|
970
|
+
const _sessionMenuReads = latestFor();
|
|
971
|
+
const SESSION_MENU_LIMIT = 200;
|
|
972
|
+
|
|
973
|
+
function scheduleSessionMenuRefresh(delayMs = 250) {
|
|
974
|
+
if (!_sessionMenuEl?.classList.contains('open')) return;
|
|
975
|
+
if (_sessionMenuRefreshTimer) clearTimeout(_sessionMenuRefreshTimer);
|
|
976
|
+
_sessionMenuRefreshTimer = setTimeout(() => {
|
|
977
|
+
_sessionMenuRefreshTimer = null;
|
|
978
|
+
refreshSessionMenuBody().catch((err) => console.warn('[ocp-v2-panel] session menu refresh failed', err));
|
|
979
|
+
}, delayMs);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
function menuItem(label, onClick, { meta = '', muted = false } = {}) {
|
|
983
|
+
const item = document.createElement('div');
|
|
984
|
+
item.className = 'ocpv2-session-item';
|
|
985
|
+
const labelEl = document.createElement('span');
|
|
986
|
+
labelEl.className = 'ocpv2-session-item-label';
|
|
987
|
+
if (muted) labelEl.style.opacity = '0.55';
|
|
988
|
+
labelEl.textContent = label;
|
|
989
|
+
if (meta) {
|
|
990
|
+
const metaEl = document.createElement('span');
|
|
991
|
+
metaEl.className = 'ocpv2-session-item-meta';
|
|
992
|
+
metaEl.textContent = meta;
|
|
993
|
+
labelEl.appendChild(metaEl);
|
|
994
|
+
}
|
|
995
|
+
item.appendChild(labelEl);
|
|
996
|
+
item.addEventListener('click', onClick);
|
|
997
|
+
return item;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function menuSeparator() {
|
|
1001
|
+
const sep = document.createElement('div');
|
|
1002
|
+
sep.className = 'ocpv2-session-menu-sep';
|
|
1003
|
+
return sep;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// The menu's frame (search box) is built once per open; the list under it is
|
|
1007
|
+
// refilled on every search keystroke and session event, so the box keeps focus.
|
|
1008
|
+
async function renderSessionMenu() {
|
|
1009
|
+
if (!_sessionMenuEl) return;
|
|
1010
|
+
_sessionMenuEl.innerHTML = '';
|
|
1011
|
+
_sessionMenuSearch = '';
|
|
1012
|
+
_sessionMenuArchived = false;
|
|
1013
|
+
|
|
1014
|
+
const searchRow = document.createElement('div');
|
|
1015
|
+
searchRow.className = 'ocpv2-session-search';
|
|
1016
|
+
const input = document.createElement('input');
|
|
1017
|
+
input.type = 'text';
|
|
1018
|
+
input.className = 'ocpv2-session-search-input';
|
|
1019
|
+
input.placeholder = 'Search sessions…';
|
|
1020
|
+
input.setAttribute('autocomplete', 'off');
|
|
1021
|
+
input.setAttribute('spellcheck', 'false');
|
|
1022
|
+
input.addEventListener('input', () => {
|
|
1023
|
+
_sessionMenuSearch = input.value;
|
|
1024
|
+
scheduleSessionMenuRefresh(180);
|
|
1025
|
+
});
|
|
1026
|
+
input.addEventListener('keydown', (event) => {
|
|
1027
|
+
if (event.key === 'Escape') { event.preventDefault(); closeSessionMenu(); }
|
|
1028
|
+
});
|
|
1029
|
+
searchRow.appendChild(input);
|
|
1030
|
+
_sessionMenuEl.appendChild(searchRow);
|
|
1031
|
+
|
|
1032
|
+
_sessionMenuBodyEl = document.createElement('div');
|
|
1033
|
+
_sessionMenuBodyEl.innerHTML = '<div class="ocpv2-session-menu-empty">Loading…</div>';
|
|
1034
|
+
_sessionMenuEl.appendChild(_sessionMenuBodyEl);
|
|
1035
|
+
_menuConfirmAnchors.clear();
|
|
1036
|
+
|
|
1037
|
+
// Whether sharing is allowed at all (D2: hidden when the config disables it).
|
|
1038
|
+
// The policy is the project's: an answer for an earlier opening of the menu,
|
|
1039
|
+
// or for the session the panel has left, is not this menu's.
|
|
1040
|
+
_sharePolicy = null;
|
|
1041
|
+
const policyIsFresh = _sharePolicyReads.begin();
|
|
1042
|
+
const policyAt = captureBinding(getDefaultStore());
|
|
1043
|
+
api.sessionSharePolicy({ cwd: policyAt.cwd || undefined }).then((res) => {
|
|
1044
|
+
if (!policyIsFresh() || !policyAt.isCurrent()) return;
|
|
1045
|
+
if (!replyFailed(res)) {
|
|
1046
|
+
_sharePolicy = res.data?.share || 'manual';
|
|
1047
|
+
scheduleSessionMenuRefresh(0);
|
|
1048
|
+
}
|
|
1049
|
+
}).catch(() => {});
|
|
1050
|
+
|
|
1051
|
+
await refreshSessionMenuBody();
|
|
1052
|
+
setTimeout(() => input.focus(), 0);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
async function refreshSessionMenuBody() {
|
|
1056
|
+
const body = _sessionMenuBodyEl;
|
|
1057
|
+
if (!body || !_sessionMenuEl?.classList.contains('open')) return;
|
|
1058
|
+
const filters = supports('feature:session-list-filters');
|
|
1059
|
+
const search = _sessionMenuSearch.trim();
|
|
1060
|
+
const archived = filters && _sessionMenuArchived;
|
|
1061
|
+
// An answer (rows or a failure) is painted only when it is the newest read,
|
|
1062
|
+
// for the menu body that asked, with the filters the menu still shows: an
|
|
1063
|
+
// older "live" list never lands under "Archived", and an old failure never
|
|
1064
|
+
// replaces a newer list.
|
|
1065
|
+
const asked = _sessionMenuReads.begin({ search, archived });
|
|
1066
|
+
const fresh = () => body === _sessionMenuBodyEl
|
|
1067
|
+
&& asked({ search: _sessionMenuSearch.trim(), archived: supports('feature:session-list-filters') && _sessionMenuArchived });
|
|
1068
|
+
|
|
1069
|
+
let sessions = [];
|
|
1070
|
+
try {
|
|
1071
|
+
const res = await api.sessionList(filters ? { search, archived, roots: true, limit: SESSION_MENU_LIMIT } : undefined);
|
|
1072
|
+
sessions = Array.isArray(res?.data) ? res.data : (res?.data?.sessions || []);
|
|
1073
|
+
} catch (err) {
|
|
1074
|
+
console.warn('[ocp-v2-panel] sessionList failed', err);
|
|
1075
|
+
if (!fresh()) return;
|
|
1076
|
+
body.innerHTML = '<div class="ocpv2-session-menu-empty">Failed to load sessions</div>';
|
|
1077
|
+
body.appendChild(menuItem('Retry', () => scheduleSessionMenuRefresh(0), { muted: true }));
|
|
1078
|
+
_menuConfirmAnchors.clear();
|
|
1079
|
+
syncSessionMenuConfirm();
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
if (!fresh()) return; // a newer refresh is on its way
|
|
1083
|
+
|
|
1084
|
+
const s = getState();
|
|
1085
|
+
const cwd = s.cwd || storage.getItem(STOR_PROJECT) || '';
|
|
1086
|
+
// Sessions of every project are listed, so one from another registered
|
|
1087
|
+
// project can be resumed; sub-agent sessions open from their parent instead.
|
|
1088
|
+
// (A link whose sharing the panel stopped is not a share: the row says so.)
|
|
1089
|
+
const all = menuSessions(sessions, { search }).slice(0, SESSION_MENU_LIMIT).map((row) => _stoppedShares.clean(row));
|
|
1090
|
+
|
|
1091
|
+
body.innerHTML = '';
|
|
1092
|
+
_menuConfirmAnchors.clear();
|
|
1093
|
+
if (!archived && !search) {
|
|
1094
|
+
body.appendChild(menuItem('+ New session', () => {
|
|
1095
|
+
closeSessionMenu();
|
|
1096
|
+
beginNewSession();
|
|
1097
|
+
}, { muted: true }));
|
|
1098
|
+
appendActiveSessionActions(body, s);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
if (!all.length) {
|
|
1102
|
+
const empty = document.createElement('div');
|
|
1103
|
+
empty.className = 'ocpv2-session-menu-empty';
|
|
1104
|
+
empty.textContent = search ? 'No session matches.' : (archived ? 'No archived sessions.' : 'No sessions yet.');
|
|
1105
|
+
body.appendChild(empty);
|
|
1106
|
+
} else {
|
|
1107
|
+
if (body.children.length) body.appendChild(menuSeparator());
|
|
1108
|
+
if (archived) {
|
|
1109
|
+
const note = document.createElement('div');
|
|
1110
|
+
note.className = 'ocpv2-session-menu-note';
|
|
1111
|
+
note.textContent = 'Archived';
|
|
1112
|
+
body.appendChild(note);
|
|
1113
|
+
}
|
|
1114
|
+
for (const sess of all) body.appendChild(buildSessionMenuItem(sess, s, cwd, { archived, filters }));
|
|
1115
|
+
if (sessions.length >= SESSION_MENU_LIMIT) {
|
|
1116
|
+
const more = document.createElement('div');
|
|
1117
|
+
more.className = 'ocpv2-session-menu-empty';
|
|
1118
|
+
more.textContent = `Showing the ${SESSION_MENU_LIMIT} most recent. Search to find older sessions.`;
|
|
1119
|
+
body.appendChild(more);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
if (filters) {
|
|
1124
|
+
body.appendChild(menuSeparator());
|
|
1125
|
+
body.appendChild(menuItem(archived ? 'Back to sessions' : 'Show archived', () => {
|
|
1126
|
+
_sessionMenuArchived = !archived;
|
|
1127
|
+
scheduleSessionMenuRefresh(0);
|
|
1128
|
+
}, { muted: true }));
|
|
1129
|
+
}
|
|
1130
|
+
// The list was built again: a question that is armed goes back under its row.
|
|
1131
|
+
syncSessionMenuConfirm();
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// Publishing is two steps, both in the panel. "Share session…" (or /share)
|
|
1135
|
+
// only asks: the question under it says what becomes public, and its button
|
|
1136
|
+
// shares. Nothing is published by one click, and never by a native dialog.
|
|
1137
|
+
async function shareActiveSession() {
|
|
1138
|
+
const sid = getState().sessionId;
|
|
1139
|
+
if (!sid) return null;
|
|
1140
|
+
if (!_sessionMenuEl?.classList.contains('open')) openSessionMenu();
|
|
1141
|
+
const result = await shareSession(getDefaultStore(), api, () => _confirms.ask({
|
|
1142
|
+
key: `share:${sid}`, surface: 'menu', text: SHARE_CONFIRM_TEXT, confirmLabel: 'Share publicly',
|
|
1143
|
+
}), { stopped: _stoppedShares });
|
|
1144
|
+
if (result && result.ok === false && !result.cancelled && !result.moved) pushError({ message: result.error || 'Share failed' });
|
|
1145
|
+
if (result?.ok && result.url) {
|
|
1146
|
+
try { await navigator.clipboard.writeText(result.url); } catch {}
|
|
1147
|
+
if (_sessionMenuEl?.classList.contains('open')) scheduleSessionMenuRefresh(0);
|
|
1148
|
+
else openSessionMenu();
|
|
1149
|
+
}
|
|
1150
|
+
return result;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// Stop sharing: the link goes from everything the panel holds of that
|
|
1154
|
+
// session (the store, the tab cache, the menu), also when the panel was on
|
|
1155
|
+
// another session by the time OpenCode answered.
|
|
1156
|
+
async function stopSharingActiveSession() {
|
|
1157
|
+
const sid = getState().sessionId;
|
|
1158
|
+
const result = await unshareSession(getDefaultStore(), api, { stopped: _stoppedShares });
|
|
1159
|
+
if (result && result.ok === false && !result.moved) pushError({ message: result.error || 'Could not stop sharing' });
|
|
1160
|
+
if (sid) forgetStoppedShare(sid);
|
|
1161
|
+
return result;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
function forgetStoppedShare(sessionId) {
|
|
1165
|
+
const cached = _tabInfoCache.get(sessionId);
|
|
1166
|
+
if (cached) _tabInfoCache.set(sessionId, _stoppedShares.clean(cached));
|
|
1167
|
+
const known = getState().knownSessions;
|
|
1168
|
+
if (known?.has?.(sessionId)) known.set(sessionId, _stoppedShares.clean(known.get(sessionId)));
|
|
1169
|
+
scheduleSessionMenuRefresh(0);
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// Share / export / fork for the session that is open.
|
|
1173
|
+
function appendActiveSessionActions(body, s) {
|
|
1174
|
+
if (!s.sessionId) return;
|
|
1175
|
+
const share = shareView(s.sessionInfo, _sharePolicy, supports);
|
|
1176
|
+
const report = (result, fallback) => {
|
|
1177
|
+
if (result && result.ok === false && !result.cancelled && !result.moved) pushError({ message: result.error || fallback });
|
|
1178
|
+
return result;
|
|
1179
|
+
};
|
|
1180
|
+
|
|
1181
|
+
// Asked every time, under this item: the click only opens the question.
|
|
1182
|
+
// (The item is there while its question is armed, whatever the policy read
|
|
1183
|
+
// says by then: /share asks before that read is back.)
|
|
1184
|
+
const shareKey = `share:${s.sessionId}`;
|
|
1185
|
+
if ((share.canShare && _sharePolicy) || _confirms.isArmed(shareKey)) {
|
|
1186
|
+
const shareItem = menuItem('Share session…', () => {
|
|
1187
|
+
shareActiveSession().catch((err) => pushError({ message: err?.message || 'Share failed' }));
|
|
1188
|
+
}, { muted: true });
|
|
1189
|
+
_menuConfirmAnchors.set(shareKey, shareItem);
|
|
1190
|
+
body.appendChild(shareItem);
|
|
1191
|
+
}
|
|
1192
|
+
if (share.showLink) {
|
|
1193
|
+
body.appendChild(menuItem('Copy share link', async () => {
|
|
1194
|
+
// A failed copy is reported in the session whose link it was.
|
|
1195
|
+
const at = captureBinding(getDefaultStore());
|
|
1196
|
+
try { await navigator.clipboard.writeText(share.url); } catch (err) {
|
|
1197
|
+
if (at.isCurrent()) pushError({ message: err?.message || 'Copy failed' });
|
|
1198
|
+
}
|
|
1199
|
+
}, { muted: true, meta: 'public' }));
|
|
1200
|
+
const url = document.createElement('div');
|
|
1201
|
+
url.className = 'ocpv2-session-share-url';
|
|
1202
|
+
url.textContent = share.url;
|
|
1203
|
+
body.appendChild(url);
|
|
1204
|
+
}
|
|
1205
|
+
if (share.canUnshare) {
|
|
1206
|
+
body.appendChild(menuItem('Stop sharing', () => {
|
|
1207
|
+
stopSharingActiveSession().catch((err) => pushError({ message: err?.message || 'Could not stop sharing' }));
|
|
1208
|
+
}, { muted: true }));
|
|
1209
|
+
}
|
|
1210
|
+
if (s.messageOrder.length) {
|
|
1211
|
+
body.appendChild(menuItem('Export transcript', () => {
|
|
1212
|
+
closeSessionMenu();
|
|
1213
|
+
exportActiveTranscript().catch((err) => pushError({ message: err?.message || 'Export failed' }));
|
|
1214
|
+
}, { muted: true }));
|
|
1215
|
+
if (supports('session:fork')) {
|
|
1216
|
+
body.appendChild(menuItem('Fork session', async () => {
|
|
1217
|
+
closeSessionMenu();
|
|
1218
|
+
const nav = _nav.begin();
|
|
1219
|
+
const result = report(await forkSession(getDefaultStore(), api), 'Fork failed');
|
|
1220
|
+
if (result?.ok) await openCreatedSession(result.session, { activate: !result.moved, nav });
|
|
1221
|
+
}, { muted: true }));
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
function buildSessionMenuItem(sess, s, cwd, { archived, filters }) {
|
|
1227
|
+
const sid = sess.id || sess.sessionID;
|
|
1228
|
+
const title = sessionTitleFor({ sessionInfo: sess, sessionId: sid });
|
|
1229
|
+
const isActive = sid === s.sessionId;
|
|
1230
|
+
const isOtherProject = cwd && sess.directory && !sessionMatchesProject(sess, cwd);
|
|
1231
|
+
const projectName = isOtherProject && sess.directory
|
|
1232
|
+
? (sess.directory.split('/').filter(Boolean).pop() || sess.directory.split('/').pop() || sess.directory)
|
|
1233
|
+
: '';
|
|
1234
|
+
|
|
1235
|
+
const item = document.createElement('div');
|
|
1236
|
+
item.className = 'ocpv2-session-item' + (isActive ? ' active' : '');
|
|
1237
|
+
if (title && title.length > 8) {
|
|
1238
|
+
const tip = (isOtherProject ? projectName + ' · ' : '') + title;
|
|
1239
|
+
const displayTip = tip.length > 200 ? tip.slice(0, 200) + '…' : tip;
|
|
1240
|
+
item.setAttribute('data-tooltip', displayTip);
|
|
1241
|
+
item.setAttribute('data-tooltip-pos', 'left');
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
const labelEl = document.createElement('span');
|
|
1245
|
+
labelEl.className = 'ocpv2-session-item-label';
|
|
1246
|
+
labelEl.textContent = title;
|
|
1247
|
+
item.appendChild(labelEl);
|
|
1248
|
+
|
|
1249
|
+
const meta = [projectName, sess.share?.url ? 'shared' : ''].filter(Boolean).join(' · ');
|
|
1250
|
+
if (meta) {
|
|
1251
|
+
const metaEl = document.createElement('span');
|
|
1252
|
+
metaEl.className = 'ocpv2-session-item-meta';
|
|
1253
|
+
metaEl.textContent = meta;
|
|
1254
|
+
labelEl.appendChild(metaEl);
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
if (filters && !isActive) {
|
|
1258
|
+
const archiveBtn = document.createElement('button');
|
|
1259
|
+
archiveBtn.type = 'button';
|
|
1260
|
+
archiveBtn.className = 'ocpv2-session-item-action';
|
|
1261
|
+
archiveBtn.textContent = archived ? 'Unarchive' : 'Archive';
|
|
1262
|
+
archiveBtn.addEventListener('click', async (event) => {
|
|
1263
|
+
event.stopPropagation();
|
|
1264
|
+
// Archiving closes the session's tab: asked first, under this row, when
|
|
1265
|
+
// prompts are still waiting there (before anything is sent).
|
|
1266
|
+
const closesTab = !archived && _tabSessionIds.includes(sid);
|
|
1267
|
+
// (Asked in the menu while it is open, else above the compose box.)
|
|
1268
|
+
const question = () => ({
|
|
1269
|
+
key: `session-archive:${sid}`, confirmLabel: 'Archive and discard',
|
|
1270
|
+
surface: _sessionMenuEl?.classList.contains('open') ? 'menu' : 'panel',
|
|
1271
|
+
});
|
|
1272
|
+
if (closesTab && !(await mayCloseTab(sid, question()))) return;
|
|
1273
|
+
// What the user agreed to lose: mayCloseTab answers for exactly these.
|
|
1274
|
+
const confirmed = closesTab ? promptsWaitingFor(sid) : [];
|
|
1275
|
+
// OpenCode keeps the session; `time.archived = 0` brings it back.
|
|
1276
|
+
const res = await api.sessionUpdate(sid, { time: { archived: archived ? 0 : Date.now() } }).catch((err) => ({ ok: false, error: err?.message }));
|
|
1277
|
+
if (replyFailed(res)) { pushError({ message: replyError(res, archived ? 'Unarchive failed' : 'Archive failed') }); return; }
|
|
1278
|
+
// The tab closes now. What is waiting for the session may no longer be
|
|
1279
|
+
// what the user agreed to lose (a send of it failed while the archive
|
|
1280
|
+
// was out, or they opened the tab and queued something else): unless it
|
|
1281
|
+
// is exactly that, they are asked again, and the tab stays when they
|
|
1282
|
+
// decline.
|
|
1283
|
+
const mayClose = closesTab && (samePrompts(promptsWaitingFor(sid), confirmed) || await mayCloseTab(sid, question()));
|
|
1284
|
+
if (mayClose && _tabSessionIds.includes(sid)) await handlePillClose(sid, { byUser: true }).catch(() => {});
|
|
1285
|
+
scheduleSessionMenuRefresh(0);
|
|
1286
|
+
});
|
|
1287
|
+
item.appendChild(archiveBtn);
|
|
1288
|
+
}
|
|
1289
|
+
_menuConfirmAnchors.set(`session-archive:${sid}`, item);
|
|
1290
|
+
_menuConfirmAnchors.set(`session-delete:${sid}`, item);
|
|
1291
|
+
|
|
1292
|
+
const delBtn = document.createElement('button');
|
|
1293
|
+
delBtn.className = 'ocpv2-session-item-delete';
|
|
1294
|
+
delBtn.type = 'button';
|
|
1295
|
+
delBtn.setAttribute('data-tooltip', 'Delete');
|
|
1296
|
+
delBtn.innerHTML = ICON_X_SMALL;
|
|
1297
|
+
delBtn.addEventListener('click', async (event) => {
|
|
1298
|
+
event.stopPropagation();
|
|
1299
|
+
// One question, under this row: the deletion, and the prompts still
|
|
1300
|
+
// waiting to be sent in that session, when there are any (they go with
|
|
1301
|
+
// it). This click only asks; the button of the question deletes.
|
|
1302
|
+
const lead = `Delete "${title}"? The session and its history are removed from OpenCode.`;
|
|
1303
|
+
if (!(await mayCloseTab(sid, { key: `session-delete:${sid}`, surface: 'menu', lead, confirmLabel: 'Delete session' }))) return;
|
|
1304
|
+
const wasRunning = sid === getState().sessionId && !!getState().running;
|
|
1305
|
+
const deletePromise = closeSessionBackend(sid, {
|
|
1306
|
+
wasRunning,
|
|
1307
|
+
context: 'session menu delete',
|
|
1308
|
+
});
|
|
1309
|
+
// The row promised a deletion: a refusal is said (the list is read again
|
|
1310
|
+
// below and shows the session still there).
|
|
1311
|
+
deletePromise.then((out) => {
|
|
1312
|
+
if (out && out.ok === false) pushError({ message: `Could not delete “${title}”: ${out.error || 'no answer'}` });
|
|
1313
|
+
}).catch(() => {});
|
|
1314
|
+
item.remove();
|
|
1315
|
+
handlePillClose(sid, { byUser: true }).catch((err) => {
|
|
1316
|
+
console.warn('[ocp-v2-panel] local session menu delete failed', err);
|
|
1317
|
+
}).finally(() => {
|
|
1318
|
+
deletePromise.finally(() => scheduleSessionMenuRefresh(0));
|
|
1319
|
+
});
|
|
1320
|
+
});
|
|
1321
|
+
item.appendChild(delBtn);
|
|
1322
|
+
|
|
1323
|
+
labelEl.addEventListener('click', async () => {
|
|
1324
|
+
closeSessionMenu();
|
|
1325
|
+
// The click is a navigation: it takes the panel, also after the unarchive
|
|
1326
|
+
// it may have to wait for, only while it is the user's latest choice.
|
|
1327
|
+
const nav = _nav.begin();
|
|
1328
|
+
let unarchived = null;
|
|
1329
|
+
const went = await navigateAfter(
|
|
1330
|
+
nav,
|
|
1331
|
+
async () => {
|
|
1332
|
+
if (archived) unarchived = await api.sessionUpdate(sid, { time: { archived: 0 } }).catch((err) => ({ ok: false, error: err?.message }));
|
|
1333
|
+
},
|
|
1334
|
+
() => switchToSession(sid, sess, { nav }),
|
|
1335
|
+
);
|
|
1336
|
+
if (!went) { ensureTab(sid, sess); renderPill(); }
|
|
1337
|
+
// The session opens either way (it can be read and continued). A refused
|
|
1338
|
+
// unarchive is said in it: it is still in the archive.
|
|
1339
|
+
if (archived && replyFailed(unarchived) && getState().sessionId === sid) {
|
|
1340
|
+
pushError({ message: `Could not unarchive “${title}”: ${replyError(unarchived, 'no answer')}` });
|
|
1341
|
+
}
|
|
1342
|
+
});
|
|
1343
|
+
return item;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
// A session this panel just created from another one (fork): keep the current
|
|
1347
|
+
// one as a tab and move to the new one.
|
|
1348
|
+
// `activate: false`: the session was made while the user moved on to another
|
|
1349
|
+
// one (the action that made it reports `moved`). It gets a tab; what is on
|
|
1350
|
+
// screen stays.
|
|
1351
|
+
// `nav`: the intent of the click that made it. Once a newer navigation began,
|
|
1352
|
+
// the session is a tab too.
|
|
1353
|
+
async function openCreatedSession(session, { activate = true, nav } = {}) {
|
|
1354
|
+
if (!session?.id) return;
|
|
1355
|
+
if (!activate || (nav && !nav.isCurrent())) {
|
|
1356
|
+
ensureOpenCodeTitleState(session.id, session, { manual: true });
|
|
1357
|
+
ensureTab(session.id, session);
|
|
1358
|
+
renderPill();
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
const prevSid = getState().sessionId;
|
|
1362
|
+
if (prevSid) ensureTab(prevSid, getState().sessionInfo);
|
|
1363
|
+
await switchToSession(session.id, session, { nav });
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
async function exportActiveTranscript() {
|
|
1367
|
+
if (!getState().sessionId) return;
|
|
1368
|
+
// Session id, metadata and file name are captured before the transcript is
|
|
1369
|
+
// read (readTranscriptExport): the download is one session's, whole, also
|
|
1370
|
+
// when another one is selected while it is being read.
|
|
1371
|
+
const out = await readTranscriptExport(getDefaultStore(), api);
|
|
1372
|
+
if (!out.ok) {
|
|
1373
|
+
if (out.moved) return;
|
|
1374
|
+
throw new Error(out.error || 'Could not read the transcript');
|
|
1375
|
+
}
|
|
1376
|
+
const { filename, json } = out;
|
|
1377
|
+
const url = URL.createObjectURL(new Blob([json], { type: 'application/json' }));
|
|
1378
|
+
const link = document.createElement('a');
|
|
1379
|
+
link.href = url;
|
|
1380
|
+
link.download = filename;
|
|
1381
|
+
document.body.appendChild(link);
|
|
1382
|
+
link.click();
|
|
1383
|
+
link.remove();
|
|
1384
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
// `nav` is the intent of the gesture this switch is the last step of (a tab
|
|
1388
|
+
// click that waited for the panel to open, a fork). Without one the switch is
|
|
1389
|
+
// the gesture itself and begins its own. A switch whose intent was overtaken
|
|
1390
|
+
// binds nothing: the session stays (or becomes) a tab.
|
|
1391
|
+
async function switchToSession(sid, info = null, { nav } = {}) {
|
|
1392
|
+
if (!sid) return;
|
|
1393
|
+
const intent = nav || _nav.begin();
|
|
1394
|
+
if (sid === getState().sessionId) return;
|
|
1395
|
+
ensureOpenCodeTitleState(sid, info, { manual: true });
|
|
1396
|
+
// Keep the previously-active session as a tab pill before swapping — but only
|
|
1397
|
+
// if it's still in the tab list. handlePillClose removes the tab BEFORE
|
|
1398
|
+
// calling us; without this guard the just-deleted session would resurrect.
|
|
1399
|
+
const prevSid = getState().sessionId;
|
|
1400
|
+
if (prevSid && _tabSessionIds.includes(prevSid)) {
|
|
1401
|
+
ensureTab(prevSid, getState().sessionInfo);
|
|
1402
|
+
}
|
|
1403
|
+
const bound = intent.apply(() => {
|
|
1404
|
+
clearMessages();
|
|
1405
|
+
setSession(sid, info);
|
|
1406
|
+
ensureTab(sid, info);
|
|
1407
|
+
bindPrimarySession(getDefaultStore(), sid);
|
|
1408
|
+
// Sync state.cwd to the picked session's directory so the projectbar + agent
|
|
1409
|
+
// both reflect where this session actually runs.
|
|
1410
|
+
const dirFromInfo = info?.directory;
|
|
1411
|
+
if (dirFromInfo && dirFromInfo !== getState().cwd) {
|
|
1412
|
+
setCwd(dirFromInfo);
|
|
1413
|
+
storage.setItem(STOR_PROJECT, dirFromInfo);
|
|
1414
|
+
}
|
|
1415
|
+
});
|
|
1416
|
+
if (!bound) { ensureTab(sid, info); renderPill(); return; }
|
|
1417
|
+
// Continue on what the session last ran with. The menu and the saved tabs
|
|
1418
|
+
// hand over list rows, which carry no model: read the whole session.
|
|
1419
|
+
const detailed = restoreSessionSelections(sid, info);
|
|
1420
|
+
// Best-effort hydrate of the transcript, run state and approvals. An answer
|
|
1421
|
+
// that comes back after the user moved on again is dropped.
|
|
1422
|
+
const hydrated = await hydrateBoundSession(sid);
|
|
1423
|
+
if (!hydrated.moved && !hydrated.transcript) console.warn('[ocp-v2-panel] sessionMessages failed', hydrated.error);
|
|
1424
|
+
await detailed;
|
|
1425
|
+
renderPill();
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
// Model, variant and agent follow the session on screen (Session.model /
|
|
1429
|
+
// .agent). Resolves once applied. The answer is for the selection that asked:
|
|
1430
|
+
// a session the panel left meanwhile is skipped, and so is an earlier visit to
|
|
1431
|
+
// the same session (A → B → A), whose answer may be older than the one the
|
|
1432
|
+
// newer selection gets (loadSelectedSession checks the binding, not the id).
|
|
1433
|
+
async function restoreSessionSelections(sid, info) {
|
|
1434
|
+
return loadSelectedSession(getDefaultStore(), api, sid, info, (full) => {
|
|
1435
|
+
// The directory first: it decides which agent list the session's agent is
|
|
1436
|
+
// checked against (ocp-v2-send.js).
|
|
1437
|
+
const dir = full.directory;
|
|
1438
|
+
if (dir && dir !== getState().cwd) {
|
|
1439
|
+
setCwd(dir);
|
|
1440
|
+
storage.setItem(STOR_PROJECT, dir);
|
|
1441
|
+
}
|
|
1442
|
+
applySessionSelections(getDefaultStore(), full);
|
|
1443
|
+
// Fresh metadata too: keeps the title right after a switch.
|
|
1444
|
+
getDefaultStore().setSessionInfo(full);
|
|
1445
|
+
_tabInfoCache.set(sid, _stoppedShares.clean({ ...(_tabInfoCache.get(sid) || {}), ...full }));
|
|
1446
|
+
if (!info) ensureTab(sid, full);
|
|
1447
|
+
});
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
function beginRenameSession() {
|
|
1451
|
+
const s = getState();
|
|
1452
|
+
if (!s.sessionId || !_sessionLabelEl) return;
|
|
1453
|
+
if (_sessionLabelEl.querySelector('.ocpv2-rename-input')) {
|
|
1454
|
+
_sessionLabelEl.querySelector('.ocpv2-rename-input').focus();
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
// The session this box renames: the one on screen when it was opened. The
|
|
1459
|
+
// box stays in the header while the user switches session, so the target is
|
|
1460
|
+
// captured here and never read from the live state afterwards.
|
|
1461
|
+
const target = captureBinding(getDefaultStore());
|
|
1462
|
+
const currentTitle = sessionTitleFor(s);
|
|
1463
|
+
const input = document.createElement('input');
|
|
1464
|
+
input.type = 'text';
|
|
1465
|
+
input.className = 'ocpv2-rename-input';
|
|
1466
|
+
input.value = currentTitle === 'New session' ? '' : currentTitle;
|
|
1467
|
+
input.placeholder = 'Session name…';
|
|
1468
|
+
input.addEventListener('click', (event) => event.stopPropagation());
|
|
1469
|
+
input.addEventListener('mousedown', (event) => event.stopPropagation());
|
|
1470
|
+
_sessionLabelEl.textContent = '';
|
|
1471
|
+
_sessionLabelEl.appendChild(input);
|
|
1472
|
+
input.focus();
|
|
1473
|
+
input.select();
|
|
1474
|
+
|
|
1475
|
+
const restoreLabel = () => {
|
|
1476
|
+
if (_sessionLabelEl.contains(input)) {
|
|
1477
|
+
_sessionLabelEl.textContent = sessionTitleFor(getState());
|
|
1478
|
+
}
|
|
1479
|
+
};
|
|
1480
|
+
// The box is gone once it has been answered, also after a rename that
|
|
1481
|
+
// worked: left in the header it would be a finished box, and the next
|
|
1482
|
+
// Rename would focus it and do nothing (renameBox).
|
|
1483
|
+
let rename = null;
|
|
1484
|
+
const finish = renameBox({
|
|
1485
|
+
currentTitle,
|
|
1486
|
+
read: () => input.value,
|
|
1487
|
+
rename: (next) => {
|
|
1488
|
+
// Pending from here until it is answered (settleRename): nothing
|
|
1489
|
+
// re-asserts this title meanwhile.
|
|
1490
|
+
rename = _pendingRenames.begin(target.sessionId, next, titleStateBefore(target.sessionId));
|
|
1491
|
+
markOpenCodeTitleManual(target.sessionId, next);
|
|
1492
|
+
cancelTitleReassert(target.sessionId);
|
|
1493
|
+
return renameSession(getDefaultStore(), api, next, { binding: target });
|
|
1494
|
+
},
|
|
1495
|
+
after: (result, next) => {
|
|
1496
|
+
// Refused, or never answered: the session keeps the title it had, here too.
|
|
1497
|
+
settleRename(rename, result.ok);
|
|
1498
|
+
if (result.ok) {
|
|
1499
|
+
// On another binding the title still belongs to its session: its tab
|
|
1500
|
+
// (and the store, should the panel be back on it) gets it by id. The
|
|
1501
|
+
// header then shows the title of the session that is on screen.
|
|
1502
|
+
if (!result.applied) updateOpenCodeTitleLocally(result.sessionId, next);
|
|
1503
|
+
return;
|
|
1504
|
+
}
|
|
1505
|
+
console.warn('[ocp-v2-panel] rename failed', result.error);
|
|
1506
|
+
// The banner is for the session that was renamed, not for the one on screen now.
|
|
1507
|
+
if (result.current) pushError({ message: result.error || 'Rename failed' });
|
|
1508
|
+
},
|
|
1509
|
+
restore: restoreLabel,
|
|
1510
|
+
});
|
|
1511
|
+
input.addEventListener('keydown', (event) => {
|
|
1512
|
+
if (event.key === 'Enter') { event.preventDefault(); finish(false); }
|
|
1513
|
+
if (event.key === 'Escape') { event.preventDefault(); finish(true); }
|
|
1514
|
+
});
|
|
1515
|
+
input.addEventListener('blur', () => finish(false));
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
async function boot() {
|
|
1519
|
+
if (_booted) return;
|
|
1520
|
+
if (_bootPromise) return _bootPromise;
|
|
1521
|
+
_bootPromise = (async () => {
|
|
1522
|
+
try {
|
|
1523
|
+
// Boot binds the panel to a session unless the user chose one while it
|
|
1524
|
+
// was starting (a tab click, New session): captured before the first
|
|
1525
|
+
// wait, and never overtaking a choice (observe).
|
|
1526
|
+
const bootNav = _nav.observe();
|
|
1527
|
+
// 1. Ensure server up + WS open
|
|
1528
|
+
const init = await api.init();
|
|
1529
|
+
setServerStatus({
|
|
1530
|
+
status: init?.ready ? 'ready' : 'offline',
|
|
1531
|
+
port: init?.port,
|
|
1532
|
+
version: init?.version,
|
|
1533
|
+
managed: init?.managed,
|
|
1534
|
+
});
|
|
1535
|
+
// 2. Register with the multi-panel manager so child sub-agent sessions
|
|
1536
|
+
// get auto-spawned panels.
|
|
1537
|
+
registerPrimaryPanel({
|
|
1538
|
+
panelId: PANEL_OWNER,
|
|
1539
|
+
store: getDefaultStore(),
|
|
1540
|
+
getSessionId: () => getState().sessionId,
|
|
1541
|
+
// The sub-agents of every tab get their panels here (one set per tab).
|
|
1542
|
+
ownsSession: (sessionId) => _tabSessionIds.includes(sessionId),
|
|
1543
|
+
// Prompts of a sub-agent panel that went away with its session.
|
|
1544
|
+
keepPrompts: (items, label) => _composer?.keep?.(items, label),
|
|
1545
|
+
});
|
|
1546
|
+
// 3. Kick off project load in parallel — used by promptNameNewSession modal
|
|
1547
|
+
loadProjectsOnce().catch(() => {});
|
|
1548
|
+
// 4. Restore last-used project as default cwd
|
|
1549
|
+
const lastProject = storage.getItem(STOR_PROJECT) || '';
|
|
1550
|
+
if (lastProject) setCwd(lastProject);
|
|
1551
|
+
// 5. Restore tab sessions from previous run (drop ones the server lost)
|
|
1552
|
+
const savedTabs = loadTabSessions();
|
|
1553
|
+
const liveTabs = [];
|
|
1554
|
+
if (savedTabs.length) {
|
|
1555
|
+
let serverSessions = [];
|
|
1556
|
+
try {
|
|
1557
|
+
const res = await api.sessionList();
|
|
1558
|
+
serverSessions = Array.isArray(res?.data) ? res.data : (res?.data?.sessions || []);
|
|
1559
|
+
} catch (err) {
|
|
1560
|
+
console.warn('[ocp-v2-panel] sessionList failed during tab restore', err);
|
|
1561
|
+
}
|
|
1562
|
+
const byId = new Map(serverSessions.map((s) => [s?.id || s?.sessionID, s]));
|
|
1563
|
+
for (const sid of savedTabs) {
|
|
1564
|
+
const info = byId.get(sid);
|
|
1565
|
+
if (info) {
|
|
1566
|
+
liveTabs.push(sid);
|
|
1567
|
+
_tabInfoCache.set(sid, _stoppedShares.clean(info));
|
|
1568
|
+
ensureOpenCodeTitleState(sid, info, { manual: true });
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
_tabSessionIds = liveTabs;
|
|
1572
|
+
saveTabSessions();
|
|
1573
|
+
}
|
|
1574
|
+
if (!liveTabs.length && _pendingAutomationAttach?.sessionId) {
|
|
1575
|
+
const { sessionId, info } = _pendingAutomationAttach;
|
|
1576
|
+
liveTabs.push(sessionId);
|
|
1577
|
+
_tabInfoCache.set(sessionId, _stoppedShares.clean(info || { id: sessionId }));
|
|
1578
|
+
ensureOpenCodeTitleState(sessionId, info, { manual: true });
|
|
1579
|
+
_tabSessionIds = [...liveTabs];
|
|
1580
|
+
saveTabSessions();
|
|
1581
|
+
}
|
|
1582
|
+
// 6. If we have a restorable tab, switch to the most recent; otherwise
|
|
1583
|
+
// auto-create one fresh session.
|
|
1584
|
+
if (!bootNav.isCurrent()) {
|
|
1585
|
+
// Another navigation bound the panel meanwhile: the tabs are restored,
|
|
1586
|
+
// the session on screen stays.
|
|
1587
|
+
renderPill();
|
|
1588
|
+
} else if (liveTabs.length) {
|
|
1589
|
+
const targetSid = liveTabs[liveTabs.length - 1];
|
|
1590
|
+
const targetInfo = _tabInfoCache.get(targetSid) || null;
|
|
1591
|
+
bootNav.apply(() => {
|
|
1592
|
+
setSession(targetSid, targetInfo);
|
|
1593
|
+
bindPrimarySession(getDefaultStore(), targetSid);
|
|
1594
|
+
});
|
|
1595
|
+
const detailed = restoreSessionSelections(targetSid, targetInfo);
|
|
1596
|
+
detailed.catch((err) => console.warn('[ocp-v2-panel] session selections restore failed', err));
|
|
1597
|
+
// Transcript, run state and approvals. An in-flight turn that survived
|
|
1598
|
+
// the reload shows as running, not "idle".
|
|
1599
|
+
const hydrated = await hydrateBoundSession(targetSid);
|
|
1600
|
+
if (!hydrated.moved && !hydrated.transcript) console.warn('[ocp-v2-panel] sessionMessages failed on boot', hydrated.error);
|
|
1601
|
+
renderPill();
|
|
1602
|
+
} else {
|
|
1603
|
+
// No tab to restore: an untouched session of the project, else a new
|
|
1604
|
+
// one, and neither once the user chose something meanwhile (also when
|
|
1605
|
+
// the lookup failed).
|
|
1606
|
+
await landWithoutTabs(bootNav, {
|
|
1607
|
+
adopt: (nav) => adoptEmptySession(lastProject, nav),
|
|
1608
|
+
create: (nav) => startFreshSession({ cwd: lastProject || undefined, nav }),
|
|
1609
|
+
});
|
|
1610
|
+
}
|
|
1611
|
+
_booted = true;
|
|
1612
|
+
// Tell ui-sidepanel-tray we own opencode pills now — it drops any
|
|
1613
|
+
// placeholder pills it was rendering from storage.
|
|
1614
|
+
try {
|
|
1615
|
+
window.dispatchEvent(new CustomEvent('sidepanel-tray:provider-loaded', {
|
|
1616
|
+
detail: { provider: 'opencode' },
|
|
1617
|
+
}));
|
|
1618
|
+
} catch {}
|
|
1619
|
+
} catch (err) {
|
|
1620
|
+
console.error('[ocp-v2-panel] boot failed:', err);
|
|
1621
|
+
pushError({ message: err?.message || 'Boot failed' });
|
|
1622
|
+
} finally {
|
|
1623
|
+
_bootPromise = null;
|
|
1624
|
+
}
|
|
1625
|
+
})();
|
|
1626
|
+
return _bootPromise;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
// Tabs of the other SynaBun windows: a session open over there is theirs.
|
|
1630
|
+
function otherWindowTabIds() {
|
|
1631
|
+
const ids = [];
|
|
1632
|
+
try {
|
|
1633
|
+
const registry = JSON.parse(storage.getItem(STOR_WINDOW_REGISTRY) || '{}');
|
|
1634
|
+
for (const windowId of Object.keys(registry)) {
|
|
1635
|
+
if (windowId === OCP_WINDOW_ID) continue;
|
|
1636
|
+
const tabs = JSON.parse(storage.getItem(`opencode-v2-tabs-${windowId}`) || '[]');
|
|
1637
|
+
if (Array.isArray(tabs)) ids.push(...tabs);
|
|
1638
|
+
}
|
|
1639
|
+
} catch {}
|
|
1640
|
+
return ids;
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
// Opening the panel with no tab to restore used to create one more empty
|
|
1644
|
+
// session every time. Take the newest untouched session of this project
|
|
1645
|
+
// instead, when there is one nobody else has open.
|
|
1646
|
+
// Resolves 'adopted', 'none' (the caller may create one) or 'moved' (another
|
|
1647
|
+
// navigation took the panel while the list was read, or failed to be read:
|
|
1648
|
+
// nothing is adopted, and the caller creates nothing either).
|
|
1649
|
+
async function adoptEmptySession(cwd, nav = _nav.observe()) {
|
|
1650
|
+
if (!cwd) return 'none';
|
|
1651
|
+
const filters = supports('feature:session-list-filters');
|
|
1652
|
+
const out = await adoptReusableSession(nav, {
|
|
1653
|
+
list: async () => {
|
|
1654
|
+
const res = await api.sessionList(filters ? { roots: true, limit: 50 } : undefined);
|
|
1655
|
+
return Array.isArray(res?.data) ? res.data : [];
|
|
1656
|
+
},
|
|
1657
|
+
pick: (sessions) => pickReusableEmptySession(sessions, cwd, { excludeIds: otherWindowTabIds() }),
|
|
1658
|
+
bind: (reusable) => {
|
|
1659
|
+
ensureOpenCodeTitleState(reusable.id, reusable, { fresh: true });
|
|
1660
|
+
setSession(reusable.id, reusable);
|
|
1661
|
+
bindPrimarySession(getDefaultStore(), reusable.id);
|
|
1662
|
+
},
|
|
1663
|
+
});
|
|
1664
|
+
if (out === 'adopted') renderPill();
|
|
1665
|
+
return out;
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
// Mirrors Claude/Codex: create the new session immediately, then surface the
|
|
1669
|
+
// rename modal as an optional follow-up. Skipping the modal still leaves the
|
|
1670
|
+
// fresh session in place.
|
|
1671
|
+
function beginNewSession() {
|
|
1672
|
+
if (openAnotherSidepanelSession(PANEL_OWNER, 'opencode', { project: getState().cwd || '' })) return;
|
|
1673
|
+
const cwd = getState().cwd || storage.getItem(STOR_PROJECT) || undefined;
|
|
1674
|
+
// Preserve the current session as a tab pill before spawning the new one.
|
|
1675
|
+
const prevSid = getState().sessionId;
|
|
1676
|
+
if (prevSid) ensureTab(prevSid, getState().sessionInfo);
|
|
1677
|
+
// The click is a navigation of its own: the session it creates takes the
|
|
1678
|
+
// panel, and gets the naming dialog, only while no newer choice was made.
|
|
1679
|
+
const nav = _nav.begin();
|
|
1680
|
+
let created = null;
|
|
1681
|
+
startFreshSession({ cwd, nav })
|
|
1682
|
+
.then((out) => { created = out; })
|
|
1683
|
+
.catch((err) => pushError({ message: err?.message || 'New session failed' }))
|
|
1684
|
+
.finally(() => {
|
|
1685
|
+
loadProjectsOnce().catch(() => {}).finally(() => {
|
|
1686
|
+
// The dialog names the session this click created, and only while
|
|
1687
|
+
// that session is what the panel shows: not after the user went
|
|
1688
|
+
// elsewhere (during the creation or during the project list), and
|
|
1689
|
+
// not when the creation failed (there is nothing to name).
|
|
1690
|
+
if (!shouldNameCreatedSession(getDefaultStore(), nav, created)) return;
|
|
1691
|
+
promptNameNewSession(created.sessionId);
|
|
1692
|
+
});
|
|
1693
|
+
});
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
// Creates a session and binds the panel to it. `nav` is the intent of the
|
|
1697
|
+
// navigation that asked for it (a click, a project change, boot); without one
|
|
1698
|
+
// it begins here. The navigation owns the panel only while it is the latest:
|
|
1699
|
+
// when the user chose something else while the new session was being created,
|
|
1700
|
+
// the new one is kept as a tab and nothing on screen is cleared or replaced.
|
|
1701
|
+
// Resolves `{ sessionId, activated }`.
|
|
1702
|
+
async function startFreshSession({ title, cwd, nav } = {}) {
|
|
1703
|
+
const owner = nav || _nav.begin();
|
|
1704
|
+
// Reset local message state so we don't show stale parts from a previous session
|
|
1705
|
+
if (owner.isCurrent()) clearMessages();
|
|
1706
|
+
const projectCwd = cwd || getState().cwd || undefined;
|
|
1707
|
+
const { session: sess, activate } = await createSessionForNavigation(getDefaultStore(), api, { title, cwd: projectCwd, nav: owner });
|
|
1708
|
+
const titleState = ensureOpenCodeTitleState(sess.id, sess, { fresh: true, manual: !!title });
|
|
1709
|
+
if (title && titleState) {
|
|
1710
|
+
titleState.title = title;
|
|
1711
|
+
saveTitleStates();
|
|
1712
|
+
}
|
|
1713
|
+
const activated = activate && owner.apply(() => {
|
|
1714
|
+
// The store stayed subscribed to the session being left while the new one
|
|
1715
|
+
// was created: messages that arrived meanwhile are that session's. (Its
|
|
1716
|
+
// pending approvals and questions are dropped by setSession itself.)
|
|
1717
|
+
if (getState().messageOrder.length) clearMessages();
|
|
1718
|
+
setSession(sess.id, sess);
|
|
1719
|
+
bindPrimarySession(getDefaultStore(), sess.id);
|
|
1720
|
+
if (cwd) setCwd(cwd);
|
|
1721
|
+
});
|
|
1722
|
+
if (!activated) ensureTab(sess.id, sess);
|
|
1723
|
+
// SDK may ignore title at create-time; rename as fallback so the label shows.
|
|
1724
|
+
if (title && sess.title !== title) {
|
|
1725
|
+
// Best effort, as before: nothing here shows the title as set, and the
|
|
1726
|
+
// panel's own title for the session is pushed again when OpenCode next
|
|
1727
|
+
// reports the session (keepOwnSessionTitle). A refusal is logged like a
|
|
1728
|
+
// request that threw.
|
|
1729
|
+
try {
|
|
1730
|
+
const named = await api.sessionUpdate(sess.id, { title });
|
|
1731
|
+
if (replyFailed(named)) console.warn('[ocp-v2-panel] sessionUpdate refused', replyError(named, 'Rename failed'));
|
|
1732
|
+
} catch (err) { console.warn('[ocp-v2-panel] sessionUpdate failed', err); }
|
|
1733
|
+
}
|
|
1734
|
+
renderPill();
|
|
1735
|
+
return { sessionId: sess.id, activated };
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
// Switching the active project on a live session is a hard cut for OpenCode:
|
|
1739
|
+
// the session is bound to the cwd at creation, so we drop it and start fresh.
|
|
1740
|
+
async function setActiveProject(path) {
|
|
1741
|
+
const next = String(path || '');
|
|
1742
|
+
const prev = getState().cwd || '';
|
|
1743
|
+
if (next === prev) return;
|
|
1744
|
+
|
|
1745
|
+
// This navigation owns the panel from here until its new session is bound,
|
|
1746
|
+
// unless the user chooses something else while it waits.
|
|
1747
|
+
const nav = _nav.begin();
|
|
1748
|
+
|
|
1749
|
+
// Persist the new project as the default cwd
|
|
1750
|
+
setCwd(next);
|
|
1751
|
+
if (next) storage.setItem(STOR_PROJECT, next);
|
|
1752
|
+
else storage.removeItem(STOR_PROJECT);
|
|
1753
|
+
|
|
1754
|
+
// If a turn is in flight, abort it before yanking the session. The session
|
|
1755
|
+
// that is aborted is the one the project was changed on; when another one
|
|
1756
|
+
// was selected while the abort was out, that selection stands: it is not
|
|
1757
|
+
// cleared, and no session is created over it.
|
|
1758
|
+
if (!(await stopForNavigation(getDefaultStore(), api, nav))) return;
|
|
1759
|
+
|
|
1760
|
+
// Drop any existing session — next send will run on the new cwd
|
|
1761
|
+
if (nav.sessionId) {
|
|
1762
|
+
nav.apply(() => {
|
|
1763
|
+
clearMessages();
|
|
1764
|
+
setSession(null, null);
|
|
1765
|
+
bindPrimarySession(getDefaultStore(), null);
|
|
1766
|
+
});
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
// Eagerly create a fresh session at the new cwd so the user can send immediately
|
|
1770
|
+
try {
|
|
1771
|
+
await startFreshSession({ cwd: next || undefined, nav });
|
|
1772
|
+
} catch (err) {
|
|
1773
|
+
pushError({ message: err?.message || 'New session failed' });
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
// Re-scope the session menu to the new project if it was open
|
|
1777
|
+
if (_sessionMenuEl?.classList.contains('open')) {
|
|
1778
|
+
try { renderSessionMenu(); } catch {}
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
async function loadProjectsOnce() {
|
|
1783
|
+
if (_projectsLoaded) return _projects;
|
|
1784
|
+
if (_projectsLoadPromise) return _projectsLoadPromise;
|
|
1785
|
+
_projectsLoadPromise = (async () => {
|
|
1786
|
+
try {
|
|
1787
|
+
const res = await fetchProjects();
|
|
1788
|
+
_projects = Array.isArray(res) ? res : (res?.projects || []);
|
|
1789
|
+
_projectsLoaded = true;
|
|
1790
|
+
} catch (err) {
|
|
1791
|
+
console.warn('[ocp-v2-panel] fetchProjects failed', err);
|
|
1792
|
+
_projects = [];
|
|
1793
|
+
} finally {
|
|
1794
|
+
_projectsLoadPromise = null;
|
|
1795
|
+
}
|
|
1796
|
+
return _projects;
|
|
1797
|
+
})();
|
|
1798
|
+
return _projectsLoadPromise;
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
async function fetchBranchesForModal(path) {
|
|
1802
|
+
if (!path) return { branches: [], current: null };
|
|
1803
|
+
try {
|
|
1804
|
+
const res = await fetch(`/api/terminal/branches?path=${encodeURIComponent(path)}`);
|
|
1805
|
+
return await res.json();
|
|
1806
|
+
} catch { return { branches: [], current: null }; }
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
// `targetSessionId` is the session the dialog names: the one that was just
|
|
1810
|
+
// created. It is fixed when the dialog opens (the session on screen then, when
|
|
1811
|
+
// none is given) and never read from the live state afterwards.
|
|
1812
|
+
function promptNameNewSession(targetSessionId = null) {
|
|
1813
|
+
if (!_panelEl) return;
|
|
1814
|
+
if (_panelEl.querySelector('.ocpv2-name-modal-overlay')) return;
|
|
1815
|
+
const target = captureBinding(getDefaultStore(), targetSessionId ? { sessionId: targetSessionId } : {});
|
|
1816
|
+
// The dialog is for a session, and for the one on screen: there is nothing
|
|
1817
|
+
// to name otherwise.
|
|
1818
|
+
if (!target.sessionId || !target.isCurrent()) return;
|
|
1819
|
+
|
|
1820
|
+
const overlay = document.createElement('div');
|
|
1821
|
+
overlay.className = 'ocpv2-name-modal-overlay';
|
|
1822
|
+
overlay.innerHTML = `
|
|
1823
|
+
<div class="ocpv2-name-modal" role="dialog" aria-modal="true">
|
|
1824
|
+
<div class="ocpv2-name-modal-title">Name this session</div>
|
|
1825
|
+
<div class="ocpv2-name-modal-row">
|
|
1826
|
+
<label class="ocpv2-name-modal-label">Project</label>
|
|
1827
|
+
<select class="ocpv2-name-modal-select" data-role="project"></select>
|
|
1828
|
+
</div>
|
|
1829
|
+
<div class="ocpv2-name-modal-row">
|
|
1830
|
+
<label class="ocpv2-name-modal-label">Branch</label>
|
|
1831
|
+
<select class="ocpv2-name-modal-select" data-role="branch" disabled></select>
|
|
1832
|
+
</div>
|
|
1833
|
+
<div class="ocpv2-name-modal-row ocpv2-name-modal-worktree" hidden>
|
|
1834
|
+
<label class="ocpv2-name-modal-label">Worktree</label>
|
|
1835
|
+
<label class="ocpv2-name-modal-check">
|
|
1836
|
+
<input type="checkbox" data-role="worktree" />
|
|
1837
|
+
<span>Run in a new git worktree</span>
|
|
1838
|
+
</label>
|
|
1839
|
+
</div>
|
|
1840
|
+
<div class="ocpv2-name-modal-worktrees" hidden></div>
|
|
1841
|
+
<input type="text" class="ocpv2-name-modal-input" placeholder="Session name..." maxlength="120" />
|
|
1842
|
+
<div class="ocpv2-name-modal-actions">
|
|
1843
|
+
<button type="button" class="ocpv2-name-modal-btn skip">Skip</button>
|
|
1844
|
+
<button type="button" class="ocpv2-name-modal-btn save">Save</button>
|
|
1845
|
+
</div>
|
|
1846
|
+
</div>
|
|
1847
|
+
`;
|
|
1848
|
+
_panelEl.appendChild(overlay);
|
|
1849
|
+
|
|
1850
|
+
const input = overlay.querySelector('.ocpv2-name-modal-input');
|
|
1851
|
+
const saveBtn = overlay.querySelector('.ocpv2-name-modal-btn.save');
|
|
1852
|
+
const skipBtn = overlay.querySelector('.ocpv2-name-modal-btn.skip');
|
|
1853
|
+
const projectSel = overlay.querySelector('select[data-role="project"]');
|
|
1854
|
+
const branchSel = overlay.querySelector('select[data-role="branch"]');
|
|
1855
|
+
|
|
1856
|
+
// Project and branch names are data (a directory or a branch can be named
|
|
1857
|
+
// anything): options are built as elements, never as markup.
|
|
1858
|
+
setSelectOptions(projectSel, projectOptionRows(_projects), { placeholder: '(none)' });
|
|
1859
|
+
setSelectOptions(branchSel, [], { placeholder: '(loading...)' });
|
|
1860
|
+
const initialProject = getState().cwd || storage.getItem(STOR_PROJECT) || '';
|
|
1861
|
+
if (projectSel && initialProject) projectSel.value = initialProject;
|
|
1862
|
+
|
|
1863
|
+
let branchOriginal = null;
|
|
1864
|
+
let branchesKnown = false;
|
|
1865
|
+
// A session in a new worktree starts from what the project has checked out
|
|
1866
|
+
// now; the project's own checkout is never switched for it.
|
|
1867
|
+
const syncBranchLock = () => {
|
|
1868
|
+
const locked = !!worktreeCheck?.checked;
|
|
1869
|
+
if (locked && branchOriginal) branchSel.value = branchOriginal;
|
|
1870
|
+
branchSel.disabled = locked || !branchesKnown;
|
|
1871
|
+
branchSel.title = locked ? 'A new worktree starts from the branch the project has checked out.' : '';
|
|
1872
|
+
};
|
|
1873
|
+
let branchRequest = 0;
|
|
1874
|
+
async function refreshBranches(path) {
|
|
1875
|
+
const request = ++branchRequest;
|
|
1876
|
+
branchesKnown = false;
|
|
1877
|
+
branchSel.disabled = true;
|
|
1878
|
+
setSelectOptions(branchSel, [], { placeholder: '(loading...)' });
|
|
1879
|
+
if (!path) {
|
|
1880
|
+
setSelectOptions(branchSel, [], { placeholder: '(none)' });
|
|
1881
|
+
branchOriginal = null;
|
|
1882
|
+
return;
|
|
1883
|
+
}
|
|
1884
|
+
const data = await fetchBranchesForModal(path);
|
|
1885
|
+
if (request !== branchRequest) return; // another project was picked meanwhile
|
|
1886
|
+
branchOriginal = data.current || null;
|
|
1887
|
+
const branches = (Array.isArray(data.branches) ? data.branches : []).filter((b) => typeof b === 'string' && b);
|
|
1888
|
+
if (!branches.length) {
|
|
1889
|
+
setSelectOptions(branchSel, [], { placeholder: '(none)' });
|
|
1890
|
+
} else {
|
|
1891
|
+
setSelectOptions(branchSel, branches);
|
|
1892
|
+
if (branchOriginal) branchSel.value = branchOriginal;
|
|
1893
|
+
branchesKnown = true;
|
|
1894
|
+
}
|
|
1895
|
+
syncBranchLock();
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
// Worktrees: an isolated checkout OpenCode creates for the session, so its
|
|
1899
|
+
// edits stay out of the main working tree. Offered only when the server can.
|
|
1900
|
+
const worktreeRow = overlay.querySelector('.ocpv2-name-modal-worktree');
|
|
1901
|
+
const worktreeCheck = overlay.querySelector('input[data-role="worktree"]');
|
|
1902
|
+
const worktreeList = overlay.querySelector('.ocpv2-name-modal-worktrees');
|
|
1903
|
+
worktreeCheck?.addEventListener('change', syncBranchLock);
|
|
1904
|
+
refreshBranches(projectSel?.value || initialProject);
|
|
1905
|
+
const worktreeReads = latestOnly();
|
|
1906
|
+
// Removing a worktree is asked in the dialog, under its row.
|
|
1907
|
+
const worktreeConfirmAnchors = new Map();
|
|
1908
|
+
const dialogQuestion = (view) => view.surface === 'dialog';
|
|
1909
|
+
const offWorktreeConfirm = _confirms.onChange(() => {
|
|
1910
|
+
syncConfirmRow(_confirms, worktreeList, 'dialog', (key) => worktreeConfirmAnchors.get(key));
|
|
1911
|
+
});
|
|
1912
|
+
async function refreshWorktrees(path) {
|
|
1913
|
+
// The rows are read again: a question about one of them is off.
|
|
1914
|
+
_confirms.cancel(dialogQuestion);
|
|
1915
|
+
worktreeConfirmAnchors.clear();
|
|
1916
|
+
const canCreate = supports('worktree:create') && !!path;
|
|
1917
|
+
worktreeRow.hidden = !canCreate;
|
|
1918
|
+
if (!canCreate) worktreeCheck.checked = false;
|
|
1919
|
+
syncBranchLock();
|
|
1920
|
+
worktreeList.hidden = true;
|
|
1921
|
+
worktreeList.textContent = '';
|
|
1922
|
+
if (!path || !supports('worktree:list', 'worktree:remove')) return;
|
|
1923
|
+
const latest = worktreeReads.begin();
|
|
1924
|
+
const res = await api.worktreeList({ cwd: path }).catch(() => null);
|
|
1925
|
+
// A newer read (another project picked, a worktree removed) owns the list.
|
|
1926
|
+
if (!latest() || replyFailed(res) || !Array.isArray(res.data) || !res.data.length || projectSel?.value !== path) return;
|
|
1927
|
+
worktreeList.hidden = false;
|
|
1928
|
+
for (const tree of res.data) {
|
|
1929
|
+
const row = document.createElement('div');
|
|
1930
|
+
row.className = 'ocpv2-permission-more';
|
|
1931
|
+
const name = document.createElement('code');
|
|
1932
|
+
name.textContent = tree.branch ? `${tree.name} (${tree.branch})` : tree.name;
|
|
1933
|
+
name.title = tree.directory;
|
|
1934
|
+
const remove = document.createElement('button');
|
|
1935
|
+
remove.type = 'button';
|
|
1936
|
+
remove.className = 'ocpv2-permission-link';
|
|
1937
|
+
remove.textContent = 'Remove';
|
|
1938
|
+
remove.addEventListener('click', async () => {
|
|
1939
|
+
// This click only asks; the button of the question removes it.
|
|
1940
|
+
const key = `worktree-remove:${tree.directory}`;
|
|
1941
|
+
worktreeConfirmAnchors.set(key, row);
|
|
1942
|
+
const agreed = await _confirms.ask({
|
|
1943
|
+
key, surface: 'dialog', confirmLabel: 'Remove worktree',
|
|
1944
|
+
text: `Remove the worktree "${tree.name}"? Its directory and uncommitted changes are deleted.`,
|
|
1945
|
+
});
|
|
1946
|
+
if (!agreed) return;
|
|
1947
|
+
const removed = await api.worktreeRemove({ cwd: path, directory: tree.directory }).catch((err) => ({ ok: false, error: err?.message }));
|
|
1948
|
+
// The failure is about the worktree, not about a session: it is shown
|
|
1949
|
+
// wherever the panel is. The list is this dialog's: it is read again
|
|
1950
|
+
// only while the dialog is open and still on that project.
|
|
1951
|
+
if (replyFailed(removed)) { pushError({ message: replyError(removed, 'Could not remove the worktree') }); return; }
|
|
1952
|
+
if (done || projectSel?.value !== path) return;
|
|
1953
|
+
refreshWorktrees(path);
|
|
1954
|
+
});
|
|
1955
|
+
row.append(name, remove);
|
|
1956
|
+
worktreeList.appendChild(row);
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
refreshWorktrees(projectSel?.value || initialProject);
|
|
1960
|
+
|
|
1961
|
+
projectSel?.addEventListener('change', () => {
|
|
1962
|
+
refreshBranches(projectSel.value);
|
|
1963
|
+
refreshWorktrees(projectSel.value);
|
|
1964
|
+
});
|
|
1965
|
+
|
|
1966
|
+
setTimeout(() => { input.focus(); input.select(); }, 10);
|
|
1967
|
+
|
|
1968
|
+
let done = false;
|
|
1969
|
+
const close = () => {
|
|
1970
|
+
if (done) return;
|
|
1971
|
+
done = true;
|
|
1972
|
+
offWorktreeConfirm();
|
|
1973
|
+
_confirms.cancel(dialogQuestion);
|
|
1974
|
+
overlay.remove();
|
|
1975
|
+
};
|
|
1976
|
+
|
|
1977
|
+
const commit = async (cancel) => {
|
|
1978
|
+
if (done) return;
|
|
1979
|
+
const nextTitle = input.value.trim();
|
|
1980
|
+
const chosenProject = projectSel?.value || '';
|
|
1981
|
+
const chosenBranch = branchSel?.value || '';
|
|
1982
|
+
const wantsWorktree = !!worktreeCheck?.checked && !!chosenProject;
|
|
1983
|
+
close();
|
|
1984
|
+
// Skip leaves the freshly-created session in place — same as Claude/Codex
|
|
1985
|
+
// where dismissing the rename modal keeps the empty new tab.
|
|
1986
|
+
if (cancel) return;
|
|
1987
|
+
|
|
1988
|
+
const plan = newSessionPlan({
|
|
1989
|
+
project: chosenProject, branch: chosenBranch, currentBranch: branchOriginal || '',
|
|
1990
|
+
wantsWorktree, cwd: getState().cwd || '',
|
|
1991
|
+
});
|
|
1992
|
+
const projectChanged = plan.projectChanged;
|
|
1993
|
+
// What Save starts is a navigation of its own when it creates a session
|
|
1994
|
+
// (in a worktree, in another project): it owns the panel from this click
|
|
1995
|
+
// on, unless the user chooses something else while it waits. A Save that
|
|
1996
|
+
// only names the session navigates nowhere and begins no intent.
|
|
1997
|
+
const nav = (plan.worktree || projectChanged) ? _nav.begin() : null;
|
|
1998
|
+
|
|
1999
|
+
// The worktree first: an isolated session never switches the branch of the
|
|
2000
|
+
// project's own checkout.
|
|
2001
|
+
if (plan.worktree) {
|
|
2002
|
+
await startSessionInNewWorktree(chosenProject, nextTitle, nav);
|
|
2003
|
+
return;
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
if (plan.checkout) {
|
|
2007
|
+
// A session started after a failed switch would run on the wrong base.
|
|
2008
|
+
let outcome;
|
|
2009
|
+
try {
|
|
2010
|
+
const res = await fetch('/api/terminal/checkout', {
|
|
2011
|
+
method: 'POST',
|
|
2012
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2013
|
+
body: JSON.stringify(plan.checkout),
|
|
2014
|
+
});
|
|
2015
|
+
outcome = checkoutOutcome(res.status, await res.json().catch(() => null));
|
|
2016
|
+
} catch (err) {
|
|
2017
|
+
outcome = { ok: false, error: `Could not switch branch: ${err?.message || 'request failed'}` };
|
|
2018
|
+
}
|
|
2019
|
+
if (!outcome.ok) {
|
|
2020
|
+
pushError({ message: outcome.error });
|
|
2021
|
+
return;
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
if (projectChanged) {
|
|
2026
|
+
if (nav.isCurrent()) {
|
|
2027
|
+
setCwd(chosenProject);
|
|
2028
|
+
storage.setItem(STOR_PROJECT, chosenProject);
|
|
2029
|
+
// Preserve the just-created session as a tab before swapping to a new cwd.
|
|
2030
|
+
const prevSid = getState().sessionId;
|
|
2031
|
+
if (prevSid) ensureTab(prevSid, getState().sessionInfo);
|
|
2032
|
+
}
|
|
2033
|
+
try {
|
|
2034
|
+
await startFreshSession({ title: nextTitle || undefined, cwd: chosenProject, nav });
|
|
2035
|
+
} catch (err) {
|
|
2036
|
+
pushError({ message: err?.message || 'New session failed' });
|
|
2037
|
+
}
|
|
2038
|
+
return;
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
// Name the session this dialog was opened for, wherever the panel is now.
|
|
2042
|
+
const sid = target.sessionId;
|
|
2043
|
+
if (sid && nextTitle) {
|
|
2044
|
+
const rename = _pendingRenames.begin(sid, nextTitle, titleStateBefore(sid));
|
|
2045
|
+
markOpenCodeTitleManual(sid, nextTitle);
|
|
2046
|
+
cancelTitleReassert(sid);
|
|
2047
|
+
const result = await renameSession(getDefaultStore(), api, nextTitle, { binding: target });
|
|
2048
|
+
// Refused, or never answered: the session was not named.
|
|
2049
|
+
settleRename(rename, result.ok);
|
|
2050
|
+
if (result.ok) {
|
|
2051
|
+
if (!result.applied) updateOpenCodeTitleLocally(sid, nextTitle);
|
|
2052
|
+
} else {
|
|
2053
|
+
console.warn('[ocp-v2-panel] rename failed', result.error);
|
|
2054
|
+
// The dialog is closed by now: without a banner the name typed into
|
|
2055
|
+
// it would be gone without a word. (In the session it was for.)
|
|
2056
|
+
if (result.current) pushError({ message: `Rename failed for “${nextTitle}”: ${result.error || 'no answer'}` });
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
};
|
|
2060
|
+
|
|
2061
|
+
saveBtn.addEventListener('click', () => commit(false));
|
|
2062
|
+
skipBtn.addEventListener('click', () => commit(true));
|
|
2063
|
+
input.addEventListener('keydown', (event) => {
|
|
2064
|
+
if (event.key === 'Enter') { event.preventDefault(); commit(false); }
|
|
2065
|
+
if (event.key === 'Escape') { event.preventDefault(); commit(true); }
|
|
2066
|
+
});
|
|
2067
|
+
overlay.addEventListener('click', (event) => {
|
|
2068
|
+
if (event.target === overlay) commit(true);
|
|
2069
|
+
});
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
// "Run in a new git worktree": OpenCode creates the worktree (and runs the
|
|
2073
|
+
// project's start command in it), then the session is created inside it. The
|
|
2074
|
+
// project stays the remembered default; only this session lives in the worktree.
|
|
2075
|
+
async function startSessionInNewWorktree(projectPath, title, nav = _nav.begin()) {
|
|
2076
|
+
// Subscribe before creating: the ready event can beat the reply. Events are
|
|
2077
|
+
// held until the reply names this worktree, so another creation finishing
|
|
2078
|
+
// (or failing) first is not mistaken for this one.
|
|
2079
|
+
const watch = watchWorktree(onEvent, { timeoutMs: 60_000 });
|
|
2080
|
+
const created = await api.worktreeCreate({ cwd: projectPath }).catch((err) => ({ ok: false, error: err?.message }));
|
|
2081
|
+
if (replyFailed(created) || !created.data?.directory) {
|
|
2082
|
+
watch.cancel();
|
|
2083
|
+
pushError({ message: replyError(created, 'Could not create the worktree') });
|
|
2084
|
+
return;
|
|
2085
|
+
}
|
|
2086
|
+
watch.identify(created.data.name);
|
|
2087
|
+
const state = await watch.result;
|
|
2088
|
+
if (!state.ok) {
|
|
2089
|
+
pushError({ message: state.error });
|
|
2090
|
+
return;
|
|
2091
|
+
}
|
|
2092
|
+
// Up to a minute has passed. The session is created in the worktree either
|
|
2093
|
+
// way; it takes the panel only when the user did not navigate meanwhile
|
|
2094
|
+
// (`nav`), and is a tab otherwise.
|
|
2095
|
+
if (nav.isCurrent()) {
|
|
2096
|
+
const prevSid = getState().sessionId;
|
|
2097
|
+
if (prevSid) ensureTab(prevSid, getState().sessionInfo);
|
|
2098
|
+
}
|
|
2099
|
+
try {
|
|
2100
|
+
await startFreshSession({ title: title || undefined, cwd: created.data.directory, nav });
|
|
2101
|
+
} catch (err) {
|
|
2102
|
+
pushError({ message: err?.message || 'New session failed' });
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
async function closeCurrentSession() {
|
|
2107
|
+
// Update the UI first; server cleanup is best-effort and must not block the
|
|
2108
|
+
// only visible tab from closing.
|
|
2109
|
+
const sid = getState().sessionId;
|
|
2110
|
+
// The tab this closes (with no session on screen and one tab left, that
|
|
2111
|
+
// tab): asked first when prompts are still waiting there. Declined: nothing
|
|
2112
|
+
// is closed and nothing is deleted.
|
|
2113
|
+
const closing = sid ? [sid] : (_tabSessionIds.length > 1 ? [] : [..._tabSessionIds]);
|
|
2114
|
+
for (const id of closing) { if (!(await mayCloseTab(id))) return; }
|
|
2115
|
+
// (Read after the question: the turn may have ended while it was up.)
|
|
2116
|
+
const wasRunning = !!sid && sid === getState().sessionId && !!getState().running;
|
|
2117
|
+
const backendClose = closeSessionBackend(sid, { wasRunning, context: 'close' });
|
|
2118
|
+
const closePromise = handlePillClose(sid, { byUser: true });
|
|
2119
|
+
backendClose.catch(() => {});
|
|
2120
|
+
try { await closePromise; }
|
|
2121
|
+
catch (err) { console.warn('[ocp-v2-panel] local close failed', err); }
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
// Best effort: closing a tab never waits for, or depends on, the server
|
|
2125
|
+
// cleanup. Resolves `{ ok, error? }` for the one caller that promised a
|
|
2126
|
+
// deletion (the session menu's Delete); a refusal is logged like a throw.
|
|
2127
|
+
function deleteSessionBestEffort(sessionId, { abort = false, context = 'delete' } = {}) {
|
|
2128
|
+
if (!sessionId) return Promise.resolve({ ok: true });
|
|
2129
|
+
return (async () => {
|
|
2130
|
+
if (abort) {
|
|
2131
|
+
try { await api.abort(sessionId); }
|
|
2132
|
+
catch (err) { console.warn(`[ocp-v2-panel] abort before ${context} failed`, err); }
|
|
2133
|
+
}
|
|
2134
|
+
let res;
|
|
2135
|
+
try { res = await api.sessionDelete(sessionId); }
|
|
2136
|
+
catch (err) { res = { ok: false, error: err?.message || String(err) }; }
|
|
2137
|
+
if (!replyFailed(res)) return { ok: true };
|
|
2138
|
+
const error = replyError(res, 'Delete failed');
|
|
2139
|
+
console.warn(`[ocp-v2-panel] ${context} sessionDelete failed`, error);
|
|
2140
|
+
return { ok: false, error };
|
|
2141
|
+
})();
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
async function showPrimaryOpencodePanel() {
|
|
2145
|
+
if (!_panelEl) buildPanel();
|
|
2146
|
+
if (_visible) {
|
|
2147
|
+
focusSidepanel(PANEL_OWNER);
|
|
2148
|
+
renderPill();
|
|
2149
|
+
return true;
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
setVisible(true);
|
|
2153
|
+
|
|
2154
|
+
// Lazy connect and boot on first open
|
|
2155
|
+
try {
|
|
2156
|
+
await connect();
|
|
2157
|
+
await boot();
|
|
2158
|
+
setTimeout(() => { if (_visible && state.lastActivePanel === 'opencode') focusCompose(); }, 60);
|
|
2159
|
+
} catch (err) {
|
|
2160
|
+
console.warn('[ocp-v2-panel] open failed', err);
|
|
2161
|
+
}
|
|
2162
|
+
return true;
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
// Which OpenCode panel is on screen (this one or a sub-agent's) is decided in
|
|
2166
|
+
// one place: decideVisibility, applied by the manager.
|
|
2167
|
+
registerMainView({ owner: PANEL_OWNER, isVisible: () => _visible });
|
|
2168
|
+
function setVisible(nextVisible) {
|
|
2169
|
+
requestOpencodeView(nextVisible ? { show: MAIN_VIEW } : { hide: MAIN_VIEW });
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
function applyVisibility(nextVisible) {
|
|
2173
|
+
if (!_panelEl) return;
|
|
2174
|
+
const next = !!nextVisible;
|
|
2175
|
+
if (next === _visible) {
|
|
2176
|
+
renderPill();
|
|
2177
|
+
return;
|
|
2178
|
+
}
|
|
2179
|
+
_visible = next;
|
|
2180
|
+
_panelEl.classList.toggle('ocpv2-open', _visible);
|
|
2181
|
+
if (_visible) {
|
|
2182
|
+
state.lastActivePanel = 'opencode';
|
|
2183
|
+
} else {
|
|
2184
|
+
// The popover is a child of <body>: it does not go with the panel by itself.
|
|
2185
|
+
_contextMenu?.close();
|
|
2186
|
+
}
|
|
2187
|
+
syncSidepanelLayout(PANEL_OWNER);
|
|
2188
|
+
emit('opencode-panel:visibility', _visible);
|
|
2189
|
+
renderPill();
|
|
2190
|
+
window.dispatchEvent(new Event('resize'));
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
// ── Tray pills (one per session "tab") ──────────────────────────────────────
|
|
2194
|
+
// Mirrors Claude/Codex panel behavior: every open session gets a tray pill.
|
|
2195
|
+
// The active session's pill is hidden while the panel is open (because the
|
|
2196
|
+
// panel IS that session). Inactive pills are clickable to switch.
|
|
2197
|
+
|
|
2198
|
+
function getTray() {
|
|
2199
|
+
return document.getElementById('term-minimized-tray');
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
function loadTabSessions() {
|
|
2203
|
+
try {
|
|
2204
|
+
const raw = storage.getItem(STOR_TABS);
|
|
2205
|
+
if (!raw) return [];
|
|
2206
|
+
const parsed = JSON.parse(raw);
|
|
2207
|
+
return Array.isArray(parsed)
|
|
2208
|
+
? parsed.filter((s) => typeof s === 'string' && s && !_foreignActiveAutomationSessions.has(s))
|
|
2209
|
+
: [];
|
|
2210
|
+
} catch { return []; }
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
function saveTabSessions() {
|
|
2214
|
+
try {
|
|
2215
|
+
const existingIds = JSON.parse(storage.getItem(STOR_TABS) || '[]');
|
|
2216
|
+
const automationRows = JSON.parse(storage.getItem(STOR_AUTOMATIONS) || '[]');
|
|
2217
|
+
const foreignSessionIds = new Set((Array.isArray(automationRows) ? automationRows : [])
|
|
2218
|
+
.filter((row) => row?.sessionId && row.ownerId !== nativeLoopWindowId)
|
|
2219
|
+
.map((row) => row.sessionId));
|
|
2220
|
+
const preserved = (Array.isArray(existingIds) ? existingIds : [])
|
|
2221
|
+
.filter((sessionId) => foreignSessionIds.has(sessionId) && !_tabSessionIds.includes(sessionId));
|
|
2222
|
+
storage.setItem(STOR_TABS, JSON.stringify([..._tabSessionIds, ...preserved]));
|
|
2223
|
+
touchOpenCodeWindow();
|
|
2224
|
+
} catch {}
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
function ensureTab(sessionId, info = null) {
|
|
2228
|
+
if (!sessionId) return;
|
|
2229
|
+
if (info) _tabInfoCache.set(sessionId, _stoppedShares.clean(info));
|
|
2230
|
+
if (!_tabSessionIds.includes(sessionId)) {
|
|
2231
|
+
_tabSessionIds.push(sessionId);
|
|
2232
|
+
saveTabSessions();
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
function dismissAutomationSession(sessionId) {
|
|
2237
|
+
const runId = _automationRunIds.get(sessionId);
|
|
2238
|
+
if (!runId) return false;
|
|
2239
|
+
window.dispatchEvent(new CustomEvent('native-loop:dismissed', { detail: { runId } }));
|
|
2240
|
+
if (_automationRunning.has(sessionId)) {
|
|
2241
|
+
fetch('/api/loop/stop', {
|
|
2242
|
+
method: 'POST',
|
|
2243
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2244
|
+
body: JSON.stringify({ runId }),
|
|
2245
|
+
}).catch(() => {});
|
|
2246
|
+
}
|
|
2247
|
+
return true;
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
function closeSessionBackend(sessionId, { wasRunning = false, context = 'close' } = {}) {
|
|
2251
|
+
const runId = _automationRunIds.get(sessionId);
|
|
2252
|
+
if (!runId) return deleteSessionBestEffort(sessionId, { abort: wasRunning, context });
|
|
2253
|
+
dismissAutomationSession(sessionId);
|
|
2254
|
+
if (_automationContinuedManually.has(sessionId) || _automationManualSendInFlight.has(sessionId)) {
|
|
2255
|
+
return deleteSessionBestEffort(sessionId, { abort: wasRunning, context });
|
|
2256
|
+
}
|
|
2257
|
+
// An automation's transcript is kept: nothing was asked of the server.
|
|
2258
|
+
return Promise.resolve({ ok: true, kept: true });
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
// Prompts still waiting to be sent in `sessionId`: its queue when it is on
|
|
2262
|
+
// screen, and what is parked for it (a queue the panel left, a prompt that
|
|
2263
|
+
// could not be delivered).
|
|
2264
|
+
// (And what is still unsent in the panels of its sub-agents: closing the tab
|
|
2265
|
+
// deletes the session, and its sub-agents' panels go with it.)
|
|
2266
|
+
function promptsWaitingFor(sessionId) {
|
|
2267
|
+
return [...(_composer?.waitingFor?.(sessionId) || []), ...subagentPromptsWaiting(sessionId)];
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
// Closing a tab discards the prompts that are waiting for its session, so the
|
|
2271
|
+
// user is asked first, in the panel, and shown which. Resolves true when
|
|
2272
|
+
// nothing is waiting, or the user agreed with the second click (the button of
|
|
2273
|
+
// the question; never a native dialog). Every close the user starts goes
|
|
2274
|
+
// through here before anything is closed or deleted; it is the only place the
|
|
2275
|
+
// question is asked.
|
|
2276
|
+
// lead asked also when nothing is waiting (the session menu's Delete)
|
|
2277
|
+
// surface where the question shows: above the compose box, or in the menu
|
|
2278
|
+
// The answer is about the prompts the question listed: when others are
|
|
2279
|
+
// waiting by the time the user agrees, they are asked again.
|
|
2280
|
+
async function mayCloseTab(sessionId, { key = `tab-close:${sessionId}`, surface = 'panel', lead = '', confirmLabel = 'Close and discard' } = {}) {
|
|
2281
|
+
for (;;) {
|
|
2282
|
+
const waiting = promptsWaitingFor(sessionId);
|
|
2283
|
+
const text = closeWithPromptsConfirm({ items: waiting, label: tabLabelFor(sessionId), lead }) || lead;
|
|
2284
|
+
if (!text) return true;
|
|
2285
|
+
// A tab can be closed from the tray while the panel is hidden: the
|
|
2286
|
+
// question is shown, not asked into a closed panel.
|
|
2287
|
+
if (surface === 'panel' && _panelEl && !_visible) setVisible(true);
|
|
2288
|
+
if (!(await _confirms.ask({ key, surface, text, confirmLabel }))) return false;
|
|
2289
|
+
if (samePrompts(promptsWaitingFor(sessionId), waiting)) return true;
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
// `byUser`: the user closed the tab (and was asked, when prompts were
|
|
2294
|
+
// waiting). Without it the session went away by itself (deleted somewhere
|
|
2295
|
+
// else, lost by OpenCode, an automation that detached): there was no moment
|
|
2296
|
+
// to ask, and what was waiting for it is kept for the user.
|
|
2297
|
+
function removeTab(sessionId, { byUser = false } = {}) {
|
|
2298
|
+
if (!sessionId) return;
|
|
2299
|
+
// The session is leaving the panel for good. Prompts that were still
|
|
2300
|
+
// waiting for it (its queue, a send that failed while the panel was
|
|
2301
|
+
// elsewhere) cannot be delivered there any more. The composer drops them
|
|
2302
|
+
// for a tab the user closed, and keeps each one on a notice otherwise
|
|
2303
|
+
// (Put back / Discard).
|
|
2304
|
+
_composer?.forgetSession?.(sessionId, { lost: !byUser, label: tabLabelFor(sessionId) });
|
|
2305
|
+
// Its sub-agents' panels and pills belong to it and go with it. What was
|
|
2306
|
+
// unsent in them is kept on notices when nobody was asked (the composer's rule).
|
|
2307
|
+
releaseSubagentPanels(sessionId, { lost: !byUser });
|
|
2308
|
+
_pendingRenames.forget(sessionId);
|
|
2309
|
+
const before = _tabSessionIds.length;
|
|
2310
|
+
_tabSessionIds = _tabSessionIds.filter((s) => s !== sessionId);
|
|
2311
|
+
if (_tabSessionIds.length !== before) saveTabSessions();
|
|
2312
|
+
_tabInfoCache.delete(sessionId);
|
|
2313
|
+
const titleState = _sessionTitleState.get(sessionId);
|
|
2314
|
+
titleState?.controller?.abort();
|
|
2315
|
+
_sessionTitleState.delete(sessionId);
|
|
2316
|
+
saveTitleStates();
|
|
2317
|
+
const titleTimer = _titleReassertTimers.get(sessionId);
|
|
2318
|
+
if (titleTimer) clearTimeout(titleTimer);
|
|
2319
|
+
_titleReassertTimers.delete(sessionId);
|
|
2320
|
+
_automationRunIds.delete(sessionId);
|
|
2321
|
+
unregisterAutomationSession(sessionId);
|
|
2322
|
+
_automationRunning.delete(sessionId);
|
|
2323
|
+
_automationContinuedManually.delete(sessionId);
|
|
2324
|
+
_automationManualSendInFlight.delete(sessionId);
|
|
2325
|
+
saveAutomationTabs();
|
|
2326
|
+
const pill = _tabPills.get(sessionId);
|
|
2327
|
+
if (pill?.isConnected) pill.remove();
|
|
2328
|
+
_tabPills.delete(sessionId);
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
function tabLabelFor(sessionId) {
|
|
2332
|
+
const activeSid = getState().sessionId;
|
|
2333
|
+
const info = sessionId === activeSid
|
|
2334
|
+
? (getState().sessionInfo || _tabInfoCache.get(sessionId))
|
|
2335
|
+
: _tabInfoCache.get(sessionId);
|
|
2336
|
+
const title = info?.title || info?.slug;
|
|
2337
|
+
if (title) return title;
|
|
2338
|
+
return `OpenCode · ${sessionId.slice(0, 8)}`;
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
function createTabPill(sessionId) {
|
|
2342
|
+
const tray = getTray();
|
|
2343
|
+
if (!tray) return null;
|
|
2344
|
+
const pill = document.createElement('div');
|
|
2345
|
+
pill.className = 'term-minimized-pill ocpv2-session-pill';
|
|
2346
|
+
pill.dataset.owner = PANEL_OWNER;
|
|
2347
|
+
pill.dataset.sessionId = sessionId;
|
|
2348
|
+
pill.innerHTML = `
|
|
2349
|
+
<span class="term-minimized-pill-icon">${OPENCODE_ICON}</span>
|
|
2350
|
+
<span class="term-minimized-pill-label">${escapeHtml(tabLabelFor(sessionId))}</span>
|
|
2351
|
+
<button class="term-minimized-pill-close" data-tooltip="Close" data-tooltip-pos="top">×</button>
|
|
2352
|
+
`;
|
|
2353
|
+
pill.addEventListener('click', (event) => {
|
|
2354
|
+
if (event.target.closest('.term-minimized-pill-close')) return;
|
|
2355
|
+
handlePillClick(sessionId);
|
|
2356
|
+
});
|
|
2357
|
+
pill.querySelector('.term-minimized-pill-close')?.addEventListener('click', async (event) => {
|
|
2358
|
+
event.stopPropagation();
|
|
2359
|
+
// Mirror header X. Native transcripts are preserved unless the user has
|
|
2360
|
+
// continued the terminal automation manually; backend cleanup is best-effort.
|
|
2361
|
+
// Asked first when prompts are still waiting in that session; declined:
|
|
2362
|
+
// the tab stays and nothing is deleted.
|
|
2363
|
+
if (!(await mayCloseTab(sessionId))) return;
|
|
2364
|
+
const wasRunning = sessionId === getState().sessionId && !!getState().running;
|
|
2365
|
+
closeSessionBackend(sessionId, { wasRunning, context: 'pill close' }).catch(() => {});
|
|
2366
|
+
try { await handlePillClose(sessionId, { byUser: true }); }
|
|
2367
|
+
catch (err) { console.warn('[ocp-v2-panel] local pill close failed', err); }
|
|
2368
|
+
});
|
|
2369
|
+
tray.appendChild(pill);
|
|
2370
|
+
return pill;
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
function escapeHtml(s) {
|
|
2374
|
+
return String(s || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
function renderPill() {
|
|
2378
|
+
const tray = getTray();
|
|
2379
|
+
if (!tray) return;
|
|
2380
|
+
const activeSid = getState().sessionId;
|
|
2381
|
+
if (activeSid) ensureTab(activeSid, getState().sessionInfo);
|
|
2382
|
+
|
|
2383
|
+
// Drop pills for sessions no longer in the tab list
|
|
2384
|
+
for (const [sid, pill] of Array.from(_tabPills.entries())) {
|
|
2385
|
+
if (!_tabSessionIds.includes(sid)) {
|
|
2386
|
+
if (pill?.isConnected) pill.remove();
|
|
2387
|
+
_tabPills.delete(sid);
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
for (const sid of _tabSessionIds) {
|
|
2392
|
+
let pill = _tabPills.get(sid);
|
|
2393
|
+
if (!pill || !pill.isConnected) {
|
|
2394
|
+
pill = createTabPill(sid);
|
|
2395
|
+
if (!pill) continue;
|
|
2396
|
+
_tabPills.set(sid, pill);
|
|
2397
|
+
}
|
|
2398
|
+
const labelEl = pill.querySelector('.term-minimized-pill-label');
|
|
2399
|
+
if (labelEl) labelEl.textContent = tabLabelFor(sid);
|
|
2400
|
+
const isActive = sid === activeSid;
|
|
2401
|
+
pill.classList.toggle('ocpv2-pill-active', isActive);
|
|
2402
|
+
pill.classList.toggle('ocpv2-pill-running', _automationRunning.has(sid) || (isActive && !!getState().running));
|
|
2403
|
+
pill.style.display = (isActive && _visible) ? 'none' : '';
|
|
2404
|
+
}
|
|
2405
|
+
// The sub-agent pills of the session on screen sit under its pill.
|
|
2406
|
+
syncSubagentTray();
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
async function handlePillClick(sessionId) {
|
|
2410
|
+
if (!sessionId) return;
|
|
2411
|
+
// A click on a tab is the user's choice of where the panel goes. It may
|
|
2412
|
+
// have to wait for the panel to open (connect, boot); a newer click made
|
|
2413
|
+
// meanwhile wins, and this one then switches nothing.
|
|
2414
|
+
const nav = _nav.begin();
|
|
2415
|
+
const activeSid = getState().sessionId;
|
|
2416
|
+
if (sessionId === activeSid) {
|
|
2417
|
+
if (_visible) setVisible(false);
|
|
2418
|
+
else await showPrimaryOpencodePanel();
|
|
2419
|
+
return;
|
|
2420
|
+
}
|
|
2421
|
+
if (!_panelEl) buildPanel();
|
|
2422
|
+
const went = await navigateAfter(
|
|
2423
|
+
nav,
|
|
2424
|
+
() => (_visible ? null : showPrimaryOpencodePanel()),
|
|
2425
|
+
() => switchToSession(sessionId, _tabInfoCache.get(sessionId) || null, { nav }),
|
|
2426
|
+
);
|
|
2427
|
+
if (went) focusSidepanel(PANEL_OWNER);
|
|
2428
|
+
renderPill();
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
async function handlePillClose(sessionId, { byUser = false } = {}) {
|
|
2432
|
+
if (!sessionId) {
|
|
2433
|
+
if (_tabSessionIds.length > 1) {
|
|
2434
|
+
const next = _tabSessionIds[_tabSessionIds.length - 1];
|
|
2435
|
+
const info = _tabInfoCache.get(next) || null;
|
|
2436
|
+
await switchToSession(next, info);
|
|
2437
|
+
} else {
|
|
2438
|
+
for (const sid of Array.from(_tabSessionIds)) removeTab(sid, { byUser });
|
|
2439
|
+
clearMessages();
|
|
2440
|
+
setSession(null, null);
|
|
2441
|
+
bindPrimarySession(getDefaultStore(), null);
|
|
2442
|
+
if (_visible) setVisible(false);
|
|
2443
|
+
renderPill();
|
|
2444
|
+
}
|
|
2445
|
+
return;
|
|
2446
|
+
}
|
|
2447
|
+
const wasActive = sessionId === getState().sessionId;
|
|
2448
|
+
if (wasActive) {
|
|
2449
|
+
// Clear active session FIRST so downstream renderPill / switchToSession
|
|
2450
|
+
// can't resurrect the tab via their ensureTab(prevSid) guards.
|
|
2451
|
+
setSession(null, null);
|
|
2452
|
+
}
|
|
2453
|
+
removeTab(sessionId, { byUser });
|
|
2454
|
+
if (wasActive) {
|
|
2455
|
+
const next = _tabSessionIds[_tabSessionIds.length - 1];
|
|
2456
|
+
if (next) {
|
|
2457
|
+
const info = _tabInfoCache.get(next) || null;
|
|
2458
|
+
await switchToSession(next, info);
|
|
2459
|
+
} else {
|
|
2460
|
+
clearMessages();
|
|
2461
|
+
bindPrimarySession(getDefaultStore(), null);
|
|
2462
|
+
if (_visible) setVisible(false);
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
renderPill();
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
// Sub-agent child panel coordination ─────────────────────────────────────────
|
|
2469
|
+
on('opencode-panel:show', async () => {
|
|
2470
|
+
try {
|
|
2471
|
+
await showPrimaryOpencodePanel();
|
|
2472
|
+
} catch (err) {
|
|
2473
|
+
console.warn('[ocp-v2-panel] show request failed', err);
|
|
2474
|
+
}
|
|
2475
|
+
});
|
|
2476
|
+
|
|
2477
|
+
// Child panel asks to be shown the primary → open + focus.
|
|
2478
|
+
on('opencode-panel:request-show', async () => {
|
|
2479
|
+
try {
|
|
2480
|
+
await showPrimaryOpencodePanel();
|
|
2481
|
+
} catch (err) {
|
|
2482
|
+
console.warn('[ocp-v2-panel] request-show failed', err);
|
|
2483
|
+
}
|
|
2484
|
+
});
|
|
2485
|
+
|
|
2486
|
+
// Child panel asks for a brand-new top-level session.
|
|
2487
|
+
on('opencode-panel:request-new-session', () => { beginNewSession(); });
|
|
2488
|
+
|
|
2489
|
+
// Whiteboard "Send to Panel" → attach image to compose footer if OCP v2 is the
|
|
2490
|
+
// active sidepanel. Mirrors the gating used by Claude/Codex/OCP v1 so the event
|
|
2491
|
+
// is consumed by exactly one panel.
|
|
2492
|
+
on('wb:send-to-panel', async ({ dataUrl }) => {
|
|
2493
|
+
if (!dataUrl) return;
|
|
2494
|
+
if (state.lastActivePanel !== 'opencode' || isHostedSessionFocused()) return;
|
|
2495
|
+
const match = dataUrl.match(/^data:(image\/[-+.\w]+);base64,/);
|
|
2496
|
+
if (!match) return;
|
|
2497
|
+
const child = getVisibleChildPanel();
|
|
2498
|
+
if (child) {
|
|
2499
|
+
child.store.addAttachedImage({ name: `whiteboard-${Date.now()}.png`, mime: match[1], dataUrl });
|
|
2500
|
+
child.focus();
|
|
2501
|
+
return;
|
|
2502
|
+
}
|
|
2503
|
+
if (!_panelEl) buildPanel();
|
|
2504
|
+
if (!_visible) {
|
|
2505
|
+
await showPrimaryOpencodePanel();
|
|
2506
|
+
}
|
|
2507
|
+
addAttachedImage({
|
|
2508
|
+
name: `whiteboard-${Date.now()}.png`,
|
|
2509
|
+
mime: match[1],
|
|
2510
|
+
dataUrl,
|
|
2511
|
+
});
|
|
2512
|
+
});
|
|
2513
|
+
|
|
2514
|
+
// ── Public API ───────────────────────────────────────────────────────────────
|
|
2515
|
+
|
|
2516
|
+
export async function endHostedOpenCodeSessions() {
|
|
2517
|
+
for (const sessionId of [..._tabSessionIds]) {
|
|
2518
|
+
await closeSessionBackend(sessionId, { wasRunning: true, context: 'detached window close' });
|
|
2519
|
+
await handlePillClose(sessionId);
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2523
|
+
export function isOpencodePanelOpen() { return _visible || isChildPanelOpen(); }
|
|
2524
|
+
|
|
2525
|
+
export async function toggleOpencodePanel() {
|
|
2526
|
+
if (_visible || isChildPanelOpen()) {
|
|
2527
|
+
requestOpencodeView({ hideAll: true });
|
|
2528
|
+
return false;
|
|
2529
|
+
}
|
|
2530
|
+
return showPrimaryOpencodePanel();
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
export async function attachOpenCodeAutomation(run, { focus = !!run?.focus, isCancelled = () => false } = {}) {
|
|
2534
|
+
const sessionId = run?.providerSessionId;
|
|
2535
|
+
if (!run?.runId || !sessionId) return { ok: false, reason: 'identity_pending' };
|
|
2536
|
+
// An automation that asks for focus takes the panel only while the user
|
|
2537
|
+
// chose nothing else since the attach began.
|
|
2538
|
+
const attachBegan = _nav.latest();
|
|
2539
|
+
// Establish ownership before connect()/boot(): native provider events use a
|
|
2540
|
+
// separate socket and may report a Task child while this attach is awaiting.
|
|
2541
|
+
const previousRunId = _automationRunIds.get(sessionId);
|
|
2542
|
+
_automationRunIds.set(sessionId, run.runId);
|
|
2543
|
+
registerAutomationSession(sessionId, run.runId);
|
|
2544
|
+
const running = run.status === 'starting' || run.status === 'running';
|
|
2545
|
+
if (running || previousRunId !== run.runId) {
|
|
2546
|
+
_automationContinuedManually.delete(sessionId);
|
|
2547
|
+
_automationManualSendInFlight.delete(sessionId);
|
|
2548
|
+
}
|
|
2549
|
+
if (running) _automationRunning.add(sessionId);
|
|
2550
|
+
else _automationRunning.delete(sessionId);
|
|
2551
|
+
saveAutomationTabs();
|
|
2552
|
+
const info = {
|
|
2553
|
+
id: sessionId,
|
|
2554
|
+
title: run.title || 'Automation',
|
|
2555
|
+
directory: run.cwd || '',
|
|
2556
|
+
mcpProfile: run.mcpProfile || null,
|
|
2557
|
+
time: { updated: Date.now() },
|
|
2558
|
+
};
|
|
2559
|
+
if (!_booted) _pendingAutomationAttach = { sessionId, info };
|
|
2560
|
+
if (!_panelEl) buildPanel();
|
|
2561
|
+
await connect();
|
|
2562
|
+
await boot();
|
|
2563
|
+
_pendingAutomationAttach = null;
|
|
2564
|
+
if (isCancelled()) {
|
|
2565
|
+
await handlePillClose(sessionId).catch(() => {});
|
|
2566
|
+
return { ok: false, reason: 'dismissed' };
|
|
2567
|
+
}
|
|
2568
|
+
ensureTab(sessionId, info);
|
|
2569
|
+
if (focus && _nav.latest() === attachBegan) {
|
|
2570
|
+
// (Its own navigation, observed after boot landed: a click made while the
|
|
2571
|
+
// panel opens still wins.)
|
|
2572
|
+
const nav = _nav.observe();
|
|
2573
|
+
if (!_visible) await showPrimaryOpencodePanel();
|
|
2574
|
+
await switchToSession(sessionId, info, { nav });
|
|
2575
|
+
} else if (getState().sessionId === sessionId) {
|
|
2576
|
+
setRunning(running);
|
|
2577
|
+
await rehydrateActiveSession().catch(() => {});
|
|
2578
|
+
}
|
|
2579
|
+
if (getState().sessionId === sessionId) setRunning(running);
|
|
2580
|
+
if (run.status === 'failed' && run.error && getState().sessionId === sessionId
|
|
2581
|
+
&& _tabInfoCache.get(sessionId)?._automationLifecycleError !== run.error) {
|
|
2582
|
+
const cachedInfo = _tabInfoCache.get(sessionId) || info;
|
|
2583
|
+
cachedInfo._automationLifecycleError = run.error;
|
|
2584
|
+
_tabInfoCache.set(sessionId, _stoppedShares.clean(cachedInfo));
|
|
2585
|
+
pushError({ message: run.error });
|
|
2586
|
+
}
|
|
2587
|
+
renderPill();
|
|
2588
|
+
return { ok: true, sessionId };
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
export async function detachOpenCodeAutomation(runId) {
|
|
2592
|
+
const entry = [..._automationRunIds].find(([, value]) => value === runId);
|
|
2593
|
+
if (!entry) return false;
|
|
2594
|
+
const [sessionId] = entry;
|
|
2595
|
+
_automationRunIds.delete(sessionId);
|
|
2596
|
+
_automationRunning.delete(sessionId);
|
|
2597
|
+
_automationContinuedManually.delete(sessionId);
|
|
2598
|
+
_automationManualSendInFlight.delete(sessionId);
|
|
2599
|
+
saveAutomationTabs();
|
|
2600
|
+
await handlePillClose(sessionId);
|
|
2601
|
+
return true;
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2604
|
+
export async function openOpencodeWithPrompt(text) {
|
|
2605
|
+
// Open panel, wait for boot, drop the text into the compose box.
|
|
2606
|
+
if (!_visible) await showPrimaryOpencodePanel();
|
|
2607
|
+
// Otherwise ensure we're booted
|
|
2608
|
+
await boot();
|
|
2609
|
+
// Find the compose textarea and prefill (don't auto-send — let user confirm)
|
|
2610
|
+
const ta = _panelEl?.querySelector('.ocpv2-compose-input');
|
|
2611
|
+
if (ta) {
|
|
2612
|
+
ta.value = prefilledDraft(ta.value, text);
|
|
2613
|
+
ta.dispatchEvent(new Event('input', { bubbles: true }));
|
|
2614
|
+
ta.focus();
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
export async function attachPathToOpencode(filePath) {
|
|
2619
|
+
const path = String(filePath || '').trim();
|
|
2620
|
+
if (!path) return false;
|
|
2621
|
+
const child = getVisibleChildPanel();
|
|
2622
|
+
if (child) {
|
|
2623
|
+
const added = child.appendPath(path);
|
|
2624
|
+
child.focus();
|
|
2625
|
+
return added;
|
|
2626
|
+
}
|
|
2627
|
+
if (!_panelEl) buildPanel();
|
|
2628
|
+
if (!_visible) {
|
|
2629
|
+
await showPrimaryOpencodePanel();
|
|
2630
|
+
} else {
|
|
2631
|
+
await boot();
|
|
2632
|
+
}
|
|
2633
|
+
const added = appendPathToCompose(path);
|
|
2634
|
+
setTimeout(() => focusCompose(), 30);
|
|
2635
|
+
return added;
|
|
2636
|
+
}
|