@tea-agent/loop-agent 0.43.0-next.9 → 0.44.0-next.1
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/AGENTS.md +1 -1
- package/CHANGELOG.md +108 -2308
- package/README.md +7 -0
- package/dist/application/dag/args.js +8 -0
- package/dist/application/task-lifecycle/advance.js +38 -7
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +2 -0
- package/dist/cli/program.js +3 -1
- package/dist/cli/update/init-surface-notifier.js +2 -0
- package/dist/cli/update/policy.js +2 -0
- package/dist/commands/client-recovery.js +20 -55
- package/dist/commands/cursor-prompt.js +2 -0
- package/dist/commands/dag-approve.js +2 -0
- package/dist/commands/dag-final-verification.js +2 -0
- package/dist/commands/dag-init-hybrid.js +2 -0
- package/dist/commands/dag-reconcile-tasks.js +2 -0
- package/dist/commands/dag-reject.js +2 -0
- package/dist/commands/dag-report.js +2 -0
- package/dist/commands/dag-rerun.js +2 -0
- package/dist/commands/dag-resume.js +2 -0
- package/dist/commands/dag-workflow-compile.js +2 -0
- package/dist/commands/dag-workflow-plan.js +2 -0
- package/dist/commands/dag-workflow-validate.js +2 -0
- package/dist/commands/delegate.js +2 -0
- package/dist/commands/examples.js +2 -0
- package/dist/commands/import-prd.js +2 -0
- package/dist/commands/init-model-catalog.js +2 -0
- package/dist/commands/init.js +19 -13
- package/dist/commands/instructions.js +2 -0
- package/dist/commands/knowledge.js +2 -0
- package/dist/commands/loop-benchmark.js +2 -0
- package/dist/commands/pi-prompt.js +2 -0
- package/dist/commands/pi-reuse-benchmark.js +2 -0
- package/dist/commands/promote-run.js +2 -0
- package/dist/commands/stats.js +1 -1
- package/dist/commands/study-init.js +2 -0
- package/dist/commands/task-advance.js +34 -0
- package/dist/commands/task-source-prepare.js +2 -0
- package/dist/commands/worktree-create.js +2 -0
- package/dist/commands/worktree-remove.js +2 -0
- package/dist/executors/dag-pi-executor.js +1650 -198
- package/dist/executors/pi-executor.js +85 -3
- package/dist/executors/pi-sdk-executor.js +18 -0
- package/dist/executors/shell-executor.js +93 -7
- package/dist/executors/shell-verification.js +3 -0
- package/dist/executors/shell-write-guard.js +27 -32
- package/dist/governance/exec-plans.js +6 -3
- package/dist/infrastructure/console/app-data.js +6 -0
- package/dist/shared/artifacts-core.js +2 -0
- package/dist/shared/backend-dogfood-preflight.js +47 -0
- package/dist/shared/dag-failure-category.js +3 -0
- package/dist/shared/git-progress.js +2 -0
- package/dist/shared/one-shot-prompt-args.js +2 -0
- package/dist/shared/operator/capabilities.js +180 -0
- package/dist/shared/package-metadata.js +6 -4
- package/dist/shared/pi-context-pressure/extension.js +26 -16
- package/dist/shared/pi-context-pressure/sift-bridge.js +1 -1
- package/dist/shared/pi-provider-recovery.js +182 -0
- package/dist/shared/pi-retry-settings.js +40 -0
- package/dist/shared/reference-context.js +2 -0
- package/dist/task/config-types.js +29 -0
- package/dist/task/contract/project.js +9 -0
- package/dist/task/contract/recover.js +6 -0
- package/dist/task/contract/schema.js +17 -0
- package/dist/task/source-prepare/artifact-meta.js +9 -2
- package/dist/task/source-prepare/build-draft.js +60 -0
- package/dist/task/source-prepare/fragment-inventory.js +4 -1
- package/dist/task/source-prepare/parse-intent.js +36 -11
- package/dist/task/task-demand-routing.js +5 -2
- package/dist/worker/console/chat/browser-automation.js +523 -0
- package/dist/worker/console/chat/browser-policy.js +6 -36
- package/dist/worker/console/chat/browser-routes.js +188 -56
- package/dist/worker/console/chat/explore-tools.js +5 -2
- package/dist/worker/console/chat/pi-mode-loop-isolation.js +155 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/ask-user-question.js +44 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/browser-tools.js +184 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/explore-tools.js +202 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/goal-tools.js +79 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/operator-tools.js +94 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/scheduled-goal-tools.js +22 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/todo-write.js +38 -0
- package/dist/worker/console/chat/pi-runtime/inspection-types.js +28 -0
- package/dist/worker/console/chat/pi-runtime/inspection.js +439 -0
- package/dist/worker/console/chat/pi-runtime/progressive-tools.js +195 -0
- package/dist/worker/console/chat/pi-runtime/sdk-bindings.js +153 -0
- package/dist/worker/console/chat/pi-runtime/sdk-session.js +5 -0
- package/dist/worker/console/chat/pi-runtime.js +208 -1138
- package/dist/worker/console/chat/routes.js +27 -7
- package/dist/worker/console/chat/scheduled-goal-booking.js +59 -0
- package/dist/worker/console/chat/scheduled-goal-delivery.js +27 -0
- package/dist/worker/console/chat/scheduled-goal-request.js +190 -0
- package/dist/worker/console/chat/sdd-data-alignment.js +1 -1
- package/dist/worker/console/chat/session-mode-view.js +5 -4
- package/dist/worker/console/chat/session-mode.js +18 -12
- package/dist/worker/console/chat/session-store.js +42 -9
- package/dist/worker/console/chat/tool-preview.js +115 -0
- package/dist/worker/console/chat/tools.js +10 -0
- package/dist/worker/console/chat/turn-order.js +13 -0
- package/dist/worker/console/chat/turn-process.js +32 -30
- package/dist/worker/console/console-update-and-init.js +1 -19
- package/dist/worker/console/console-update-runtime.js +5 -20
- package/dist/worker/console/operator-actions.js +63 -1
- package/dist/worker/console/prd-intake-bridge.js +54 -1
- package/dist/worker/console/routes.js +8 -0
- package/dist/worker/console/scheduled-goal-host.js +98 -0
- package/dist/worker/console/scheduled-goal-operation-adapter.js +127 -0
- package/dist/worker/console/server.js +24 -1
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-D_fNbWc_.js → abnfDiagram-N423BO3Z-8-j6y-sd.js} +1 -1
- package/dist/worker/console/static/assets/{arc-_HuqLW6m.js → arc-eoQiMvuk.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-Djy2qY1m.js → architectureDiagram-T3A2C74G-C4A3uMcI.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-bB-1TSyY.js → blockDiagram-VBNYF7ZC-D4zD2F-Q.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-C5Hmghrz.js → c4Diagram-5PPSVZJV-j1RkJziL.js} +1 -1
- package/dist/worker/console/static/assets/channel-ChE7y-cx.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-Duyl39oy.js → chunk-2GRJ4B5K-YBHmhik1.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-x6mdnSyf.js → chunk-2Q5K7J3B-COfWyo9P.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-WlBuFj7W.js → chunk-5RXB4S5H-I99OUkHY.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CTF429HX.js → chunk-5VM5RSS4-XKxoJNJ4.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Dwd6r-SF.js → chunk-6Q2QTUOP-DLT_cYx1.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-C-Br82D1.js → chunk-GF5L2VYU-CxcKZ9mV.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CuGp0DeE.js → chunk-JWPE2WC7-V1EqXdjY.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-CJhFX6Lh.js → chunk-KBJHAD2P-DebTtdFp.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DpGOa_jT.js → chunk-RYQCIY6F-B-ivNRDf.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DXJaTynd.js → chunk-XXDRQBXY-DC_Ds11b.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C01TCf2X.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C01TCf2X.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CRBZWjE4.js → cose-bilkent-JH36ORCC-sWEqKwIb.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BdXcxtlb.js → cynefin-VYW2F7L2-BXa_dcu4.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-07UKsOts.js → cynefinDiagram-MW4NZA55-C-Mle84F.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-D38p9__S.js → dagre-VZM6K2ZE-CXPDBITe.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-nSiBNNjH.js → diagram-7IWD3JNH-CC-WJQfa.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BFOTot5D.js → diagram-B4RE2ZJO-CDAV5Vs4.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DypKaZ6k.js → diagram-LBJQPF4R-CmFiAcNz.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-B6yoOQeW.js → diagram-Q27KOJAE-DPBZHuyn.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BHYS6viX.js → diagram-UB23O5K3-jUlm_Ds3.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-D2VD5Snj.js → ebnfDiagram-BXEA7PRR-DWhQ3mfY.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-E98mWxN1.js → erDiagram-JOGREHBK-Tr2gMqet.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CT1zxhqP.js → flowDiagram-UKHOOZJN-DJjVQHPA.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-6GcngNcy.js → ganttDiagram-PKOTCBZU-D74dQ4u0.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DCL6NvKe.js → gitGraphDiagram-DS77QQ5N-DwW0tZ0X.js} +1 -1
- package/dist/worker/console/static/assets/index-BWkIfcrK.css +1 -0
- package/dist/worker/console/static/assets/index-Cdkvw_H6.js +469 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-C0627e-9.js → infoDiagram-6WML65LV-ILCbxJyb.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-C_u3P6Ik.js → ishikawaDiagram-WSZJBQD7-DeuWBQ89.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DPDPv9HL.js → journeyDiagram-NVQOT4AX-CF5ih8Fk.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-D3OHdWiO.js → kanban-definition-27J2QSJJ-C7yOSuRO.js} +1 -1
- package/dist/worker/console/static/assets/{linear-DI88zD7n.js → linear-BDZ9riWi.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-p2l8IrWY.js → mermaid.core-7pKqYtpZ.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-B1bAX-oS.js → mindmap-definition-FAOFIHXS-LeJDybSU.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DwOnWxdD.js → pegDiagram-VL7TDLO6-BJvT3pMD.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-xRkTXnJC.js → pieDiagram-7S7Q4E2Y-_rGqpMan.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-_9UC5Zio.js → quadrantDiagram-CIZ2JOQS-DPcSv5aA.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-CgEUirWJ.js → railroadDiagram-AXF67PYL-Drxx4hkJ.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-Czbpwiyz.js → requirementDiagram-LRYGKXZP-BCTUdU4z.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-_j7_KiqY.js → sankeyDiagram-W5VNT64P-B6wzbZmB.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BNJa6Hyi.js → sequenceDiagram-SI44F4Z6-BGt8d3QQ.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DedpKtOc.js → sizeCapture-X5ZJPWSS-7nlhJKo7.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-BBvthr7c.js → stateDiagram-OKZ733FA-Cv2stqMA.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DC7V5vcq.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-Ds7umgEM.js → swimlanes-SLNWSIFB-qDAo4Yc1.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Bwy4QUTO.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CKa-p1nF.js → timeline-definition-Z64GVDOM-CKbDNKTr.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-LoUNR56o.js → vennDiagram-T6HMQDX7-DI-9EHic.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-Bqbl6ym7.js → wardleyDiagram-T6FBY63Y-BrzzRDpX.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-B8dUg4lf.js → xychartDiagram-ELKLHX3M-BclH5hGh.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/chat-scroll-position.js +24 -0
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +41 -11
- package/dist/worker/console/static-src/operator-chat/compaction-message.js +3 -17
- package/dist/worker/console/static-src/operator-chat/composer-session-ledger.js +192 -0
- package/dist/worker/console/static-src/operator-chat/pending-user-message.js +9 -1
- package/dist/worker/console/static-src/operator-chat/slash-palette-nav.js +15 -5
- package/dist/worker/console/static-src/operator-chat/timeline-merge.js +29 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +92 -38
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +145 -70
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +38 -55
- package/dist/worker/console/static-src/operator-chat/useComposer.js +182 -69
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +2 -0
- package/dist/worker/console/static-src/prd-file-import.js +3 -1
- package/dist/worker/console/workspace-context.js +22 -0
- package/dist/worker/console/workspace-initialization.js +209 -0
- package/dist/worker/delivery/git-transaction.js +2 -0
- package/dist/worker/feature/fullstack-validate.js +2 -0
- package/dist/worker/loop-agent/loop-agent-client.js +8 -2
- package/dist/worker/materialize/harness-task-materializer.js +4 -1
- package/dist/worker/metrics/projector.js +1 -1
- package/dist/worker/observability/event-store.js +4 -1
- package/dist/worker/observability/read-model.js +4 -1
- package/dist/worker/observe/node-transparency.js +23 -1
- package/dist/worker/observe/routes.js +4 -0
- package/dist/worker/observe/static/operator-chrome.js +3 -1
- package/dist/worker/observe/static/styles.css +7 -4
- package/dist/worker/observe/static/views/dag-inspector.js +5 -19
- package/dist/worker/outcomes/declared-artifacts.js +2 -0
- package/dist/worker/preflight.js +3 -0
- package/dist/worker/scheduler/scheduled-goal-dispatch.js +117 -0
- package/dist/worker/scheduler/scheduled-goal-evidence.js +167 -0
- package/dist/worker/scheduler/scheduled-goal-recovery.js +62 -0
- package/dist/worker/scheduler/scheduled-goal-store.js +699 -0
- package/dist/worker/scheduler/scheduled-goal-supervisor.js +132 -0
- package/dist/worker/scheduler/scheduled-goal-time.js +102 -0
- package/dist/worker/scheduler/scheduled-goal-types.js +95 -0
- package/dist/worker/task-spec/validate.js +4 -1
- package/dist/workflows/dag/backend-test-markdown-workflow.js +6 -24
- package/dist/workflows/dag/backend-test-plan-protocol.js +82 -5
- package/dist/workflows/dag/dag-retry-schema.js +11 -0
- package/dist/workflows/dag/decision-envelope.js +2 -0
- package/dist/workflows/dag/dynamic-runtime/shared.js +2 -2
- package/dist/workflows/dag/frontend-closeout.js +3 -1
- package/dist/workflows/dag/frontend-committed-facts.js +461 -0
- package/dist/workflows/dag/frontend-durable-tools.js +15 -3
- package/dist/workflows/dag/frontend-implementation-contract.js +369 -12
- package/dist/workflows/dag/frontend-plan-canary.js +53 -0
- package/dist/workflows/dag/frontend-plan-decision-contract.js +803 -0
- package/dist/workflows/dag/frontend-plan-render.js +0 -2
- package/dist/workflows/dag/frontend-provider-capability-matrix.js +8 -61
- package/dist/workflows/dag/frontend-recovery-run.js +57 -0
- package/dist/workflows/dag/frontend-repair.js +55 -76
- package/dist/workflows/dag/frontend-review-context.js +42 -68
- package/dist/workflows/dag/frontend-review-scopes.js +2 -2
- package/dist/workflows/dag/frontend-risk.js +92 -10
- package/dist/workflows/dag/frontend-session-budget.js +117 -3
- package/dist/workflows/dag/frontend-shape.js +11 -55
- package/dist/workflows/dag/frontend-test-execution-evidence.js +35 -10
- package/dist/workflows/dag/frontend-typed-event-store.js +32 -25
- package/dist/workflows/dag/frontend-verification-trace.js +33 -54
- package/dist/workflows/dag/frontend-writer-admission.js +3 -47
- package/dist/workflows/dag/frontend-writer-status.js +0 -23
- package/dist/workflows/dag/init-hybrid.js +47 -19
- package/dist/workflows/dag/interrupt-request.js +2 -0
- package/dist/workflows/dag/lifecycle.js +11 -2
- package/dist/workflows/dag/node-execution.js +50 -13
- package/dist/workflows/dag/reconcile-run.js +2 -0
- package/dist/workflows/dag/repair-artifact.js +3 -1
- package/dist/workflows/dag/report.js +2 -0
- package/dist/workflows/dag/rerun-plan.js +10 -0
- package/dist/workflows/dag/rerun-task.js +79 -1
- package/dist/workflows/dag/retry-policy.js +18 -0
- package/dist/workflows/dag/scheduler.js +2 -4
- package/dist/workflows/dag/types.js +44 -6
- package/dist/workflows/dag/validate.js +4 -0
- package/docs/README.md +1 -0
- package/docs/architecture/runtime-boundaries.md +3 -3
- package/docs/governance/README.md +1 -0
- package/docs/init-surface.manifest.json +1 -0
- package/docs/operations/README.md +2 -0
- package/docs/templates/README.md +1 -1
- package/docs/templates/agent-dag.schema.json +2 -2
- package/docs/templates/backend-test-dag.json +14 -10
- package/docs/templates/frontend-implementation-contract.schema.json +0 -7
- package/package.json +9 -3
- package/skills/frontend-bounded-implement/references/code-standards.md +3 -3
- package/skills/frontend-contract/references/contract-protocol.md +3 -1
- package/skills/frontend-plan/SKILL.md +8 -6
- package/skills/frontend-plan/references/decision-contract.md +3 -3
- package/skills/frontend-review/SKILL.md +18 -28
- package/skills/frontend-review/references/review-findings.md +8 -3
- package/dist/commands/new-task.js +0 -5
- package/dist/commands/status.js +0 -57
- package/dist/commands/task-contract.js +0 -269
- package/dist/shared/prompts.js +0 -26
- package/dist/task/config.js +0 -2
- package/dist/task/contract/validate-draft.js +0 -106
- package/dist/task/goal.js +0 -3
- package/dist/task/index.js +0 -12
- package/dist/task/lifecycle.js +0 -1
- package/dist/task/operator/capabilities.js +0 -6
- package/dist/task/operator/envelope.js +0 -2
- package/dist/task/operator/index.js +0 -5
- package/dist/task/operator/registry.js +0 -2
- package/dist/task/operator/types.js +0 -1
- package/dist/task/paths.js +0 -1
- package/dist/task/source-state.js +0 -1
- package/dist/task/subagent-guidance.js +0 -1
- package/dist/worker/console/chat/sift-bridge.js +0 -1
- package/dist/worker/console/index.js +0 -20
- package/dist/worker/console/static/assets/channel-C0A6UEIa.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DzguNppy.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DzguNppy.js +0 -1
- package/dist/worker/console/static/assets/index-24jp3aBh.js +0 -468
- package/dist/worker/console/static/assets/index-CY6y6I8M.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BABVQnfM.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D0qsLaxR.js +0 -8
- package/dist/worker/scheduler/index.js +0 -25
- package/dist/workflows/dag/facts.js +0 -4
- package/dist/workflows/dag/frontend-shadow-dual-write.js +0 -975
- package/dist/workflows/dag/index.js +0 -6
- package/dist/workflows/dynamic/index.js +0 -7
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { createInputHistoryState, navigateInputHistory, recordInputHistoryEntry, resetInputHistoryBrowsing, } from "./input-history.js";
|
|
2
|
+
export const DUPLICATE_SUBMISSION_GESTURE_MS = 350;
|
|
3
|
+
export function composerSessionKey(identity) {
|
|
4
|
+
return `${identity.workspaceId ?? ""}\u0000${identity.sessionId}`;
|
|
5
|
+
}
|
|
6
|
+
function cloneHistory(history) {
|
|
7
|
+
return { ...history, entries: [...history.entries] };
|
|
8
|
+
}
|
|
9
|
+
function cloneImages(images) {
|
|
10
|
+
return [...images];
|
|
11
|
+
}
|
|
12
|
+
function createRecord() {
|
|
13
|
+
return {
|
|
14
|
+
draft: "",
|
|
15
|
+
images: [],
|
|
16
|
+
history: createInputHistoryState(),
|
|
17
|
+
revision: 0,
|
|
18
|
+
sequence: 0,
|
|
19
|
+
initialized: false,
|
|
20
|
+
attempts: new Map(),
|
|
21
|
+
failedSubmissionCount: 0,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function sameImages(left, right) {
|
|
25
|
+
return (left.length === right.length &&
|
|
26
|
+
left.every((image, index) => {
|
|
27
|
+
const other = right[index];
|
|
28
|
+
return (other !== undefined &&
|
|
29
|
+
image.name === other.name &&
|
|
30
|
+
image.type === other.type &&
|
|
31
|
+
image.mimeType === other.mimeType &&
|
|
32
|
+
image.data === other.data);
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Browser-local composer state addressed by durable session identity.
|
|
37
|
+
*
|
|
38
|
+
* Network callbacks settle immutable attempts against their owning record;
|
|
39
|
+
* they never need to mutate whichever composer happens to be visible.
|
|
40
|
+
*/
|
|
41
|
+
export class ComposerSessionLedger {
|
|
42
|
+
now;
|
|
43
|
+
records = new Map();
|
|
44
|
+
constructor(now = Date.now) {
|
|
45
|
+
this.now = now;
|
|
46
|
+
}
|
|
47
|
+
has(identity) {
|
|
48
|
+
return this.records.has(composerSessionKey(identity));
|
|
49
|
+
}
|
|
50
|
+
snapshot(identity) {
|
|
51
|
+
const record = this.record(identity);
|
|
52
|
+
return {
|
|
53
|
+
draft: record.draft,
|
|
54
|
+
images: cloneImages(record.images),
|
|
55
|
+
history: cloneHistory(record.history),
|
|
56
|
+
revision: record.revision,
|
|
57
|
+
failedSubmissionCount: record.failedSubmissionCount,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
seedDraft(identity, draft) {
|
|
61
|
+
const record = this.record(identity);
|
|
62
|
+
if (record.initialized || record.revision !== 0 || record.attempts.size > 0)
|
|
63
|
+
return false;
|
|
64
|
+
record.draft = draft;
|
|
65
|
+
record.initialized = true;
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
setDraft(identity, draft) {
|
|
69
|
+
const record = this.record(identity);
|
|
70
|
+
if (record.draft === draft)
|
|
71
|
+
return false;
|
|
72
|
+
record.draft = draft;
|
|
73
|
+
record.history = resetInputHistoryBrowsing(record.history);
|
|
74
|
+
record.revision += 1;
|
|
75
|
+
record.initialized = true;
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
setImages(identity, images) {
|
|
79
|
+
const record = this.record(identity);
|
|
80
|
+
record.images = cloneImages(images);
|
|
81
|
+
record.revision += 1;
|
|
82
|
+
record.initialized = true;
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
recordHistory(identity, text) {
|
|
86
|
+
const record = this.record(identity);
|
|
87
|
+
record.history = recordInputHistoryEntry(record.history, text);
|
|
88
|
+
}
|
|
89
|
+
navigateHistory(identity, direction, context) {
|
|
90
|
+
const record = this.record(identity);
|
|
91
|
+
const decision = navigateInputHistory(record.history, direction, context);
|
|
92
|
+
if (!decision.handled)
|
|
93
|
+
return decision;
|
|
94
|
+
record.history = decision.state;
|
|
95
|
+
if (record.draft !== decision.text) {
|
|
96
|
+
record.draft = decision.text;
|
|
97
|
+
record.revision += 1;
|
|
98
|
+
record.initialized = true;
|
|
99
|
+
}
|
|
100
|
+
return decision;
|
|
101
|
+
}
|
|
102
|
+
beginSubmission(identity, sessionGeneration, clientRequestId) {
|
|
103
|
+
const record = this.record(identity);
|
|
104
|
+
if (!record.draft.trim() && record.images.length === 0)
|
|
105
|
+
return null;
|
|
106
|
+
const now = this.now();
|
|
107
|
+
const sinceLastClaim = record.lastClaim
|
|
108
|
+
? now - record.lastClaim.at
|
|
109
|
+
: Number.POSITIVE_INFINITY;
|
|
110
|
+
if (record.lastClaim &&
|
|
111
|
+
sinceLastClaim >= 0 &&
|
|
112
|
+
sinceLastClaim <= DUPLICATE_SUBMISSION_GESTURE_MS &&
|
|
113
|
+
record.lastClaim.draft === record.draft &&
|
|
114
|
+
sameImages(record.lastClaim.images, record.images))
|
|
115
|
+
return null;
|
|
116
|
+
record.sequence += 1;
|
|
117
|
+
const sequence = record.sequence;
|
|
118
|
+
const controller = new AbortController();
|
|
119
|
+
const draftSnapshot = record.draft;
|
|
120
|
+
const imageSnapshot = cloneImages(record.images);
|
|
121
|
+
record.lastClaim = {
|
|
122
|
+
draft: draftSnapshot,
|
|
123
|
+
images: cloneImages(imageSnapshot),
|
|
124
|
+
at: now,
|
|
125
|
+
};
|
|
126
|
+
record.history = recordInputHistoryEntry(record.history, draftSnapshot);
|
|
127
|
+
record.draft = "";
|
|
128
|
+
record.images = [];
|
|
129
|
+
record.revision += 1;
|
|
130
|
+
record.initialized = true;
|
|
131
|
+
const attempt = {
|
|
132
|
+
attemptId: `${composerSessionKey(identity)}:${sequence}:${clientRequestId}`,
|
|
133
|
+
identity: { ...identity },
|
|
134
|
+
sessionGeneration,
|
|
135
|
+
sequence,
|
|
136
|
+
clientRequestId,
|
|
137
|
+
draftSnapshot,
|
|
138
|
+
imageSnapshot,
|
|
139
|
+
committedRevision: record.revision,
|
|
140
|
+
signal: controller.signal,
|
|
141
|
+
};
|
|
142
|
+
record.attempts.set(attempt.attemptId, { attempt, controller });
|
|
143
|
+
return attempt;
|
|
144
|
+
}
|
|
145
|
+
settle(attempt, outcome) {
|
|
146
|
+
const key = composerSessionKey(attempt.identity);
|
|
147
|
+
const record = this.records.get(key);
|
|
148
|
+
const owned = record?.attempts.get(attempt.attemptId);
|
|
149
|
+
if (!record || !owned || owned.attempt !== attempt)
|
|
150
|
+
return "stale";
|
|
151
|
+
record.attempts.delete(attempt.attemptId);
|
|
152
|
+
owned.controller.abort();
|
|
153
|
+
if (outcome === "accepted")
|
|
154
|
+
return "accepted";
|
|
155
|
+
if (record.revision === attempt.committedRevision &&
|
|
156
|
+
record.draft === "" &&
|
|
157
|
+
record.images.length === 0) {
|
|
158
|
+
record.draft = attempt.draftSnapshot;
|
|
159
|
+
record.images = cloneImages(attempt.imageSnapshot);
|
|
160
|
+
record.revision += 1;
|
|
161
|
+
return "restored";
|
|
162
|
+
}
|
|
163
|
+
record.failedSubmissionCount = Math.min(Number.MAX_SAFE_INTEGER, record.failedSubmissionCount + 1);
|
|
164
|
+
return "retained";
|
|
165
|
+
}
|
|
166
|
+
settleByRequest(identity, clientRequestId, outcome) {
|
|
167
|
+
const record = this.records.get(composerSessionKey(identity));
|
|
168
|
+
const owned = record
|
|
169
|
+
? [...record.attempts.values()].find((entry) => entry.attempt.clientRequestId === clientRequestId)
|
|
170
|
+
: undefined;
|
|
171
|
+
return owned ? this.settle(owned.attempt, outcome) : "stale";
|
|
172
|
+
}
|
|
173
|
+
release(identity) {
|
|
174
|
+
const key = composerSessionKey(identity);
|
|
175
|
+
const record = this.records.get(key);
|
|
176
|
+
if (!record)
|
|
177
|
+
return;
|
|
178
|
+
for (const owned of record.attempts.values())
|
|
179
|
+
owned.controller.abort();
|
|
180
|
+
record.attempts.clear();
|
|
181
|
+
this.records.delete(key);
|
|
182
|
+
}
|
|
183
|
+
record(identity) {
|
|
184
|
+
const key = composerSessionKey(identity);
|
|
185
|
+
let record = this.records.get(key);
|
|
186
|
+
if (!record) {
|
|
187
|
+
record = createRecord();
|
|
188
|
+
this.records.set(key, record);
|
|
189
|
+
}
|
|
190
|
+
return record;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -27,7 +27,15 @@ export function replacePendingUserMessage(messages, clientRequestId, durable) {
|
|
|
27
27
|
continue;
|
|
28
28
|
replaced = true;
|
|
29
29
|
const { pending: _pending, ...rest } = message;
|
|
30
|
-
next.push({
|
|
30
|
+
next.push({
|
|
31
|
+
...rest,
|
|
32
|
+
...durable,
|
|
33
|
+
// Durable reconciliation replaces values in the established optimistic
|
|
34
|
+
// seat. A later server timestamp must not move the user below the empty
|
|
35
|
+
// active assistant cursor when turn groups sort by display time.
|
|
36
|
+
createdAt: Math.min(message.createdAt, durable.createdAt),
|
|
37
|
+
clientRequestId,
|
|
38
|
+
});
|
|
31
39
|
}
|
|
32
40
|
return replaced ? next : [...next, durable];
|
|
33
41
|
}
|
|
@@ -27,8 +27,11 @@ export function buildPaletteModel(entries) {
|
|
|
27
27
|
for (const source of SLASH_GROUP_DISPLAY_ORDER) {
|
|
28
28
|
const items = [];
|
|
29
29
|
for (let i = 0; i < entries.length; i += 1) {
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
const entry = entries[i];
|
|
31
|
+
if (!entry)
|
|
32
|
+
continue;
|
|
33
|
+
if (entry.source === source) {
|
|
34
|
+
items.push({ entry, globalIndex: i });
|
|
32
35
|
}
|
|
33
36
|
}
|
|
34
37
|
if (items.length === 0)
|
|
@@ -69,6 +72,8 @@ export function movePaletteIndex(currentIndex, model, columns, direction) {
|
|
|
69
72
|
if (groupPos < 0)
|
|
70
73
|
return currentIndex;
|
|
71
74
|
const group = model[groupPos];
|
|
75
|
+
if (!group)
|
|
76
|
+
return currentIndex;
|
|
72
77
|
const offset = group.items.findIndex((card) => card.globalIndex === currentIndex);
|
|
73
78
|
if (offset < 0)
|
|
74
79
|
return currentIndex;
|
|
@@ -84,25 +89,30 @@ export function movePaletteIndex(currentIndex, model, columns, direction) {
|
|
|
84
89
|
}
|
|
85
90
|
if (Math.floor(targetOffset / columns) !== row)
|
|
86
91
|
return currentIndex;
|
|
87
|
-
|
|
92
|
+
const targetCard = group.items[targetOffset];
|
|
93
|
+
return targetCard ? targetCard.globalIndex : currentIndex;
|
|
88
94
|
}
|
|
89
95
|
const delta = direction === "up" ? -1 : 1;
|
|
90
96
|
const targetOffset = offset + delta * columns;
|
|
91
97
|
if (targetOffset >= 0 && targetOffset < group.items.length) {
|
|
92
|
-
|
|
98
|
+
const targetCard = group.items[targetOffset];
|
|
99
|
+
return targetCard ? targetCard.globalIndex : currentIndex;
|
|
93
100
|
}
|
|
94
101
|
// Cross-group: walk to the previous/next non-empty group and target the
|
|
95
102
|
// same column on its last (up) / first (down) row; stick when that column
|
|
96
103
|
// does not exist in the partial edge row.
|
|
97
104
|
for (let cursor = groupPos + delta; cursor >= 0 && cursor < model.length; cursor += delta) {
|
|
98
105
|
const candidate = model[cursor];
|
|
106
|
+
if (!candidate)
|
|
107
|
+
continue;
|
|
99
108
|
if (candidate.items.length === 0)
|
|
100
109
|
continue;
|
|
101
110
|
const lastRowStart = Math.floor((candidate.items.length - 1) / columns) * columns;
|
|
102
111
|
const target = direction === "up" ? lastRowStart + col : col;
|
|
103
112
|
if (target >= candidate.items.length)
|
|
104
113
|
return currentIndex;
|
|
105
|
-
|
|
114
|
+
const targetCard = candidate.items[target];
|
|
115
|
+
return targetCard ? targetCard.globalIndex : currentIndex;
|
|
106
116
|
}
|
|
107
117
|
return currentIndex;
|
|
108
118
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
function anchorAt(group) {
|
|
2
|
+
return group.user?.createdAt ?? group.assistants[0]?.createdAt ?? 0;
|
|
3
|
+
}
|
|
4
|
+
function pendingAt(item) {
|
|
5
|
+
const at = Date.parse(item.updatedAt);
|
|
6
|
+
return Number.isFinite(at) ? at : Number.MAX_SAFE_INTEGER;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Merge pending (dispatching/queued) user steering into the message timeline
|
|
10
|
+
* by timestamp instead of always rendering at the very end. A pending message
|
|
11
|
+
* whose `updatedAt` precedes an already-rendered turn keeps its real slot; a
|
|
12
|
+
* stable sort preserves current group order for equal anchors (turns first).
|
|
13
|
+
*/
|
|
14
|
+
export function mergePendingIntoTimeline(input) {
|
|
15
|
+
const entries = [];
|
|
16
|
+
for (const group of input.turnGroups) {
|
|
17
|
+
entries.push({ kind: "group", group });
|
|
18
|
+
}
|
|
19
|
+
for (const item of input.pendingSteering) {
|
|
20
|
+
if (item.state !== "dispatching")
|
|
21
|
+
continue;
|
|
22
|
+
entries.push({ kind: "pending", item });
|
|
23
|
+
}
|
|
24
|
+
return entries.sort((a, b) => {
|
|
25
|
+
const atA = a.kind === "group" ? anchorAt(a.group) : pendingAt(a.item);
|
|
26
|
+
const atB = b.kind === "group" ? anchorAt(b.group) : pendingAt(b.item);
|
|
27
|
+
return atA - atB;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -180,7 +180,7 @@ function mergeCompactionMessages(messages, compactions) {
|
|
|
180
180
|
export function useChatSessions(params) {
|
|
181
181
|
const { origin, refs, setSession, thread, composer, browser, setError, setNotice, onCompactionChange, setProcessOpen, setAutoScroll, } = params;
|
|
182
182
|
const { applyActivatedSession, resetForNewSession, setMessages } = thread;
|
|
183
|
-
const {
|
|
183
|
+
const { seedSession, releaseSession } = composer;
|
|
184
184
|
const { setPreviews, setActivePreviewPath } = browser;
|
|
185
185
|
const { workspaceId, blocked } = params;
|
|
186
186
|
const workspaceQuery = workspaceId
|
|
@@ -198,7 +198,11 @@ export function useChatSessions(params) {
|
|
|
198
198
|
return owner ? `?workspaceId=${encodeURIComponent(owner)}` : "";
|
|
199
199
|
}, [refs]);
|
|
200
200
|
const [sessionList, setSessionList] = useState([]);
|
|
201
|
+
const [selectedSession, setSelectedSession] = useState(null);
|
|
202
|
+
const selectedSessionRef = useRef(null);
|
|
203
|
+
selectedSessionRef.current = selectedSession;
|
|
201
204
|
const sessionListRef = useRef([]);
|
|
205
|
+
const sessionListRequestVersionRef = useRef(0);
|
|
202
206
|
const pendingDeletedIdsRef = useRef(new Set());
|
|
203
207
|
const pendingCreatedRowsRef = useRef(new Map());
|
|
204
208
|
const sessionMutationVersionRef = useRef(0);
|
|
@@ -240,8 +244,6 @@ export function useChatSessions(params) {
|
|
|
240
244
|
refs.settledTurnIdsRef.current = new Set();
|
|
241
245
|
refs.activeTurnIdRef.current = null;
|
|
242
246
|
resetForNewSession();
|
|
243
|
-
setInput("");
|
|
244
|
-
setPendingImages([]);
|
|
245
247
|
setPreviews([]);
|
|
246
248
|
setActivePreviewPath(null);
|
|
247
249
|
setProcessOpen(false);
|
|
@@ -249,8 +251,6 @@ export function useChatSessions(params) {
|
|
|
249
251
|
}, [
|
|
250
252
|
refs,
|
|
251
253
|
resetForNewSession,
|
|
252
|
-
setInput,
|
|
253
|
-
setPendingImages,
|
|
254
254
|
setPreviews,
|
|
255
255
|
setActivePreviewPath,
|
|
256
256
|
setProcessOpen,
|
|
@@ -259,6 +259,7 @@ export function useChatSessions(params) {
|
|
|
259
259
|
const refreshSessionList = useCallback(async () => {
|
|
260
260
|
if (blocked)
|
|
261
261
|
return; // AC-R5-002: fail-closed, zero session requests
|
|
262
|
+
const version = ++sessionListRequestVersionRef.current;
|
|
262
263
|
try {
|
|
263
264
|
// The sidebar is a multi-workspace index. Keep one unscoped summary
|
|
264
265
|
// collection so expanding or switching a workspace never projects the
|
|
@@ -268,6 +269,8 @@ export function useChatSessions(params) {
|
|
|
268
269
|
credentials: "include",
|
|
269
270
|
});
|
|
270
271
|
const body = (await res.json().catch(() => ({})));
|
|
272
|
+
if (version !== sessionListRequestVersionRef.current)
|
|
273
|
+
return;
|
|
271
274
|
if (!res.ok) {
|
|
272
275
|
setError(body.error?.message ?? `会话列表加载失败(HTTP ${res.status})`);
|
|
273
276
|
return;
|
|
@@ -285,6 +288,8 @@ export function useChatSessions(params) {
|
|
|
285
288
|
}
|
|
286
289
|
}
|
|
287
290
|
catch (error) {
|
|
291
|
+
if (version !== sessionListRequestVersionRef.current)
|
|
292
|
+
return;
|
|
288
293
|
setError(`会话列表加载失败:${error instanceof Error ? error.message : String(error)}`);
|
|
289
294
|
}
|
|
290
295
|
}, [origin, blocked, setError]);
|
|
@@ -296,12 +301,22 @@ export function useChatSessions(params) {
|
|
|
296
301
|
const { next: removedList, removed, index: removedIndex } = removeSessionById(currentList, sessionId);
|
|
297
302
|
if (!removed || removedIndex < 0)
|
|
298
303
|
return false;
|
|
304
|
+
// No pre-mutation snapshot may overwrite this deletion or its rollback.
|
|
305
|
+
sessionListRequestVersionRef.current += 1;
|
|
299
306
|
pendingDeletedIdsRef.current.add(sessionId);
|
|
300
307
|
deleteRequestVersionsRef.current.set(sessionId, requestId);
|
|
301
308
|
const activeSession = refs.sessionRef.current?.sessionId === sessionId
|
|
302
309
|
? refs.sessionRef.current
|
|
303
310
|
: null;
|
|
304
|
-
const
|
|
311
|
+
const selectedForDelete = selectedSessionRef.current?.sessionId === sessionId
|
|
312
|
+
? selectedSessionRef.current
|
|
313
|
+
: null;
|
|
314
|
+
const deletingIdentity = selectedForDelete ??
|
|
315
|
+
activeSession ?? {
|
|
316
|
+
sessionId,
|
|
317
|
+
...(removed.workspaceId ? { workspaceId: removed.workspaceId } : {}),
|
|
318
|
+
};
|
|
319
|
+
const activeWorkspaceId = deletingIdentity.workspaceId ?? workspaceId;
|
|
305
320
|
const deletingRow = {
|
|
306
321
|
...removed,
|
|
307
322
|
optimisticState: "deleting",
|
|
@@ -320,16 +335,17 @@ export function useChatSessions(params) {
|
|
|
320
335
|
const restore = () => {
|
|
321
336
|
if (deleteRequestVersionsRef.current.get(sessionId) !== requestId)
|
|
322
337
|
return;
|
|
338
|
+
sessionListRequestVersionRef.current += 1;
|
|
323
339
|
pendingDeletedIdsRef.current.delete(sessionId);
|
|
324
340
|
deleteRequestVersionsRef.current.delete(sessionId);
|
|
325
|
-
const restored = restoreSessionAt(sessionListRef.current, removed, removedIndex);
|
|
341
|
+
const restored = restoreSessionAt(sessionListRef.current.filter((item) => item.sessionId !== sessionId), removed, removedIndex);
|
|
326
342
|
if (restored === sessionListRef.current)
|
|
327
343
|
return;
|
|
328
344
|
sessionListRef.current = restored;
|
|
329
345
|
setSessionList(restored);
|
|
330
346
|
};
|
|
331
347
|
let deletionSelectionVersion = null;
|
|
332
|
-
if (activeSession) {
|
|
348
|
+
if (activeSession || selectedForDelete) {
|
|
333
349
|
deletionSelectionVersion = ++sessionSelectionVersionRef.current;
|
|
334
350
|
refs.sessionRef.current = null;
|
|
335
351
|
resetActiveProjection();
|
|
@@ -337,17 +353,16 @@ export function useChatSessions(params) {
|
|
|
337
353
|
forgetActiveChatSession(window.localStorage, activeWorkspaceId, sessionId);
|
|
338
354
|
}
|
|
339
355
|
const restoreActiveSession = async () => {
|
|
340
|
-
if (activeSession &&
|
|
356
|
+
if ((activeSession || selectedForDelete) &&
|
|
341
357
|
deletionSelectionVersion === sessionSelectionVersionRef.current &&
|
|
342
358
|
!refs.sessionRef.current) {
|
|
343
|
-
await activateSessionRef.current?.(
|
|
359
|
+
await activateSessionRef.current?.(deletingIdentity.sessionId, deletingIdentity.workspaceId);
|
|
344
360
|
}
|
|
345
361
|
};
|
|
346
362
|
try {
|
|
347
363
|
// AC-002: delete follows the session's durable owner, not the
|
|
348
364
|
// global active workspace.
|
|
349
|
-
const ownerQuery = sessionOwnerQuery(
|
|
350
|
-
?.workspaceId ?? null);
|
|
365
|
+
const ownerQuery = sessionOwnerQuery(removed.workspaceId ?? null);
|
|
351
366
|
const res = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(sessionId)}${ownerQuery}`, {
|
|
352
367
|
method: "DELETE",
|
|
353
368
|
credentials: "include",
|
|
@@ -362,6 +377,13 @@ export function useChatSessions(params) {
|
|
|
362
377
|
return false;
|
|
363
378
|
}
|
|
364
379
|
titleWatcherRef.current?.cancel(sessionId);
|
|
380
|
+
const next = sessionListRef.current.filter((item) => item.sessionId !== sessionId);
|
|
381
|
+
sessionListRef.current = next;
|
|
382
|
+
setSessionList(next);
|
|
383
|
+
if (deletionSelectionVersion === sessionSelectionVersionRef.current &&
|
|
384
|
+
selectedSessionRef.current?.sessionId === sessionId)
|
|
385
|
+
setSelectedSession(null);
|
|
386
|
+
releaseSession(deletingIdentity);
|
|
365
387
|
void refreshSessionList().finally(() => {
|
|
366
388
|
pendingDeletedIdsRef.current.delete(sessionId);
|
|
367
389
|
deleteRequestVersionsRef.current.delete(sessionId);
|
|
@@ -379,6 +401,7 @@ export function useChatSessions(params) {
|
|
|
379
401
|
refs,
|
|
380
402
|
setSession,
|
|
381
403
|
resetActiveProjection,
|
|
404
|
+
releaseSession,
|
|
382
405
|
refreshSessionList,
|
|
383
406
|
setError,
|
|
384
407
|
sessionOwnerQuery,
|
|
@@ -388,25 +411,45 @@ export function useChatSessions(params) {
|
|
|
388
411
|
const active = refs.sessionRef.current;
|
|
389
412
|
const listed = sessionListRef.current.find((item) => item.sessionId === sessionId);
|
|
390
413
|
const scope = ownerWorkspaceId ?? listed?.workspaceId ?? active?.workspaceId ?? workspaceId;
|
|
414
|
+
const identity = {
|
|
415
|
+
sessionId,
|
|
416
|
+
...(scope ? { workspaceId: scope } : {}),
|
|
417
|
+
};
|
|
391
418
|
forgetActiveChatSession(window.localStorage, scope, sessionId);
|
|
419
|
+
releaseSession(identity);
|
|
392
420
|
pendingDeletedIdsRef.current.add(sessionId);
|
|
393
421
|
const { next } = removeSessionById(sessionListRef.current, sessionId);
|
|
394
422
|
sessionListRef.current = next;
|
|
395
423
|
setSessionList(next);
|
|
396
|
-
if (active?.sessionId === sessionId
|
|
424
|
+
if (active?.sessionId === sessionId ||
|
|
425
|
+
selectedSessionRef.current?.sessionId === sessionId) {
|
|
397
426
|
sessionSelectionVersionRef.current += 1;
|
|
398
427
|
refs.sessionRef.current = null;
|
|
428
|
+
setSelectedSession(null);
|
|
399
429
|
resetActiveProjection();
|
|
400
430
|
setSession(null);
|
|
401
431
|
}
|
|
402
432
|
void refreshSessionList();
|
|
403
|
-
}, [refs, resetActiveProjection, setSession, workspaceId, refreshSessionList]);
|
|
433
|
+
}, [refs, releaseSession, resetActiveProjection, setSession, workspaceId, refreshSessionList]);
|
|
404
434
|
const activateSession = useCallback(async (sessionId, ownerWorkspaceId) => {
|
|
405
435
|
const selectionVersion = ++sessionSelectionVersionRef.current;
|
|
436
|
+
const listedOwner = sessionListRef.current.find((item) => item.sessionId === sessionId)?.workspaceId;
|
|
437
|
+
const requestedOwner = ownerWorkspaceId ?? listedOwner;
|
|
438
|
+
const requested = {
|
|
439
|
+
sessionId,
|
|
440
|
+
...(requestedOwner ? { workspaceId: requestedOwner } : {}),
|
|
441
|
+
};
|
|
442
|
+
// DSH-style staging: selection identity changes synchronously; detail
|
|
443
|
+
// hydration follows. The previous session loses send authority now.
|
|
444
|
+
setSelectedSession(requested);
|
|
445
|
+
resetActiveProjection({ keepStreams: true });
|
|
446
|
+
refs.sessionRef.current = null;
|
|
447
|
+
setSession(null);
|
|
448
|
+
setError(null);
|
|
406
449
|
// AC-002: activation targets the session's durable owning workspace —
|
|
407
450
|
// never the global active workspace (no cross-workspace leak when
|
|
408
451
|
// Operate/Inspect switch their scope while a session stays open).
|
|
409
|
-
const ownerQuery = sessionOwnerQuery(
|
|
452
|
+
const ownerQuery = sessionOwnerQuery(requested.workspaceId);
|
|
410
453
|
try {
|
|
411
454
|
const res = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(sessionId)}${ownerQuery}`, { credentials: "include" });
|
|
412
455
|
const body = (await res.json());
|
|
@@ -427,7 +470,7 @@ export function useChatSessions(params) {
|
|
|
427
470
|
// AC-002: session switch (same or cross workspace) never aborts other
|
|
428
471
|
// sessions' background request controllers; the single /events owner is
|
|
429
472
|
// re-bound by useChatStream's session effect.
|
|
430
|
-
|
|
473
|
+
setSelectedSession(next);
|
|
431
474
|
refs.sessionRef.current = next;
|
|
432
475
|
refs.activeTurnIdRef.current = body.session.activeTurnId ?? null;
|
|
433
476
|
const visuallyStopped = body.session.turnPhase === "stopping";
|
|
@@ -447,9 +490,8 @@ export function useChatSessions(params) {
|
|
|
447
490
|
refs.assistantIdRef.current = null;
|
|
448
491
|
refs.abortRef.current =
|
|
449
492
|
refs.sessionAbortControllersRef.current.get(sessionId) ?? null;
|
|
450
|
-
setPendingImages([]);
|
|
451
493
|
setPreviews([]);
|
|
452
|
-
|
|
494
|
+
seedSession(next, body.session.composerDraft ?? "");
|
|
453
495
|
const hydrated = hydrateSessionThread(body.session.messages ?? []);
|
|
454
496
|
hydrated.messages = mergeCompactionMessages(hydrated.messages, body.compactions);
|
|
455
497
|
applyActivatedSession({
|
|
@@ -482,9 +524,8 @@ export function useChatSessions(params) {
|
|
|
482
524
|
origin,
|
|
483
525
|
refs,
|
|
484
526
|
setSession,
|
|
485
|
-
setPendingImages,
|
|
486
527
|
setPreviews,
|
|
487
|
-
|
|
528
|
+
seedSession,
|
|
488
529
|
applyActivatedSession,
|
|
489
530
|
resetActiveProjection,
|
|
490
531
|
setActivePreviewPath,
|
|
@@ -518,9 +559,7 @@ export function useChatSessions(params) {
|
|
|
518
559
|
}
|
|
519
560
|
const watchSessionTitle = useCallback((sessionId) => titleWatcherRef.current?.startIfEligible(sessionId), []);
|
|
520
561
|
useEffect(() => {
|
|
521
|
-
titleWatcherRef.current?.reconcile(sessionList
|
|
522
|
-
.filter((item) => item.state !== "archived" && !item.title)
|
|
523
|
-
.map((item) => item.sessionId));
|
|
562
|
+
titleWatcherRef.current?.reconcile(sessionList.flatMap((item) => item.state !== "archived" && !item.title ? [item.sessionId] : []));
|
|
524
563
|
}, [sessionList]);
|
|
525
564
|
useEffect(() => () => titleWatcherRef.current?.dispose(), []);
|
|
526
565
|
// Browser refresh/workspace-switch recovery: selection is client state keyed
|
|
@@ -537,15 +576,22 @@ export function useChatSessions(params) {
|
|
|
537
576
|
setSession(null);
|
|
538
577
|
const savedSessionId = readActiveChatSessionId(window.localStorage, workspaceId);
|
|
539
578
|
if (!savedSessionId) {
|
|
579
|
+
setSelectedSession(null);
|
|
540
580
|
// Keep the per-workspace map intact, but clear the legacy global pointer
|
|
541
581
|
// so Operate/Models cannot link a session from another workspace.
|
|
542
582
|
forgetActiveChatSession(window.localStorage, workspaceId);
|
|
543
583
|
return;
|
|
544
584
|
}
|
|
585
|
+
setSelectedSession({
|
|
586
|
+
sessionId: savedSessionId,
|
|
587
|
+
...(workspaceId ? { workspaceId } : {}),
|
|
588
|
+
});
|
|
545
589
|
void (async () => {
|
|
546
590
|
try {
|
|
547
591
|
const res = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(savedSessionId)}${workspaceQuery}`, { credentials: "include" });
|
|
548
592
|
if (!res.ok) {
|
|
593
|
+
if (selectionVersion === sessionSelectionVersionRef.current)
|
|
594
|
+
setSelectedSession(null);
|
|
549
595
|
forgetActiveChatSession(window.localStorage, workspaceId, savedSessionId);
|
|
550
596
|
return;
|
|
551
597
|
}
|
|
@@ -557,6 +603,7 @@ export function useChatSessions(params) {
|
|
|
557
603
|
return;
|
|
558
604
|
const restored = projectActiveChatSession(body.session, workspaceId);
|
|
559
605
|
refs.sessionGenerationRef.current += 1;
|
|
606
|
+
setSelectedSession(restored);
|
|
560
607
|
refs.sessionRef.current = restored;
|
|
561
608
|
// Do not seed Last-Event-ID here: session messages are restored
|
|
562
609
|
// from the durable store, but tool/gate/operation projections live
|
|
@@ -577,8 +624,8 @@ export function useChatSessions(params) {
|
|
|
577
624
|
refs.settledTurnIdsRef.current = new Set();
|
|
578
625
|
refs.lastEventIdRef.current = null;
|
|
579
626
|
setSession(restored);
|
|
627
|
+
seedSession(restored, body.session.composerDraft ?? "");
|
|
580
628
|
rememberActiveChatSession(window.localStorage, restored.workspaceId ?? workspaceId, restored.sessionId);
|
|
581
|
-
setInput(body.session.composerDraft ?? "");
|
|
582
629
|
const hydrated = hydrateSessionThread(body.session.messages ?? []);
|
|
583
630
|
hydrated.messages = mergeCompactionMessages(hydrated.messages, body.compactions);
|
|
584
631
|
applyActivatedSession({
|
|
@@ -610,7 +657,7 @@ export function useChatSessions(params) {
|
|
|
610
657
|
workspaceQuery,
|
|
611
658
|
refs,
|
|
612
659
|
setSession,
|
|
613
|
-
|
|
660
|
+
seedSession,
|
|
614
661
|
applyActivatedSession,
|
|
615
662
|
blocked,
|
|
616
663
|
resetActiveProjection,
|
|
@@ -622,7 +669,7 @@ export function useChatSessions(params) {
|
|
|
622
669
|
// either explicit (sidebar create-in-workspace) or the current valid
|
|
623
670
|
// selection. No scope → fail closed client-side (the server would reject
|
|
624
671
|
// with WORKSPACE_REQUIRED anyway); never a default-workspace fallback.
|
|
625
|
-
const createSessionAndPrimeTitleWatcherForHook = useCallback((options) => {
|
|
672
|
+
const createSessionAndPrimeTitleWatcherForHook = useCallback(async (options) => {
|
|
626
673
|
const scope = options?.workspaceId?.trim() ?? workspaceId?.trim();
|
|
627
674
|
if (!scope) {
|
|
628
675
|
setError("创建会话需要选择一个有效的工作区(不会回退默认工作区)");
|
|
@@ -644,25 +691,27 @@ export function useChatSessions(params) {
|
|
|
644
691
|
const optimisticList = insertOptimisticSession(sessionListRef.current, optimisticRow);
|
|
645
692
|
sessionListRef.current = optimisticList;
|
|
646
693
|
setSessionList(optimisticList);
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
694
|
+
try {
|
|
695
|
+
const created = await createSessionAndPrimeTitleWatcher({
|
|
696
|
+
origin,
|
|
697
|
+
sessionListRef,
|
|
698
|
+
setSessionList,
|
|
699
|
+
titleWatcher: titleWatcherRef.current,
|
|
700
|
+
refreshSessionList,
|
|
701
|
+
options: { ...options, workspaceId: scope, optimisticSessionId },
|
|
702
|
+
setCreatingSession,
|
|
703
|
+
creatingRef: creatingSessionRef,
|
|
704
|
+
});
|
|
657
705
|
pendingCreatedRowsRef.current.delete(optimisticSessionId);
|
|
658
706
|
return created;
|
|
659
|
-
}
|
|
707
|
+
}
|
|
708
|
+
catch (error) {
|
|
660
709
|
pendingCreatedRowsRef.current.delete(optimisticSessionId);
|
|
661
710
|
const cleaned = sessionListRef.current.filter((item) => item.sessionId !== optimisticSessionId);
|
|
662
711
|
sessionListRef.current = cleaned;
|
|
663
712
|
setSessionList(cleaned);
|
|
664
713
|
throw error;
|
|
665
|
-
}
|
|
714
|
+
}
|
|
666
715
|
}, [origin, refreshSessionList, workspaceId, setError]);
|
|
667
716
|
const createSession = useCallback(async (options) => {
|
|
668
717
|
// Suppress concurrent create from any entry (sidebar / Landing / chip).
|
|
@@ -708,6 +757,7 @@ export function useChatSessions(params) {
|
|
|
708
757
|
// createSession().then(sendPrompt) reads the new session even
|
|
709
758
|
// though sendPrompt's closure captured the stale (null) value.
|
|
710
759
|
refs.sessionRef.current = created;
|
|
760
|
+
setSelectedSession(created);
|
|
711
761
|
rememberActiveChatSession(window.localStorage, created.workspaceId ?? options?.workspaceId ?? workspaceId, created.sessionId);
|
|
712
762
|
setSession(created);
|
|
713
763
|
return created;
|
|
@@ -834,6 +884,7 @@ export function useChatSessions(params) {
|
|
|
834
884
|
const activeSessionId = refs.sessionRef.current.sessionId;
|
|
835
885
|
sessionSelectionVersionRef.current += 1;
|
|
836
886
|
refs.sessionRef.current = null;
|
|
887
|
+
setSelectedSession(null);
|
|
837
888
|
resetActiveProjection();
|
|
838
889
|
setSession(null);
|
|
839
890
|
forgetActiveChatSession(window.localStorage, activeWorkspaceId, activeSessionId);
|
|
@@ -841,6 +892,9 @@ export function useChatSessions(params) {
|
|
|
841
892
|
};
|
|
842
893
|
return {
|
|
843
894
|
sessionList,
|
|
895
|
+
selectedSession,
|
|
896
|
+
sessionSwitching: Boolean(selectedSession &&
|
|
897
|
+
refs.sessionRef.current?.sessionId !== selectedSession.sessionId),
|
|
844
898
|
creatingSession,
|
|
845
899
|
activateSession,
|
|
846
900
|
refreshSessionList,
|