synabun 1.3.0 → 2.0.0-beta
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 +197 -662
- package/LICENSE-COMMERCIAL.md +33 -0
- package/NOTICE +17 -0
- package/README.md +175 -853
- package/THIRD-PARTY-LICENSES.md +131 -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/browser-v2.md +109 -0
- package/docs/hook-integrity.md +89 -0
- package/docs/judgments.md +182 -0
- package/docs/style-guide.md +246 -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/lib/claude-hooks.js +84 -0
- package/lib/claude-usage-audit.js +141 -0
- package/lib/client-config-repair.js +144 -0
- package/lib/data-home-migration.js +498 -0
- package/lib/first-launch-protection.js +240 -0
- package/lib/paths.d.ts +21 -0
- package/lib/paths.js +103 -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 +6983 -0
- package/neural-interface/i18n/pt-BR.json +6987 -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 +579 -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 +50 -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-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 +3296 -0
- package/neural-interface/lib/claude-bypass-policy.js +99 -0
- package/neural-interface/lib/claude-history.js +429 -0
- package/neural-interface/lib/claude-model-catalog.js +483 -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 +650 -0
- package/neural-interface/lib/native-loop-goal.js +33 -0
- package/neural-interface/lib/native-loop-providers.js +1016 -0
- package/neural-interface/lib/native-loop-runtime.js +747 -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 +227 -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 +305 -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 +4682 -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/logo-synabun.png +0 -0
- package/neural-interface/public/logoHD.png +0 -0
- package/neural-interface/public/mcp.svg +4 -0
- package/neural-interface/public/offline.html +335 -299
- package/neural-interface/public/onboarding.html +692 -155
- 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 +311 -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 +589 -0
- package/neural-interface/public/shared/assistant/asst-render.js +3296 -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 +1684 -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/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 +107 -12
- 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/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-search.js +463 -0
- package/neural-interface/public/shared/settings/settings-shell.js +570 -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 +148 -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 +47 -0
- package/neural-interface/public/shared/styleguide/sg-kit.js +281 -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 +229 -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 +124 -0
- package/neural-interface/public/shared/styles.css +7883 -1155
- 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-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-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 +4 -4
- package/neural-interface/public/shared/ui-memory-maintenance.js +111 -0
- package/neural-interface/public/shared/ui-menubar.js +89 -4
- 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 +5793 -2190
- 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 +7877 -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 +8 -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 +556 -521
- package/neural-interface/public/variant/2d/settings-gfx.js +18 -24
- package/neural-interface/public/variant/3d/main.js +516 -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 +37 -23
- 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 +33948 -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 +119 -53
- package/postinstall.js +125 -26
- package/preuninstall.js +187 -0
- package/scripts/audit-claude-hook-usage.mjs +12 -0
- package/scripts/audit-memory-duplicates.mjs +44 -0
- package/scripts/measure-claude-hook-overhead.mjs +77 -0
- package/scripts/recover-user-config.mjs +254 -0
- package/scripts/restore-data-home-from-backup.mjs +239 -0
- package/setup.js +804 -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,3296 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════
|
|
2
|
+
// SynaBun Assistant — transcript renderer
|
|
3
|
+
// ═══════════════════════════════════════════
|
|
4
|
+
// Messages, run cards, route lines and notices are rows; every burst of agent
|
|
5
|
+
// work (tool calls, thinking, short narration) is ONE activity rack: a stage
|
|
6
|
+
// where the SynaBun mascot acts out the call it is on and says the literal
|
|
7
|
+
// command in a bubble, a ledger with one row per tool kind (asst-tool-kinds.js)
|
|
8
|
+
// and a receipt once it settles (asst-rack-styles.js). Text deltas stream into
|
|
9
|
+
// a row throttled to 100 ms ticks and the final `assistant` message replaces
|
|
10
|
+
// it; a short narration directly followed by a call folds into the rack.
|
|
11
|
+
// Replay runs the same handleEvent path with { silent: true }: live minus
|
|
12
|
+
// motion, durations from the journal's `at`. Every row is inserted before the
|
|
13
|
+
// "Working…" indicator so it always stays last.
|
|
14
|
+
// The empty state's character (the hero) outlives a send: parked where it
|
|
15
|
+
// stood, thinking, until the first stage of the turn takes it over (the
|
|
16
|
+
// stage's face flies in from the hero's box) or other content arrives (it
|
|
17
|
+
// fades out). mascot() names the character on screen for the panel's
|
|
18
|
+
// director (asst-mascot.js), which may hold its pose; stills are painted SVGs,
|
|
19
|
+
// never rigs.
|
|
20
|
+
|
|
21
|
+
import { assistantEventKey, fmtCost, fmtElapsed, isRemovableRun, isRunIdle, isTerminalRunStatus, modelShortName, providerShortLabel, routeTargetLabel, runTone, summarizeMailbox } from './asst-state.js';
|
|
22
|
+
import { createRouteLine } from './asst-route.js';
|
|
23
|
+
import { computerToolName, describeComputerAction, frameUrls, markerPercent, openFrameLightbox, parseComputerResult, swapImage } from './asst-computer.js';
|
|
24
|
+
import * as mascotLib from '../synabun-mascot.js';
|
|
25
|
+
import { isReducedMotion, subscribe } from '../synabun-ticker.js';
|
|
26
|
+
import { KIND_SPECS, callOutcome, commandOf, editDiff, editPaths, fmtDuration, groupSubject, kindOf, maskEcho, mcpServerOf, middleEllipsis, planState, plural, prettyServer, receiptSentence, redactInput, resultDetail, sayCall, shouldFoldNarration, stationAggregate, stationKey, targetKey, toolWords, verbText } from './asst-tool-kinds.js';
|
|
27
|
+
import { injectRackStyles } from './asst-rack-styles.js';
|
|
28
|
+
import { formatRunTokens } from './asst-usage.js';
|
|
29
|
+
import { sanitizeHtmlString, sanitizeInto } from './asst-sanitize.js';
|
|
30
|
+
|
|
31
|
+
// ── Markdown (lazy marked, same CDN build as the sidepanels) ───────────────
|
|
32
|
+
let _marked = null;
|
|
33
|
+
let _markedLoading = null;
|
|
34
|
+
function loadMarked() {
|
|
35
|
+
if (_marked || _markedLoading) return _markedLoading;
|
|
36
|
+
_markedLoading = import('https://cdn.jsdelivr.net/npm/marked@14/lib/marked.esm.js')
|
|
37
|
+
.then((m) => {
|
|
38
|
+
_marked = m?.marked || m?.default || null;
|
|
39
|
+
if (_marked?.setOptions) _marked.setOptions({ breaks: true, gfm: true });
|
|
40
|
+
})
|
|
41
|
+
.catch(() => { _marked = null; });
|
|
42
|
+
return _markedLoading;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function esc(value) {
|
|
46
|
+
return String(value ?? '')
|
|
47
|
+
.replace(/&/g, '&')
|
|
48
|
+
.replace(/</g, '<')
|
|
49
|
+
.replace(/>/g, '>')
|
|
50
|
+
.replace(/"/g, '"');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** marked's HTML for `text`: raw, so it only ever reaches the page through asst-sanitize.js. */
|
|
54
|
+
function mdHtml(text) {
|
|
55
|
+
const src = String(text ?? '');
|
|
56
|
+
if (!_marked) return esc(src).replace(/\n/g, '<br>');
|
|
57
|
+
try { return _marked.parse(src); } catch { return esc(src).replace(/\n/g, '<br>'); }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Markdown → sanitized HTML (asst-sanitize.js allowlist): what a brain says is not trusted. */
|
|
61
|
+
export function md(text) {
|
|
62
|
+
return sanitizeHtmlString(mdHtml(text));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Markdown drawn into `node` as fresh, allowlisted nodes. */
|
|
66
|
+
export function mdInto(node, text) {
|
|
67
|
+
return sanitizeInto(node, mdHtml(text));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// The SynaBun two-pill mark — the same glyph as the tab, the keybinds icon and provider-icons.js.
|
|
71
|
+
export const ICON_ASSISTANT = '<svg viewBox="0 0 24 24" fill="currentColor"><rect x="8" y="5.5" width="11" height="5" rx="2.5" transform="rotate(-12 13.5 8)"/><rect x="4.5" y="12.5" width="11" height="5" rx="2.5" transform="rotate(-12 10 15)"/></svg>';
|
|
72
|
+
const ICON_MEMORY = '<svg viewBox="0 0 24 24" fill="currentColor"><rect x="8" y="5.5" width="11" height="5" rx="2.5" transform="rotate(-12 13.5 8)"/><rect x="4.5" y="12.5" width="11" height="5" rx="2.5" transform="rotate(-12 10 15)"/></svg>';
|
|
73
|
+
const ICON_ESCALATE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V5M5 12l7-7 7 7"/></svg>';
|
|
74
|
+
// SynaBun's own MCP tools: Claude/Codex `mcp__SynaBun__x`, OpenCode `SynaBun_x`.
|
|
75
|
+
const SYNABUN_TOOL_RE = /^(?:mcp__synabun__|synabun_)(.+)$/i;
|
|
76
|
+
export function synabunToolName(name) { const m = SYNABUN_TOOL_RE.exec(String(name || '')); return m ? m[1] : null; }
|
|
77
|
+
const RESULT_ERRORS = { error_max_turns: 'Stopped: the turn limit was reached.', error_max_budget_usd: 'Stopped: the budget cap was reached.', error_during_execution: 'The turn failed.', error_max_structured_output_retries: 'Stopped: structured output kept failing.' };
|
|
78
|
+
const INTERRUPTED_RE = /\b(?:interrupted|aborted|cancell?ed)\b/i;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The error line a `result` event deserves, or '' — a success, or a turn the
|
|
82
|
+
* user stopped (terminal_reason aborted_*, or only "interrupted" errors).
|
|
83
|
+
* `t(key, fallback)` localizes the stock lines.
|
|
84
|
+
*/
|
|
85
|
+
export function resultErrorText(ev, t = (_key, fallback) => fallback) {
|
|
86
|
+
if (!ev || (!ev.is_error && !String(ev.subtype || '').startsWith('error'))) return '';
|
|
87
|
+
if (String(ev.terminal_reason || '').startsWith('aborted')) return '';
|
|
88
|
+
if (typeof ev.result === 'string' && ev.result.trim()) return ev.result.trim();
|
|
89
|
+
const errors = Array.isArray(ev.errors) ? ev.errors.filter((x) => typeof x === 'string' && x.trim()) : [];
|
|
90
|
+
if (errors.length) return errors.every((x) => INTERRUPTED_RE.test(x)) ? '' : errors.slice(0, 3).join('\n');
|
|
91
|
+
const sub = RESULT_ERRORS[ev.subtype] ? ev.subtype : 'error_during_execution';
|
|
92
|
+
return t(`assistant.result.${sub}`, RESULT_ERRORS[sub]);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Whitespace-blind text key: streamed deltas and the final message join parts differently. */
|
|
96
|
+
const textKey = (s) => String(s || '').replace(/\s+/g, '');
|
|
97
|
+
const COMPUTER_TOOL_TITLES = { computer: 'Computer', computer_apps: 'Apps', computer_ax: 'Accessibility', computer_status: 'Computer status' };
|
|
98
|
+
const RESULT_LIMIT = 3000;
|
|
99
|
+
const STREAM_TICK_MS = 100;
|
|
100
|
+
const MAX_MESSAGES = 600;
|
|
101
|
+
const MAX_INLINE_FRAMES = 6; // computer cards per turn that keep an inline thumbnail
|
|
102
|
+
|
|
103
|
+
function el(tag, className, text) {
|
|
104
|
+
const node = document.createElement(tag);
|
|
105
|
+
if (className) node.className = className;
|
|
106
|
+
if (text != null) node.textContent = text;
|
|
107
|
+
return node;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Turn a memory node/summary into the `<memory>` block appended to a prompt. */
|
|
111
|
+
export function formatMemoryForPrompt(memory) {
|
|
112
|
+
const p = memory?.payload || memory || {};
|
|
113
|
+
const id = memory?.id || p.id || '';
|
|
114
|
+
const category = p.category || memory?.category || '';
|
|
115
|
+
const content = p.content || memory?.text || memory?.title || '';
|
|
116
|
+
return `<memory id="${id}" category="${category}">\n${content}\n</memory>`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function memoryTitle(memory) {
|
|
120
|
+
const p = memory?.payload || memory || {};
|
|
121
|
+
const raw = memory?.title || p.title || (p.content || memory?.text || '').split('\n')[0];
|
|
122
|
+
return String(raw || '').replace(/^#+\s*/, '').slice(0, 60) || memory?.id || 'memory';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── Run cards (shared with the agents dock) ────────────────────────────────
|
|
126
|
+
|
|
127
|
+
function runWaitingForRoute(run) {
|
|
128
|
+
return run?.state === 'awaiting_route' || run?.turnState === 'awaiting_route' || run?.route?.status === 'pending';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* buildRunCard(run, { t, providerIcon, providerColor, onFocus, onStop, onRead, onEscalate, compact, result, index })
|
|
133
|
+
* Returns a card element; call updateRunCard(el, run, opts) to refresh it in place.
|
|
134
|
+
*/
|
|
135
|
+
export function buildRunCard(run, opts = {}) {
|
|
136
|
+
const card = el('div', `asst-run${opts.compact ? ' compact' : ''}`);
|
|
137
|
+
card.dataset.runId = run?.runId || '';
|
|
138
|
+
card.setAttribute('role', 'group');
|
|
139
|
+
card.innerHTML = `
|
|
140
|
+
<span class="asst-run-icon" aria-hidden="true"></span>
|
|
141
|
+
<div class="asst-run-main"><span class="asst-run-title"></span><span class="asst-pill" data-tone="running">${mascotLib.mascotCameoSvg()}<span class="asst-pill-label"></span></span></div>
|
|
142
|
+
<div class="asst-run-actions"></div>
|
|
143
|
+
<div class="asst-run-meta"></div>
|
|
144
|
+
`;
|
|
145
|
+
const actions = card.querySelector('.asst-run-actions');
|
|
146
|
+
const t = (key, fallback) => {
|
|
147
|
+
const v = typeof opts.t === 'function' ? opts.t(key) : undefined;
|
|
148
|
+
return v && v !== key && typeof v === 'string' ? v : fallback;
|
|
149
|
+
};
|
|
150
|
+
const mk = (id, label, icon, handler) => {
|
|
151
|
+
const btn = el('button', 'asst-iconbtn');
|
|
152
|
+
btn.type = 'button';
|
|
153
|
+
btn.dataset.action = id;
|
|
154
|
+
btn.setAttribute('data-tooltip', label);
|
|
155
|
+
btn.setAttribute('aria-label', label);
|
|
156
|
+
btn.innerHTML = icon;
|
|
157
|
+
btn.addEventListener('click', (e) => { e.stopPropagation(); handler?.(card.dataset.runId); });
|
|
158
|
+
actions.appendChild(btn);
|
|
159
|
+
return btn;
|
|
160
|
+
};
|
|
161
|
+
mk('focus', t('assistant.run.focus', 'Focus in sidepanel'), '<svg viewBox="0 0 24 24"><path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M21 14v5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5"/></svg>', opts.onFocus);
|
|
162
|
+
mk('read', t('assistant.run.read', 'Read result'), '<svg viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/><path d="M16 13H8M16 17H8"/></svg>', opts.onRead);
|
|
163
|
+
mk('stop', t('assistant.run.stop', 'Stop run'), '<svg viewBox="0 0 24 24"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>', opts.onStop);
|
|
164
|
+
// The agents tray only: a finished run can be removed from it.
|
|
165
|
+
if (typeof opts.onRemove === 'function') mk('remove', t('assistant.run.remove', 'Remove from list'), '<svg viewBox="0 0 24 24"><path d="M3 6h18"/><path d="M8 6V4h8v2"/><path d="M19 6l-1 14H6L5 6"/></svg>', opts.onRemove).hidden = true;
|
|
166
|
+
card._runOpts = opts;
|
|
167
|
+
updateRunCard(card, run, opts);
|
|
168
|
+
return card;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function updateRunCard(card, run, opts = {}) {
|
|
172
|
+
if (!card || !run) return;
|
|
173
|
+
if (card._usageRun?.usageObserved && !run.usageObserved) run = { ...run, usageTokens: card._usageRun.usageTokens, usageFidelity: card._usageRun.usageFidelity, usageObserved: true };
|
|
174
|
+
card._usageRun = run;
|
|
175
|
+
const t = (key, fallback, params) => {
|
|
176
|
+
const v = typeof opts.t === 'function' ? opts.t(key, params) : undefined;
|
|
177
|
+
if (v && v !== key && typeof v === 'string') return v;
|
|
178
|
+
return params ? String(fallback).replace(/\{(\w+)\}/g, (_, k) => (params[k] != null ? params[k] : `{${k}}`)) : fallback;
|
|
179
|
+
};
|
|
180
|
+
card.dataset.runId = run.runId || card.dataset.runId;
|
|
181
|
+
card.dataset.status = run.status || '';
|
|
182
|
+
const provider = run.provider || 'claude-code';
|
|
183
|
+
const icon = card.querySelector('.asst-run-icon');
|
|
184
|
+
if (icon) {
|
|
185
|
+
icon.innerHTML = typeof opts.providerIcon === 'function' ? (opts.providerIcon(provider) || '') : '';
|
|
186
|
+
const color = typeof opts.providerColor === 'function' ? opts.providerColor(provider) : '';
|
|
187
|
+
if (color) card.style.setProperty('--asst-provider-color', color);
|
|
188
|
+
}
|
|
189
|
+
const title = card.querySelector('.asst-run-title');
|
|
190
|
+
const label = run.title || run.task || run.runId || '';
|
|
191
|
+
if (title) { title.textContent = label; title.title = label; }
|
|
192
|
+
const waitingRoute = runWaitingForRoute(run);
|
|
193
|
+
const pill = card.querySelector('.asst-pill');
|
|
194
|
+
if (pill) {
|
|
195
|
+
const tone = waitingRoute ? 'action' : runTone(run);
|
|
196
|
+
pill.dataset.tone = tone;
|
|
197
|
+
(pill.querySelector('.asst-pill-label') || pill).textContent = waitingRoute ? t('assistant.run.awaitingRoute', 'waiting for your model choice')
|
|
198
|
+
: run.turnState === 'awaiting_permission' ? t('assistant.run.needsApproval', 'needs approval')
|
|
199
|
+
: run.turnState === 'needs_input' ? t('assistant.run.needsInput', 'needs input')
|
|
200
|
+
: run.queued && run.status !== 'running' ? t('assistant.run.queued', 'queued')
|
|
201
|
+
// A warm worker between turns reports what its last turn ended with, not "running".
|
|
202
|
+
: isRunIdle(run) ? (run.outcome || run.lastResult?.status
|
|
203
|
+
? t('assistant.run.idleWith', '{outcome} · idle', { outcome: run.outcome || run.lastResult.status })
|
|
204
|
+
: t('assistant.run.idle', 'idle'))
|
|
205
|
+
: (run.status || tone);
|
|
206
|
+
}
|
|
207
|
+
const meta = card.querySelector('.asst-run-meta');
|
|
208
|
+
if (meta) {
|
|
209
|
+
// One sentence: "GPT-5.5 via Codex, high effort, 1m 35s, $0.21", then what the route and Jev said.
|
|
210
|
+
const bits = [];
|
|
211
|
+
const modelName = run.modelInfo?.label || (run.model ? modelShortName(run.model) : '');
|
|
212
|
+
const who = modelName ? t('assistant.run.via', '{model} via {provider}', { model: modelName, provider: providerShortLabel(provider) }) : providerShortLabel(provider);
|
|
213
|
+
bits.push(`<span class="asst-run-model">${esc(who)}</span>`);
|
|
214
|
+
if (run.accountLabel || (run.accountId && run.accountId !== 'default')) bits.push(`<span>${esc(t('assistant.run.asAccount', 'as {account}', { account: run.accountLabel || run.accountId }))}</span>`);
|
|
215
|
+
if (run.effort) bits.push(`<span>${esc(t('assistant.run.effort', '{effort} effort', { effort: run.effort }))}</span>`);
|
|
216
|
+
const end = isTerminalRunStatus(run.status) ? (run.completedAt || run.updatedAt) : Date.now();
|
|
217
|
+
const elapsed = fmtElapsed(run.startedAt, end);
|
|
218
|
+
if (elapsed) bits.push(`<span class="asst-run-elapsed">${esc(elapsed)}</span>`);
|
|
219
|
+
const cost = fmtCost(run.costUsd);
|
|
220
|
+
if (cost && Number(run.costUsd) > 0) bits.push(`<span class="asst-run-cost">${esc(cost)}</span>`);
|
|
221
|
+
const runTokens = run.usageTokens || run.tokens;
|
|
222
|
+
const usage = runTokens?.total > 0 || run.usageObserved ? formatRunTokens(runTokens, run.usageFidelity || run.tokens?.fidelity, t) : null;
|
|
223
|
+
if (usage) bits.push(`<span class="asst-usage-run-tokens" aria-label="${esc(usage.label)}">${esc(usage.text)}</span>`);
|
|
224
|
+
if (run.currentIteration > 1) bits.push(`<span>${esc(t('assistant.run.turn', 'turn {n}', { n: run.currentIteration }))}</span>`);
|
|
225
|
+
if (run.route && typeof run.route === 'object') {
|
|
226
|
+
const r = run.route;
|
|
227
|
+
const routeBits = [r.taskClassLabel || r.taskClass || t('assistant.routes.task', 'task')];
|
|
228
|
+
if (r.status === 'pending') routeBits.push(t('assistant.routes.state.pending', 'waiting for your choice'));
|
|
229
|
+
else if (r.decidedBy === 'user') routeBits.push(t('assistant.routes.youChose', 'you chose'));
|
|
230
|
+
else if (r.remembered || r.decidedBy === 'remembered') routeBits.push(t('assistant.routes.remembered', 'remembered'));
|
|
231
|
+
else if (Number.isFinite(Number(r.confidence))) routeBits.push(`${Math.round(Number(r.confidence) <= 1 ? Number(r.confidence) * 100 : Number(r.confidence))}%`);
|
|
232
|
+
const tip = r.target ? `${t('assistant.routes.routedTo', 'Routed to')} ${routeTargetLabel(r.target, { effort: true })}` : t('assistant.routes.title', 'Model routing');
|
|
233
|
+
bits.push(`<span class="asst-run-route" data-tooltip="${esc(tip)}">⇄ ${esc(routeBits.join(', '))}</span>`);
|
|
234
|
+
}
|
|
235
|
+
if (run.usesComputer) bits.push(`<span class="asst-run-computer">${esc(t('assistant.run.usesComputer', 'uses the computer'))}</span>`);
|
|
236
|
+
// Jev's reading of the result: a "done" its own output does not show, and a status read without a ## Result block.
|
|
237
|
+
const judged = run.lastResult && typeof run.lastResult === 'object' ? run.lastResult : null;
|
|
238
|
+
if (judged?.unverifiedClaim) {
|
|
239
|
+
const tip = t('assistant.run.unverifiedTip', 'Reported done, but {evidence}. Verify before relying on it.', { evidence: String(judged.unverifiedClaim.evidence || '') });
|
|
240
|
+
bits.push(`<span class="asst-run-unverified" data-tooltip="${esc(tip)}" style="color:var(--asst-warn)">${esc(t('assistant.run.unverified', 'unverified'))}</span>`);
|
|
241
|
+
}
|
|
242
|
+
if (judged?.source === 'jev') bits.push(`<span class="asst-run-jev">${esc(t('assistant.run.statusByJev', 'status read by Jev'))}</span>`);
|
|
243
|
+
meta.innerHTML = bits.join('<span class="asst-run-sep">, </span>');
|
|
244
|
+
}
|
|
245
|
+
const finished = isTerminalRunStatus(run.status);
|
|
246
|
+
const removable = isRemovableRun(run);
|
|
247
|
+
const stopBtn = card.querySelector('[data-action="stop"]');
|
|
248
|
+
const removeBtn = card.querySelector('[data-action="remove"]');
|
|
249
|
+
if (stopBtn) { stopBtn.disabled = finished; stopBtn.hidden = removable && !!removeBtn; }
|
|
250
|
+
if (removeBtn) { removeBtn.hidden = !removable; removeBtn.disabled = !removable; }
|
|
251
|
+
const readBtn = card.querySelector('[data-action="read"]');
|
|
252
|
+
if (readBtn) readBtn.disabled = run.status === 'queued';
|
|
253
|
+
|
|
254
|
+
// Escalation offer ("Escalate to ‹label›", or "Retry on ‹label›" after a
|
|
255
|
+
// temporary failure) when the server proposes one; a note without a button
|
|
256
|
+
// when a stronger model would hit the same wall (access, the user's input).
|
|
257
|
+
let esc8 = card.querySelector('.asst-run-escalate');
|
|
258
|
+
const escalationRaw = run.escalation && typeof run.escalation === 'object' ? run.escalation : null;
|
|
259
|
+
const escalation = escalationRaw?.to ? escalationRaw : null;
|
|
260
|
+
const needsNote = escalationRaw && escalationRaw.kind === 'none' ? escalationRaw : null;
|
|
261
|
+
const onEscalate = opts.onEscalate || card._runOpts?.onEscalate;
|
|
262
|
+
if (esc8 && esc8.dataset.kind !== (needsNote ? 'note' : 'offer')) { esc8.remove(); esc8 = null; card._escalation = null; }
|
|
263
|
+
if (needsNote) {
|
|
264
|
+
if (!esc8) {
|
|
265
|
+
esc8 = el('div', 'asst-run-escalate');
|
|
266
|
+
esc8.dataset.kind = 'note';
|
|
267
|
+
esc8.innerHTML = '<span class="asst-run-escalate-reason"></span>';
|
|
268
|
+
card.appendChild(esc8);
|
|
269
|
+
}
|
|
270
|
+
const needs = String(needsNote.needs || '').trim();
|
|
271
|
+
const line = needsNote.cause === 'access'
|
|
272
|
+
? t('assistant.run.needsAccess', 'Needs access: {needs}', { needs })
|
|
273
|
+
: t('assistant.run.needsUser', 'Needs your input: {needs}', { needs });
|
|
274
|
+
esc8.querySelector('.asst-run-escalate-reason').textContent = needs ? line : line.replace(/[::]\s*$/, '');
|
|
275
|
+
} else if (escalation && typeof onEscalate === 'function' && !escalation.started) {
|
|
276
|
+
const to = escalation.to;
|
|
277
|
+
const retry = escalation.kind === 'retry';
|
|
278
|
+
const toLabel = routeTargetLabel(to) || t('assistant.run.strongerModel', 'a stronger model');
|
|
279
|
+
if (!esc8) {
|
|
280
|
+
esc8 = el('div', 'asst-run-escalate');
|
|
281
|
+
esc8.dataset.kind = 'offer';
|
|
282
|
+
esc8.innerHTML = `<span class="asst-run-escalate-reason"></span><button type="button" class="asst-btn asst-btn-secondary asst-run-escalate-btn"><span class="asst-btn-icon" aria-hidden="true">${ICON_ESCALATE}</span><span class="asst-btn-label"></span></button>`;
|
|
283
|
+
esc8.querySelector('button').addEventListener('click', (e) => {
|
|
284
|
+
e.stopPropagation();
|
|
285
|
+
const btn = e.currentTarget;
|
|
286
|
+
const current = card._escalation;
|
|
287
|
+
if (!current || btn.disabled) return;
|
|
288
|
+
btn.disabled = true;
|
|
289
|
+
Promise.resolve(onEscalate(card.dataset.runId, current.to)).finally(() => { btn.disabled = false; });
|
|
290
|
+
});
|
|
291
|
+
card.appendChild(esc8);
|
|
292
|
+
}
|
|
293
|
+
card._escalation = escalation;
|
|
294
|
+
const reason = retry ? t('assistant.run.retryTransient', 'Temporary failure')
|
|
295
|
+
: { failed: t('assistant.run.escalateFailed', 'Run failed'), blocked: t('assistant.run.escalateBlocked', 'Run is blocked'), no_result: t('assistant.run.escalateNoResult', 'No usable result') }[escalation.reason] || '';
|
|
296
|
+
esc8.querySelector('.asst-run-escalate-reason').textContent = reason;
|
|
297
|
+
esc8.querySelector('.asst-btn-label').textContent = retry
|
|
298
|
+
? t('assistant.run.retryOn', 'Retry on {label}', { label: toLabel })
|
|
299
|
+
: t('assistant.run.escalateTo', 'Escalate to {label}', { label: toLabel });
|
|
300
|
+
} else if (esc8) {
|
|
301
|
+
esc8.remove();
|
|
302
|
+
card._escalation = null;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
let summary = card.querySelector('.asst-run-summary');
|
|
306
|
+
const result = opts.result ?? run.lastResult ?? null;
|
|
307
|
+
const summaryText = summaryFromResult(result, run);
|
|
308
|
+
if (summaryText) {
|
|
309
|
+
if (!summary) { summary = el('div', 'asst-run-summary'); card.appendChild(summary); }
|
|
310
|
+
summary.textContent = summaryText;
|
|
311
|
+
} else if (summary && !opts.keepSummary) {
|
|
312
|
+
summary.remove();
|
|
313
|
+
}
|
|
314
|
+
updateRunMedia(card, run, { t, lightboxT: opts.t || card._runOpts?.t });
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const MAX_RUN_MEDIA = 8;
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Image / video creation: the files the run generated (run.media, served by
|
|
321
|
+
* /api/assistant/runs/:runId/media/:n), right after the summary; a design run's
|
|
322
|
+
* screenshots and images the same way. Images open in the screenshot lightbox;
|
|
323
|
+
* videos play inline. Rebuilt only when the list changes, so a playing video
|
|
324
|
+
* keeps playing across card refreshes.
|
|
325
|
+
*/
|
|
326
|
+
function updateRunMedia(card, run, { t, lightboxT }) {
|
|
327
|
+
const items = (Array.isArray(run.media) ? run.media : [])
|
|
328
|
+
.filter((m) => m && typeof m.url === 'string' && m.url && (m.kind === 'image' || m.kind === 'video'))
|
|
329
|
+
.slice(0, MAX_RUN_MEDIA);
|
|
330
|
+
let strip = card.querySelector('.asst-run-media');
|
|
331
|
+
if (!items.length) { strip?.remove(); return; }
|
|
332
|
+
const anchor = card.querySelector('.asst-run-summary');
|
|
333
|
+
if (!strip) {
|
|
334
|
+
strip = el('div', 'asst-run-media');
|
|
335
|
+
strip.setAttribute('role', 'list');
|
|
336
|
+
const design = (run.taskClass || run.route?.taskClass) === 'design';
|
|
337
|
+
strip.setAttribute('aria-label', design ? t('assistant.run.designMedia', 'Design screenshots and images') : t('assistant.run.media', 'Generated media'));
|
|
338
|
+
}
|
|
339
|
+
if (anchor ? strip.previousElementSibling !== anchor : strip.parentNode !== card) {
|
|
340
|
+
if (anchor) anchor.after(strip); else card.appendChild(strip);
|
|
341
|
+
}
|
|
342
|
+
const key = items.map((m) => m.url).join('|');
|
|
343
|
+
if (strip.dataset.key === key) return;
|
|
344
|
+
strip.dataset.key = key;
|
|
345
|
+
strip.innerHTML = '';
|
|
346
|
+
for (const m of items) {
|
|
347
|
+
const name = String(m.path || m.url).split(/[\\/]/).pop();
|
|
348
|
+
if (m.kind === 'video') {
|
|
349
|
+
const video = document.createElement('video');
|
|
350
|
+
video.className = 'asst-run-media-video';
|
|
351
|
+
video.setAttribute('role', 'listitem');
|
|
352
|
+
video.controls = true;
|
|
353
|
+
video.preload = 'metadata';
|
|
354
|
+
video.src = m.url;
|
|
355
|
+
video.title = m.path || name;
|
|
356
|
+
video.setAttribute('aria-label', t('assistant.run.mediaVideo', 'Video {name}', { name }));
|
|
357
|
+
strip.appendChild(video);
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
const btn = el('button', 'asst-run-media-thumb');
|
|
361
|
+
btn.type = 'button';
|
|
362
|
+
btn.setAttribute('role', 'listitem');
|
|
363
|
+
btn.title = m.path || name;
|
|
364
|
+
btn.setAttribute('aria-label', t('assistant.run.mediaOpen', 'Open image {name}', { name }));
|
|
365
|
+
const img = document.createElement('img');
|
|
366
|
+
img.alt = '';
|
|
367
|
+
img.loading = 'lazy';
|
|
368
|
+
img.addEventListener('error', () => btn.classList.add('failed'), { once: true });
|
|
369
|
+
img.src = m.url;
|
|
370
|
+
btn.appendChild(img);
|
|
371
|
+
btn.addEventListener('click', (e) => {
|
|
372
|
+
e.stopPropagation();
|
|
373
|
+
openFrameLightbox({ src: m.url, caption: m.path || name, t: lightboxT, returnFocus: btn });
|
|
374
|
+
});
|
|
375
|
+
strip.appendChild(btn);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function summaryFromResult(result, run) {
|
|
380
|
+
if (result && typeof result === 'object') {
|
|
381
|
+
const status = result.status ? `[${result.status}] ` : '';
|
|
382
|
+
const text = result.summary || result.text || result.raw || '';
|
|
383
|
+
if (text) return `${status}${String(text).slice(0, 800)}`;
|
|
384
|
+
if (result.status) return `[${result.status}]`;
|
|
385
|
+
} else if (typeof result === 'string' && result.trim()) {
|
|
386
|
+
return result.slice(0, 800);
|
|
387
|
+
}
|
|
388
|
+
if (run?.status === 'failed' && run.error) return String(run.error).slice(0, 400);
|
|
389
|
+
return '';
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** Refresh only the elapsed counters (cheap, for timers). */
|
|
393
|
+
export function tickRunCard(card, run) {
|
|
394
|
+
if (!card || !run || isTerminalRunStatus(run.status)) return;
|
|
395
|
+
const node = card.querySelector('.asst-run-elapsed');
|
|
396
|
+
const value = fmtElapsed(run.startedAt, Date.now());
|
|
397
|
+
if (node && value) node.textContent = value;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// ── Renderer ────────────────────────────────────────────────────────────────
|
|
401
|
+
|
|
402
|
+
// Activity rack timing (asst-tool-kinds.js decides what a call says).
|
|
403
|
+
const WAKE_MS = 350; // a row wakes once its call has run this long
|
|
404
|
+
const LINGER_MS = 1200; // the last awake call to finish stays awake this long
|
|
405
|
+
const HOLD_MS = 900; // the bubble holds a call at least this long
|
|
406
|
+
const THINK_HOLD_MS = 3000; // … and a thinking caption this long
|
|
407
|
+
const BURST_MS = 800; // 3+ calls started within this window read "newest ×N"
|
|
408
|
+
const STATUS_GAP_MS = 1500; // screen-reader announcements at most this often
|
|
409
|
+
const FOLD_MS = 180; // narration collapsing into the stage caption
|
|
410
|
+
const COLLAPSE_MS = 260; // the stage folding into the receipt
|
|
411
|
+
const FLIP_MS = 320; // the hero flying into the first stage (--asst-dur-slow)
|
|
412
|
+
const HERO_LEAVE_MS = 140; // a parked hero nobody took over fades out
|
|
413
|
+
const MAX_TICKS = 60;
|
|
414
|
+
const NARROW_TICKS = 12;
|
|
415
|
+
const PREVIEW_LINES = 5;
|
|
416
|
+
const THINK_PREVIEW_LINES = 10;
|
|
417
|
+
const MAX_DIFF_LINES = 400; // an edit's mini diff draws this many lines; "Show full input" draws the rest
|
|
418
|
+
// The empty state's suggestions (asst-panel.js ids) and the kind each one is about.
|
|
419
|
+
const SUGGESTION_KINDS = { computer: 'computer', web: 'web', fix: 'edit', schedule: 'plan', remember: 'memory' };
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* createRenderer(transcriptEl, hooks)
|
|
423
|
+
* hooks: { t, providerIcon(provider), providerColor(provider), onFocusRun, onStopRun, onReadRun, onEscalateRun,
|
|
424
|
+
* onMemoryClick(id), onResult(ev), onInit(ev), onModeChanged(mode), copy(text),
|
|
425
|
+
* emptySuggestions() → [{ id, label, template }], onSuggestion(template),
|
|
426
|
+
* brainLabel() → string, onChangeRoutes(), onRouteEvent(ev) → bool (true = handled by an open card),
|
|
427
|
+
* heroOrigin() → DOMRect | null (where the empty state's character flies in from: the host's placeholder),
|
|
428
|
+
* onMascotChange() (the character on screen changed: mascot()) }
|
|
429
|
+
*/
|
|
430
|
+
export function createRenderer(transcriptEl, hooks = {}) {
|
|
431
|
+
loadMarked();
|
|
432
|
+
injectRackStyles();
|
|
433
|
+
const $msgs = transcriptEl;
|
|
434
|
+
const scopes = new Map(); // parent_tool_use_id → { container, stream, key }
|
|
435
|
+
const runCards = new Map(); // runId → card element
|
|
436
|
+
const routeLines = new Map(); // routeId → line handle
|
|
437
|
+
let working = null;
|
|
438
|
+
let workingFace = null; // the "Thinking" row's character: { still, rig?, el, target? } (syncWorkingFace)
|
|
439
|
+
let followTail = true;
|
|
440
|
+
let emptyEl = null;
|
|
441
|
+
let hero = null; // the empty state's character: { rig, el, box, parked, leaving, held, dock, egg, target }
|
|
442
|
+
let rigsOn = true; // this panel's rigs may animate (asst-mascot.js lets one panel at a time)
|
|
443
|
+
const mcpWarned = new Set();
|
|
444
|
+
let lastErrorText = ''; // a synthetic API-error message's text, so the result does not repeat it
|
|
445
|
+
let turnFrames = []; // computer cards with inline thumbnails in the current turn
|
|
446
|
+
const runInfo = new Map(); // runId → { title, state, provider } from dispatch cards and agent_* results
|
|
447
|
+
let turnActive = false; // between showWorking() and finishTurn()
|
|
448
|
+
let turn = { calls: 0, last: null, errored: false, answerRow: null }; // what the turn did: the mascot's verdict, the announced answer
|
|
449
|
+
let grace = null; // the stage on screen when the result arrived: it waits for the turn's verdict (finishTurn)
|
|
450
|
+
let waiting = null; // a card waits on the user: { id: the tool_use id it holds or null, since } (setWaiting)
|
|
451
|
+
|
|
452
|
+
// ── Activity racks ──
|
|
453
|
+
// Every burst of agent work (tool calls, thinking, short narration) is one
|
|
454
|
+
// `.asst-rack`: a stage where the mascot acts out the call it is on, a
|
|
455
|
+
// ledger with one row per tool kind, a receipt once it settles. The state
|
|
456
|
+
// lives on the DOM tail, so the live path and a replayed journal agree.
|
|
457
|
+
const racks = new Set();
|
|
458
|
+
const stepsById = new Map(); // tool_use id → step (updates are idempotent across racks)
|
|
459
|
+
const headers = new WeakMap(); // station header → { rack, st }
|
|
460
|
+
let rackSeq = 0;
|
|
461
|
+
let stepSeq = 0;
|
|
462
|
+
let unsubTick = null;
|
|
463
|
+
let muted = false; // inside handleEvent(…, { silent: true }): no motion, no announcements
|
|
464
|
+
let replayDraw = false; // inside handleEvent(…, { replay: true }): a reattach's missed packets (drawn like the journal)
|
|
465
|
+
let clockAt = null; // the journal `at` of the entry being replayed
|
|
466
|
+
|
|
467
|
+
// ── What is on screen (dedupe) ──
|
|
468
|
+
// Identities of the events drawn, live or from the journal: SDK uuids, each
|
|
469
|
+
// assistant message's key, its API message id and its tool_use ids. The
|
|
470
|
+
// panel skips an assistant event it drew already; a reattach's buffered
|
|
471
|
+
// replay (planReplay) skips everything the journal snapshot or the live
|
|
472
|
+
// path drew. `snapshot`: the last renderTranscript's exact event copies and
|
|
473
|
+
// its newest `at` / `seq`, for the one replay that follows it.
|
|
474
|
+
const seen = new Set();
|
|
475
|
+
const SEEN_MAX = 5000;
|
|
476
|
+
let snapshot = null;
|
|
477
|
+
|
|
478
|
+
$msgs.addEventListener('scroll', () => {
|
|
479
|
+
followTail = isNearBottom();
|
|
480
|
+
}, { passive: true });
|
|
481
|
+
|
|
482
|
+
const t = (key, fallback, params) => {
|
|
483
|
+
const v = typeof hooks.t === 'function' ? hooks.t(key, params) : undefined;
|
|
484
|
+
if (v && v !== key && typeof v === 'string') return v;
|
|
485
|
+
return params ? String(fallback).replace(/\{(\w+)\}/g, (_, k) => (params[k] != null ? params[k] : `{${k}}`)) : fallback;
|
|
486
|
+
};
|
|
487
|
+
const sayCtx = {
|
|
488
|
+
t,
|
|
489
|
+
runLabel: (step) => runLabel(step),
|
|
490
|
+
runState: (step) => { const ids = stepRunIds(step); return ids.length ? runInfo.get(ids[0])?.state || '' : ''; },
|
|
491
|
+
describeComputer: (input, name) => describeComputerAction(input, name),
|
|
492
|
+
};
|
|
493
|
+
/** What a step says; a call still streaming its input says "…" until the input arrives. */
|
|
494
|
+
const sayOf = (step) => {
|
|
495
|
+
const say = sayCall(step, sayCtx);
|
|
496
|
+
if (step.ghost && step.type === 'call' && !Object.keys(step.input || {}).length) { say.text = '…'; say.detail = '…'; say.segments = null; say.mono = false; say.meta = []; }
|
|
497
|
+
return say;
|
|
498
|
+
};
|
|
499
|
+
const nowMs = () => (clockAt ?? Date.now());
|
|
500
|
+
const isTimed = () => !muted || clockAt != null;
|
|
501
|
+
const locale = () => (typeof document !== 'undefined' && document.documentElement?.lang) || 'en';
|
|
502
|
+
|
|
503
|
+
function reducedMotion() {
|
|
504
|
+
try { return !!isReducedMotion(); } catch { return !!globalThis.matchMedia?.('(prefers-reduced-motion: reduce)').matches; }
|
|
505
|
+
}
|
|
506
|
+
/** No motion: replay, reduced motion, or the UI is being dragged. */
|
|
507
|
+
const calm = () => muted || reducedMotion() || !!document.body?.classList.contains('ui-interacting');
|
|
508
|
+
|
|
509
|
+
function isNearBottom() {
|
|
510
|
+
return $msgs.scrollHeight - $msgs.scrollTop - $msgs.clientHeight < 48;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function scrollEnd(force = false) {
|
|
514
|
+
if (!force && !followTail) return;
|
|
515
|
+
requestAnimationFrame(() => { $msgs.scrollTop = $msgs.scrollHeight; });
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function root() { return { container: $msgs, stream: null, key: null, msgId: null, lastAt: 0 }; }
|
|
519
|
+
const rootScope = root();
|
|
520
|
+
|
|
521
|
+
function scopeFor(parentId) {
|
|
522
|
+
if (!parentId) return rootScope;
|
|
523
|
+
let scope = scopes.get(parentId);
|
|
524
|
+
if (scope && (scope.container === $msgs || scope.container.isConnected)) return scope;
|
|
525
|
+
const card = stepsById.get(parentId)?.card || $msgs.querySelector(`.asst-tool[data-tool-id="${CSS.escape(parentId)}"]`);
|
|
526
|
+
let container;
|
|
527
|
+
if (card) {
|
|
528
|
+
container = card.querySelector(':scope > .asst-agent-feed');
|
|
529
|
+
if (!container) { container = el('div', 'asst-agent-feed'); card.appendChild(container); }
|
|
530
|
+
} else {
|
|
531
|
+
container = $msgs;
|
|
532
|
+
}
|
|
533
|
+
scope = { container, stream: null, key: parentId, msgId: null, lastAt: 0 };
|
|
534
|
+
scopes.set(parentId, scope);
|
|
535
|
+
return scope;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/** The empty state goes; `park` (the user sent a prompt) keeps its character on screen, thinking. */
|
|
539
|
+
function hideEmpty({ park = false } = {}) {
|
|
540
|
+
if (!emptyEl) return;
|
|
541
|
+
if (!(park && parkHero())) dropHero();
|
|
542
|
+
emptyEl.remove();
|
|
543
|
+
emptyEl = null;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// ── The hero ──
|
|
547
|
+
const cssEase = (name, fallback) => {
|
|
548
|
+
try { return getComputedStyle($msgs).getPropertyValue(name).trim() || fallback; } catch { return fallback; }
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
function heroTarget(h) {
|
|
552
|
+
return {
|
|
553
|
+
kind: 'hero',
|
|
554
|
+
rig: h.rig,
|
|
555
|
+
el: h.el,
|
|
556
|
+
parked: () => !!h.parked,
|
|
557
|
+
hold(on) { h.held = !!on; },
|
|
558
|
+
restore() { if (hero === h) h.rig.setPose(h.parked ? 'think' : 'idle'); },
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Park the hero where it stands, in an overlay of the transcript's parent,
|
|
564
|
+
* so it outlives the empty state and thinks until the first stage of the
|
|
565
|
+
* turn takes it over. Not on replay, under reduced motion or while the UI
|
|
566
|
+
* is dragged, and not when it is off screen.
|
|
567
|
+
*/
|
|
568
|
+
function parkHero() {
|
|
569
|
+
const h = hero;
|
|
570
|
+
const layer = $msgs.parentElement;
|
|
571
|
+
if (!h?.rig || h.parked || calm() || !layer || !h.el.isConnected) return false;
|
|
572
|
+
const box = h.el.getBoundingClientRect();
|
|
573
|
+
const view = $msgs.getBoundingClientRect();
|
|
574
|
+
if (!box.width || !box.height || box.bottom <= view.top || box.top >= view.bottom) return false;
|
|
575
|
+
if (getComputedStyle(layer).position === 'static') return false;
|
|
576
|
+
const base = layer.getBoundingClientRect();
|
|
577
|
+
const dock = el('div', 'asst-hero-dock');
|
|
578
|
+
dock.setAttribute('aria-hidden', 'true');
|
|
579
|
+
dock.style.left = `${box.left - base.left - layer.clientLeft}px`;
|
|
580
|
+
dock.style.top = `${box.top - base.top - layer.clientTop}px`;
|
|
581
|
+
dock.style.width = `${box.width}px`;
|
|
582
|
+
dock.style.height = `${box.height}px`;
|
|
583
|
+
dock.appendChild(h.el);
|
|
584
|
+
layer.appendChild(dock);
|
|
585
|
+
h.dock = dock;
|
|
586
|
+
h.parked = true;
|
|
587
|
+
if (!h.held) h.rig.setPose('think');
|
|
588
|
+
mascotChanged();
|
|
589
|
+
return true;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function dropHero() {
|
|
593
|
+
const h = hero;
|
|
594
|
+
if (!h) return;
|
|
595
|
+
hero = null;
|
|
596
|
+
clearTimeout(h.leaveTimer);
|
|
597
|
+
try { h.rig.destroy(); } catch { /* gone */ }
|
|
598
|
+
h.dock?.remove();
|
|
599
|
+
mascotChanged();
|
|
600
|
+
syncWorkingFace(); // the "Thinking" row may wear the character now
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/** A parked hero no stage took over leaves: it fades out (at once when calm), after `delay` ms. */
|
|
604
|
+
function dismissHero(delay = 0) {
|
|
605
|
+
const h = hero;
|
|
606
|
+
if (!h?.parked || h.leaving) return;
|
|
607
|
+
h.leaving = true;
|
|
608
|
+
const go = () => {
|
|
609
|
+
if (hero !== h) return;
|
|
610
|
+
if (calm() || typeof h.dock.animate !== 'function') { dropHero(); return; }
|
|
611
|
+
const anim = h.dock.animate([{ opacity: 1 }, { opacity: 0 }], { duration: HERO_LEAVE_MS, easing: cssEase('--asst-ease-exit', 'cubic-bezier(.2,0,1,.9)'), fill: 'forwards' });
|
|
612
|
+
anim.onfinish = () => { if (hero === h) dropHero(); };
|
|
613
|
+
h.leaveTimer = setTimeout(() => { if (hero === h) dropHero(); }, HERO_LEAVE_MS + 120);
|
|
614
|
+
};
|
|
615
|
+
if (delay > 0) h.leaveTimer = setTimeout(go, delay);
|
|
616
|
+
else go();
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* FLIP `node` from `from` (a client rect) into its own box: transform and
|
|
621
|
+
* opacity, 320 ms, --asst-ease-emphasized. `fade` starts it transparent (the
|
|
622
|
+
* character was gone for a moment). Returns the animation, or null when calm.
|
|
623
|
+
*/
|
|
624
|
+
function flipFrom(node, from, { fade = false } = {}) {
|
|
625
|
+
if (!node || !from?.width || calm() || typeof node.animate !== 'function') return null;
|
|
626
|
+
const to = node.getBoundingClientRect();
|
|
627
|
+
if (!to.width || !to.height) return null;
|
|
628
|
+
const dx = (from.left + from.width / 2) - (to.left + to.width / 2);
|
|
629
|
+
const dy = (from.top + from.height / 2) - (to.top + to.height / 2);
|
|
630
|
+
const s = from.width / to.width;
|
|
631
|
+
if (Math.abs(dx) < 1 && Math.abs(dy) < 1 && Math.abs(s - 1) < 0.01 && !fade) return null;
|
|
632
|
+
return node.animate([
|
|
633
|
+
{ transform: `translate(${dx}px, ${dy}px) scale(${s})`, opacity: fade ? 0 : 1 },
|
|
634
|
+
{ transform: 'none', opacity: 1 },
|
|
635
|
+
], { duration: FLIP_MS, easing: cssEase('--asst-ease-emphasized', 'cubic-bezier(.4,.14,.3,1)') });
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/** The hand-off: the first stage's face flies in from the parked hero, which leaves in the same frame. */
|
|
639
|
+
function flipIntoStage(rack) {
|
|
640
|
+
const h = hero;
|
|
641
|
+
const from = h.el.getBoundingClientRect();
|
|
642
|
+
dropHero();
|
|
643
|
+
rack.row.classList.add('is-flip');
|
|
644
|
+
const anim = flipFrom(rack.stage.face, from);
|
|
645
|
+
const done = () => rack.row.classList.remove('is-flip');
|
|
646
|
+
if (!anim) { done(); return; }
|
|
647
|
+
anim.onfinish = done;
|
|
648
|
+
anim.oncancel = done;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/** Triple-click the character: its eyes part and re-form, once per mount. Silent. */
|
|
652
|
+
function wireEgg(h) {
|
|
653
|
+
const hit = (e) => {
|
|
654
|
+
const r = h.el.getBoundingClientRect();
|
|
655
|
+
return hero === h && !h.parked && e.clientX >= r.left && e.clientX <= r.right && e.clientY >= r.top && e.clientY <= r.bottom;
|
|
656
|
+
};
|
|
657
|
+
h.box.parentElement?.addEventListener('mousedown', (e) => { if (e.detail > 1 && hit(e)) e.preventDefault(); });
|
|
658
|
+
h.box.parentElement?.addEventListener('click', (e) => {
|
|
659
|
+
if (e.detail !== 3 || h.egg || !hit(e)) return;
|
|
660
|
+
h.egg = true;
|
|
661
|
+
h.rig.react?.('split');
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// ── The character on screen (for the panel's director) ──
|
|
666
|
+
function stageTarget(rack) {
|
|
667
|
+
return {
|
|
668
|
+
kind: 'stage',
|
|
669
|
+
rig: rack.rig,
|
|
670
|
+
el: rack.rig.el,
|
|
671
|
+
hold(on) { rack.held = !!on; },
|
|
672
|
+
restore() { if (rack.rig) { rack.pose = null; poseStage(rack); } },
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/** The newest stage on screen (live, or held for its one-shot). */
|
|
677
|
+
function newestStage() {
|
|
678
|
+
let best = null;
|
|
679
|
+
for (const rack of racks) if (rack.staged && rack.rig && !rack.nested && (!best || rack.id > best.id)) best = rack;
|
|
680
|
+
return best;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/** The mascot on screen: the hero, else the newest stage, else the "Thinking" row's, else none. */
|
|
684
|
+
function visibleMascot() {
|
|
685
|
+
if (hero?.rig) return hero.target;
|
|
686
|
+
return newestStage()?.target || workingFace?.target || null;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function mascotChanged() {
|
|
690
|
+
activateRigs();
|
|
691
|
+
try { hooks.onMascotChange?.(); } catch { /* the director is gone */ }
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/** Whether this panel's rigs animate (another panel may lead); still frames otherwise. */
|
|
695
|
+
function setRigsEnabled(on) {
|
|
696
|
+
if (rigsOn === !!on) return;
|
|
697
|
+
rigsOn = !!on;
|
|
698
|
+
activateRigs();
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function prune() {
|
|
702
|
+
const rows = $msgs.children;
|
|
703
|
+
while (rows.length > MAX_MESSAGES && rows[0] !== working) rows[0].remove();
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/** Insert a row before "Working…" so the indicator stays the last row. */
|
|
707
|
+
function append(node, scope = rootScope, { park = false } = {}) {
|
|
708
|
+
hideEmpty({ park });
|
|
709
|
+
if (scope.container === $msgs && working && working.parentNode === $msgs) $msgs.insertBefore(node, working);
|
|
710
|
+
else scope.container.appendChild(node);
|
|
711
|
+
if (scope === rootScope) prune();
|
|
712
|
+
// Text, a card, a notice…: the parked hero would sit on it. Only a rack (its stage takes it over) and the prompt keep it.
|
|
713
|
+
if (hero?.parked && scope === rootScope && !node._rack && !node.classList.contains('msg-user')) dismissHero();
|
|
714
|
+
if (racks.size) syncRacks();
|
|
715
|
+
scrollEnd();
|
|
716
|
+
return node;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* A message row. `props` (its text, message id…) land before the row is
|
|
721
|
+
* appended: append() re-renders the racks, and a live rack looks through
|
|
722
|
+
* one short text row at its tail (liveTail), which it can only do once the
|
|
723
|
+
* row says what it holds.
|
|
724
|
+
*/
|
|
725
|
+
function assistantRow(scope, props = null) {
|
|
726
|
+
const row = el('div', 'asst-msg msg-assistant');
|
|
727
|
+
const wrap = el('div', 'asst-msg-content');
|
|
728
|
+
row.appendChild(wrap);
|
|
729
|
+
if (props) Object.assign(row, props);
|
|
730
|
+
append(row, scope);
|
|
731
|
+
return { row, wrap };
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function linkify(container) {
|
|
735
|
+
const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT);
|
|
736
|
+
const nodes = [];
|
|
737
|
+
while (walker.nextNode()) nodes.push(walker.currentNode);
|
|
738
|
+
const pathRe = /(?:(?:[A-Za-z]:)?(?:[/\\][\w.@\-]+){2,}(?::\d+)?)(?=\s|$|[)"',;])/g;
|
|
739
|
+
for (const node of nodes) {
|
|
740
|
+
if (node.parentElement?.closest('pre, code, a')) continue;
|
|
741
|
+
const text = node.textContent;
|
|
742
|
+
pathRe.lastIndex = 0;
|
|
743
|
+
if (!pathRe.test(text)) continue;
|
|
744
|
+
pathRe.lastIndex = 0;
|
|
745
|
+
const frag = document.createDocumentFragment();
|
|
746
|
+
let last = 0;
|
|
747
|
+
let match;
|
|
748
|
+
while ((match = pathRe.exec(text))) {
|
|
749
|
+
if (match.index > last) frag.appendChild(document.createTextNode(text.slice(last, match.index)));
|
|
750
|
+
const a = el('a', 'file-link', match[0]);
|
|
751
|
+
a.href = '#';
|
|
752
|
+
a.title = t('assistant.copyPath', 'Click to copy path');
|
|
753
|
+
const path = match[0].replace(/:\d+$/, '');
|
|
754
|
+
a.addEventListener('click', (e) => { e.preventDefault(); hooks.copy?.(path); a.style.opacity = '0.5'; setTimeout(() => { a.style.opacity = ''; }, 300); });
|
|
755
|
+
frag.appendChild(a);
|
|
756
|
+
last = match.index + match[0].length;
|
|
757
|
+
}
|
|
758
|
+
if (last < text.length) frag.appendChild(document.createTextNode(text.slice(last)));
|
|
759
|
+
if (last > 0) node.parentNode.replaceChild(frag, node);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function renderMarkdownInto(body, text) {
|
|
764
|
+
mdInto(body, text);
|
|
765
|
+
body.querySelectorAll('pre code').forEach((code) => {
|
|
766
|
+
const lang = (code.className || '').replace('language-', '').trim();
|
|
767
|
+
if (lang) code.parentElement.dataset.lang = lang;
|
|
768
|
+
});
|
|
769
|
+
body.querySelectorAll('a[href]').forEach((a) => {
|
|
770
|
+
if (a.classList.contains('file-link')) return;
|
|
771
|
+
if (/^https?:/i.test(a.getAttribute('href') || '')) { a.target = '_blank'; a.rel = 'noopener noreferrer'; }
|
|
772
|
+
});
|
|
773
|
+
linkify(body);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function copyText(text) {
|
|
777
|
+
const value = String(text ?? '');
|
|
778
|
+
if (!value) return;
|
|
779
|
+
if (typeof hooks.copy === 'function') { hooks.copy(value); return; }
|
|
780
|
+
try { navigator.clipboard?.writeText(value)?.catch?.(() => {}); } catch { /* no clipboard */ }
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function copyButton(getText) {
|
|
784
|
+
const btn = el('button', 'asst-copy', t('assistant.rack.copy', 'Copy'));
|
|
785
|
+
btn.type = 'button';
|
|
786
|
+
btn.addEventListener('click', (e) => {
|
|
787
|
+
e.stopPropagation();
|
|
788
|
+
copyText(getText());
|
|
789
|
+
btn.textContent = t('assistant.rack.copied', 'Copied');
|
|
790
|
+
setTimeout(() => { btn.textContent = t('assistant.rack.copy', 'Copy'); }, 1200);
|
|
791
|
+
});
|
|
792
|
+
return btn;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// ── Streaming ──
|
|
796
|
+
// Text streams into a normal row first (it may fold into the rack as
|
|
797
|
+
// narration once a call follows); thinking and calls stream into the rack.
|
|
798
|
+
function ensureStream(scope) {
|
|
799
|
+
if (!scope.stream) scope.stream = { row: null, wrap: null, bodyEl: null, textBuf: '', mdTimer: null, thinkTimer: null, think: null, blockType: null, msgId: scope.msgId || null, claude: false };
|
|
800
|
+
return scope.stream;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function flushStream(scope) {
|
|
804
|
+
const s = scope.stream;
|
|
805
|
+
if (!s) return;
|
|
806
|
+
if (s.mdTimer) { clearTimeout(s.mdTimer); s.mdTimer = null; }
|
|
807
|
+
if (s.thinkTimer) { clearTimeout(s.thinkTimer); s.thinkTimer = null; }
|
|
808
|
+
if (s.bodyEl && s.textBuf.trim()) renderMarkdownInto(s.bodyEl, s.textBuf);
|
|
809
|
+
if (s.think) refreshStep(s.think);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/** The stream's text row goes (the final message renders the text again). */
|
|
813
|
+
function dropStreamText(scope) {
|
|
814
|
+
const s = scope.stream;
|
|
815
|
+
if (!s) return;
|
|
816
|
+
if (s.mdTimer) { clearTimeout(s.mdTimer); s.mdTimer = null; }
|
|
817
|
+
s.row?.remove();
|
|
818
|
+
s.row = null; s.wrap = null; s.bodyEl = null; s.textBuf = '';
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function thinkingBlock(text = '') {
|
|
822
|
+
const thinkEl = el('details', 'asst-thinking');
|
|
823
|
+
thinkEl.innerHTML = `<summary><span class="asst-thinking-chev" aria-hidden="true">›</span><span>${esc(t('assistant.thinking', 'Thinking'))}</span></summary><div class="asst-thinking-content"></div>`;
|
|
824
|
+
thinkEl.querySelector('.asst-thinking-content').textContent = text;
|
|
825
|
+
return thinkEl;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function streamThink(scope, s, redacted = false) {
|
|
829
|
+
if (s.think) return s.think;
|
|
830
|
+
const rack = rackFor(scope, { msgId: s.msgId, claude: s.claude });
|
|
831
|
+
s.think = addThink(rack, { text: '', redacted, running: true });
|
|
832
|
+
renderRack(rack);
|
|
833
|
+
scrollEnd();
|
|
834
|
+
return s.think;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function handleStreamEvent(apiEvent, scope) {
|
|
838
|
+
const type = apiEvent?.type;
|
|
839
|
+
if (type === 'message_start') {
|
|
840
|
+
if (scope.stream?.think?.state === 'running') finishThink(scope.stream.think);
|
|
841
|
+
scope.msgId = apiEvent.message?.id || null;
|
|
842
|
+
scope.stream = null;
|
|
843
|
+
ensureStream(scope);
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
if (type === 'message_stop') {
|
|
847
|
+
// Keep the streamed row until the final `assistant` message replaces it.
|
|
848
|
+
flushStream(scope);
|
|
849
|
+
if (scope.stream?.row) scope.stream.row.classList.remove('streaming');
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
if (type === 'content_block_start') {
|
|
853
|
+
const s = ensureStream(scope);
|
|
854
|
+
s.claude = true; // only the SDK announces blocks
|
|
855
|
+
const cb = apiEvent.content_block;
|
|
856
|
+
s.blockType = cb?.type || null;
|
|
857
|
+
if (s.blockType === 'tool_use' && cb?.id && cb?.name !== 'AskUserQuestion') {
|
|
858
|
+
// A call is a step from its first streamed byte: the rack shows it now.
|
|
859
|
+
if (!stepsById.has(cb.id)) {
|
|
860
|
+
const rack = rackFor(scope, { msgId: s.msgId, claude: true });
|
|
861
|
+
addCall(rack, { id: cb.id, name: cb.name, input: {} }, { ghost: true, extra: { msgId: s.msgId } });
|
|
862
|
+
renderRack(rack);
|
|
863
|
+
scrollEnd();
|
|
864
|
+
}
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
if (s.blockType === 'thinking' || s.blockType === 'redacted_thinking') streamThink(scope, s, s.blockType === 'redacted_thinking');
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
if (type === 'content_block_delta') {
|
|
871
|
+
const s = ensureStream(scope);
|
|
872
|
+
const delta = apiEvent.delta;
|
|
873
|
+
if (!delta) return;
|
|
874
|
+
if (delta.type === 'thinking_delta' && delta.thinking) {
|
|
875
|
+
// Codex and OpenCode send bare thinking deltas (no content_block_start).
|
|
876
|
+
const step = streamThink(scope, s);
|
|
877
|
+
step.text += delta.thinking;
|
|
878
|
+
if (!s.thinkTimer) s.thinkTimer = setTimeout(() => { s.thinkTimer = null; refreshStep(step); if (step.rack.live) paintStage(step.rack); }, STREAM_TICK_MS);
|
|
879
|
+
} else if (delta.type === 'text_delta' && delta.text != null) {
|
|
880
|
+
s.textBuf += delta.text;
|
|
881
|
+
if (!s.bodyEl && s.textBuf.trim()) {
|
|
882
|
+
const { row, wrap } = assistantRow(scope, { _text: s.textBuf, _msgId: s.msgId, _claude: s.claude });
|
|
883
|
+
row.classList.add('streaming');
|
|
884
|
+
noteAnswer(scope, row);
|
|
885
|
+
s.row = row;
|
|
886
|
+
s.wrap = wrap;
|
|
887
|
+
s.bodyEl = el('div', 'asst-md');
|
|
888
|
+
wrap.appendChild(s.bodyEl);
|
|
889
|
+
}
|
|
890
|
+
if (s.row) s.row._text = s.textBuf;
|
|
891
|
+
if (s.bodyEl && !s.mdTimer) {
|
|
892
|
+
s.mdTimer = setTimeout(() => {
|
|
893
|
+
s.mdTimer = null;
|
|
894
|
+
if (!s.bodyEl) return;
|
|
895
|
+
renderMarkdownInto(s.bodyEl, s.textBuf);
|
|
896
|
+
scrollEnd();
|
|
897
|
+
}, STREAM_TICK_MS);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
if (type === 'content_block_stop') {
|
|
903
|
+
const s = scope.stream;
|
|
904
|
+
if (s?.bodyEl && s.mdTimer) { clearTimeout(s.mdTimer); s.mdTimer = null; renderMarkdownInto(s.bodyEl, s.textBuf); }
|
|
905
|
+
if (s?.think && s.thinkTimer) { clearTimeout(s.thinkTimer); s.thinkTimer = null; refreshStep(s.think); }
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// ── Computer frames ──
|
|
910
|
+
function frameCaption(card) {
|
|
911
|
+
const info = card._computer || {};
|
|
912
|
+
return [info.description, info.app].filter(Boolean).join(' · ');
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function setComputerFrame(card, { thumb, full, size, coordinate, app }) {
|
|
916
|
+
if (!card) return;
|
|
917
|
+
const info = card._computer || (card._computer = {});
|
|
918
|
+
info.thumb = thumb;
|
|
919
|
+
info.full = full || thumb;
|
|
920
|
+
info.size = size || null;
|
|
921
|
+
info.app = app || info.app || '';
|
|
922
|
+
info.marker = size ? markerPercent(coordinate, size) : null;
|
|
923
|
+
let frame = card.querySelector('.asst-computer-frame');
|
|
924
|
+
if (!frame) {
|
|
925
|
+
frame = el('button', 'asst-computer-frame');
|
|
926
|
+
frame.type = 'button';
|
|
927
|
+
frame.innerHTML = '<img alt="" hidden><span class="asst-frame-marker" hidden></span>';
|
|
928
|
+
frame.addEventListener('click', (e) => {
|
|
929
|
+
e.stopPropagation();
|
|
930
|
+
const data = card._computer || {};
|
|
931
|
+
if (!data.full) return;
|
|
932
|
+
openFrameLightbox({ src: data.full, marker: data.marker, caption: frameCaption(card), t: hooks.t, returnFocus: frame });
|
|
933
|
+
});
|
|
934
|
+
const hdr = card.querySelector('.asst-tool-hdr');
|
|
935
|
+
hdr?.after(frame);
|
|
936
|
+
}
|
|
937
|
+
frame.setAttribute('aria-label', t('assistant.computer.openFrameOf', 'Open screenshot: {action}', { action: info.description || t('assistant.computer.frame', 'Screenshot') }));
|
|
938
|
+
if (size?.width && size?.height) frame.style.aspectRatio = `${size.width} / ${size.height}`;
|
|
939
|
+
const mark = frame.querySelector('.asst-frame-marker');
|
|
940
|
+
if (info.marker) { mark.style.left = `${info.marker.x}%`; mark.style.top = `${info.marker.y}%`; mark.hidden = false; } else mark.hidden = true;
|
|
941
|
+
frame.classList.remove('compact');
|
|
942
|
+
const img = frame.querySelector('img');
|
|
943
|
+
swapImage(img, thumb).then((ok) => { if (!ok) frame.classList.add('failed'); });
|
|
944
|
+
if (!turnFrames.includes(card)) turnFrames.push(card);
|
|
945
|
+
while (turnFrames.length > MAX_INLINE_FRAMES) {
|
|
946
|
+
const old = turnFrames.shift();
|
|
947
|
+
const oldFrame = old?.querySelector('.asst-computer-frame');
|
|
948
|
+
if (!oldFrame) continue;
|
|
949
|
+
// Drop the bitmap, keep a small "view frame" button for the lightbox.
|
|
950
|
+
const oldImg = oldFrame.querySelector('img');
|
|
951
|
+
if (oldImg) { oldImg.removeAttribute('src'); oldImg.hidden = true; delete oldImg.dataset.src; }
|
|
952
|
+
oldFrame.classList.add('compact');
|
|
953
|
+
oldFrame.style.aspectRatio = '';
|
|
954
|
+
if (!oldFrame.querySelector('.asst-computer-frame-label')) oldFrame.appendChild(el('span', 'asst-computer-frame-label', t('assistant.computer.viewFrame', 'View screenshot')));
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function imageBlockUrl(content) {
|
|
959
|
+
if (!Array.isArray(content)) return '';
|
|
960
|
+
for (const b of content) {
|
|
961
|
+
if (b?.type !== 'image') continue;
|
|
962
|
+
const src = b.source || {};
|
|
963
|
+
if (src.type === 'base64' && src.data) return `data:${src.media_type || 'image/png'};base64,${src.data}`;
|
|
964
|
+
if (src.type === 'url' && src.url) return src.url;
|
|
965
|
+
}
|
|
966
|
+
return '';
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// ── Full messages ──
|
|
970
|
+
/** A message row: the text of a real message (or narration no call followed yet). */
|
|
971
|
+
function textRow(scope, text, ctx = {}) {
|
|
972
|
+
const { row, wrap } = assistantRow(scope, { _text: text, _msgId: ctx.msgId || null, _claude: !!ctx.claude });
|
|
973
|
+
const body = el('div', 'asst-md');
|
|
974
|
+
renderMarkdownInto(body, text);
|
|
975
|
+
wrap.appendChild(body);
|
|
976
|
+
noteAnswer(scope, row);
|
|
977
|
+
return row;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
/** The turn's latest message at the root: what the panel announces when the turn ends. */
|
|
981
|
+
function noteAnswer(scope, row) {
|
|
982
|
+
if (scope === rootScope && turnActive && !muted) turn.answerRow = row;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
function renderAssistant(message, scope = rootScope, ev = null) {
|
|
986
|
+
const content = Array.isArray(message?.content) ? message.content : (typeof message?.content === 'string' ? [{ type: 'text', text: message.content }] : []);
|
|
987
|
+
const thinks = content.filter(b => (b?.type === 'thinking' && b.thinking) || b?.type === 'redacted_thinking');
|
|
988
|
+
const texts = content.filter(b => b?.type === 'text' && typeof b.text === 'string');
|
|
989
|
+
const tools = content.filter(b => b?.type === 'tool_use' && b.name !== 'AskUserQuestion'); // AskUserQuestion: a control card
|
|
990
|
+
const ctx = { msgId: message?.id || scope.msgId || null, claude: !!ev?.uuid };
|
|
991
|
+
// Text and thinking replace what was streamed for them. A tool-only message
|
|
992
|
+
// (Codex/OpenCode send each call mid-stream; the SDK one per block) keeps the
|
|
993
|
+
// streamed text as a row and closes it, so later deltas start below.
|
|
994
|
+
const s = scope.stream;
|
|
995
|
+
const liveText = !!s?.textBuf.trim();
|
|
996
|
+
const streamedThink = s?.think || null;
|
|
997
|
+
if (s) {
|
|
998
|
+
flushStream(scope);
|
|
999
|
+
if (texts.length) dropStreamText(scope);
|
|
1000
|
+
else if (s.row) {
|
|
1001
|
+
s.row.classList.remove('streaming');
|
|
1002
|
+
if (!s.wrap.children.length) s.row.remove();
|
|
1003
|
+
else if (s.textBuf.trim()) (scope.kept ||= new Set()).add(textKey(s.textBuf));
|
|
1004
|
+
}
|
|
1005
|
+
if (streamedThink && !thinks.length) finishThink(streamedThink);
|
|
1006
|
+
scope.stream = null;
|
|
1007
|
+
}
|
|
1008
|
+
if (!thinks.length && !texts.length && !tools.length) return null;
|
|
1009
|
+
let rack = null;
|
|
1010
|
+
let row = null;
|
|
1011
|
+
if (thinks.length) {
|
|
1012
|
+
const text = thinks.map(b => b.thinking || '').join('\n').trim();
|
|
1013
|
+
if (streamedThink?.rack.row.isConnected) { finishThink(streamedThink, text); rack = streamedThink.rack; }
|
|
1014
|
+
else { rack = rackFor(scope, ctx); addThink(rack, { text, redacted: !text, startedAt: scope.lastAt || nowMs() }); }
|
|
1015
|
+
}
|
|
1016
|
+
const rawMd = texts.map(b => b.text).join('\n');
|
|
1017
|
+
// Codex and OpenCode end a turn by re-sending its latest text; when that text
|
|
1018
|
+
// came before the last tool call, the rack (or the row kept above) already has it.
|
|
1019
|
+
const alreadyShown = !liveText && !!scope.kept?.has(textKey(rawMd));
|
|
1020
|
+
if (rawMd.trim() && !alreadyShown) {
|
|
1021
|
+
// Short narration directly followed by a call in the same message folds into the rack.
|
|
1022
|
+
if (tools.length && shouldFoldNarration(rawMd)) {
|
|
1023
|
+
rack = rackFor(scope, ctx);
|
|
1024
|
+
foldNarration(rack, null, rawMd.trim());
|
|
1025
|
+
} else row = textRow(scope, rawMd, ctx);
|
|
1026
|
+
}
|
|
1027
|
+
const fresh = [];
|
|
1028
|
+
for (const block of tools) {
|
|
1029
|
+
const known = block.id ? stepsById.get(block.id) : null;
|
|
1030
|
+
if (known) updateCall(known, block); else fresh.push(block);
|
|
1031
|
+
}
|
|
1032
|
+
if (fresh.length) {
|
|
1033
|
+
rack = rackFor(scope, ctx);
|
|
1034
|
+
for (const block of fresh) addCall(rack, block, { extra: { msgId: ctx.msgId } });
|
|
1035
|
+
}
|
|
1036
|
+
const touched = new Set([rack, ...tools.map(b => stepsById.get(b.id)?.rack)].filter(Boolean));
|
|
1037
|
+
for (const r of touched) renderRack(r);
|
|
1038
|
+
scrollEnd();
|
|
1039
|
+
return row || rack?.row || null;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
function resultText(content) {
|
|
1043
|
+
if (Array.isArray(content)) return content.map(b => (typeof b === 'string' ? b : (b?.text || (b?.type === 'image' ? '[image]' : '') || ''))).join('\n');
|
|
1044
|
+
if (typeof content === 'string') return content;
|
|
1045
|
+
if (content != null) { try { return JSON.stringify(content, null, 2); } catch { return String(content); } }
|
|
1046
|
+
return '';
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
function updateToolResult(ev) {
|
|
1050
|
+
const id = ev?.tool_use_id;
|
|
1051
|
+
const step = id ? stepsById.get(id) : null;
|
|
1052
|
+
if (!step || step.type !== 'call') return;
|
|
1053
|
+
const text = resultText(ev.content);
|
|
1054
|
+
// The same result again (a replay of what is on screen): nothing changes, not even the clock.
|
|
1055
|
+
if (step.state !== 'running' && !step.stale && step.result === text) return;
|
|
1056
|
+
step.result = text;
|
|
1057
|
+
step.stale = false;
|
|
1058
|
+
let outcome = callOutcome(step, text, !!ev.is_error, t, ev.detail || null);
|
|
1059
|
+
const card = step.card;
|
|
1060
|
+
if (step.kind === 'computer' && card?._computer) {
|
|
1061
|
+
const parsed = parseComputerResult(text);
|
|
1062
|
+
if (parsed.ok === false) outcome = { state: 'error', code: null, detail: parsed.message || parsed.code || '' };
|
|
1063
|
+
if (parsed.app) { card._computer.app = parsed.app; step.app = parsed.app; }
|
|
1064
|
+
step.computerState = parsed.ok === false ? (parsed.code || t('assistant.tool.error', 'error')) : (parsed.app || t('assistant.tool.done', 'done'));
|
|
1065
|
+
const size = parsed.width && parsed.height ? { width: parsed.width, height: parsed.height } : null;
|
|
1066
|
+
const input = card._computer.input || {};
|
|
1067
|
+
if (parsed.frameId) {
|
|
1068
|
+
const urls = frameUrls(parsed.frameId);
|
|
1069
|
+
setComputerFrame(card, { thumb: urls.thumb, full: urls.full, size, coordinate: input.coordinate || input.start_coordinate, app: parsed.app });
|
|
1070
|
+
} else {
|
|
1071
|
+
const inline = imageBlockUrl(ev.content);
|
|
1072
|
+
if (inline) setComputerFrame(card, { thumb: inline, full: inline, size, coordinate: input.coordinate, app: parsed.app });
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
step.state = outcome.state;
|
|
1076
|
+
step.detail = outcome.detail || '';
|
|
1077
|
+
step.code = outcome.code ?? null;
|
|
1078
|
+
step.endedAt = nowMs();
|
|
1079
|
+
step.timed = step.timed && isTimed();
|
|
1080
|
+
if (step.kind === 'runs') noteAgentResult(text);
|
|
1081
|
+
refreshStep(step);
|
|
1082
|
+
if (step.bodyBuilt) fillCallBody(step);
|
|
1083
|
+
if (step.state === 'error') onFailure(step);
|
|
1084
|
+
renderRack(step.rack);
|
|
1085
|
+
// A subagent that returned settles the racks in its feed; a child's result updates its parent's strip.
|
|
1086
|
+
if (step.kind === 'subagent') for (const r of racks) if (r.scope.key === step.id) renderRack(r);
|
|
1087
|
+
if (step.parent?.rack) renderRack(step.parent.rack);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// ── Rack steps ──
|
|
1091
|
+
function parentOf(scope) {
|
|
1092
|
+
return scope?.key ? stepsById.get(scope.key) || null : null;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
function addCall(rack, block, { ghost = false, extra = null } = {}) {
|
|
1096
|
+
const kind = kindOf(block.name, block.input);
|
|
1097
|
+
const step = {
|
|
1098
|
+
type: 'call', id: block.id || `call-${++stepSeq}`, seq: ++stepSeq, name: block.name || 'tool', kind, key: stationKey(kind, block.name),
|
|
1099
|
+
input: block.input || {}, state: 'running', result: '', detail: '', code: null, startedAt: nowMs(), endedAt: 0, progress: 0,
|
|
1100
|
+
timed: isTimed(), ghost, stale: false, rack, station: null, card: null, parent: parentOf(rack.scope), bodyBuilt: false,
|
|
1101
|
+
...(extra || {}),
|
|
1102
|
+
};
|
|
1103
|
+
if (step.state !== 'running' && !step.endedAt) step.endedAt = step.startedAt;
|
|
1104
|
+
if (block.id) stepsById.set(block.id, step);
|
|
1105
|
+
placeStep(rack, step);
|
|
1106
|
+
if (!muted && turnActive && !step.auto) { turn.calls += 1; turn.last = step; }
|
|
1107
|
+
if (rack.steps.length === 1 && !muted && turnActive) announce(rack, stepSentence(step, step.state === 'running'));
|
|
1108
|
+
return step;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
/** The final block of a streamed call, or the same block replayed: the step learns its input, nothing is added. */
|
|
1112
|
+
function updateCall(step, block) {
|
|
1113
|
+
if (block.name) step.name = block.name;
|
|
1114
|
+
if (block.input && typeof block.input === 'object' && Object.keys(block.input).length) step.input = block.input;
|
|
1115
|
+
step.ghost = false;
|
|
1116
|
+
if (step.card?._computer) { step.card._computer.input = step.input; step.card._computer.description = describeComputerAction(step.input, step.name); }
|
|
1117
|
+
refreshStep(step);
|
|
1118
|
+
if (step.bodyBuilt) fillCallBody(step);
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
function addThink(rack, { text = '', redacted = false, running = false, startedAt = null } = {}) {
|
|
1122
|
+
const step = {
|
|
1123
|
+
type: 'think', id: `think-${++stepSeq}`, seq: stepSeq, name: 'thinking', kind: 'think', key: 'think', input: {}, text, redacted,
|
|
1124
|
+
state: running ? 'running' : 'ok', startedAt: startedAt ?? nowMs(), endedAt: running ? 0 : nowMs(), timed: isTimed(),
|
|
1125
|
+
stale: false, rack, station: null, card: null, parent: parentOf(rack.scope), bodyBuilt: false,
|
|
1126
|
+
};
|
|
1127
|
+
placeStep(rack, step);
|
|
1128
|
+
return step;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
function finishThink(step, text) {
|
|
1132
|
+
if (typeof text === 'string' && text) step.text = text;
|
|
1133
|
+
if (!step.text) step.redacted = true;
|
|
1134
|
+
if (step.state === 'running') { step.state = 'ok'; step.endedAt = nowMs(); }
|
|
1135
|
+
refreshStep(step);
|
|
1136
|
+
if (step.bodyBuilt) fillCallBody(step);
|
|
1137
|
+
renderRack(step.rack);
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
function placeStep(rack, step) {
|
|
1141
|
+
rack.steps.push(step);
|
|
1142
|
+
rack.timeline.push(step);
|
|
1143
|
+
const st = ensureStation(rack, step);
|
|
1144
|
+
st.steps.push(step);
|
|
1145
|
+
step.station = st;
|
|
1146
|
+
step.card = buildCallNode(step);
|
|
1147
|
+
st.log.appendChild(step.card);
|
|
1148
|
+
if (st.open) buildCallBody(step);
|
|
1149
|
+
if (!rack.firstAt) rack.firstAt = step.startedAt;
|
|
1150
|
+
if (step.parent) {
|
|
1151
|
+
(step.parent.children ||= []).push(step);
|
|
1152
|
+
if (step.parent.rack.row.isConnected) renderRack(step.parent.rack);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// ── Narration ──
|
|
1157
|
+
/** The row before `node`, skipping rows on their way out. */
|
|
1158
|
+
function prevRow(node) {
|
|
1159
|
+
let prev = node?.previousElementSibling || null;
|
|
1160
|
+
while (prev && prev.classList.contains('asst-folding')) prev = prev.previousElementSibling;
|
|
1161
|
+
return prev;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
/** The scope's last row, skipping "Working…" and rows folding into a rack. */
|
|
1165
|
+
function tailRow(scope) {
|
|
1166
|
+
let node = scope.container.lastElementChild;
|
|
1167
|
+
while (node && (node === working || node.classList.contains('asst-folding'))) node = node.previousElementSibling;
|
|
1168
|
+
return node;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
/** A short text row that can still fold into the rack above it (the call after it has not come yet). */
|
|
1172
|
+
function narrationCandidate(row) {
|
|
1173
|
+
if (!row || row._sealed || typeof row._text !== 'string' || !row.classList.contains('msg-assistant')) return false;
|
|
1174
|
+
if (row.classList.contains('asst-msg-error') || row.classList.contains('asst-local-output')) return false;
|
|
1175
|
+
return shouldFoldNarration(row._text);
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
function narrationOf(row, ctx = {}) {
|
|
1179
|
+
if (!narrationCandidate(row)) return null;
|
|
1180
|
+
// Claude: narration folds only into a call of the same assistant message.
|
|
1181
|
+
if (ctx.claude && row._claude && ctx.msgId && row._msgId && ctx.msgId !== row._msgId) return null;
|
|
1182
|
+
return row._text.trim();
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
function usableRack(rack, scope) {
|
|
1186
|
+
return !!rack && !rack.closed && rack.scope === scope && rack.row.isConnected;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
/** The rack a new call, thinking block or memory step joins: the tail rack, else a new one; narration just above folds in. */
|
|
1190
|
+
function rackFor(scope, ctx = {}) {
|
|
1191
|
+
const tail = tailRow(scope);
|
|
1192
|
+
if (usableRack(tail?._rack, scope)) return tail._rack;
|
|
1193
|
+
const text = tail && !ctx.noFold ? narrationOf(tail, ctx) : null;
|
|
1194
|
+
if (text != null) {
|
|
1195
|
+
const prev = prevRow(tail);
|
|
1196
|
+
const rack = usableRack(prev?._rack, scope) ? prev._rack : createRack(scope);
|
|
1197
|
+
foldNarration(rack, tail, text);
|
|
1198
|
+
return rack;
|
|
1199
|
+
}
|
|
1200
|
+
return createRack(scope);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
/** The rack at the end of the scope, looking through one short text row that may still fold into it. */
|
|
1204
|
+
function liveTail(scope) {
|
|
1205
|
+
let node = tailRow(scope);
|
|
1206
|
+
if (node && !node._rack && narrationCandidate(node)) node = prevRow(node);
|
|
1207
|
+
return node?._rack || null;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
/** Narration becomes the stage caption and a "said" entry; its row collapses into the rack (instantly on replay). */
|
|
1211
|
+
function foldNarration(rack, row, text) {
|
|
1212
|
+
const said = { type: 'said', text, at: nowMs(), seq: ++stepSeq };
|
|
1213
|
+
rack.said.push(said);
|
|
1214
|
+
rack.timeline.push(said);
|
|
1215
|
+
(rack.scope.kept ||= new Set()).add(textKey(text));
|
|
1216
|
+
if (row) {
|
|
1217
|
+
const s = rack.scope.stream;
|
|
1218
|
+
if (s?.row === row) { if (s.mdTimer) clearTimeout(s.mdTimer); s.mdTimer = null; s.row = null; s.wrap = null; s.bodyEl = null; s.textBuf = ''; }
|
|
1219
|
+
collapseRow(row);
|
|
1220
|
+
}
|
|
1221
|
+
if (!muted && turnActive) announce(rack, text);
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
function collapseRow(row) {
|
|
1225
|
+
if (calm() || !row.isConnected || typeof row.animate !== 'function') { row.remove(); return; }
|
|
1226
|
+
row.classList.add('asst-folding');
|
|
1227
|
+
const h = row.offsetHeight;
|
|
1228
|
+
const anim = row.animate(
|
|
1229
|
+
[{ height: `${h}px`, opacity: 1 }, { height: '0px', opacity: 0, marginTop: '0px' }],
|
|
1230
|
+
{ duration: FOLD_MS, easing: 'cubic-bezier(.2,0,.38,.9)', fill: 'forwards' },
|
|
1231
|
+
);
|
|
1232
|
+
row.style.overflow = 'hidden';
|
|
1233
|
+
const done = () => row.remove();
|
|
1234
|
+
anim.onfinish = done;
|
|
1235
|
+
anim.oncancel = done;
|
|
1236
|
+
setTimeout(done, FOLD_MS + 120);
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
/** A turn boundary: the tail text is a message for good, and every rack of the scope is closed. */
|
|
1240
|
+
function sealScope(scope) {
|
|
1241
|
+
const tail = tailRow(scope);
|
|
1242
|
+
if (tail && !tail._rack) tail._sealed = true;
|
|
1243
|
+
for (const rack of racks) if (rack.scope === scope) rack.closed = true;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
// ── Racks ──
|
|
1247
|
+
function createRack(scope) {
|
|
1248
|
+
const id = ++rackSeq;
|
|
1249
|
+
const nested = scope.container !== $msgs;
|
|
1250
|
+
const row = el('div', 'asst-rack');
|
|
1251
|
+
row.dataset.state = 'settled';
|
|
1252
|
+
row.dataset.view = 'kind';
|
|
1253
|
+
row.setAttribute('role', 'group');
|
|
1254
|
+
const ledgerId = `asst-rack-${id}-ledger`;
|
|
1255
|
+
row.innerHTML = `<div class="asst-rack-stage">
|
|
1256
|
+
<div class="asst-rack-face" aria-hidden="true"></div>
|
|
1257
|
+
<div class="asst-rack-caption"></div>
|
|
1258
|
+
<div class="asst-bubble" aria-live="off" hidden><span class="asst-bubble-verb"></span><span class="asst-bubble-subject" role="button" tabindex="-1" aria-expanded="false"><span class="asst-bubble-text"></span><span class="asst-bubble-caret" aria-hidden="true"></span></span><span class="asst-bubble-meta"></span><span class="asst-bubble-err" hidden></span></div>
|
|
1259
|
+
<div class="asst-rack-sweep" aria-hidden="true"></div>
|
|
1260
|
+
</div>
|
|
1261
|
+
<button type="button" class="asst-rack-receipt" aria-expanded="false" aria-controls="${ledgerId}"><span class="asst-rack-receipt-face" aria-hidden="true"></span><span class="asst-rack-receipt-text"></span><span class="asst-rack-receipt-failed" hidden></span><span class="asst-rack-receipt-clock"></span><span class="asst-rack-chev" aria-hidden="true">›</span></button>
|
|
1262
|
+
<div class="asst-rack-view" role="group" aria-label="${esc(t('assistant.rack.view.label', 'Show calls'))}"><button type="button" data-view="kind" aria-pressed="true">${esc(t('assistant.rack.view.kind', 'By kind'))}</button><button type="button" data-view="time" aria-pressed="false">${esc(t('assistant.rack.view.time', 'Timeline'))}</button></div>
|
|
1263
|
+
<ol class="asst-rack-ledger" id="${ledgerId}"></ol>
|
|
1264
|
+
<div class="asst-rack-status" role="status" aria-live="polite"></div><div class="asst-rack-alert" role="alert"></div>`;
|
|
1265
|
+
const q = (sel) => row.querySelector(sel);
|
|
1266
|
+
const rack = {
|
|
1267
|
+
id, row, scope, nested, closed: false, live: false, staged: false, open: false, view: 'kind', everAwake: false,
|
|
1268
|
+
stations: new Map(), steps: [], timeline: [], said: [], firstAt: 0,
|
|
1269
|
+
stage: { el: q('.asst-rack-stage'), face: q('.asst-rack-face'), caption: q('.asst-rack-caption'), bubble: q('.asst-bubble'), verb: q('.asst-bubble-verb'), subject: q('.asst-bubble-subject'), text: q('.asst-bubble-text'), meta: q('.asst-bubble-meta'), err: q('.asst-bubble-err') },
|
|
1270
|
+
receipt: { btn: q('.asst-rack-receipt'), face: q('.asst-rack-receipt-face'), text: q('.asst-rack-receipt-text'), failed: q('.asst-rack-receipt-failed'), clock: q('.asst-rack-receipt-clock'), faceBuilt: false, pose: '', svg: null },
|
|
1271
|
+
ledger: q('.asst-rack-ledger'), statusEl: q('.asst-rack-status'), alertEl: q('.asst-rack-alert'), timelineEl: null, timelineSig: '',
|
|
1272
|
+
rig: null, target: null, held: false, holdUntil: 0, holdTimer: 0, pose: null, awake: null, awakeKey: null,
|
|
1273
|
+
bubble: { step: null, since: 0, key: '', open: false }, stageTimer: 0, clearTimer: 0,
|
|
1274
|
+
sr: { last: 0, timer: 0, pending: '' },
|
|
1275
|
+
};
|
|
1276
|
+
row._rack = rack;
|
|
1277
|
+
rack.receipt.btn.addEventListener('click', () => setRackOpen(rack, !rack.open));
|
|
1278
|
+
row.querySelector('.asst-rack-view').addEventListener('click', (e) => {
|
|
1279
|
+
const btn = e.target.closest('button[data-view]');
|
|
1280
|
+
if (btn) setRackView(rack, btn.dataset.view);
|
|
1281
|
+
});
|
|
1282
|
+
rack.ledger.addEventListener('click', (e) => {
|
|
1283
|
+
const hdr = e.target.closest('.asst-station-hdr');
|
|
1284
|
+
const owner = hdr ? headers.get(hdr) : null;
|
|
1285
|
+
if (owner?.rack === rack) setStationOpen(rack, owner.st, !owner.st.open);
|
|
1286
|
+
});
|
|
1287
|
+
rack.ledger.addEventListener('keydown', (e) => onLedgerKey(rack, e));
|
|
1288
|
+
rack.stage.subject.addEventListener('click', () => setBubbleOpen(rack, !rack.bubble.open));
|
|
1289
|
+
rack.stage.subject.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setBubbleOpen(rack, !rack.bubble.open); } });
|
|
1290
|
+
racks.add(rack);
|
|
1291
|
+
append(row, scope);
|
|
1292
|
+
return rack;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
function ensureStation(rack, step) {
|
|
1296
|
+
let st = rack.stations.get(step.key);
|
|
1297
|
+
if (st) return st;
|
|
1298
|
+
const li = el('li', 'asst-station');
|
|
1299
|
+
li.dataset.kind = step.kind;
|
|
1300
|
+
li.dataset.state = 'idle';
|
|
1301
|
+
const logId = `asst-rack-${rack.id}-${step.key.replace(/[^\w-]/g, '_')}`;
|
|
1302
|
+
li.innerHTML = `<button type="button" class="asst-station-hdr" aria-expanded="false" aria-controls="${logId}" tabindex="${rack.stations.size ? -1 : 0}"><span class="asst-station-face" aria-hidden="true"></span><span class="asst-station-label"></span><span class="asst-station-agg"></span><span class="asst-station-ticks"></span><span class="asst-station-time"></span></button>${step.kind === 'subagent' ? '<div class="asst-station-sub"></div>' : ''}<ol class="asst-station-log" id="${logId}" hidden></ol>`;
|
|
1303
|
+
const q = (sel) => li.querySelector(sel);
|
|
1304
|
+
st = {
|
|
1305
|
+
key: step.key, kind: step.kind, li, hdr: q('.asst-station-hdr'), face: q('.asst-station-face'), label: q('.asst-station-label'), agg: q('.asst-station-agg'),
|
|
1306
|
+
ticks: q('.asst-station-ticks'), time: q('.asst-station-time'), sub: q('.asst-station-sub'), log: q('.asst-station-log'),
|
|
1307
|
+
steps: [], open: false, faceMode: '', tickSig: '', tickCount: 0, subSig: '', server: step.kind === 'mcp' ? mcpServerOf(step.name) : '',
|
|
1308
|
+
};
|
|
1309
|
+
headers.set(st.hdr, { rack, st });
|
|
1310
|
+
rack.stations.set(step.key, st);
|
|
1311
|
+
rack.ledger.appendChild(li);
|
|
1312
|
+
if (!muted && rack.live && typeof li.animate === 'function' && !calm()) li.animate([{ opacity: 0, transform: 'translateY(4px)' }, { opacity: 1, transform: 'none' }], { duration: 140, easing: 'cubic-bezier(0,0,.38,.9)' });
|
|
1313
|
+
// Screenshots stay in view: the Computer row always lists its calls (their details open with the row).
|
|
1314
|
+
if (step.kind === 'computer') st.log.hidden = false;
|
|
1315
|
+
return st;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
function setStationOpen(rack, st, open) {
|
|
1319
|
+
st.open = !!open;
|
|
1320
|
+
st.li.classList.toggle('is-open', st.open);
|
|
1321
|
+
st.hdr.setAttribute('aria-expanded', st.open ? 'true' : 'false');
|
|
1322
|
+
st.log.hidden = !st.open && st.kind !== 'computer';
|
|
1323
|
+
if (st.open) for (const step of st.steps) buildCallBody(step);
|
|
1324
|
+
renderStationFace(rack, st);
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
function setRackOpen(rack, open) {
|
|
1328
|
+
rack.open = !!open;
|
|
1329
|
+
rack.row.classList.toggle('is-open', rack.open);
|
|
1330
|
+
rack.receipt.btn.setAttribute('aria-expanded', rack.open ? 'true' : 'false');
|
|
1331
|
+
// One call: opening the receipt shows the call itself.
|
|
1332
|
+
if (rack.open && rack.steps.length === 1) { const st = rack.stations.values().next().value; if (st && !st.open) setStationOpen(rack, st, true); }
|
|
1333
|
+
if (rack.open) for (const st of rack.stations.values()) renderStationFace(rack, st);
|
|
1334
|
+
if (rack.open && rack.view === 'time') renderTimeline(rack);
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
function setRackView(rack, view) {
|
|
1338
|
+
rack.view = view === 'time' ? 'time' : 'kind';
|
|
1339
|
+
rack.row.dataset.view = rack.view;
|
|
1340
|
+
for (const btn of rack.row.querySelectorAll(':scope > .asst-rack-view > button')) btn.setAttribute('aria-pressed', btn.dataset.view === rack.view ? 'true' : 'false');
|
|
1341
|
+
if (rack.view === 'time') renderTimeline(rack);
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
/** Rows are a roving tab stop: ↑/↓ move, →/← open and close, Home/End jump. */
|
|
1345
|
+
function onLedgerKey(rack, e) {
|
|
1346
|
+
const hdr = e.target.closest?.('.asst-station-hdr');
|
|
1347
|
+
const owner = hdr ? headers.get(hdr) : null;
|
|
1348
|
+
if (!owner || owner.rack !== rack) return;
|
|
1349
|
+
const list = [...rack.stations.values()].filter(st => st.hdr.getClientRects().length);
|
|
1350
|
+
const at = list.indexOf(owner.st);
|
|
1351
|
+
let next = null;
|
|
1352
|
+
if (e.key === 'ArrowDown') next = list[Math.min(list.length - 1, at + 1)];
|
|
1353
|
+
else if (e.key === 'ArrowUp') next = list[Math.max(0, at - 1)];
|
|
1354
|
+
else if (e.key === 'Home') next = list[0];
|
|
1355
|
+
else if (e.key === 'End') next = list[list.length - 1];
|
|
1356
|
+
else if (e.key === 'ArrowRight') { e.preventDefault(); if (!owner.st.open) setStationOpen(rack, owner.st, true); return; }
|
|
1357
|
+
else if (e.key === 'ArrowLeft') { e.preventDefault(); if (owner.st.open) setStationOpen(rack, owner.st, false); return; }
|
|
1358
|
+
else return;
|
|
1359
|
+
e.preventDefault();
|
|
1360
|
+
if (!next) return;
|
|
1361
|
+
for (const st of rack.stations.values()) st.hdr.tabIndex = st === next ? 0 : -1;
|
|
1362
|
+
next.hdr.focus();
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
function setBubbleOpen(rack, open) {
|
|
1366
|
+
rack.bubble.open = !!open;
|
|
1367
|
+
rack.stage.subject.setAttribute('aria-expanded', rack.bubble.open ? 'true' : 'false');
|
|
1368
|
+
rack.bubble.key = '';
|
|
1369
|
+
paintBubble(rack, Date.now(), { swap: false });
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
/** Live: the burst is the open tail of a running turn, or a call of it is still running. */
|
|
1373
|
+
function rackLive(rack) {
|
|
1374
|
+
if (!turnActive || !rack.row.isConnected) return false;
|
|
1375
|
+
if (rack.steps.some(s => s.state === 'running' && !s.stale)) return true;
|
|
1376
|
+
return !rack.closed && liveTail(rack.scope) === rack && scopeBusy(rack.scope);
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
/** A subagent's feed is busy until its Agent/Task call has a result. */
|
|
1380
|
+
function scopeBusy(scope) {
|
|
1381
|
+
if (scope === rootScope || !scope.key) return true;
|
|
1382
|
+
const parent = stepsById.get(scope.key);
|
|
1383
|
+
return !parent || (parent.state === 'running' && !parent.stale);
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
// ── Waiting on a card ──
|
|
1387
|
+
/** A card waits on the user (true + the call's tool_use id when known), or none (false). */
|
|
1388
|
+
function setWaiting(on, toolUseId = null) {
|
|
1389
|
+
const id = on ? (toolUseId ? String(toolUseId) : null) : null;
|
|
1390
|
+
if (!on && !waiting) return;
|
|
1391
|
+
if (on && waiting && waiting.id === id) return;
|
|
1392
|
+
waiting = on ? { id, since: Date.now() } : null;
|
|
1393
|
+
for (const rack of [...racks]) if (rack.live || rack.waiting) renderRack(rack);
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
/** The rack a waiting card holds: the one with its call, else the newest live stage. */
|
|
1397
|
+
function waitingRack() {
|
|
1398
|
+
if (!waiting) return null;
|
|
1399
|
+
const step = waiting.id ? stepsById.get(waiting.id) : null;
|
|
1400
|
+
if (step?.rack?.live && !step.rack.nested && step.rack.row.isConnected) return step.rack;
|
|
1401
|
+
let best = null;
|
|
1402
|
+
for (const rack of racks) if (rack.live && !rack.nested && rack.row.isConnected && (!best || rack.id > best.id)) best = rack;
|
|
1403
|
+
return best;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
/** The call the card holds in `rack`: its own, else the newest one still running. */
|
|
1407
|
+
function waitingStep(rack) {
|
|
1408
|
+
if (!rack?.waiting) return null;
|
|
1409
|
+
const own = waiting?.id ? stepsById.get(waiting.id) : null;
|
|
1410
|
+
if (own && own.rack === rack && own.state === 'running' && !own.stale) return own;
|
|
1411
|
+
let best = null;
|
|
1412
|
+
for (const s of rack.steps) if (s.type === 'call' && s.state === 'running' && !s.stale && (!best || s.startedAt >= best.startedAt)) best = s;
|
|
1413
|
+
return best;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
/** The rack's clock: stopped while a card holds it. */
|
|
1417
|
+
const rackNow = (rack) => (rack.waiting && rack.waitingSince ? rack.waitingSince : Date.now());
|
|
1418
|
+
|
|
1419
|
+
function renderRack(rack) {
|
|
1420
|
+
const { row } = rack;
|
|
1421
|
+
if (!row.isConnected) { dropRack(rack); return; }
|
|
1422
|
+
const live = rackLive(rack);
|
|
1423
|
+
const was = rack.live;
|
|
1424
|
+
rack.live = live;
|
|
1425
|
+
rack.waiting = live && !rack.nested && !!waiting && waitingRack() === rack;
|
|
1426
|
+
rack.waitingSince = rack.waiting ? waiting.since : 0;
|
|
1427
|
+
const now = Date.now();
|
|
1428
|
+
if (live) evalAwake(rack, now);
|
|
1429
|
+
else { rack.awake = null; rack.awakeKey = null; }
|
|
1430
|
+
const single = rack.steps.length === 1;
|
|
1431
|
+
row.dataset.state = rack.waiting ? 'waiting' : live ? 'live' : 'settled';
|
|
1432
|
+
row.classList.toggle('is-live', live);
|
|
1433
|
+
row.classList.toggle('is-single', single);
|
|
1434
|
+
row.classList.toggle('is-nested', rack.nested);
|
|
1435
|
+
if (live && rack.awake) row.dataset.kind = rack.awake.step.kind; else delete row.dataset.kind;
|
|
1436
|
+
// A settled stage stays up while it holds for the turn's success or error (holdStage).
|
|
1437
|
+
const holding = rack.staged && rack.holdUntil > now;
|
|
1438
|
+
setStaged(rack, (live || holding) && !rack.nested && (rack.steps.length > 1 || rack.everAwake));
|
|
1439
|
+
for (const st of rack.stations.values()) renderStation(rack, st);
|
|
1440
|
+
const sentence = receiptSentence(groupsOf(rack), { t, locale: locale() }); // once per render: receipt and label share it
|
|
1441
|
+
renderReceipt(rack, sentence);
|
|
1442
|
+
renderRackLabel(rack, sentence);
|
|
1443
|
+
if (live) { if (rack.staged) paintStage(rack, now); scheduleStage(rack, now); needTicker(); }
|
|
1444
|
+
else if (was) settle(rack);
|
|
1445
|
+
if (rack.open && rack.view === 'time') renderTimeline(rack);
|
|
1446
|
+
if (working?.isConnected) syncWorkingFace();
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
// ── Stage ──
|
|
1450
|
+
/** The awake call: the newest one still running that has run 350 ms; else the last awake one to finish, for 1.2 s. */
|
|
1451
|
+
function evalAwake(rack, now) {
|
|
1452
|
+
// A card holds a call: that call is on stage, whether or not it has run 350 ms.
|
|
1453
|
+
const held = waitingStep(rack);
|
|
1454
|
+
if (held) {
|
|
1455
|
+
held.woke = true;
|
|
1456
|
+
rack.awake = { step: held };
|
|
1457
|
+
rack.awakeKey = held.key;
|
|
1458
|
+
rack.everAwake = true;
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
let best = null;
|
|
1462
|
+
for (const s of rack.steps) {
|
|
1463
|
+
if (s.state !== 'running' || s.stale || now - s.startedAt < WAKE_MS) continue;
|
|
1464
|
+
if (!best || s.startedAt > best.startedAt || (s.startedAt === best.startedAt && s.seq > best.seq)) best = s;
|
|
1465
|
+
}
|
|
1466
|
+
if (best) best.woke = true;
|
|
1467
|
+
else {
|
|
1468
|
+
for (const s of rack.steps) if (s.woke && s.endedAt && now - s.endedAt < LINGER_MS && (!best || s.endedAt > best.endedAt)) best = s;
|
|
1469
|
+
}
|
|
1470
|
+
rack.awake = best ? { step: best } : null;
|
|
1471
|
+
rack.awakeKey = best ? best.key : null;
|
|
1472
|
+
if (best) rack.everAwake = true;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
function scheduleStage(rack, now) {
|
|
1476
|
+
clearTimeout(rack.stageTimer);
|
|
1477
|
+
rack.stageTimer = 0;
|
|
1478
|
+
if (!rack.live) return;
|
|
1479
|
+
let at = Infinity;
|
|
1480
|
+
for (const s of rack.steps) {
|
|
1481
|
+
if (s.state === 'running' && !s.stale && !s.woke && now - s.startedAt < WAKE_MS) at = Math.min(at, s.startedAt + WAKE_MS);
|
|
1482
|
+
if (s.woke && s.endedAt && now - s.endedAt < LINGER_MS) at = Math.min(at, s.endedAt + LINGER_MS);
|
|
1483
|
+
}
|
|
1484
|
+
if (rack.bubble.pendingAt) at = Math.min(at, rack.bubble.pendingAt);
|
|
1485
|
+
if (at === Infinity) return;
|
|
1486
|
+
rack.stageTimer = setTimeout(() => { rack.stageTimer = 0; renderRack(rack); }, Math.max(16, at - now + 5));
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
function setStaged(rack, staged) {
|
|
1490
|
+
if (rack.staged === staged) return;
|
|
1491
|
+
rack.staged = staged;
|
|
1492
|
+
rack.stage.subject.tabIndex = staged ? 0 : -1;
|
|
1493
|
+
if (staged) {
|
|
1494
|
+
clearTimeout(rack.clearTimer);
|
|
1495
|
+
rack.clearTimer = 0;
|
|
1496
|
+
rack.row.classList.add('is-staged');
|
|
1497
|
+
if (!rack.rig) {
|
|
1498
|
+
// The parked hero hands over: the stage's character starts in its pose and flies in from its box.
|
|
1499
|
+
const heir = hero?.parked && !hero.leaving && !calm() ? hero : null;
|
|
1500
|
+
try { rack.rig = mascotLib.createMascot(rack.stage.face, { width: 72, height: 36, rangeX: 10, rangeY: 7, active: rigsOn, pose: heir ? heir.rig.pose : 'idle' }); } catch { rack.rig = null; }
|
|
1501
|
+
rack.pose = null;
|
|
1502
|
+
rack.target = rack.rig ? stageTarget(rack) : null;
|
|
1503
|
+
if (heir && rack.rig) flipIntoStage(rack);
|
|
1504
|
+
else if (hero?.parked) dropHero();
|
|
1505
|
+
}
|
|
1506
|
+
mascotChanged();
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
// Fold the stage into the receipt; a rack above the viewport folds at once and the scroll position absorbs it.
|
|
1510
|
+
// Keyboard focus inside the stage (the bubble, its Copy) moves to the receipt, which takes the stage's place.
|
|
1511
|
+
const row = rack.row;
|
|
1512
|
+
const focused = rack.stage.el.contains(document.activeElement);
|
|
1513
|
+
let above = false;
|
|
1514
|
+
if (!calm() && row.isConnected) {
|
|
1515
|
+
const box = row.getBoundingClientRect();
|
|
1516
|
+
above = box.bottom <= $msgs.getBoundingClientRect().top + 1;
|
|
1517
|
+
}
|
|
1518
|
+
if (calm() || above) {
|
|
1519
|
+
const h = above ? rack.stage.el.offsetHeight : 0;
|
|
1520
|
+
row.classList.add('is-instant');
|
|
1521
|
+
row.classList.remove('is-staged');
|
|
1522
|
+
void row.offsetHeight; // apply without the transition
|
|
1523
|
+
row.classList.remove('is-instant');
|
|
1524
|
+
if (h) $msgs.scrollTop = Math.max(0, $msgs.scrollTop - h);
|
|
1525
|
+
} else {
|
|
1526
|
+
row.classList.remove('is-staged');
|
|
1527
|
+
}
|
|
1528
|
+
if (focused) {
|
|
1529
|
+
try { rack.receipt.btn.focus({ preventScroll: true }); } catch { /* not focusable yet */ }
|
|
1530
|
+
}
|
|
1531
|
+
clearTimeout(rack.clearTimer);
|
|
1532
|
+
rack.clearTimer = setTimeout(() => { rack.clearTimer = 0; clearStage(rack); }, calm() || above ? 0 : COLLAPSE_MS);
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
/** A folded stage keeps nothing: its rig goes, its texts empty (a settled rack is the same live or replayed). */
|
|
1536
|
+
function clearStage(rack) {
|
|
1537
|
+
if (rack.staged) return;
|
|
1538
|
+
const had = !!rack.rig;
|
|
1539
|
+
rack.rig?.destroy?.();
|
|
1540
|
+
rack.rig = null;
|
|
1541
|
+
rack.target = null;
|
|
1542
|
+
rack.held = false;
|
|
1543
|
+
rack.pose = null;
|
|
1544
|
+
if (had) mascotChanged();
|
|
1545
|
+
const s = rack.stage;
|
|
1546
|
+
s.face.textContent = '';
|
|
1547
|
+
s.caption.textContent = '';
|
|
1548
|
+
s.bubble.hidden = true;
|
|
1549
|
+
delete s.bubble.dataset.state;
|
|
1550
|
+
delete s.bubble.dataset.kind;
|
|
1551
|
+
s.verb.textContent = '';
|
|
1552
|
+
s.text.textContent = '';
|
|
1553
|
+
s.subject.removeAttribute('title');
|
|
1554
|
+
s.subject.className = 'asst-bubble-subject';
|
|
1555
|
+
s.subject.setAttribute('aria-expanded', 'false');
|
|
1556
|
+
s.meta.textContent = '';
|
|
1557
|
+
s.err.textContent = '';
|
|
1558
|
+
s.err.hidden = true;
|
|
1559
|
+
s.bubble.querySelector('.asst-bubble-copy')?.remove();
|
|
1560
|
+
rack.bubble = { step: null, since: 0, key: '', open: false };
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
/** One rig animates: the hero, else the newest stage, else the "Thinking" row's; none while another panel leads (setRigsEnabled). */
|
|
1564
|
+
function activateRigs() {
|
|
1565
|
+
const newest = hero?.rig ? null : newestStage();
|
|
1566
|
+
for (const rack of racks) if (rack.rig) rack.rig.setActive?.(rigsOn && rack === newest);
|
|
1567
|
+
hero?.rig?.setActive?.(rigsOn);
|
|
1568
|
+
workingFace?.rig?.setActive?.(rigsOn && !hero?.rig && !newest);
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
/** The bubble's call: the awake one, else the newest; it holds 900 ms (3 s for thinking) before the next wins. */
|
|
1572
|
+
function bubbleTarget(rack) {
|
|
1573
|
+
if (rack.awake) return rack.awake.step;
|
|
1574
|
+
let best = null;
|
|
1575
|
+
for (const s of rack.steps) if (!best || s.startedAt > best.startedAt || (s.startedAt === best.startedAt && s.seq > best.seq)) best = s;
|
|
1576
|
+
return best;
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
/** Pose options the rig understands: memory recalls or saves, how many subagents run, how far the plan got. */
|
|
1580
|
+
function poseOptions(rack, step) {
|
|
1581
|
+
if (!step) return undefined;
|
|
1582
|
+
if (step.kind === 'memory') return { mode: /remember|reflect/.test(String(step.name).toLowerCase()) ? 'remember' : 'recall' };
|
|
1583
|
+
if (step.kind === 'subagent') return { count: Math.max(1, Math.min(3, rack.steps.filter(s => s.kind === 'subagent' && s.state === 'running' && !s.stale).length)) };
|
|
1584
|
+
if (step.kind === 'plan') return { ticks: Math.min(3, planState(step.input).done) };
|
|
1585
|
+
return undefined;
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
/**
|
|
1589
|
+
* The bubble's call failed and no call started after it: the stage keeps
|
|
1590
|
+
* the error pose (only a success smiles) until the next call begins.
|
|
1591
|
+
*/
|
|
1592
|
+
function failedOnStage(rack) {
|
|
1593
|
+
const step = rack.bubble.step;
|
|
1594
|
+
if (!step || step.state !== 'error') return null;
|
|
1595
|
+
return rack.steps.some(s => s !== step && s.startedAt > (step.endedAt || step.startedAt)) ? null : step;
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
/** The stage's character acts out the awake call (idle between calls); a pose the director holds stays. */
|
|
1599
|
+
function poseStage(rack) {
|
|
1600
|
+
if (!rack.rig || rack.held) return;
|
|
1601
|
+
const failed = failedOnStage(rack);
|
|
1602
|
+
const step = failed || (rack.awake ? rack.awake.step : null);
|
|
1603
|
+
const kind = failed ? 'error' : step ? step.kind : 'idle';
|
|
1604
|
+
const opts = failed ? undefined : poseOptions(rack, step);
|
|
1605
|
+
const poseKey = `${kind}|${opts ? JSON.stringify(opts) : ''}`;
|
|
1606
|
+
if (rack.pose === poseKey) return;
|
|
1607
|
+
const wasStep = rack.poseStep;
|
|
1608
|
+
rack.rig.setPose?.(kind, opts);
|
|
1609
|
+
rack.pose = poseKey;
|
|
1610
|
+
rack.poseStep = step;
|
|
1611
|
+
// A new call answers with a small gesture: the computer clicks, the plan ticks a box.
|
|
1612
|
+
if (step && !failed && step !== wasStep && !calm()) {
|
|
1613
|
+
if (step.kind === 'computer' && /click/.test(String(step.input?.action || ''))) rack.rig.react?.('click');
|
|
1614
|
+
else if (step.kind === 'plan') rack.rig.react?.('pop');
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
function paintStage(rack, now) {
|
|
1619
|
+
const target = bubbleTarget(rack);
|
|
1620
|
+
const shown = rack.bubble.step;
|
|
1621
|
+
rack.bubble.pendingAt = 0;
|
|
1622
|
+
if (target && target !== shown) {
|
|
1623
|
+
const hold = shown?.type === 'think' ? THINK_HOLD_MS : HOLD_MS;
|
|
1624
|
+
// A card holding a call takes the bubble at once; otherwise the shown call keeps it its dwell time.
|
|
1625
|
+
if (!rack.waiting && shown && shown.rack === rack && now - rack.bubble.since < hold) rack.bubble.pendingAt = rack.bubble.since + hold;
|
|
1626
|
+
else { rack.bubble.step = target; rack.bubble.since = now; }
|
|
1627
|
+
}
|
|
1628
|
+
poseStage(rack);
|
|
1629
|
+
paintBubble(rack, now, { swap: true });
|
|
1630
|
+
paintCaption(rack);
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
function paintCaption(rack) {
|
|
1634
|
+
const step = rack.bubble.step;
|
|
1635
|
+
const say = step ? sayOf(step) : null;
|
|
1636
|
+
const text = (say?.caption) || rack.said[rack.said.length - 1]?.text || '';
|
|
1637
|
+
if (rack.stage.caption.textContent !== text) rack.stage.caption.textContent = text;
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
/** Same-kind calls beside the bubble's call (one message, or running side by side): "a.js, b.js +1", or a re-read "server.js ×2". */
|
|
1641
|
+
function groupedSubject(rack, step) {
|
|
1642
|
+
if (step.type !== 'call') return '';
|
|
1643
|
+
const running = (x) => x.state === 'running' && !x.stale;
|
|
1644
|
+
const peers = step.station.steps.filter(s => s !== step && s.type === 'call' && ((step.msgId && s.msgId === step.msgId) || (running(s) && running(step))));
|
|
1645
|
+
return groupSubject(step.kind, [...peers, step], step.station.steps);
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
function paintBubble(rack, now, { swap = false } = {}) {
|
|
1649
|
+
const s = rack.stage;
|
|
1650
|
+
const step = rack.bubble.step;
|
|
1651
|
+
if (!step) { s.bubble.hidden = true; return; }
|
|
1652
|
+
const say = sayOf(step);
|
|
1653
|
+
const running = step.state === 'running' && !step.stale;
|
|
1654
|
+
const held = rack.waiting && step === waitingStep(rack);
|
|
1655
|
+
const verb = held ? t('assistant.rack.waiting', 'Waiting for you') : verbText(say.verb, running, t);
|
|
1656
|
+
const grouped = groupedSubject(rack, step);
|
|
1657
|
+
const full = grouped || say.text;
|
|
1658
|
+
const literal = !grouped && say.full ? say.full : full; // the command as written: the title and the opened bubble
|
|
1659
|
+
const open = rack.bubble.open;
|
|
1660
|
+
// Narrow stages cut from the head, so the file name (the tail) survives.
|
|
1661
|
+
const cut = subjectCut(rack);
|
|
1662
|
+
const budget = subjectBudget(rack, verb, cut);
|
|
1663
|
+
const shownText = open ? literal : cut === 'head' ? headEllipsis(full, budget) : middleEllipsis(full, budget, 24);
|
|
1664
|
+
const key = `${step.id}|${verb}|${shownText}|${say.muted ? 1 : 0}`;
|
|
1665
|
+
s.bubble.hidden = !full && !verb;
|
|
1666
|
+
s.bubble.dataset.state = held ? 'waiting' : running ? 'running' : step.state;
|
|
1667
|
+
s.bubble.dataset.kind = step.kind;
|
|
1668
|
+
if (key !== rack.bubble.key) {
|
|
1669
|
+
const changedStep = !rack.bubble.key.startsWith(`${step.id}|`);
|
|
1670
|
+
rack.bubble.key = key;
|
|
1671
|
+
const write = () => {
|
|
1672
|
+
s.verb.textContent = verb;
|
|
1673
|
+
s.subject.className = `asst-bubble-subject${say.mono ? ' is-mono' : ''}${say.muted ? ' is-muted' : ''}`;
|
|
1674
|
+
writeSubject(s.text, grouped || open ? null : say.segments, shownText);
|
|
1675
|
+
if (literal !== shownText || open) s.subject.title = literal; else s.subject.removeAttribute('title');
|
|
1676
|
+
};
|
|
1677
|
+
if (swap && changedStep && !calm() && typeof s.subject.animate === 'function' && s.text.textContent) {
|
|
1678
|
+
s.subject.getAnimations?.().forEach(a => a.cancel());
|
|
1679
|
+
const out = s.subject.animate([{ opacity: 1 }, { opacity: 0 }], { duration: 80, easing: 'cubic-bezier(.2,0,1,.9)', fill: 'forwards' });
|
|
1680
|
+
out.onfinish = () => {
|
|
1681
|
+
write();
|
|
1682
|
+
out.cancel();
|
|
1683
|
+
s.subject.animate([{ opacity: 0, transform: 'translateY(4px)' }, { opacity: 1, transform: 'none' }], { duration: 140, easing: 'cubic-bezier(0,0,.38,.9)' });
|
|
1684
|
+
};
|
|
1685
|
+
} else write();
|
|
1686
|
+
}
|
|
1687
|
+
paintBubbleMeta(rack, step, say, now);
|
|
1688
|
+
const err = step.state === 'error' ? (step.detail || exitText(step)) : '';
|
|
1689
|
+
s.err.hidden = !err;
|
|
1690
|
+
if (s.err.textContent !== err) s.err.textContent = err;
|
|
1691
|
+
let copy = s.bubble.querySelector('.asst-bubble-copy');
|
|
1692
|
+
if (open && (say.copy || full)) {
|
|
1693
|
+
if (!copy) { copy = el('button', 'asst-bubble-copy', t('assistant.rack.copy', 'Copy')); copy.type = 'button'; copy.addEventListener('click', (e) => { e.stopPropagation(); const st = rack.bubble.step; copyText(st ? (sayOf(st).copy || sayOf(st).text) : ''); copy.textContent = t('assistant.rack.copied', 'Copied'); setTimeout(() => { copy.textContent = t('assistant.rack.copy', 'Copy'); }, 1200); }); s.bubble.appendChild(copy); }
|
|
1694
|
+
} else copy?.remove();
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
/**
|
|
1698
|
+
* How many characters of subject fit the bubble: two lines of the column
|
|
1699
|
+
* beside the verb, measured on the laid-out stage. The text is cut in the
|
|
1700
|
+
* middle so its tail (the file, the last argument) stays; on a narrow stage
|
|
1701
|
+
* from the head (subjectCut).
|
|
1702
|
+
*/
|
|
1703
|
+
function subjectBudget(rack, verb, cut = 'middle') {
|
|
1704
|
+
const s = rack.stage;
|
|
1705
|
+
const width = s.el.clientWidth;
|
|
1706
|
+
if (!width) return 96;
|
|
1707
|
+
const face = s.face.getBoundingClientRect();
|
|
1708
|
+
const bubble = s.bubble.hidden ? null : s.bubble.getBoundingClientRect();
|
|
1709
|
+
const beside = !bubble || bubble.left >= face.right - 1;
|
|
1710
|
+
const verbPx = Math.max(s.verb.getBoundingClientRect().width, verb.length * 7);
|
|
1711
|
+
const column = width - 28 - 22 - (beside ? face.width + 8 : 0) - verbPx - 7;
|
|
1712
|
+
const perLine = Math.max(12, Math.floor(column / 7));
|
|
1713
|
+
// Two lines, less what wrapping at token boundaries wastes (a head cut wraps anywhere: two full lines).
|
|
1714
|
+
const lines = getComputedStyle(s.subject).whiteSpace === 'nowrap' ? 1 : cut === 'head' ? 2 : 1.7;
|
|
1715
|
+
return Math.max(24, Math.min(240, Math.floor(perLine * lines) - 1));
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
/** 'head' where the stylesheet says so (≤320 px: keep the tail), else 'middle'. */
|
|
1719
|
+
function subjectCut(rack) {
|
|
1720
|
+
try { return getComputedStyle(rack.stage.subject).getPropertyValue('--rk-cut').trim() === 'head' ? 'head' : 'middle'; } catch { return 'middle'; }
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
/** "…ender-events.browser.mjs": the last `max` chars, from a token boundary when one is near. */
|
|
1724
|
+
function headEllipsis(text, max) {
|
|
1725
|
+
const s = String(text ?? '');
|
|
1726
|
+
if (s.length <= max) return s;
|
|
1727
|
+
let tail = s.slice(s.length - Math.max(8, max - 1));
|
|
1728
|
+
const cut = tail.search(/[\s/]/);
|
|
1729
|
+
if (cut > 0 && cut < tail.length * 0.4) tail = tail.slice(tail[cut] === '/' ? cut : cut + 1);
|
|
1730
|
+
return `…${tail}`;
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
function writeSubject(node, segments, text) {
|
|
1734
|
+
node.textContent = '';
|
|
1735
|
+
if (Array.isArray(segments) && segments.length && segments.map(x => x.text).join('') === text) {
|
|
1736
|
+
for (const seg of segments) node.appendChild(el('span', seg.cls ? `is-${seg.cls}` : '', seg.text));
|
|
1737
|
+
} else node.textContent = text;
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
/** How many calls of the rack act on the bubble call's target (itself included): a re-run or re-read reads ×N. */
|
|
1741
|
+
function repeatCount(step) {
|
|
1742
|
+
if (step.type !== 'call') return 1;
|
|
1743
|
+
const key = targetKey(step);
|
|
1744
|
+
return key ? step.station.steps.filter(s => s.type === 'call' && targetKey(s) === key).length : 1;
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
/** Row 2 of the bubble: numbers in their own tabular spans (time, ×N, counts). */
|
|
1748
|
+
function paintBubbleMeta(rack, step, say, now) {
|
|
1749
|
+
const meta = rack.stage.meta;
|
|
1750
|
+
const parts = [];
|
|
1751
|
+
const running = step.state === 'running' && !step.stale;
|
|
1752
|
+
// A card holding the call stops its clock where the wait began.
|
|
1753
|
+
const clock = rack.waiting && step === waitingStep(rack) ? rack.waitingSince : now;
|
|
1754
|
+
const ms = running ? Math.max((step.progress || 0) * 1000, clock - step.startedAt) : (step.endedAt && step.timed ? step.endedAt - step.startedAt : null);
|
|
1755
|
+
const time = ms != null && (running ? ms >= 1000 : true) ? fmtDuration(ms, t) : '';
|
|
1756
|
+
for (const m of say.meta) parts.push(['m', m]);
|
|
1757
|
+
if (say.tag) parts.push(['m', say.tag]);
|
|
1758
|
+
// A subagent's bubble follows its child: the step the agent is on now.
|
|
1759
|
+
if (step.kind === 'subagent' && step.children?.length) {
|
|
1760
|
+
const child = step.children.filter(c => c.type === 'call' || (c.state === 'running' && !c.stale)).reduce((a, b) => (!a || b.startedAt >= a.startedAt ? b : a), null);
|
|
1761
|
+
if (child) parts.push(['m', middleEllipsis(stepSentence(child, child.state === 'running' && !child.stale), 60, 24)]);
|
|
1762
|
+
}
|
|
1763
|
+
// ×N only for the same target again (a re-run, a re-read): a number the ledger agrees with.
|
|
1764
|
+
// Other calls in the same 800 ms burst read as a sentence instead.
|
|
1765
|
+
const grouped = groupedSubject(rack, step);
|
|
1766
|
+
const repeats = repeatCount(step);
|
|
1767
|
+
if (repeats >= 2 && !/×\d+$/.test(grouped)) parts.push(['m', `×${repeats}`]);
|
|
1768
|
+
else if (repeats < 2 && !grouped) {
|
|
1769
|
+
const burst = rack.steps.filter(s => s !== step && s.type === 'call' && step.startedAt - s.startedAt >= 0 && step.startedAt - s.startedAt <= BURST_MS).length;
|
|
1770
|
+
if (burst >= 2) parts.push(['m', plural(t, 'assistant.rack.moreSteps', burst, '+1 more step', '+{count} more steps')]);
|
|
1771
|
+
}
|
|
1772
|
+
const sig = parts.map(p => p[1]).join('\u0001');
|
|
1773
|
+
if (meta._sig !== sig) {
|
|
1774
|
+
meta._sig = sig;
|
|
1775
|
+
meta.textContent = '';
|
|
1776
|
+
for (const [, text] of parts) meta.appendChild(el('span', 'asst-bubble-num', text));
|
|
1777
|
+
meta._time = null;
|
|
1778
|
+
}
|
|
1779
|
+
let node = meta._time;
|
|
1780
|
+
if (time && !node?.isConnected) { node = el('span', 'asst-bubble-num asst-odo'); meta.appendChild(node); meta._time = node; node._odo = ''; }
|
|
1781
|
+
if (node) { if (time) setOdometer(node, time); else { node.remove(); meta._time = null; } }
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
/** Digits roll when they change (a tabular odometer); no motion when calm. */
|
|
1785
|
+
function setOdometer(node, text) {
|
|
1786
|
+
if (node._odo === text) return;
|
|
1787
|
+
const prev = node._odo || '';
|
|
1788
|
+
node._odo = text;
|
|
1789
|
+
if (calm() || prev.length !== text.length) {
|
|
1790
|
+
node.textContent = '';
|
|
1791
|
+
for (const ch of text) node.appendChild(el('span', 'asst-odo-d', ch));
|
|
1792
|
+
return;
|
|
1793
|
+
}
|
|
1794
|
+
const digits = node.children;
|
|
1795
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
1796
|
+
const d = digits[i];
|
|
1797
|
+
if (!d || d.textContent === text[i]) continue;
|
|
1798
|
+
d.textContent = text[i];
|
|
1799
|
+
if (/\d/.test(text[i]) && typeof d.animate === 'function') d.animate([{ transform: 'translateY(45%)', opacity: 0.3 }, { transform: 'none', opacity: 1 }], { duration: 140, easing: 'cubic-bezier(0,0,.38,.9)' });
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
function settle(rack) {
|
|
1804
|
+
clearTimeout(rack.stageTimer);
|
|
1805
|
+
rack.stageTimer = 0;
|
|
1806
|
+
if (!(rack.holdUntil > Date.now())) rack.rig?.setActive?.(false);
|
|
1807
|
+
activateRigs();
|
|
1808
|
+
if (!muted) announce(rack, finishSentence(rack));
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
const exitText = (step) => (step.code != null ? t('assistant.rack.say.exit', 'exit {code}', { code: step.code }) : '');
|
|
1812
|
+
|
|
1813
|
+
function onFailure(step) {
|
|
1814
|
+
const rack = step.rack;
|
|
1815
|
+
if (muted) return;
|
|
1816
|
+
if (rack.staged && !rack.held) rack.rig?.react?.('wince');
|
|
1817
|
+
if (!turnActive) return;
|
|
1818
|
+
const detail = step.detail || exitText(step);
|
|
1819
|
+
alertRack(rack, t('assistant.rack.sr.failed', '{what} failed{detail}', { what: stepSentence(step, false), detail: detail ? `: ${detail}` : '' }));
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
// ── Ledger rows ──
|
|
1823
|
+
function stationLabel(st) {
|
|
1824
|
+
if (st.kind === 'mcp') return prettyServer(st.server);
|
|
1825
|
+
const [key, fallback] = KIND_SPECS[st.kind]?.label || KIND_SPECS.mcp.label;
|
|
1826
|
+
return t(key, fallback);
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
function renderStation(rack, st) {
|
|
1830
|
+
const label = stationLabel(st);
|
|
1831
|
+
if (st.label.textContent !== label) st.label.textContent = label;
|
|
1832
|
+
const agg = stationAggregate(st.kind, st.steps, { t, server: st.server });
|
|
1833
|
+
if (st.agg.textContent !== agg) st.agg.textContent = agg;
|
|
1834
|
+
const failed = st.steps.some(s => s.state === 'error');
|
|
1835
|
+
const awake = rack.live && rack.awakeKey === st.key;
|
|
1836
|
+
st.li.dataset.state = awake ? (rack.waiting ? 'waiting' : 'awake') : failed ? 'failed' : 'idle';
|
|
1837
|
+
const held = waitingStep(rack);
|
|
1838
|
+
const plan = st.kind === 'plan' ? planTicks(st) : null;
|
|
1839
|
+
const sig = plan ? `p${plan.map(s => s.status[0]).join('')}${awake ? 'a' : ''}` : st.steps.map(s => (s.stale ? 'x' : s === held ? 'w' : s.state[0])).join('');
|
|
1840
|
+
if (sig !== st.tickSig) paintTicks(rack, st, sig, plan);
|
|
1841
|
+
const time = stationTime(st, rack.live, rackNow(rack));
|
|
1842
|
+
if (st.time.textContent !== time) st.time.textContent = time;
|
|
1843
|
+
st.hdr.setAttribute('aria-label', time ? t('assistant.rack.aria.row', '{label}: {summary}, {time}', { label, summary: agg, time }) : t('assistant.rack.aria.rowUntimed', '{label}: {summary}', { label, summary: agg }));
|
|
1844
|
+
if (st.sub) paintSubstrip(rack, st);
|
|
1845
|
+
renderStationFace(rack, st);
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
/**
|
|
1849
|
+
* A row's glyph, built once the row is on screen (live ledger, or an opened
|
|
1850
|
+
* receipt): the kind's prop alone, so rows differ at a squint; the awake
|
|
1851
|
+
* row wears the eyes (a still face in its kind's pose).
|
|
1852
|
+
*/
|
|
1853
|
+
function renderStationFace(rack, st) {
|
|
1854
|
+
if (!(rack.staged || rack.open || (rack.nested && rack.live))) return;
|
|
1855
|
+
const mode = rack.live && rack.awakeKey === st.key ? 'eyes' : 'prop';
|
|
1856
|
+
if (st.faceMode === mode) return;
|
|
1857
|
+
st.faceMode = mode;
|
|
1858
|
+
if (mode === 'eyes') mountStill(st.face, st.kind, KIND_SPECS[st.kind]?.glyph || '#', { staticGlyph: true });
|
|
1859
|
+
else mountProp(st.face, st.kind, KIND_SPECS[st.kind]?.glyph || '#');
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
/**
|
|
1863
|
+
* A still mini face: the mascot's SVG painted with the pose's still frame
|
|
1864
|
+
* (paintMascot + stillFrame), never a rig, so a long transcript registers
|
|
1865
|
+
* and observes nothing. Without poses: the plain mascot (receipt) or the
|
|
1866
|
+
* kind's glyph (rows). Returns the SVG to repaint, or null.
|
|
1867
|
+
*/
|
|
1868
|
+
function mountStill(host, pose, glyph, { staticGlyph = false, width = 28, height = 14 } = {}) {
|
|
1869
|
+
host.textContent = '';
|
|
1870
|
+
if (typeof mascotLib.paintMascot === 'function' && typeof mascotLib.stillFrame === 'function') {
|
|
1871
|
+
try {
|
|
1872
|
+
host.innerHTML = mascotLib.mascotSvg({ width, height, className: 'synabun-mascot asst-still' });
|
|
1873
|
+
const svg = host.firstElementChild;
|
|
1874
|
+
mascotLib.paintMascot(svg, mascotLib.stillFrame(pose));
|
|
1875
|
+
return svg;
|
|
1876
|
+
} catch { host.textContent = ''; }
|
|
1877
|
+
}
|
|
1878
|
+
if (!staticGlyph && typeof mascotLib.mascotSvg === 'function') { host.innerHTML = mascotLib.mascotSvg({ width, height }); return null; }
|
|
1879
|
+
host.appendChild(el('span', 'asst-still-glyph', glyph));
|
|
1880
|
+
return null;
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
/** A kind's prop alone (synabun-mascot.js mascotPropSvg), else its letter glyph. */
|
|
1884
|
+
function mountProp(host, kind, glyph) {
|
|
1885
|
+
host.textContent = '';
|
|
1886
|
+
if (typeof mascotLib.mascotPropSvg === 'function') {
|
|
1887
|
+
try {
|
|
1888
|
+
const markup = mascotLib.mascotPropSvg(kind, { width: 24, height: 16, className: 'asst-prop-glyph' });
|
|
1889
|
+
if (markup) {
|
|
1890
|
+
host.innerHTML = markup;
|
|
1891
|
+
mascotLib.paintMascotProp(host.firstElementChild, kind);
|
|
1892
|
+
return host.firstElementChild;
|
|
1893
|
+
}
|
|
1894
|
+
} catch { host.textContent = ''; }
|
|
1895
|
+
}
|
|
1896
|
+
host.appendChild(el('span', 'asst-still-glyph', glyph));
|
|
1897
|
+
return null;
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
function repaintStill(svg, pose) {
|
|
1901
|
+
try { if (svg) mascotLib.paintMascot(svg, mascotLib.stillFrame(pose)); } catch { /* the SVG is gone */ }
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
const TICK_MARK = { error: '✕', neutral: '○' };
|
|
1905
|
+
function tickName(step, held = false) {
|
|
1906
|
+
const say = sayOf(step);
|
|
1907
|
+
const what = String(say.detail ?? say.text ?? '').replace(/^\$ /, '');
|
|
1908
|
+
const state = held ? t('assistant.rack.state.waiting', 'waiting for you') : stateWord(step);
|
|
1909
|
+
return t('assistant.rack.aria.tick', '{tool}: {what} — {state}', { tool: stepTitle(step), what: middleEllipsis(what, 60, 24), state });
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
function tickEl(step, cls = 'asst-tick', held = false) {
|
|
1913
|
+
const tick = el('span', cls);
|
|
1914
|
+
tick.dataset.state = held ? 'waiting' : step.stale ? 'running' : step.state;
|
|
1915
|
+
tick.setAttribute('role', 'img');
|
|
1916
|
+
const name = tickName(step, held);
|
|
1917
|
+
tick.setAttribute('aria-label', name);
|
|
1918
|
+
tick.title = name;
|
|
1919
|
+
const mark = TICK_MARK[step.state];
|
|
1920
|
+
if (mark && !held) tick.textContent = mark;
|
|
1921
|
+
return tick;
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
/** "+10": the calls the ticks leave out (the row's log still lists every one), named for screen readers. */
|
|
1925
|
+
function overflowTick(count, cls) {
|
|
1926
|
+
const more = el('span', `asst-tick-more ${cls}`, `+${count}`);
|
|
1927
|
+
more.setAttribute('role', 'img');
|
|
1928
|
+
const name = plural(t, 'assistant.rack.aria.earlier', count, 'and 1 earlier call', 'and {count} earlier calls');
|
|
1929
|
+
more.setAttribute('aria-label', name);
|
|
1930
|
+
more.title = name;
|
|
1931
|
+
return more;
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
/** A plan row ticks its steps, not its calls: the latest plan's items (done filled, the current one live). */
|
|
1935
|
+
function planTicks(st) {
|
|
1936
|
+
const last = [...st.steps].reverse().find(s => s.type === 'call' && Object.keys(s.input || {}).length && !/todoread/i.test(s.name));
|
|
1937
|
+
const items = last ? planState(last.input).items : [];
|
|
1938
|
+
return items.length ? items : null;
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
function paintTicks(rack, st, sig, plan = null) {
|
|
1942
|
+
const before = st.tickCount || 0;
|
|
1943
|
+
st.tickSig = sig;
|
|
1944
|
+
const frag = document.createDocumentFragment();
|
|
1945
|
+
let count;
|
|
1946
|
+
if (plan) {
|
|
1947
|
+
count = plan.length;
|
|
1948
|
+
const live = rack.live && rack.awakeKey === st.key;
|
|
1949
|
+
plan.slice(0, MAX_TICKS).forEach((item, i) => {
|
|
1950
|
+
const tick = el('span', 'asst-tick is-step');
|
|
1951
|
+
tick.dataset.state = item.status === 'completed' ? 'ok' : item.status === 'in_progress' && live ? 'running' : 'pending';
|
|
1952
|
+
tick.setAttribute('role', 'img');
|
|
1953
|
+
const state = item.status === 'completed' ? t('assistant.rack.state.done', 'done') : item.status === 'in_progress' ? t('assistant.rack.state.inProgress', 'in progress') : t('assistant.rack.state.pending', 'pending');
|
|
1954
|
+
const name = t('assistant.rack.aria.planStep', 'Step {n}: {step} — {state}', { n: i + 1, step: middleEllipsis(item.text, 60, 24), state });
|
|
1955
|
+
tick.setAttribute('aria-label', name);
|
|
1956
|
+
tick.title = name;
|
|
1957
|
+
frag.appendChild(tick);
|
|
1958
|
+
});
|
|
1959
|
+
} else {
|
|
1960
|
+
const list = st.steps;
|
|
1961
|
+
count = list.length;
|
|
1962
|
+
const held = waitingStep(rack);
|
|
1963
|
+
const shown = list.slice(-MAX_TICKS);
|
|
1964
|
+
if (list.length > MAX_TICKS) frag.appendChild(overflowTick(list.length - MAX_TICKS, 'is-wide'));
|
|
1965
|
+
if (list.length > NARROW_TICKS) frag.appendChild(overflowTick(list.length - NARROW_TICKS, 'is-narrow'));
|
|
1966
|
+
shown.forEach((step, i) => {
|
|
1967
|
+
const tick = tickEl(step, 'asst-tick', step === held);
|
|
1968
|
+
if (shown.length - i > NARROW_TICKS) tick.classList.add('is-old');
|
|
1969
|
+
frag.appendChild(tick);
|
|
1970
|
+
});
|
|
1971
|
+
}
|
|
1972
|
+
st.tickCount = count;
|
|
1973
|
+
st.ticks.textContent = '';
|
|
1974
|
+
st.ticks.appendChild(frag);
|
|
1975
|
+
if (!plan && !calm() && rack.live && count > before) {
|
|
1976
|
+
const fresh = [...st.ticks.querySelectorAll('.asst-tick')].slice(-(count - before));
|
|
1977
|
+
for (const tick of fresh) tick.animate?.([{ opacity: 0, transform: 'translateY(3px)' }, { opacity: 1, transform: 'none' }], { duration: 140, easing: 'cubic-bezier(0,0,.38,.9)' });
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
/** Subagents: each agent's own calls as a compact tick strip inside the row. */
|
|
1982
|
+
function paintSubstrip(rack, st) {
|
|
1983
|
+
const agents = st.steps.filter(s => s.children?.length);
|
|
1984
|
+
const sig = agents.map(a => `${a.id}:${a.children.map(c => c.state[0]).join('')}`).join('|');
|
|
1985
|
+
if (sig === st.subSig) return;
|
|
1986
|
+
st.subSig = sig;
|
|
1987
|
+
st.sub.textContent = '';
|
|
1988
|
+
for (const agent of agents) {
|
|
1989
|
+
const row = el('div', 'asst-sub');
|
|
1990
|
+
row.dataset.toolId = agent.id;
|
|
1991
|
+
row.appendChild(el('span', 'asst-sub-label', sayOf(agent).text));
|
|
1992
|
+
const ticks = el('span', 'asst-sub-ticks');
|
|
1993
|
+
const calls = agent.children.filter(c => c.type === 'call');
|
|
1994
|
+
if (calls.length > MAX_TICKS) ticks.appendChild(overflowTick(calls.length - MAX_TICKS, 'is-wide'));
|
|
1995
|
+
for (const child of calls.slice(-MAX_TICKS)) ticks.appendChild(tickEl(child));
|
|
1996
|
+
row.appendChild(ticks);
|
|
1997
|
+
st.sub.appendChild(row);
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
/** Union of the calls' run time (overlapping calls count once); `now` stops at a card's wait. */
|
|
2002
|
+
function spanMs(steps, live, now = Date.now()) {
|
|
2003
|
+
const spans = [];
|
|
2004
|
+
for (const s of steps) {
|
|
2005
|
+
if (!s.timed || !s.startedAt) return null;
|
|
2006
|
+
const end = s.endedAt || (live && s.state === 'running' && !s.stale ? now : 0);
|
|
2007
|
+
if (!end) continue;
|
|
2008
|
+
spans.push([s.startedAt, Math.max(s.startedAt, end)]);
|
|
2009
|
+
}
|
|
2010
|
+
if (!spans.length) return null;
|
|
2011
|
+
spans.sort((a, b) => a[0] - b[0]);
|
|
2012
|
+
let total = 0;
|
|
2013
|
+
let [from, to] = spans[0];
|
|
2014
|
+
for (const [a, b] of spans.slice(1)) {
|
|
2015
|
+
if (a > to) { total += to - from; from = a; to = b; } else to = Math.max(to, b);
|
|
2016
|
+
}
|
|
2017
|
+
return total + (to - from);
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
function stationTime(st, live, now = Date.now()) {
|
|
2021
|
+
const ms = spanMs(st.steps, live, now);
|
|
2022
|
+
return ms == null || (ms < 1000 && st.steps.some(s => s.state === 'running' && !s.stale)) ? '' : fmtDuration(ms, t);
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
/** The burst's wall time, first call to last result; unknown when a replayed entry had no timestamp. */
|
|
2026
|
+
function rackMs(rack) {
|
|
2027
|
+
const steps = rack.steps;
|
|
2028
|
+
if (!steps.length || steps.some(s => !s.timed)) return null;
|
|
2029
|
+
const start = Math.min(...steps.map(s => s.startedAt));
|
|
2030
|
+
const running = steps.some(s => s.state === 'running' && !s.stale);
|
|
2031
|
+
if (running && !rack.live) return null;
|
|
2032
|
+
const end = running ? Date.now() : Math.max(...steps.map(s => s.endedAt || s.startedAt));
|
|
2033
|
+
return Math.max(0, end - start);
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
function groupsOf(rack) {
|
|
2037
|
+
return [...rack.stations.values()].map(st => ({ kind: st.kind, calls: st.steps, server: st.server }));
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
function stepTitle(step) {
|
|
2041
|
+
if (step.type === 'think') return t('assistant.rack.kind.think', 'Thinking');
|
|
2042
|
+
if (step.kind === 'computer') { const base = computerToolName(step.name) || 'computer'; return t(`assistant.computer.tool.${base}`, COMPUTER_TOOL_TITLES[base] || 'Computer'); }
|
|
2043
|
+
if (step.kind === 'mcp' || synabunToolName(step.name) != null) return toolWords(step.name);
|
|
2044
|
+
return step.name || 'tool';
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
function stateWord(step) {
|
|
2048
|
+
if (step.state === 'running' && !step.stale) return t('assistant.rack.state.running', 'running');
|
|
2049
|
+
if (step.state === 'running') return t('assistant.rack.state.noResult', 'no result');
|
|
2050
|
+
if (step.state === 'error') return t('assistant.rack.state.failed', 'failed');
|
|
2051
|
+
if (step.state === 'neutral') return t('assistant.rack.state.noMatches', 'no matches');
|
|
2052
|
+
return t('assistant.rack.state.done', 'done');
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
/** "Running npm test" / "Ran npm test". */
|
|
2056
|
+
function stepSentence(step, running) {
|
|
2057
|
+
const say = sayOf(step);
|
|
2058
|
+
return `${verbText(say.verb, running, t)} ${String(say.text || '').replace(/^\$ /, '')}`.trim();
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
/** The receipt: one call says what it did; a burst says it in one sentence. */
|
|
2062
|
+
function renderReceipt(rack, sentence) {
|
|
2063
|
+
const r = rack.receipt;
|
|
2064
|
+
const single = rack.steps.length === 1 ? rack.steps[0] : null;
|
|
2065
|
+
const live = rack.live;
|
|
2066
|
+
let sig;
|
|
2067
|
+
if (single && single.type === 'think' && !(single.state === 'running' && !single.stale)) {
|
|
2068
|
+
sig = `t|${sentence}`;
|
|
2069
|
+
if (r._sig !== sig) r.text.textContent = sentence;
|
|
2070
|
+
} else if (single) {
|
|
2071
|
+
const say = sayOf(single);
|
|
2072
|
+
const running = single.state === 'running' && !single.stale;
|
|
2073
|
+
const verb = verbText(say.verb, running, t);
|
|
2074
|
+
sig = `s|${verb}|${say.text}|${say.mono}`;
|
|
2075
|
+
if (r._sig !== sig) {
|
|
2076
|
+
r.text.textContent = '';
|
|
2077
|
+
r.text.appendChild(el('span', 'asst-rack-receipt-verb', verb));
|
|
2078
|
+
if (say.text) {
|
|
2079
|
+
r.text.appendChild(document.createTextNode(' '));
|
|
2080
|
+
const subject = el('span', `asst-rack-receipt-subject${say.mono ? ' is-mono' : ''}`);
|
|
2081
|
+
writeSubject(subject, say.segments, say.text);
|
|
2082
|
+
if (say.full) subject.title = say.full;
|
|
2083
|
+
r.text.appendChild(subject);
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
} else {
|
|
2087
|
+
sig = `m|${sentence}`;
|
|
2088
|
+
if (r._sig !== sig) r.text.textContent = sentence;
|
|
2089
|
+
}
|
|
2090
|
+
r._sig = sig;
|
|
2091
|
+
const failed = rack.steps.filter(s => s.state === 'error').length;
|
|
2092
|
+
r.failed.hidden = !failed;
|
|
2093
|
+
const failedText = failed ? t('assistant.rack.failed', '{count} failed', { count: failed }) : '';
|
|
2094
|
+
if (r.failed.textContent !== failedText) r.failed.textContent = failedText;
|
|
2095
|
+
const ms = live ? null : rackMs(rack);
|
|
2096
|
+
const clock = ms != null ? fmtDuration(ms, t) : '';
|
|
2097
|
+
if (r.clock.textContent !== clock) r.clock.textContent = clock;
|
|
2098
|
+
// The still mini mascot, once the receipt is what the rack shows; it winces when a call failed.
|
|
2099
|
+
const pose = failed ? 'error' : 'success';
|
|
2100
|
+
if (!r.faceBuilt && !rack.staged && !(rack.nested && live)) {
|
|
2101
|
+
r.faceBuilt = true;
|
|
2102
|
+
r.pose = pose;
|
|
2103
|
+
r.svg = mountStill(r.face, pose, '✓', { width: 32, height: 16 });
|
|
2104
|
+
} else if (r.faceBuilt && r.pose !== pose) {
|
|
2105
|
+
r.pose = pose;
|
|
2106
|
+
repaintStill(r.svg, pose);
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
function renderRackLabel(rack, sentence) {
|
|
2111
|
+
const single = rack.steps.length === 1 ? rack.steps[0] : null;
|
|
2112
|
+
let summary = single && single.type === 'call' ? stepSentence(single, single.state === 'running' && !single.stale) : sentence;
|
|
2113
|
+
const failed = rack.steps.filter(s => s.state === 'error').length;
|
|
2114
|
+
if (failed) summary = `${summary}, ${t('assistant.rack.failed', '{count} failed', { count: failed })}`;
|
|
2115
|
+
const ms = rack.live ? null : rackMs(rack);
|
|
2116
|
+
const time = ms != null ? fmtDuration(ms, t) : '';
|
|
2117
|
+
if (time) summary = t('assistant.rack.aria.timed', '{summary}, in {time}', { summary, time });
|
|
2118
|
+
const label = rack.waiting ? t('assistant.rack.aria.waiting', 'Waiting for you: {summary}', { summary })
|
|
2119
|
+
: rack.live ? t('assistant.rack.aria.live', 'Working: {summary}', { summary }) : summary;
|
|
2120
|
+
if (rack.row.getAttribute('aria-label') !== label) rack.row.setAttribute('aria-label', label);
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
function finishSentence(rack) {
|
|
2124
|
+
const calls = rack.steps.filter(s => s.type === 'call').length;
|
|
2125
|
+
const what = plural(t, 'assistant.rack.sr.calls', calls, '1 tool call', '{count} tool calls');
|
|
2126
|
+
const ms = rackMs(rack);
|
|
2127
|
+
const time = ms != null ? fmtDuration(ms, t) : '';
|
|
2128
|
+
return time ? t('assistant.rack.sr.done', 'Done: {calls} in {time}', { calls: what, time }) : t('assistant.rack.sr.doneUntimed', 'Done: {calls}', { calls: what });
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
/** One polite announcement per rack at most every 1.5 s; the latest text wins. */
|
|
2132
|
+
function announce(rack, text) {
|
|
2133
|
+
if (muted || !text) return;
|
|
2134
|
+
const sr = rack.sr;
|
|
2135
|
+
sr.pending = text;
|
|
2136
|
+
if (sr.timer) return;
|
|
2137
|
+
sr.timer = setTimeout(() => {
|
|
2138
|
+
sr.timer = 0;
|
|
2139
|
+
sr.last = Date.now();
|
|
2140
|
+
if (rack.statusEl.isConnected) rack.statusEl.textContent = sr.pending;
|
|
2141
|
+
}, Math.max(0, sr.last + STATUS_GAP_MS - Date.now()));
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
function alertRack(rack, text) {
|
|
2145
|
+
if (muted || !text || !rack.alertEl.isConnected) return;
|
|
2146
|
+
rack.alertEl.textContent = text;
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
// ── Timeline ──
|
|
2150
|
+
function renderTimeline(rack) {
|
|
2151
|
+
let ol = rack.timelineEl;
|
|
2152
|
+
if (!ol) { ol = el('ol', 'asst-rack-timeline'); rack.row.insertBefore(ol, rack.statusEl); rack.timelineEl = ol; }
|
|
2153
|
+
const sig = rack.timeline.map(e => `${e.seq}${e.state || ''}`).join(',');
|
|
2154
|
+
if (sig === rack.timelineSig) return;
|
|
2155
|
+
rack.timelineSig = sig;
|
|
2156
|
+
// One grid: the verb (right-aligned) · what it acted on (and a state that is not "done") · how long it took.
|
|
2157
|
+
// No offset column: a time on the right is always a duration.
|
|
2158
|
+
ol.textContent = '';
|
|
2159
|
+
for (const entry of rack.timeline) {
|
|
2160
|
+
const li = el('li', 'asst-tl');
|
|
2161
|
+
li.dataset.type = entry.type;
|
|
2162
|
+
if (entry.type === 'said') {
|
|
2163
|
+
li.appendChild(el('span', 'asst-tl-said', entry.text));
|
|
2164
|
+
} else {
|
|
2165
|
+
const say = sayOf(entry);
|
|
2166
|
+
const running = entry.state === 'running' && !entry.stale;
|
|
2167
|
+
li.dataset.state = entry.state;
|
|
2168
|
+
li.appendChild(el('span', 'asst-tl-verb', verbText(say.verb, running, t)));
|
|
2169
|
+
const what = el('span', 'asst-tl-what');
|
|
2170
|
+
const subject = el('span', `asst-tl-subject${say.mono ? ' is-mono' : ''}`, say.text);
|
|
2171
|
+
if (say.full || say.text) subject.title = say.full || say.text;
|
|
2172
|
+
what.appendChild(subject);
|
|
2173
|
+
if (entry.state !== 'ok') what.appendChild(el('span', 'asst-tl-state', stateWord(entry)));
|
|
2174
|
+
li.appendChild(what);
|
|
2175
|
+
const dur = entry.endedAt && entry.timed ? fmtDuration(entry.endedAt - entry.startedAt, t) : '';
|
|
2176
|
+
li.appendChild(el('span', 'asst-tl-dur', dur));
|
|
2177
|
+
}
|
|
2178
|
+
ol.appendChild(li);
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
// ── Calls inside a row ──
|
|
2183
|
+
function buildCallNode(step) {
|
|
2184
|
+
const li = el('li', 'asst-tool');
|
|
2185
|
+
li.dataset.kind = step.kind;
|
|
2186
|
+
li.dataset.state = step.state;
|
|
2187
|
+
if (step.type === 'think') { li.classList.add('asst-think'); li.dataset.stepId = step.id; return li; }
|
|
2188
|
+
li.dataset.toolId = step.id;
|
|
2189
|
+
if (step.kind === 'computer') {
|
|
2190
|
+
li.dataset.computer = computerToolName(step.name) || 'computer';
|
|
2191
|
+
li._computer = { input: step.input || {}, tool: li.dataset.computer, description: describeComputerAction(step.input || {}, step.name) };
|
|
2192
|
+
}
|
|
2193
|
+
li.innerHTML = '<div class="asst-tool-hdr"><span class="asst-tool-mark" aria-hidden="true"></span><span class="asst-tool-name"></span><span class="asst-tool-detail"></span><span class="asst-tool-meta"></span><span class="asst-tool-state"></span><span class="asst-tool-time"></span></div>';
|
|
2194
|
+
if (step.kind === 'subagent') li.appendChild(el('div', 'asst-agent-feed'));
|
|
2195
|
+
step.card = li;
|
|
2196
|
+
refreshStep(step);
|
|
2197
|
+
return li;
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
/** A call row's header (and the rack's clocks) after its input, state or result changed. */
|
|
2201
|
+
function refreshStep(step) {
|
|
2202
|
+
const li = step.card;
|
|
2203
|
+
if (!li) return;
|
|
2204
|
+
li.dataset.state = step.state;
|
|
2205
|
+
if (step.type === 'think') { if (step.bodyBuilt) fillCallBody(step); return; }
|
|
2206
|
+
const say = sayOf(step);
|
|
2207
|
+
const q = (sel) => li.querySelector(`:scope > .asst-tool-hdr > ${sel}`);
|
|
2208
|
+
q('.asst-tool-name').textContent = stepTitle(step);
|
|
2209
|
+
const detail = q('.asst-tool-detail');
|
|
2210
|
+
detail.className = `asst-tool-detail${say.mono ? ' is-mono' : ''}`;
|
|
2211
|
+
const detailText = say.detail ?? say.text;
|
|
2212
|
+
writeSubject(detail, say.detail != null ? null : say.segments, detailText);
|
|
2213
|
+
detail.title = say.detail == null && say.full ? say.full : detailText; // a command's title is the line as written
|
|
2214
|
+
q('.asst-tool-meta').textContent = step.kind === 'computer' ? '' : say.meta.join(', ');
|
|
2215
|
+
let state = '';
|
|
2216
|
+
if (step.kind === 'computer' && step.computerState) state = step.computerState;
|
|
2217
|
+
else if (step.state === 'running' && !step.stale) state = step.progress >= 1 ? `${step.progress}s` : '';
|
|
2218
|
+
else if (step.state === 'error') state = t('assistant.tool.error', 'error');
|
|
2219
|
+
else if (step.state === 'neutral') state = t('assistant.rack.state.noMatches', 'no matches');
|
|
2220
|
+
else if (step.state === 'ok') state = t('assistant.tool.done', 'done');
|
|
2221
|
+
q('.asst-tool-state').textContent = state;
|
|
2222
|
+
q('.asst-tool-time').textContent = step.endedAt && step.timed ? fmtDuration(step.endedAt - step.startedAt, t) : '';
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
/** Bodies are built on first expand: a replayed history costs a header per call. */
|
|
2226
|
+
function buildCallBody(step) {
|
|
2227
|
+
if (step.bodyBuilt || !step.card) return;
|
|
2228
|
+
step.bodyBuilt = true;
|
|
2229
|
+
fillCallBody(step);
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
function fillCallBody(step) {
|
|
2233
|
+
const li = step.card;
|
|
2234
|
+
if (step.type === 'think') {
|
|
2235
|
+
li.textContent = '';
|
|
2236
|
+
const details = thinkingBlock('');
|
|
2237
|
+
details.open = true;
|
|
2238
|
+
const content = details.querySelector('.asst-thinking-content');
|
|
2239
|
+
previewInto(content, step.redacted && !step.text ? '…' : step.text, THINK_PREVIEW_LINES, details);
|
|
2240
|
+
li.appendChild(details);
|
|
2241
|
+
return;
|
|
2242
|
+
}
|
|
2243
|
+
let body = li.querySelector(':scope > .asst-tool-body');
|
|
2244
|
+
if (!body) {
|
|
2245
|
+
body = el('div', 'asst-tool-body');
|
|
2246
|
+
const anchor = li.querySelector(':scope > .asst-computer-frame') || li.querySelector(':scope > .asst-tool-hdr');
|
|
2247
|
+
anchor.after(body);
|
|
2248
|
+
}
|
|
2249
|
+
// Keep a Show toggle's keyboard focus across the rebuild below.
|
|
2250
|
+
const refocus = body.contains(document.activeElement) && document.activeElement.classList.contains('asst-reveal');
|
|
2251
|
+
body.textContent = '';
|
|
2252
|
+
const input = step.input || {};
|
|
2253
|
+
const say = sayOf(step);
|
|
2254
|
+
// What the call typed and any credential stay masked until this call's Show (never remembered across a replay).
|
|
2255
|
+
const red = redactInput(step.name, input, t);
|
|
2256
|
+
const hidden = red.hidden.length && !step.revealed ? red.hidden : null;
|
|
2257
|
+
switch (step.kind) {
|
|
2258
|
+
case 'shell': {
|
|
2259
|
+
const cmd = commandOf(input);
|
|
2260
|
+
if (cmd) section(body, t('assistant.rack.body.command', 'Command'), cmd, { copy: true });
|
|
2261
|
+
break;
|
|
2262
|
+
}
|
|
2263
|
+
case 'read': {
|
|
2264
|
+
const path = input.file_path || input.filePath || input.path || input.notebook_path || '';
|
|
2265
|
+
const range = /:\d+(?:-\d+)?$/.exec(say.text)?.[0] || '';
|
|
2266
|
+
if (path) section(body, t('assistant.rack.body.file', 'File'), `${path}${range}`, { copy: true, copyText: path });
|
|
2267
|
+
break;
|
|
2268
|
+
}
|
|
2269
|
+
case 'edit': {
|
|
2270
|
+
const paths = editPaths(input);
|
|
2271
|
+
if (paths.length) section(body, paths.length > 1 ? t('assistant.rack.body.files', 'Files') : t('assistant.rack.body.file', 'File'), paths.join('\n'), { copy: true });
|
|
2272
|
+
const diff = editDiff(step.name, input);
|
|
2273
|
+
if (diff.length) diffSection(body, diff);
|
|
2274
|
+
break;
|
|
2275
|
+
}
|
|
2276
|
+
case 'plan': {
|
|
2277
|
+
const plan = planState(input);
|
|
2278
|
+
if (plan.items.length) {
|
|
2279
|
+
const list = el('ul', 'asst-plan-list');
|
|
2280
|
+
for (const item of plan.items) {
|
|
2281
|
+
const row = el('li', 'asst-plan-item', item.status === 'in_progress' && item.active ? item.active : item.text);
|
|
2282
|
+
row.dataset.status = item.status;
|
|
2283
|
+
list.appendChild(row);
|
|
2284
|
+
}
|
|
2285
|
+
body.appendChild(list);
|
|
2286
|
+
}
|
|
2287
|
+
break;
|
|
2288
|
+
}
|
|
2289
|
+
case 'memory': {
|
|
2290
|
+
if (step.memories?.length) { body.appendChild(memoryChips(step.memories)); break; }
|
|
2291
|
+
inputSection(body, step, red);
|
|
2292
|
+
break;
|
|
2293
|
+
}
|
|
2294
|
+
default:
|
|
2295
|
+
inputSection(body, step, red);
|
|
2296
|
+
}
|
|
2297
|
+
if (step.result && step.kind !== 'plan') {
|
|
2298
|
+
const output = hidden ? maskEcho(step.result, hidden, t) : step.result;
|
|
2299
|
+
section(body, t('assistant.rack.body.output', 'Output'), output, { copy: true });
|
|
2300
|
+
}
|
|
2301
|
+
if (!body.children.length) { body.remove(); return; }
|
|
2302
|
+
if (refocus) body.querySelector('.asst-reveal')?.focus({ preventScroll: true });
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
const json = (value) => { try { return JSON.stringify(value, null, 2); } catch { return String(value); } };
|
|
2306
|
+
|
|
2307
|
+
/** The call's input as JSON; typed values and credentials masked, with this call's Show / Hide. */
|
|
2308
|
+
function inputSection(body, step, red) {
|
|
2309
|
+
const input = step.input || {};
|
|
2310
|
+
if (!Object.keys(input).length) return;
|
|
2311
|
+
const masked = red.hidden.length > 0;
|
|
2312
|
+
const sec = section(body, t('assistant.rack.body.input', 'Input'), masked && !step.revealed ? json(red.input) : json(input));
|
|
2313
|
+
if (masked) sec.querySelector('.asst-tool-sec-head').appendChild(revealButton(step));
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
/** Show / Hide for one call's masked values (its input and the output's echo of them). */
|
|
2317
|
+
function revealButton(step) {
|
|
2318
|
+
const btn = el('button', 'asst-reveal', step.revealed ? t('assistant.rack.hide', 'Hide') : t('assistant.rack.show', 'Show'));
|
|
2319
|
+
btn.type = 'button';
|
|
2320
|
+
btn.setAttribute('aria-pressed', step.revealed ? 'true' : 'false');
|
|
2321
|
+
btn.setAttribute('aria-label', step.revealed ? t('assistant.rack.hideValues', 'Hide typed values') : t('assistant.rack.showValues', 'Show typed values'));
|
|
2322
|
+
btn.addEventListener('click', (e) => {
|
|
2323
|
+
e.stopPropagation();
|
|
2324
|
+
step.revealed = !step.revealed;
|
|
2325
|
+
fillCallBody(step);
|
|
2326
|
+
});
|
|
2327
|
+
return btn;
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
/** A labelled block: the first 5 lines, "+N lines" for the rest, Copy for commands, paths and output. */
|
|
2331
|
+
function section(body, label, text, { copy = false, copyText: copyValue = null } = {}) {
|
|
2332
|
+
const sec = el('div', 'asst-tool-sec');
|
|
2333
|
+
const head = el('div', 'asst-tool-sec-head');
|
|
2334
|
+
head.appendChild(el('span', 'asst-tool-label', label));
|
|
2335
|
+
if (copy) head.appendChild(copyButton(() => copyValue ?? text));
|
|
2336
|
+
sec.appendChild(head);
|
|
2337
|
+
const pre = el('pre', 'asst-tool-section');
|
|
2338
|
+
sec.appendChild(pre);
|
|
2339
|
+
previewInto(pre, text, PREVIEW_LINES, sec);
|
|
2340
|
+
body.appendChild(sec);
|
|
2341
|
+
return sec;
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
function previewInto(node, text, lines, host) {
|
|
2345
|
+
const capped = String(text ?? '').length > RESULT_LIMIT ? `${String(text).slice(0, RESULT_LIMIT)}\n…` : String(text ?? '');
|
|
2346
|
+
const all = capped.split('\n');
|
|
2347
|
+
if (all.length <= lines) { node.textContent = capped; return; }
|
|
2348
|
+
node.textContent = all.slice(0, lines).join('\n');
|
|
2349
|
+
const more = el('button', 'asst-tool-more', plural(t, 'assistant.rack.moreLines', all.length - lines, '+1 line', '+{count} lines'));
|
|
2350
|
+
more.type = 'button';
|
|
2351
|
+
more.addEventListener('click', (e) => { e.stopPropagation(); node.textContent = capped; more.remove(); });
|
|
2352
|
+
host.appendChild(more);
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
function diffSection(body, diff) {
|
|
2356
|
+
const sec = el('div', 'asst-tool-sec');
|
|
2357
|
+
const head = el('div', 'asst-tool-sec-head');
|
|
2358
|
+
head.appendChild(el('span', 'asst-tool-label', t('assistant.rack.body.changes', 'Changes')));
|
|
2359
|
+
sec.appendChild(head);
|
|
2360
|
+
const pre = el('pre', 'asst-tool-section asst-diff');
|
|
2361
|
+
const draw = (lines) => {
|
|
2362
|
+
pre.textContent = '';
|
|
2363
|
+
for (const line of lines) {
|
|
2364
|
+
const row = el('span', 'asst-diff-line');
|
|
2365
|
+
row.dataset.sign = line.sign;
|
|
2366
|
+
row.appendChild(el('span', 'asst-diff-sign', line.sign === '@' ? '⋯' : line.sign.trim()));
|
|
2367
|
+
row.appendChild(el('span', 'asst-diff-text', line.text));
|
|
2368
|
+
pre.appendChild(row);
|
|
2369
|
+
}
|
|
2370
|
+
};
|
|
2371
|
+
const capped = diff.slice(0, MAX_DIFF_LINES);
|
|
2372
|
+
draw(capped.slice(0, PREVIEW_LINES));
|
|
2373
|
+
sec.appendChild(pre);
|
|
2374
|
+
if (capped.length > PREVIEW_LINES) {
|
|
2375
|
+
const more = el('button', 'asst-tool-more', plural(t, 'assistant.rack.moreLines', capped.length - PREVIEW_LINES, '+1 line', '+{count} lines'));
|
|
2376
|
+
more.type = 'button';
|
|
2377
|
+
more.addEventListener('click', (e) => { e.stopPropagation(); draw(capped); more.remove(); });
|
|
2378
|
+
sec.appendChild(more);
|
|
2379
|
+
}
|
|
2380
|
+
// Over the cap the rest is one click away: "Show full input" (drawn only then), and Copy takes all of it.
|
|
2381
|
+
if (diff.length > MAX_DIFF_LINES) {
|
|
2382
|
+
const whole = diff.every(line => line.sign === '+') ? diff.map(line => line.text).join('\n') : diff.map(line => `${line.sign === '@' ? '@@' : line.sign}${line.text}`).join('\n');
|
|
2383
|
+
head.appendChild(copyButton(() => whole));
|
|
2384
|
+
const full = el('button', 'asst-tool-more asst-tool-full', t('assistant.rack.showFull', 'Show full input'));
|
|
2385
|
+
full.type = 'button';
|
|
2386
|
+
full.addEventListener('click', (e) => {
|
|
2387
|
+
e.stopPropagation();
|
|
2388
|
+
draw(diff);
|
|
2389
|
+
sec.querySelectorAll('.asst-tool-more').forEach(b => b.remove());
|
|
2390
|
+
});
|
|
2391
|
+
sec.appendChild(full);
|
|
2392
|
+
}
|
|
2393
|
+
body.appendChild(sec);
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
function memoryChips(memories) {
|
|
2397
|
+
const row = el('div', 'asst-chips asst-memories');
|
|
2398
|
+
row.setAttribute('role', 'list');
|
|
2399
|
+
for (const m of memories) {
|
|
2400
|
+
const chip = el('button', 'asst-chip memory');
|
|
2401
|
+
chip.type = 'button';
|
|
2402
|
+
chip.setAttribute('role', 'listitem');
|
|
2403
|
+
chip.dataset.memoryId = m.id || '';
|
|
2404
|
+
chip.innerHTML = `<span class="asst-icon">${ICON_MEMORY}</span><span class="asst-chip-text">${esc(m.title || memoryTitle(m))}</span>${m.category ? `<span class="asst-chip-meta">${esc(m.category)}</span>` : ''}${Number.isFinite(Number(m.score)) ? `<span class="asst-chip-meta">${esc(Math.round(Number(m.score) * 100))}%</span>` : ''}`;
|
|
2405
|
+
chip.title = (m.text || '').slice(0, 400);
|
|
2406
|
+
chip.addEventListener('click', () => hooks.onMemoryClick?.(m.id, m));
|
|
2407
|
+
row.appendChild(chip);
|
|
2408
|
+
}
|
|
2409
|
+
return row;
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
// ── Runs (agent_* calls) ──
|
|
2413
|
+
/** agent_wait / agent_status / agent_read results carry run states: the stage reports them. */
|
|
2414
|
+
function noteAgentResult(text) {
|
|
2415
|
+
if (!text || text[0] !== '{') return;
|
|
2416
|
+
let data;
|
|
2417
|
+
try { data = JSON.parse(text); } catch { return; }
|
|
2418
|
+
const runs = [].concat(data?.run || [], data?.runs || [], data?.pending || [], data?.done || [], data?.runId ? [data] : []);
|
|
2419
|
+
for (const r of runs) {
|
|
2420
|
+
if (!r?.runId) continue;
|
|
2421
|
+
const next = { ...runInfo.get(r.runId) };
|
|
2422
|
+
for (const [k, v] of Object.entries({ state: r.state || r.status, title: r.title || r.task, provider: r.provider })) if (v) next[k] = v;
|
|
2423
|
+
runInfo.set(r.runId, next);
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
function stepRunIds(step) {
|
|
2428
|
+
const i = step?.input || {};
|
|
2429
|
+
return [].concat(i.run_ids || [], i.run_id || []).filter(x => typeof x === 'string' && x);
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
function runLabel(step) {
|
|
2433
|
+
const ids = stepRunIds(step);
|
|
2434
|
+
if (ids.length > 1) return t('assistant.work.agents', '{count} agents', { count: ids.length });
|
|
2435
|
+
if (!ids.length) return '';
|
|
2436
|
+
const title = String(runInfo.get(ids[0])?.title || '').split('\n')[0].trim();
|
|
2437
|
+
return title ? (title.length > 44 ? `${title.slice(0, 43).trimEnd()}…` : title) : t('assistant.work.agentId', 'agent {id}', { id: ids[0].slice(0, 8) });
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
// ── Rack upkeep ──
|
|
2441
|
+
function needTicker() {
|
|
2442
|
+
if (unsubTick) return;
|
|
2443
|
+
try { unsubTick = subscribe(tickRacks, { fps: 1 }); } catch { unsubTick = null; }
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
function stopTicker() {
|
|
2447
|
+
if (!unsubTick) return;
|
|
2448
|
+
try { unsubTick(); } catch { /* already gone */ }
|
|
2449
|
+
unsubTick = null;
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
/** Once a second while a rack is live: its clocks (and any wake-up the timers have not caught). */
|
|
2453
|
+
function tickRacks() {
|
|
2454
|
+
let any = false;
|
|
2455
|
+
for (const rack of [...racks]) {
|
|
2456
|
+
if (!rack.row.isConnected) { dropRack(rack); continue; }
|
|
2457
|
+
if (!rack.live) continue;
|
|
2458
|
+
any = true;
|
|
2459
|
+
const now = Date.now();
|
|
2460
|
+
if (rack.staged && rack.bubble.step) paintBubble(rack, now, { swap: false }); // clocks, and the cut after a resize
|
|
2461
|
+
for (const st of rack.stations.values()) { const time = stationTime(st, true, rackNow(rack)); if (st.time.textContent !== time) st.time.textContent = time; }
|
|
2462
|
+
for (const step of rack.steps) if (step.state === 'running' && step.type === 'call' && step.progress >= 1) refreshStep(step);
|
|
2463
|
+
}
|
|
2464
|
+
if (!any) stopTicker();
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
function dropRack(rack) {
|
|
2468
|
+
clearTimeout(rack.stageTimer);
|
|
2469
|
+
clearTimeout(rack.clearTimer);
|
|
2470
|
+
clearTimeout(rack.sr.timer);
|
|
2471
|
+
clearTimeout(rack.holdTimer);
|
|
2472
|
+
rack.stageTimer = 0;
|
|
2473
|
+
rack.clearTimer = 0;
|
|
2474
|
+
rack.sr.timer = 0;
|
|
2475
|
+
rack.holdTimer = 0;
|
|
2476
|
+
const had = !!rack.rig;
|
|
2477
|
+
rack.rig?.destroy?.();
|
|
2478
|
+
rack.rig = null;
|
|
2479
|
+
rack.target = null;
|
|
2480
|
+
for (const step of rack.steps) if (stepsById.get(step.id) === step) stepsById.delete(step.id);
|
|
2481
|
+
racks.delete(rack);
|
|
2482
|
+
if (grace === rack) grace = null;
|
|
2483
|
+
if (had) mascotChanged();
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
/** Keep the stage up `ms` longer (the turn ended while it was on screen: its one-shot plays), then fold it. */
|
|
2487
|
+
function holdStage(rack, ms) {
|
|
2488
|
+
clearTimeout(rack.holdTimer);
|
|
2489
|
+
rack.holdUntil = Date.now() + ms;
|
|
2490
|
+
rack.holdTimer = setTimeout(() => {
|
|
2491
|
+
rack.holdTimer = 0;
|
|
2492
|
+
rack.holdUntil = 0;
|
|
2493
|
+
if (rack.row.isConnected) renderRack(rack);
|
|
2494
|
+
}, ms);
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
/** A stage kept for a verdict that never came (no finishTurn after the result) folds now. */
|
|
2498
|
+
function releaseGrace() {
|
|
2499
|
+
const rack = grace;
|
|
2500
|
+
grace = null;
|
|
2501
|
+
if (!rack || rack.holdTimer) return;
|
|
2502
|
+
rack.holdUntil = 0;
|
|
2503
|
+
if (rack.row.isConnected) renderRack(rack);
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
/** Re-evaluate the racks whose state can change: the live ones and each scope's tail. */
|
|
2507
|
+
function syncRacks() {
|
|
2508
|
+
for (const rack of [...racks]) {
|
|
2509
|
+
if (!rack.row.isConnected) { dropRack(rack); continue; }
|
|
2510
|
+
if (rack.live || liveTail(rack.scope) === rack) renderRack(rack);
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
/** Memory chips (the prompt-time recall) attach to a recall step in the Memory row; they are not a row of their own. */
|
|
2515
|
+
function attachMemories(memories, scope = rootScope) {
|
|
2516
|
+
const list = Array.isArray(memories) ? memories.filter(m => m && (m.id || m.title)) : [];
|
|
2517
|
+
if (!list.length) return null;
|
|
2518
|
+
const rack = rackFor(scope, { noFold: true });
|
|
2519
|
+
const step = addCall(rack, { name: 'mcp__SynaBun__recall', input: {} }, { extra: { auto: true, memories: list, state: 'ok' } });
|
|
2520
|
+
renderRack(rack);
|
|
2521
|
+
return step.card;
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
// ── Public appenders ──
|
|
2525
|
+
/** A prompt written outside the panel ('whatsapp'): a small "via WhatsApp" line under it (with its picture count). */
|
|
2526
|
+
function originLabel(origin, imageCount = 0) {
|
|
2527
|
+
if (origin !== 'whatsapp') return '';
|
|
2528
|
+
const n = Number(imageCount) || 0;
|
|
2529
|
+
const via = t('assistant.origin.whatsapp', 'via WhatsApp');
|
|
2530
|
+
return n > 0 ? `${via} · ${t(n === 1 ? 'assistant.origin.image' : 'assistant.origin.images', n === 1 ? '1 image' : '{n} images', { n })}` : via;
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
function appendUser(text, { images = [], files = [], memories = [], origin = null, imageCount = 0 } = {}) {
|
|
2534
|
+
turnFrames = [];
|
|
2535
|
+
rootScope.kept = null;
|
|
2536
|
+
lastErrorText = '';
|
|
2537
|
+
const row = el('div', 'asst-msg msg-user');
|
|
2538
|
+
const content = el('div', 'asst-msg-content');
|
|
2539
|
+
const chips = [];
|
|
2540
|
+
for (const img of images) chips.push(`<span class="asst-chip static"><img src="data:${esc(img.mediaType || 'image/png')};base64,${esc(img.base64)}" alt=""><span class="asst-chip-text">${esc(img.name || 'image')}</span></span>`);
|
|
2541
|
+
for (const f of files) chips.push(`<span class="asst-chip static"><span class="asst-chip-text">${esc(f.name || 'file')}</span></span>`);
|
|
2542
|
+
for (const m of memories) chips.push(`<span class="asst-chip memory static"><span class="asst-icon">${ICON_MEMORY}</span><span class="asst-chip-text">${esc(memoryTitle(m))}</span></span>`);
|
|
2543
|
+
if (chips.length) {
|
|
2544
|
+
const att = el('div', 'asst-msg-attachments');
|
|
2545
|
+
att.innerHTML = chips.join('');
|
|
2546
|
+
content.appendChild(att);
|
|
2547
|
+
}
|
|
2548
|
+
if (text) content.appendChild(el('div', 'asst-msg-text', text));
|
|
2549
|
+
row.appendChild(content);
|
|
2550
|
+
const via = originLabel(origin, imageCount);
|
|
2551
|
+
if (via) { row.dataset.origin = origin; row.appendChild(el('div', 'asst-msg-origin', via)); }
|
|
2552
|
+
append(row, rootScope, { park: !muted });
|
|
2553
|
+
scrollEnd(true);
|
|
2554
|
+
return row;
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
function appendAssistantMarkdown(text) {
|
|
2558
|
+
const { wrap } = assistantRow(rootScope);
|
|
2559
|
+
const body = el('div', 'asst-md');
|
|
2560
|
+
renderMarkdownInto(body, text);
|
|
2561
|
+
wrap.appendChild(body);
|
|
2562
|
+
return wrap;
|
|
2563
|
+
}
|
|
2564
|
+
|
|
2565
|
+
function appendStatus(text, tone = '') {
|
|
2566
|
+
if (!text) return null;
|
|
2567
|
+
return append(el('div', `asst-status${tone ? ` ${tone}` : ''}`, text));
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
function appendError(text) {
|
|
2571
|
+
return appendStatus(text || t('assistant.errorGeneric', 'Something went wrong'), 'error');
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
function appendDivider(label) {
|
|
2575
|
+
return append(el('div', 'asst-divider', label || ''));
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
/** Place an arbitrary node (control / route card) as a transcript row. */
|
|
2579
|
+
function appendNode(node) {
|
|
2580
|
+
if (!node) return null;
|
|
2581
|
+
return append(node);
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
function showWorking(label) {
|
|
2585
|
+
if (!working) {
|
|
2586
|
+
working = el('div', 'asst-working');
|
|
2587
|
+
working.setAttribute('role', 'status');
|
|
2588
|
+
working.setAttribute('aria-live', 'polite');
|
|
2589
|
+
working.innerHTML = `<span class="asst-working-face" aria-hidden="true"></span><span class="asst-working-label"></span>`;
|
|
2590
|
+
}
|
|
2591
|
+
working.querySelector('.asst-working-label').textContent = label || t('assistant.rack.verb.think.now', 'Thinking');
|
|
2592
|
+
hideEmpty({ park: true });
|
|
2593
|
+
$msgs.appendChild(working);
|
|
2594
|
+
if (!turnActive) turn = { calls: 0, last: null, errored: false, answerRow: null };
|
|
2595
|
+
turnActive = true;
|
|
2596
|
+
if (racks.size) syncRacks();
|
|
2597
|
+
syncWorkingFace();
|
|
2598
|
+
scrollEnd();
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2601
|
+
function hideWorking() {
|
|
2602
|
+
if (working) working.remove();
|
|
2603
|
+
syncWorkingFace();
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
// ── The "Thinking" row's character ──
|
|
2607
|
+
// The row wears the mascot, thinking, only when no other character is on
|
|
2608
|
+
// screen: no parked hero, no stage (a live rack right above it hides the
|
|
2609
|
+
// row altogether). A rig, so the director can dress it; a painted still
|
|
2610
|
+
// when calm.
|
|
2611
|
+
function workingCovered() {
|
|
2612
|
+
let prev = working?.previousElementSibling || null;
|
|
2613
|
+
while (prev && prev.classList.contains('asst-folding')) prev = prev.previousElementSibling;
|
|
2614
|
+
return !!prev?._rack?.live;
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2617
|
+
function syncWorkingFace() {
|
|
2618
|
+
const host = working?.querySelector('.asst-working-face');
|
|
2619
|
+
const wanted = !!host && working.isConnected && !hero && !newestStage() && !workingCovered();
|
|
2620
|
+
if (!wanted) { dropWorkingFace(); return; }
|
|
2621
|
+
if (workingFace && workingFace.still === calm()) return;
|
|
2622
|
+
dropWorkingFace();
|
|
2623
|
+
if (calm()) {
|
|
2624
|
+
workingFace = { still: true, el: mountStill(host, 'think', '…', { width: 56, height: 28 }) };
|
|
2625
|
+
return;
|
|
2626
|
+
}
|
|
2627
|
+
try {
|
|
2628
|
+
const rig = mascotLib.createMascot(host, { width: 56, height: 28, active: rigsOn, pose: 'think' });
|
|
2629
|
+
const face = { still: false, rig, el: rig.el, held: false };
|
|
2630
|
+
face.target = { kind: 'working', rig, el: rig.el, hold(on) { face.held = !!on; }, restore() { if (workingFace === face) rig.setPose('think'); } };
|
|
2631
|
+
workingFace = face;
|
|
2632
|
+
} catch { workingFace = null; }
|
|
2633
|
+
mascotChanged();
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
function dropWorkingFace() {
|
|
2637
|
+
const face = workingFace;
|
|
2638
|
+
if (!face) return;
|
|
2639
|
+
workingFace = null;
|
|
2640
|
+
try { face.rig?.destroy(); } catch { /* gone */ }
|
|
2641
|
+
working?.querySelector('.asst-working-face')?.replaceChildren();
|
|
2642
|
+
if (face.rig) mascotChanged();
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2645
|
+
function appendMemoryChips(memories) {
|
|
2646
|
+
return attachMemories(memories);
|
|
2647
|
+
}
|
|
2648
|
+
|
|
2649
|
+
function appendMemorySaved(info) {
|
|
2650
|
+
const row = el('div', 'asst-chips');
|
|
2651
|
+
const chip = el('button', 'asst-chip saved memory');
|
|
2652
|
+
chip.type = 'button';
|
|
2653
|
+
chip.dataset.memoryId = info?.id || '';
|
|
2654
|
+
chip.innerHTML = `<span class="asst-icon">${ICON_MEMORY}</span><span class="asst-chip-text">${esc(t('assistant.memories.saved', 'Memory saved'))}${info?.title ? ` · ${esc(info.title)}` : ''}</span>`;
|
|
2655
|
+
chip.addEventListener('click', () => hooks.onMemoryClick?.(info?.id, info));
|
|
2656
|
+
row.appendChild(chip);
|
|
2657
|
+
return append(row);
|
|
2658
|
+
}
|
|
2659
|
+
|
|
2660
|
+
function runCardOpts(result = null) {
|
|
2661
|
+
return {
|
|
2662
|
+
t: hooks.t,
|
|
2663
|
+
providerIcon: hooks.providerIcon,
|
|
2664
|
+
providerColor: hooks.providerColor,
|
|
2665
|
+
onFocus: hooks.onFocusRun,
|
|
2666
|
+
onStop: hooks.onStopRun,
|
|
2667
|
+
onRead: hooks.onReadRun,
|
|
2668
|
+
onEscalate: hooks.onEscalateRun,
|
|
2669
|
+
result,
|
|
2670
|
+
keepSummary: true,
|
|
2671
|
+
};
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
function upsertRunCard(run, { result = null } = {}) {
|
|
2675
|
+
if (!run?.runId) return null;
|
|
2676
|
+
runInfo.set(run.runId, { ...runInfo.get(run.runId), title: run.title || run.task || runInfo.get(run.runId)?.title, state: run.status || runInfo.get(run.runId)?.state, provider: run.provider });
|
|
2677
|
+
const cardOpts = runCardOpts(result);
|
|
2678
|
+
let card = runCards.get(run.runId);
|
|
2679
|
+
if (card && card.isConnected) {
|
|
2680
|
+
updateRunCard(card, run, cardOpts);
|
|
2681
|
+
for (const rack of racks) if (rack.live) renderRack(rack); // "Waiting on <title>" learns the title
|
|
2682
|
+
return card;
|
|
2683
|
+
}
|
|
2684
|
+
card = buildRunCard(run, cardOpts);
|
|
2685
|
+
runCards.set(run.runId, card);
|
|
2686
|
+
const { wrap } = assistantRow(rootScope);
|
|
2687
|
+
wrap.appendChild(card);
|
|
2688
|
+
return card;
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2691
|
+
function patchRunUsage(runId, tokens, fidelity) {
|
|
2692
|
+
const card = runCards.get(runId);
|
|
2693
|
+
if (!card?.isConnected || !card._usageRun) return;
|
|
2694
|
+
updateRunCard(card, { ...card._usageRun, usageTokens: tokens, usageFidelity: fidelity, usageObserved: true }, card._runOpts || runCardOpts());
|
|
2695
|
+
}
|
|
2696
|
+
|
|
2697
|
+
/** One line for a settled clarification (agent_clarify): what the user answered, or that they skipped. */
|
|
2698
|
+
function clarifyLine(ev) {
|
|
2699
|
+
const phase = ev?.phase || '';
|
|
2700
|
+
const round = ev?.brief?.round || {};
|
|
2701
|
+
const answers = (Array.isArray(round.answers) ? round.answers : [])
|
|
2702
|
+
.filter((a) => Array.isArray(a?.answers) && a.answers.length)
|
|
2703
|
+
.map((a) => (a.header ? `${a.header}: ${a.answers.join(', ')}` : a.answers.join(', ')));
|
|
2704
|
+
if (phase === 'answered') return `${t('assistant.clarify.answered', 'Clarified')}${answers.length ? ` · ${answers.join(' · ')}` : ''}`;
|
|
2705
|
+
if (phase === 'chat') return t('assistant.clarify.chat', 'Clarified in chat');
|
|
2706
|
+
if (phase === 'declined') return t('assistant.clarify.skipped', 'Questions skipped: the assistant goes with its assumptions');
|
|
2707
|
+
return t('assistant.clarify.cancelled', 'Questions cancelled');
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2710
|
+
function appendMailbox(events) {
|
|
2711
|
+
const summary = summarizeMailbox(events);
|
|
2712
|
+
if (!summary.length) return null;
|
|
2713
|
+
const row = el('div', 'asst-chips asst-mailbox');
|
|
2714
|
+
row.setAttribute('role', 'list');
|
|
2715
|
+
row.innerHTML = `<span class="asst-chips-label">${esc(t('assistant.mailbox.label', 'Mailbox'))}</span>`;
|
|
2716
|
+
for (const entry of summary) {
|
|
2717
|
+
const chip = el('button', `asst-chip mailbox${/permission|needs_input|failed|stalled|interrupted|budget/.test(entry.kind) ? ' attention' : ''}`);
|
|
2718
|
+
chip.type = 'button';
|
|
2719
|
+
chip.setAttribute('role', 'listitem');
|
|
2720
|
+
chip.innerHTML = `<span class="asst-chip-text">${esc(t(`assistant.mailbox.${entry.kind}`, entry.kind.replace(/_/g, ' ')))}</span>${entry.count > 1 ? `<span class="asst-chip-meta">×${entry.count}</span>` : ''}`;
|
|
2721
|
+
if (entry.runIds.length) chip.addEventListener('click', () => hooks.onFocusRun?.(entry.runIds[0]));
|
|
2722
|
+
row.appendChild(chip);
|
|
2723
|
+
}
|
|
2724
|
+
return append(row);
|
|
2725
|
+
}
|
|
2726
|
+
|
|
2727
|
+
// ── Route lines ──
|
|
2728
|
+
function routeLineOpts(phase) {
|
|
2729
|
+
return { phase, t: hooks.t, providerIcon: hooks.providerIcon, brainLabel: hooks.brainLabel, onChangeRoutes: hooks.onChangeRoutes };
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2732
|
+
/** Create or update the line for `route.routeId` in place. */
|
|
2733
|
+
function upsertRouteLine(route, phase = 'decided') {
|
|
2734
|
+
const routeId = route?.routeId;
|
|
2735
|
+
if (!routeId) return null;
|
|
2736
|
+
const existing = routeLines.get(routeId);
|
|
2737
|
+
if (existing && existing.el.isConnected) { existing.update(route, phase); return existing; }
|
|
2738
|
+
const line = createRouteLine(route, routeLineOpts(phase));
|
|
2739
|
+
routeLines.set(routeId, line);
|
|
2740
|
+
append(line.el);
|
|
2741
|
+
return line;
|
|
2742
|
+
}
|
|
2743
|
+
|
|
2744
|
+
/** Adopt a line created by a collapsing route card so later events update it. */
|
|
2745
|
+
function registerRouteLine(routeId, line) {
|
|
2746
|
+
if (routeId && line) routeLines.set(routeId, line);
|
|
2747
|
+
}
|
|
2748
|
+
|
|
2749
|
+
function removeRouteLine(routeId) {
|
|
2750
|
+
const line = routeLines.get(routeId);
|
|
2751
|
+
if (line) { line.el.remove(); routeLines.delete(routeId); }
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2754
|
+
// ── Empty state ──
|
|
2755
|
+
function showEmpty() {
|
|
2756
|
+
if (emptyEl || $msgs.querySelector(':scope > :not(.asst-working)')) return;
|
|
2757
|
+
emptyEl = el('div', 'asst-empty');
|
|
2758
|
+
emptyEl.innerHTML = `
|
|
2759
|
+
<div class="asst-empty-mascot" aria-hidden="true"></div>
|
|
2760
|
+
<div class="asst-empty-title" role="heading" aria-level="2">${esc(t('assistant.empty.heading', 'What should we do?'))}</div>
|
|
2761
|
+
<div class="asst-empty-body">${esc(t('assistant.empty.lead', 'I can use your Mac, look things up on the web, edit files and run commands, and remember or schedule things.'))}</div>
|
|
2762
|
+
<div class="asst-suggestions" role="list"></div>`;
|
|
2763
|
+
const box = emptyEl.querySelector('.asst-empty-mascot');
|
|
2764
|
+
try {
|
|
2765
|
+
const rig = mascotLib.createMascot(box, { width: 120, height: 60, active: rigsOn });
|
|
2766
|
+
hero = { rig, el: rig.el, box, parked: false, leaving: false, held: false, dock: null, egg: false, leaveTimer: 0 };
|
|
2767
|
+
hero.target = heroTarget(hero);
|
|
2768
|
+
wireEgg(hero);
|
|
2769
|
+
} catch { hero = null; }
|
|
2770
|
+
renderSuggestions();
|
|
2771
|
+
$msgs.appendChild(emptyEl);
|
|
2772
|
+
// The host's "Starting…" character flies into place (not on replay or reduced motion).
|
|
2773
|
+
let from = null;
|
|
2774
|
+
try { from = hooks.heroOrigin?.() || null; } catch { from = null; }
|
|
2775
|
+
if (from && hero) flipFrom(hero.el, from); // the SVG: the box is as wide as the empty state
|
|
2776
|
+
mascotChanged();
|
|
2777
|
+
}
|
|
2778
|
+
|
|
2779
|
+
function renderSuggestions() {
|
|
2780
|
+
if (!emptyEl) return;
|
|
2781
|
+
const host = emptyEl.querySelector('.asst-suggestions');
|
|
2782
|
+
host.innerHTML = '';
|
|
2783
|
+
let list = [];
|
|
2784
|
+
try { list = typeof hooks.emptySuggestions === 'function' ? (hooks.emptySuggestions() || []) : []; } catch { list = []; }
|
|
2785
|
+
for (const s of list) {
|
|
2786
|
+
// glyph | seam | label: the kind's prop, and the character acts it out while the suggestion is hovered or focused.
|
|
2787
|
+
const chip = el('button', 'asst-chip asst-suggestion');
|
|
2788
|
+
chip.type = 'button';
|
|
2789
|
+
chip.setAttribute('role', 'listitem');
|
|
2790
|
+
chip.dataset.suggestion = s.id || '';
|
|
2791
|
+
const kind = s.kind || SUGGESTION_KINDS[s.id] || null;
|
|
2792
|
+
if (kind) {
|
|
2793
|
+
chip.dataset.kind = kind;
|
|
2794
|
+
const icon = el('span', 'asst-icon');
|
|
2795
|
+
icon.setAttribute('aria-hidden', 'true');
|
|
2796
|
+
mountProp(icon, kind, '');
|
|
2797
|
+
chip.appendChild(icon);
|
|
2798
|
+
chip.addEventListener('pointerenter', () => previewHero(kind));
|
|
2799
|
+
chip.addEventListener('pointerleave', () => previewHero(null));
|
|
2800
|
+
chip.addEventListener('focus', () => previewHero(kind));
|
|
2801
|
+
chip.addEventListener('blur', () => previewHero(null));
|
|
2802
|
+
}
|
|
2803
|
+
chip.appendChild(el('span', 'asst-chip-text', s.label));
|
|
2804
|
+
chip.addEventListener('click', () => hooks.onSuggestion?.(s.template, s));
|
|
2805
|
+
host.appendChild(chip);
|
|
2806
|
+
}
|
|
2807
|
+
host.hidden = !list.length;
|
|
2808
|
+
}
|
|
2809
|
+
|
|
2810
|
+
/** The empty state's character tries a suggestion on (null: back to idle); never over a pose the director holds. */
|
|
2811
|
+
function previewHero(kind) {
|
|
2812
|
+
const h = hero;
|
|
2813
|
+
if (!h?.rig || h.parked || h.held) return;
|
|
2814
|
+
h.rig.setPose?.(kind || 'idle');
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
/** `system` messages: the SDK's, plus the bridge's runtime_notice / retry / compact_started / session_reset / plan_file_written. */
|
|
2818
|
+
function handleSystem(ev, silent) {
|
|
2819
|
+
const sub = ev.subtype;
|
|
2820
|
+
const msg = String(ev.message || ev.text || ev.content || '');
|
|
2821
|
+
const k = (n) => (n >= 1000 ? `${Math.round(n / 100) / 10}k` : String(n));
|
|
2822
|
+
switch (sub) {
|
|
2823
|
+
case 'init': hooks.onInit?.(ev); return true;
|
|
2824
|
+
case 'compact_boundary': case 'compacted': {
|
|
2825
|
+
const m = ev.compact_metadata || {};
|
|
2826
|
+
const pre = Number(m.pre_tokens) || 0;
|
|
2827
|
+
const post = Number(m.post_tokens) || 0;
|
|
2828
|
+
appendDivider(`${t('assistant.compacted', 'context compacted')}${pre ? ` · ${k(pre)}${post ? ` → ${k(post)}` : ''}` : ''}`);
|
|
2829
|
+
return true;
|
|
2830
|
+
}
|
|
2831
|
+
case 'status':
|
|
2832
|
+
// Live only: a replayed status carries the mode of its time, not the session's.
|
|
2833
|
+
if (ev.permissionMode && !silent) hooks.onModeChanged?.(ev.permissionMode);
|
|
2834
|
+
if (ev.compact_result === 'failed') appendError(`${t('assistant.status.compactFailed', 'Compaction failed')}${ev.compact_error ? `: ${ev.compact_error}` : ''}`);
|
|
2835
|
+
if (!silent && 'status' in ev) hooks.onStatus?.(ev.status || null);
|
|
2836
|
+
if (ev.text) appendStatus(ev.text);
|
|
2837
|
+
return true;
|
|
2838
|
+
case 'api_retry':
|
|
2839
|
+
if (!silent) appendStatus(t('assistant.status.apiRetry', 'API error{code}, retrying in {seconds}s (attempt {attempt} of {max})', { code: ev.error_status ? ` ${ev.error_status}` : '', seconds: Math.max(1, Math.round(Number(ev.retry_delay_ms) / 1000) || 1), attempt: ev.attempt ?? '?', max: ev.max_retries ?? '?' }), 'warn');
|
|
2840
|
+
return true;
|
|
2841
|
+
case 'retry': case 'runtime_notice': case 'compact_started': case 'notification':
|
|
2842
|
+
if (!silent && msg) appendStatus(msg, sub === 'compact_started' ? '' : (sub === 'notification' || ev.level === 'info' ? 'info' : 'warn'));
|
|
2843
|
+
return true;
|
|
2844
|
+
case 'session_reset': case 'informational': // informational 'info' is transcript-mode only
|
|
2845
|
+
if (msg && ev.level !== 'info') appendStatus(msg, ev.level === 'warning' ? 'warn' : 'info');
|
|
2846
|
+
return true;
|
|
2847
|
+
case 'local_command_output':
|
|
2848
|
+
if (msg) appendAssistantMarkdown(msg).classList.add('asst-local-output');
|
|
2849
|
+
return true;
|
|
2850
|
+
case 'permission_denied':
|
|
2851
|
+
appendStatus(t('assistant.status.permissionDenied', '{tool} was denied{reason}', { tool: ev.tool_name || 'tool', reason: ev.decision_reason ? ` — ${ev.decision_reason}` : '' }), 'warn');
|
|
2852
|
+
return true;
|
|
2853
|
+
case 'model_refusal_fallback':
|
|
2854
|
+
appendStatus(t('assistant.status.refusalFallback', 'Switched from {from} to {to} after a refusal', { from: modelShortName(ev.original_model) || ev.original_model, to: modelShortName(ev.fallback_model) || ev.fallback_model }), 'warn');
|
|
2855
|
+
return true;
|
|
2856
|
+
case 'model_refusal_no_fallback':
|
|
2857
|
+
appendError(msg || t('assistant.status.refused', 'The model declined this request.'));
|
|
2858
|
+
return true;
|
|
2859
|
+
case 'task_notification':
|
|
2860
|
+
if (!ev.skip_transcript && ev.summary) appendStatus(`${ev.status === 'completed' ? t('assistant.status.taskDone', 'Background task finished') : t('assistant.status.taskEnded', 'Background task {status}', { status: ev.status || 'ended' })}: ${ev.summary}`, ev.status === 'completed' ? 'info' : 'warn');
|
|
2861
|
+
return true;
|
|
2862
|
+
case 'plan_file_written':
|
|
2863
|
+
appendStatus(t('assistant.status.planSaved', 'Plan saved: {name}', { name: ev.name || String(ev.path || '').split(/[/\\]/).pop() }), 'info');
|
|
2864
|
+
return true;
|
|
2865
|
+
case 'mcp_status': // live only: a replayed failure may long since have recovered
|
|
2866
|
+
if (silent) return true;
|
|
2867
|
+
for (const s of Array.isArray(ev.servers) ? ev.servers : []) {
|
|
2868
|
+
const name = s?.name || s?.server || s?.serverName;
|
|
2869
|
+
if (!name || mcpWarned.has(name) || !/fail|error/i.test(String(s?.status || s?.state || ''))) continue;
|
|
2870
|
+
mcpWarned.add(name);
|
|
2871
|
+
appendStatus(t('assistant.status.mcpFailed', 'MCP server {name} failed to connect', { name }), 'warn');
|
|
2872
|
+
}
|
|
2873
|
+
return true;
|
|
2874
|
+
default:
|
|
2875
|
+
return true; // commands_list, thread_title, task progress, hooks…: no transcript row
|
|
2876
|
+
}
|
|
2877
|
+
}
|
|
2878
|
+
|
|
2879
|
+
function markSeen(key) {
|
|
2880
|
+
if (!key) return;
|
|
2881
|
+
seen.delete(key);
|
|
2882
|
+
seen.add(key);
|
|
2883
|
+
while (seen.size > SEEN_MAX) seen.delete(seen.values().next().value);
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2886
|
+
/** An event's identities: its SDK uuid; an assistant message's key, API message id and tool_use ids. */
|
|
2887
|
+
function idKeys(ev) {
|
|
2888
|
+
const keys = [];
|
|
2889
|
+
if (ev?.uuid) keys.push(`u:${ev.uuid}`);
|
|
2890
|
+
if (ev?.type === 'assistant' && ev.message?.id) {
|
|
2891
|
+
keys.push(`a:${assistantEventKey(ev)}`, `m:${ev.message.id}`);
|
|
2892
|
+
for (const b of Array.isArray(ev.message.content) ? ev.message.content : []) if (b?.type === 'tool_use' && b.id) keys.push(`t:${b.id}`);
|
|
2893
|
+
}
|
|
2894
|
+
return keys;
|
|
2895
|
+
}
|
|
2896
|
+
|
|
2897
|
+
/** An event's exact copy (the journal stores the packets the sockets got): type, length and a djb2 hash. */
|
|
2898
|
+
function contentKey(ev) {
|
|
2899
|
+
let s = '';
|
|
2900
|
+
try { s = JSON.stringify(ev); } catch { return ''; }
|
|
2901
|
+
let h = 5381;
|
|
2902
|
+
for (let i = 0; i < s.length; i += 1) h = ((h << 5) + h + s.charCodeAt(i)) | 0;
|
|
2903
|
+
return `c:${ev?.type}:${s.length}:${h >>> 0}`;
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2906
|
+
const scopeKeyOf = (ev) => ev?.parent_tool_use_id || ev?.message?.parent_tool_use_id || '';
|
|
2907
|
+
const drawnKey = (keys) => keys.find(k => (k.startsWith('a:') || k.startsWith('u:')) && seen.has(k));
|
|
2908
|
+
|
|
2909
|
+
/**
|
|
2910
|
+
* The live gate: false for an assistant event already drawn (a subagent's
|
|
2911
|
+
* may repeat, as before). What it lets through is registered.
|
|
2912
|
+
*/
|
|
2913
|
+
function claimEvent(ev) {
|
|
2914
|
+
if (ev?.type === 'assistant' && ev.message?.id && !scopeKeyOf(ev) && seen.has(`a:${assistantEventKey(ev)}`)) return false;
|
|
2915
|
+
for (const k of idKeys(ev)) markSeen(k);
|
|
2916
|
+
return true;
|
|
2917
|
+
}
|
|
2918
|
+
|
|
2919
|
+
/**
|
|
2920
|
+
* A reattach's buffered replay (the packets that went out while this panel
|
|
2921
|
+
* was away), minus what is on screen: events drawn by the journal snapshot
|
|
2922
|
+
* or live (their ids, or the snapshot's exact copies), stream packets that
|
|
2923
|
+
* a message later in the replay (or already drawn) carries in full, and
|
|
2924
|
+
* packets older than the snapshot's newest entry when they carry `at` or
|
|
2925
|
+
* `seq`. Returns the packets to process, in order; ends the snapshot.
|
|
2926
|
+
*/
|
|
2927
|
+
function planReplay(packets) {
|
|
2928
|
+
const list = (Array.isArray(packets) ? packets : []).filter(p => p && typeof p === 'object');
|
|
2929
|
+
const snap = snapshot;
|
|
2930
|
+
snapshot = null;
|
|
2931
|
+
const lastMessage = new Map(); // scope → index of the last assistant event in the replay
|
|
2932
|
+
list.forEach((p, i) => { if (p.type === 'event' && p.event?.type === 'assistant') lastMessage.set(scopeKeyOf(p.event), i); });
|
|
2933
|
+
const drawnStream = new Map(); // scope → inside the stream of a message already drawn
|
|
2934
|
+
const out = [];
|
|
2935
|
+
list.forEach((p, i) => {
|
|
2936
|
+
if (snap) {
|
|
2937
|
+
const at = Date.parse(p.at ?? p.event?.at ?? '');
|
|
2938
|
+
if (Number.isFinite(at) && at < snap.at) return;
|
|
2939
|
+
const seq = Number(p.seq ?? p.event?.seq);
|
|
2940
|
+
if ((p.seq ?? p.event?.seq) != null && Number.isFinite(seq) && seq <= snap.seq) return;
|
|
2941
|
+
}
|
|
2942
|
+
const ev = p.type === 'event' ? p.event : null;
|
|
2943
|
+
if (!ev?.type) { out.push(p); return; }
|
|
2944
|
+
const scope = scopeKeyOf(ev);
|
|
2945
|
+
if (ev.type === 'stream_event') {
|
|
2946
|
+
const inner = ev.event || {};
|
|
2947
|
+
if (inner.type === 'message_start') drawnStream.set(scope, !!inner.message?.id && seen.has(`m:${inner.message.id}`));
|
|
2948
|
+
if (drawnStream.get(scope)) return; // its message is on screen already
|
|
2949
|
+
if ((lastMessage.get(scope) ?? -1) > i) return; // a message later in the replay carries it
|
|
2950
|
+
out.push(p);
|
|
2951
|
+
return;
|
|
2952
|
+
}
|
|
2953
|
+
if (drawnKey(idKeys(ev))) return;
|
|
2954
|
+
if (snap?.keys.has(contentKey(ev))) return;
|
|
2955
|
+
out.push(p);
|
|
2956
|
+
});
|
|
2957
|
+
return out;
|
|
2958
|
+
}
|
|
2959
|
+
|
|
2960
|
+
/** A journal entry the snapshot drew: its identities, its exact copy, the newest `at` / `seq`. */
|
|
2961
|
+
function noteSnapshot(entry) {
|
|
2962
|
+
const packet = entry.packet && typeof entry.packet === 'object' ? entry.packet : entry;
|
|
2963
|
+
const ev = packet.type === 'event' ? packet.event : (packet.role === 'assistant' && packet.message ? { type: 'assistant', message: packet.message } : null);
|
|
2964
|
+
if (ev?.type) {
|
|
2965
|
+
for (const k of idKeys(ev)) markSeen(k);
|
|
2966
|
+
if (snapshot && ev.type !== 'assistant' && ev.type !== 'user' && ev.type !== 'stream_event') snapshot.keys.add(contentKey(ev));
|
|
2967
|
+
}
|
|
2968
|
+
if (!snapshot) return;
|
|
2969
|
+
const ms = Date.parse(entry.at ?? '');
|
|
2970
|
+
if (Number.isFinite(ms)) snapshot.at = Math.max(snapshot.at, ms);
|
|
2971
|
+
const seq = Number(entry.seq);
|
|
2972
|
+
if (entry.seq != null && Number.isFinite(seq)) snapshot.seq = Math.max(snapshot.seq, seq);
|
|
2973
|
+
}
|
|
2974
|
+
|
|
2975
|
+
/**
|
|
2976
|
+
* Main entry for `event` packets. Returns true when handled.
|
|
2977
|
+
* opts.silent: a replayed event (no motion, no announcements, no stale notices);
|
|
2978
|
+
* opts.replay: a reattach's missed packet (silent, and drawn like the journal: prompts too);
|
|
2979
|
+
* opts.at: its journal timestamp, so replayed racks know how long things took.
|
|
2980
|
+
*/
|
|
2981
|
+
function handleEvent(ev, { silent = false, at = null, replay = false } = {}) {
|
|
2982
|
+
if (!ev?.type) return false;
|
|
2983
|
+
const was = { muted, clockAt, replayDraw };
|
|
2984
|
+
muted = muted || !!silent || !!replay;
|
|
2985
|
+
replayDraw = replayDraw || !!replay;
|
|
2986
|
+
if (at != null) { const ms = Date.parse(at); clockAt = Number.isFinite(ms) ? ms : clockAt; }
|
|
2987
|
+
try { return dispatch(ev, muted); }
|
|
2988
|
+
finally { muted = was.muted; clockAt = was.clockAt; replayDraw = was.replayDraw; }
|
|
2989
|
+
}
|
|
2990
|
+
|
|
2991
|
+
function dispatch(ev, silent) {
|
|
2992
|
+
if (ev.type !== 'stream_event') for (const k of idKeys(ev)) markSeen(k); // deltas are never replayed as themselves
|
|
2993
|
+
const parentId = ev.parent_tool_use_id || ev.message?.parent_tool_use_id || null;
|
|
2994
|
+
const scope = scopeFor(parentId);
|
|
2995
|
+
const handled = route(ev, scope, parentId, silent);
|
|
2996
|
+
scope.lastAt = nowMs();
|
|
2997
|
+
return handled;
|
|
2998
|
+
}
|
|
2999
|
+
|
|
3000
|
+
function route(ev, scope, parentId, silent) {
|
|
3001
|
+
switch (ev.type) {
|
|
3002
|
+
case 'stream_event':
|
|
3003
|
+
if (ev.event) handleStreamEvent(ev.event, scope);
|
|
3004
|
+
return true;
|
|
3005
|
+
case 'assistant': {
|
|
3006
|
+
const row = ev.message ? renderAssistant(ev.message, scope, ev) : null;
|
|
3007
|
+
if (row && ev.error && row.classList.contains('asst-msg')) { // a synthetic API-error message
|
|
3008
|
+
row.classList.add('asst-msg-error');
|
|
3009
|
+
const content = Array.isArray(ev.message.content) ? ev.message.content : [];
|
|
3010
|
+
lastErrorText = typeof ev.message.content === 'string' ? ev.message.content.trim()
|
|
3011
|
+
: content.filter((b) => b?.type === 'text').map((b) => b.text).join('\n').trim();
|
|
3012
|
+
}
|
|
3013
|
+
return true;
|
|
3014
|
+
}
|
|
3015
|
+
case 'tool_result':
|
|
3016
|
+
updateToolResult(ev);
|
|
3017
|
+
return true;
|
|
3018
|
+
case 'user': {
|
|
3019
|
+
const content = Array.isArray(ev.message?.content) ? ev.message.content : [];
|
|
3020
|
+
const results = content.filter(block => block?.type === 'tool_result');
|
|
3021
|
+
// Claude's structured result (stdout / stderr apart) belongs to the message's one tool_result.
|
|
3022
|
+
const detail = results.length === 1 ? resultDetail(ev.tool_use_result) : null;
|
|
3023
|
+
let handled = false;
|
|
3024
|
+
for (const block of results) { updateToolResult({ tool_use_id: block.tool_use_id, content: block.content, is_error: block.is_error, detail }); handled = true; }
|
|
3025
|
+
if (!handled && !silent && typeof ev.message?.content === 'string' && ev.replay) appendUser(ev.message.content);
|
|
3026
|
+
return true;
|
|
3027
|
+
}
|
|
3028
|
+
case 'result': {
|
|
3029
|
+
flushStream(scope);
|
|
3030
|
+
// Codex/OpenCode also send an `error` packet; the SDK reports only here.
|
|
3031
|
+
const errorText = !parentId && ev.brain !== 'codex' && ev.brain !== 'opencode' ? resultErrorText(ev, t) : '';
|
|
3032
|
+
if (errorText && !muted) turn.errored = true;
|
|
3033
|
+
// The stage on screen waits for the turn's verdict (onResult → finishTurn) before it folds.
|
|
3034
|
+
if (!parentId && !muted) grace = newestStage();
|
|
3035
|
+
if (grace) grace.holdUntil = Infinity;
|
|
3036
|
+
if (!parentId) endBursts(rootScope);
|
|
3037
|
+
hooks.onResult?.(ev);
|
|
3038
|
+
if (errorText && errorText !== lastErrorText) appendError(errorText);
|
|
3039
|
+
if (!parentId) { lastErrorText = ''; rootScope.kept = null; releaseGrace(); }
|
|
3040
|
+
return true;
|
|
3041
|
+
}
|
|
3042
|
+
case 'system':
|
|
3043
|
+
return handleSystem(ev, silent);
|
|
3044
|
+
case 'tool_progress': {
|
|
3045
|
+
const step = ev.tool_use_id ? stepsById.get(ev.tool_use_id) : null;
|
|
3046
|
+
if (step && step.type === 'call' && step.state === 'running' && Number(ev.elapsed_time_seconds) >= 1) {
|
|
3047
|
+
step.progress = Math.round(Number(ev.elapsed_time_seconds)) || 0;
|
|
3048
|
+
refreshStep(step);
|
|
3049
|
+
}
|
|
3050
|
+
return true;
|
|
3051
|
+
}
|
|
3052
|
+
case 'tool_use_summary':
|
|
3053
|
+
if (ev.summary) appendStatus(String(ev.summary));
|
|
3054
|
+
return true;
|
|
3055
|
+
case 'auth_status':
|
|
3056
|
+
if (ev.error) appendError(String(ev.error));
|
|
3057
|
+
else if (!silent && Array.isArray(ev.output) && ev.output.length) appendStatus(ev.output.join('\n'), 'info');
|
|
3058
|
+
return true;
|
|
3059
|
+
case 'rate_limit_event':
|
|
3060
|
+
// Never a transcript row: providers repeat it with every response. The panel keeps one
|
|
3061
|
+
// notice and replaces it in place (live only: a replayed report may long since be over).
|
|
3062
|
+
if (!silent) hooks.onLimit?.(ev.rate_limit_info || {});
|
|
3063
|
+
return true;
|
|
3064
|
+
case 'conversation_reset':
|
|
3065
|
+
appendDivider(t('assistant.conversationReset', 'conversation reset'));
|
|
3066
|
+
return true;
|
|
3067
|
+
case 'mode_changed':
|
|
3068
|
+
hooks.onModeChanged?.(ev.mode, ev.planMode);
|
|
3069
|
+
return true;
|
|
3070
|
+
case 'subagent':
|
|
3071
|
+
return true;
|
|
3072
|
+
case 'synabun.memories':
|
|
3073
|
+
attachMemories(ev.memories, scope);
|
|
3074
|
+
return true;
|
|
3075
|
+
case 'synabun.memory_saved':
|
|
3076
|
+
appendMemorySaved(ev);
|
|
3077
|
+
return true;
|
|
3078
|
+
case 'synabun.dispatch':
|
|
3079
|
+
if (ev.run) upsertRunCard(ev.run);
|
|
3080
|
+
return true;
|
|
3081
|
+
case 'synabun.dispatch_result':
|
|
3082
|
+
if (ev.run) upsertRunCard(ev.run, { result: ev.result ?? null });
|
|
3083
|
+
return true;
|
|
3084
|
+
case 'synabun.user_prompt':
|
|
3085
|
+
if ((!silent || replayDraw) && ev.text) appendUser(ev.text, { origin: ev.origin || null, imageCount: ev.images });
|
|
3086
|
+
return true;
|
|
3087
|
+
case 'synabun.error':
|
|
3088
|
+
appendError(ev.message);
|
|
3089
|
+
return true;
|
|
3090
|
+
case 'synabun.mailbox':
|
|
3091
|
+
appendMailbox(ev.items || ev.events || ev);
|
|
3092
|
+
return true;
|
|
3093
|
+
case 'synabun.route': {
|
|
3094
|
+
const phase = ev.phase || 'decided';
|
|
3095
|
+
if (hooks.onRouteEvent?.(ev)) return true; // an open card represents it
|
|
3096
|
+
if (phase === 'card' && silent) return true; // replayed card: the pending control re-sends it
|
|
3097
|
+
upsertRouteLine(ev.route || {}, phase);
|
|
3098
|
+
return true;
|
|
3099
|
+
}
|
|
3100
|
+
case 'synabun.clarify':
|
|
3101
|
+
// The card itself arrives as a control_request; its outcome is a line (the panel's card shows it live).
|
|
3102
|
+
if (ev.phase !== 'card') appendStatus(clarifyLine(ev), 'info');
|
|
3103
|
+
return true;
|
|
3104
|
+
default:
|
|
3105
|
+
return false;
|
|
3106
|
+
}
|
|
3107
|
+
}
|
|
3108
|
+
|
|
3109
|
+
/** The turn result: bursts end here, what is still running never will, and the tail text is a message. */
|
|
3110
|
+
function endBursts(scope) {
|
|
3111
|
+
sealScope(scope);
|
|
3112
|
+
for (const rack of [...racks]) {
|
|
3113
|
+
if (rack.scope !== scope && !(scope === rootScope && rack.nested)) continue;
|
|
3114
|
+
let changed = false;
|
|
3115
|
+
for (const step of rack.steps) {
|
|
3116
|
+
if (step.state !== 'running' || step.stale) continue;
|
|
3117
|
+
if (step.type === 'think') { step.state = 'ok'; step.endedAt = nowMs(); } else step.stale = true;
|
|
3118
|
+
refreshStep(step);
|
|
3119
|
+
changed = true;
|
|
3120
|
+
}
|
|
3121
|
+
if (changed || rack.live) renderRack(rack);
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
|
|
3125
|
+
function closeStreamThinks() {
|
|
3126
|
+
for (const scope of [rootScope, ...scopes.values()]) {
|
|
3127
|
+
const think = scope.stream?.think;
|
|
3128
|
+
if (think && think.state === 'running') { think.state = 'ok'; think.endedAt = nowMs(); refreshStep(think); }
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
3131
|
+
|
|
3132
|
+
/**
|
|
3133
|
+
* The turn ended. `holdMs`: the panel plays a one-shot (success, error) on
|
|
3134
|
+
* the character on screen, so its stage stays up that long before it folds
|
|
3135
|
+
* and a parked hero waits as long before it fades.
|
|
3136
|
+
*/
|
|
3137
|
+
function finishTurn({ holdMs = 0 } = {}) {
|
|
3138
|
+
flushStream(rootScope);
|
|
3139
|
+
if (rootScope.stream?.row) rootScope.stream.row.classList.remove('streaming');
|
|
3140
|
+
for (const scope of scopes.values()) flushStream(scope);
|
|
3141
|
+
closeStreamThinks();
|
|
3142
|
+
hideWorking();
|
|
3143
|
+
turnActive = false;
|
|
3144
|
+
const stage = grace || newestStage();
|
|
3145
|
+
grace = null;
|
|
3146
|
+
const hold = holdMs > 0 && !calm();
|
|
3147
|
+
if (stage) {
|
|
3148
|
+
if (hold) holdStage(stage, holdMs);
|
|
3149
|
+
else if (!stage.holdTimer) stage.holdUntil = 0; // a hold already running (the `done` after a result) keeps its time
|
|
3150
|
+
}
|
|
3151
|
+
endBursts(rootScope);
|
|
3152
|
+
for (const rack of [...racks]) {
|
|
3153
|
+
const tail = liveTail(rack.scope) === rack;
|
|
3154
|
+
rack.closed = true;
|
|
3155
|
+
if (rack.live || tail || rack === stage) renderRack(rack);
|
|
3156
|
+
}
|
|
3157
|
+
if (hero?.parked) dismissHero(hold ? holdMs : 0);
|
|
3158
|
+
}
|
|
3159
|
+
|
|
3160
|
+
/**
|
|
3161
|
+
* Rehydrate from GET /api/assistant/sessions/:id. Accepts transcript entries in
|
|
3162
|
+
* any of these shapes: raw envelope packets ({type:'event', event}), user turns
|
|
3163
|
+
* ({type:'query'|'user', prompt|text}), or role-tagged messages. The last burst
|
|
3164
|
+
* stays open (the window may end mid-turn; the live events continue it).
|
|
3165
|
+
*/
|
|
3166
|
+
function renderTranscript(entries) {
|
|
3167
|
+
const list = Array.isArray(entries) ? entries : [];
|
|
3168
|
+
let failed = 0;
|
|
3169
|
+
let firstError = null;
|
|
3170
|
+
// The replay a reattach sends next repeats part of this window: planReplay leaves it out.
|
|
3171
|
+
snapshot = { keys: new Set(), at: -Infinity, seq: -Infinity };
|
|
3172
|
+
for (const entry of list) {
|
|
3173
|
+
if (!entry || typeof entry !== 'object') continue;
|
|
3174
|
+
// An entry that cannot be drawn costs that entry, never the rest of the history.
|
|
3175
|
+
try { renderEntry(entry); } catch (err) { failed += 1; firstError ||= err; }
|
|
3176
|
+
}
|
|
3177
|
+
if (failed) console.warn(`[assistant] ${failed} of ${list.length} transcript entries could not be drawn`, firstError);
|
|
3178
|
+
flushStream(rootScope);
|
|
3179
|
+
if (rootScope.stream?.row) rootScope.stream.row.classList.remove('streaming');
|
|
3180
|
+
for (const scope of scopes.values()) flushStream(scope);
|
|
3181
|
+
hideWorking();
|
|
3182
|
+
turnActive = false;
|
|
3183
|
+
for (const rack of [...racks]) renderRack(rack);
|
|
3184
|
+
scrollEnd(true);
|
|
3185
|
+
}
|
|
3186
|
+
|
|
3187
|
+
function renderEntry(entry) {
|
|
3188
|
+
const was = { muted, clockAt };
|
|
3189
|
+
muted = true;
|
|
3190
|
+
const ms = entry.at != null ? Date.parse(entry.at) : NaN;
|
|
3191
|
+
clockAt = Number.isFinite(ms) ? ms : null;
|
|
3192
|
+
try { drawEntry(entry); noteSnapshot(entry); }
|
|
3193
|
+
finally { muted = was.muted; clockAt = was.clockAt; }
|
|
3194
|
+
}
|
|
3195
|
+
|
|
3196
|
+
function drawEntry(entry) {
|
|
3197
|
+
const packet = entry.packet && typeof entry.packet === 'object' ? entry.packet : entry;
|
|
3198
|
+
const role = packet.role || packet.type;
|
|
3199
|
+
if (role === 'query' || role === 'user' || role === 'prompt') {
|
|
3200
|
+
const text = packet.prompt ?? packet.text ?? (typeof packet.content === 'string' ? packet.content : null);
|
|
3201
|
+
if (text != null && !packet.event) { appendUser(String(text), { images: Array.isArray(packet.images) ? packet.images : [] }); return; }
|
|
3202
|
+
}
|
|
3203
|
+
// The runtime journals the user's prompts only as `synabun.user_prompt` events.
|
|
3204
|
+
if (packet.type === 'event' && packet.event?.type === 'synabun.user_prompt') {
|
|
3205
|
+
if (packet.event.text) appendUser(String(packet.event.text), { origin: packet.event.origin || null, imageCount: packet.event.images });
|
|
3206
|
+
return;
|
|
3207
|
+
}
|
|
3208
|
+
if (packet.type === 'event' && packet.event) { handleEvent(packet.event, { silent: true }); return; }
|
|
3209
|
+
if (role === 'assistant' && (packet.message || packet.content)) { renderAssistant(packet.message || packet); return; }
|
|
3210
|
+
if (packet.type && handleEvent(packet, { silent: true })) return;
|
|
3211
|
+
if (role === 'system' && packet.text) appendStatus(packet.text);
|
|
3212
|
+
}
|
|
3213
|
+
|
|
3214
|
+
function dropAll() {
|
|
3215
|
+
for (const rack of [...racks]) dropRack(rack);
|
|
3216
|
+
stopTicker();
|
|
3217
|
+
stepsById.clear();
|
|
3218
|
+
}
|
|
3219
|
+
|
|
3220
|
+
function clear() {
|
|
3221
|
+
for (const scope of scopes.values()) { if (scope.stream?.mdTimer) clearTimeout(scope.stream.mdTimer); if (scope.stream?.thinkTimer) clearTimeout(scope.stream.thinkTimer); }
|
|
3222
|
+
if (rootScope.stream?.mdTimer) clearTimeout(rootScope.stream.mdTimer);
|
|
3223
|
+
if (rootScope.stream?.thinkTimer) clearTimeout(rootScope.stream.thinkTimer);
|
|
3224
|
+
scopes.clear();
|
|
3225
|
+
runCards.clear();
|
|
3226
|
+
routeLines.clear();
|
|
3227
|
+
turnFrames = [];
|
|
3228
|
+
rootScope.stream = null;
|
|
3229
|
+
rootScope.kept = null;
|
|
3230
|
+
rootScope.msgId = null;
|
|
3231
|
+
lastErrorText = '';
|
|
3232
|
+
hideWorking();
|
|
3233
|
+
turnActive = false;
|
|
3234
|
+
dropAll();
|
|
3235
|
+
runInfo.clear();
|
|
3236
|
+
dropHero();
|
|
3237
|
+
grace = null;
|
|
3238
|
+
turn = { calls: 0, last: null, errored: false, answerRow: null };
|
|
3239
|
+
mcpWarned.clear();
|
|
3240
|
+
seen.clear();
|
|
3241
|
+
snapshot = null;
|
|
3242
|
+
waiting = null;
|
|
3243
|
+
$msgs.innerHTML = '';
|
|
3244
|
+
emptyEl = null;
|
|
3245
|
+
followTail = true;
|
|
3246
|
+
}
|
|
3247
|
+
|
|
3248
|
+
return {
|
|
3249
|
+
el: $msgs,
|
|
3250
|
+
clear,
|
|
3251
|
+
destroy: () => { hideWorking(); dropHero(); dropAll(); },
|
|
3252
|
+
appendUser,
|
|
3253
|
+
appendAssistantMarkdown,
|
|
3254
|
+
appendStatus,
|
|
3255
|
+
appendError,
|
|
3256
|
+
appendDivider,
|
|
3257
|
+
appendNode,
|
|
3258
|
+
appendMemoryChips,
|
|
3259
|
+
appendMemorySaved,
|
|
3260
|
+
appendMailbox,
|
|
3261
|
+
upsertRunCard,
|
|
3262
|
+
patchRunUsage,
|
|
3263
|
+
upsertRouteLine,
|
|
3264
|
+
registerRouteLine,
|
|
3265
|
+
removeRouteLine,
|
|
3266
|
+
getRouteLine: (routeId) => routeLines.get(routeId) || null,
|
|
3267
|
+
getRunCard: (runId) => runCards.get(runId) || null,
|
|
3268
|
+
handleEvent,
|
|
3269
|
+
/** The live gate for an event (false: already drawn); registers what it lets through. */
|
|
3270
|
+
claimEvent,
|
|
3271
|
+
/** A reattach's buffered packets minus what is on screen (see planReplay). */
|
|
3272
|
+
planReplay,
|
|
3273
|
+
/** Forget what was drawn (a brain switch starts a new id space). */
|
|
3274
|
+
forgetSeen: () => { seen.clear(); snapshot = null; },
|
|
3275
|
+
/** A card waits on the user: `toolUseId` names the call it holds (the newest live stage when unknown); false clears it. */
|
|
3276
|
+
setWaiting,
|
|
3277
|
+
finishTurn,
|
|
3278
|
+
renderTranscript,
|
|
3279
|
+
showWorking,
|
|
3280
|
+
hideWorking,
|
|
3281
|
+
isWorkingShown: () => !!working?.isConnected,
|
|
3282
|
+
showEmpty,
|
|
3283
|
+
refreshEmpty: renderSuggestions,
|
|
3284
|
+
isEmpty: () => !!emptyEl,
|
|
3285
|
+
scrollEnd,
|
|
3286
|
+
isNearBottom,
|
|
3287
|
+
md,
|
|
3288
|
+
/** The character on screen, for the panel's director: { kind, rig, el, hold(on), restore() } or null. */
|
|
3289
|
+
mascot: visibleMascot,
|
|
3290
|
+
setRigsEnabled,
|
|
3291
|
+
/** What this turn did: real calls (not the prompt-time recall), whether the last one failed, a turn error. */
|
|
3292
|
+
turnStats: () => ({ calls: turn.calls, lastFailed: turn.last?.state === 'error', errored: turn.errored }),
|
|
3293
|
+
/** The text of the turn's last message at the root ('' when it folded into a rack or there was none). */
|
|
3294
|
+
turnAnswer: () => (turn.answerRow?.isConnected ? (turn.answerRow.querySelector('.asst-md')?.textContent || '').replace(/\s+/g, ' ').trim() : ''),
|
|
3295
|
+
};
|
|
3296
|
+
}
|