@tea-agent/loop-agent 0.42.0-next.9 → 0.42.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/CHANGELOG.md +111 -45
- package/dist/application/dag/run-dag.js +8 -2
- package/dist/application/evaluation/budget.js +19 -1
- package/dist/application/task-lifecycle/advance.js +26 -8
- package/dist/application/task-lifecycle/observe.js +43 -29
- package/dist/application/task-lifecycle/plan-transitions.js +5 -4
- package/dist/build-stamp.json +3 -3
- package/dist/cli/program.js +1 -1
- package/dist/commands/dag-rerun-task.js +2 -0
- package/dist/commands/task-source-prepare.js +3 -1
- package/dist/executors/dag-pi-executor.js +2584 -600
- package/dist/executors/pi-executor.js +22 -1
- package/dist/executors/pi-extension-resolver.js +14 -2
- package/dist/executors/pi-sdk-executor.js +140 -39
- package/dist/executors/shell-executor.js +135 -55
- package/dist/shared/dag-failure-category.js +6 -0
- package/dist/shared/frontend-execution-policy.js +23 -0
- package/dist/task/config-types.js +4 -0
- package/dist/task/contract/apply.js +36 -2
- package/dist/task/source-prepare/ledger-reconciliation.js +2 -2
- package/dist/task/source-prepare/ledger-review.js +6 -9
- package/dist/task/source-prepare/parse-intent.js +7 -0
- package/dist/task/source-prepare/semantic-intake.js +16 -26
- package/dist/task/source-prepare/source-fidelity-pi.js +28 -9
- package/dist/task/source-references.js +48 -23
- package/dist/worker/console/chat/assistant-content.js +23 -2
- package/dist/worker/console/chat/browser-policy.js +143 -0
- package/dist/worker/console/chat/browser-routes.js +148 -0
- package/dist/worker/console/chat/chat-event-store.js +4 -2
- package/dist/worker/console/chat/explore-tools.js +13 -0
- package/dist/worker/console/chat/pi-runtime.js +173 -94
- package/dist/worker/console/chat/resource-loader.js +4 -1
- package/dist/worker/console/chat/routes.js +208 -66
- package/dist/worker/console/chat/scm-routes.js +217 -0
- package/dist/worker/console/chat/scm-service.js +283 -0
- package/dist/worker/console/chat/scm-tools.js +111 -0
- package/dist/worker/console/chat/sdd-data-alignment.js +78 -11
- package/dist/worker/console/chat/session-catalog.js +32 -0
- package/dist/worker/console/chat/session-mode-view.js +48 -0
- package/dist/worker/console/chat/session-mode.js +218 -0
- package/dist/worker/console/chat/session-store.js +27 -7
- package/dist/worker/console/chat/shortcuts.js +6 -0
- package/dist/worker/console/chat/subagents/agent-tool.js +62 -0
- package/dist/worker/console/chat/subagents/explore-agent.js +169 -0
- package/dist/worker/console/chat/subagents/index.js +5 -0
- package/dist/worker/console/chat/subagents/orchestrator.js +273 -0
- package/dist/worker/console/chat/subagents/tool-policy.js +53 -0
- package/dist/worker/console/chat/subagents/types.js +13 -0
- package/dist/worker/console/chat/terminal-routes.js +216 -0
- package/dist/worker/console/chat/terminal-sessions.js +284 -0
- package/dist/worker/console/chat/terminal-tools.js +199 -0
- package/dist/worker/console/chat/tool-preview.js +21 -0
- package/dist/worker/console/chat/tools.js +13 -1
- package/dist/worker/console/chat/turn-process.js +1 -0
- package/dist/worker/console/interview/tools.js +1 -0
- package/dist/worker/console/server.js +2 -28
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C6n9_m0P.js → abnfDiagram-N423BO3Z-C9eI7nEo.js} +1 -1
- package/dist/worker/console/static/assets/{arc-DQh-IfZ1.js → arc-VJWsxWhB.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-54NnrwUC.js → architectureDiagram-T3A2C74G-BGcyiMSo.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-pivRALGK.js → blockDiagram-VBNYF7ZC-BScnFwyi.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BR7OV2NJ.js → c4Diagram-5PPSVZJV-Bl9_BsLi.js} +1 -1
- package/dist/worker/console/static/assets/channel-B6sQYuOE.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-B1Aq6BcK.js → chunk-2GRJ4B5K-F1uKxiUt.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CbdU0rjo.js → chunk-2Q5K7J3B-rSRMynVu.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-Dqi2GJeD.js → chunk-5RXB4S5H-DAsJm1WD.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BYn1Hu4R.js → chunk-5VM5RSS4-DQIdVI0a.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DRLjDL0k.js → chunk-6Q2QTUOP-4A_8rJ-Q.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CY9Xx2jV.js → chunk-GF5L2VYU-BHnT-vjh.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BNCZs7_z.js → chunk-JWPE2WC7-Rq7QKBkn.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DZ8AStLL.js → chunk-KBJHAD2P-BXh-AI2u.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DsxjYrzz.js → chunk-RYQCIY6F-BblcLy8m.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Ddx1KC1l.js → chunk-XXDRQBXY-zHZmVnB6.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Z6s5QoeI.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Z6s5QoeI.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-W9TveCnK.js → cose-bilkent-JH36ORCC-Cmwz0Wi4.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-l9j_ztZH.js → cynefin-VYW2F7L2-Dq76MHQU.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BMJMKi4G.js → cynefinDiagram-MW4NZA55-zPahV1fM.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-Bb4mG9pH.js → dagre-VZM6K2ZE-DQJ3xPc_.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-BMgL_Qv0.js → diagram-7IWD3JNH-BwX7u6fv.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Bvo2T4OQ.js → diagram-B4RE2ZJO-CodnDCco.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-_5kWRN9c.js → diagram-LBJQPF4R-gAjtWkxG.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DqdMrltM.js → diagram-Q27KOJAE-CGCqz0cS.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-CDDYsNkp.js → diagram-UB23O5K3-RHwYEywd.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-c4nfnZUV.js → ebnfDiagram-BXEA7PRR-B6seb_p_.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DnkUcNMs.js → erDiagram-JOGREHBK-CmwCKYe8.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-Dz0KGLZE.js → flowDiagram-UKHOOZJN-BfNsaZ0V.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-Cj1t1uka.js → ganttDiagram-PKOTCBZU-zmYgi_Z3.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-tp2FrBHd.js → gitGraphDiagram-DS77QQ5N-BDEzzSKF.js} +1 -1
- package/dist/worker/console/static/assets/index-DgenUAfc.js +468 -0
- package/dist/worker/console/static/assets/index-aF4u-Y__.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DQwJS8DH.js → infoDiagram-6WML65LV-DeSz8l42.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-eimCQWD4.js → ishikawaDiagram-WSZJBQD7-B1LV29aH.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-Dd9Gbwgv.js → journeyDiagram-NVQOT4AX-CCw4W2oU.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Qve3cyft.js → kanban-definition-27J2QSJJ-BozaMoRJ.js} +1 -1
- package/dist/worker/console/static/assets/{linear-UXKSe36Z.js → linear-G1JR40SX.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-CWhj4JXN.js → mermaid.core-CCgPF0oA.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-yTcwf4SJ.js → mindmap-definition-FAOFIHXS-CEqnCHnD.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-_7qToaZP.js → pegDiagram-VL7TDLO6-DpmQPKuI.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CXmrKmDm.js → pieDiagram-7S7Q4E2Y-BP501AxR.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-hMmrJyaS.js → quadrantDiagram-CIZ2JOQS-D1dBhl_A.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BXw8tCBe.js → railroadDiagram-AXF67PYL-DgZDS5pY.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BmjgnLZl.js → requirementDiagram-LRYGKXZP-BzB3pyIq.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Bj77SPpN.js → sankeyDiagram-W5VNT64P-B9ZprFFP.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-B2FDVysL.js → sequenceDiagram-SI44F4Z6-CC2-_XrA.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-UChNY9ZZ.js → sizeCapture-X5ZJPWSS-UVtAUqgB.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-wbAEkbd7.js → stateDiagram-OKZ733FA-CWcS5JYG.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BooX8u1Q.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DCEkU8HR.js → swimlanes-SLNWSIFB-C4p-dn06.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D64_eqqE.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-BGp7H06U.js → timeline-definition-Z64GVDOM-Dnz52OHX.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DsCIQuWR.js → vennDiagram-T6HMQDX7-CzTE0Nfu.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-cRZLP4Xe.js → wardleyDiagram-T6FBY63Y-NAR9cjQQ.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CP0xLR9Y.js → xychartDiagram-ELKLHX3M-DVMk8413.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/chat-view-types.js +1 -1
- package/dist/worker/console/static-src/operator-chat/chat-link.js +94 -0
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +142 -7
- package/dist/worker/console/static-src/operator-chat/pending-user-message.js +44 -0
- package/dist/worker/console/static-src/operator-chat/process-label.js +41 -0
- package/dist/worker/console/static-src/operator-chat/tools-catalog.js +21 -2
- package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +13 -0
- package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +2 -0
- package/dist/worker/console/static-src/operator-chat/use-searchable-hidden.js +51 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +33 -18
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +48 -16
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +170 -7
- package/dist/worker/console/static-src/operator-chat/useComposer.js +13 -7
- package/dist/worker/console/static-src/operator-chat/user-turn-anchor-equality.js +21 -0
- package/dist/worker/console/static-src/shell/workspace-route.js +4 -0
- package/dist/worker/console/workspace-context.js +15 -1
- package/dist/worker/observe/node-transparency.js +81 -72
- package/dist/worker/observe/routes.js +20 -1
- package/dist/worker/observe/static/constants.js +22 -22
- package/dist/worker/observe/static/dag-context-reason-labels.js +19 -0
- package/dist/worker/observe/static/dag-history-labels.js +1 -0
- package/dist/worker/observe/static/dag-inspector-humanize.d.ts +16 -0
- package/dist/worker/observe/static/dag-inspector-humanize.js +342 -0
- package/dist/worker/observe/static/dag-node-purpose.js +10 -10
- package/dist/worker/observe/static/dom.js +20 -1
- package/dist/worker/observe/static/format-pool.d.ts +2 -0
- package/dist/worker/observe/static/format-pool.js +6 -0
- package/dist/worker/observe/static/format.js +7 -0
- package/dist/worker/observe/static/index.html +4 -4
- package/dist/worker/observe/static/inspect-workspace.js +34 -7
- package/dist/worker/observe/static/inspector-submission.js +32 -0
- package/dist/worker/observe/static/kpi.js +1 -0
- package/dist/worker/observe/static/prompt-restart-candidates.js +4 -2
- package/dist/worker/observe/static/relations.js +7 -5
- package/dist/worker/observe/static/router.js +13 -0
- package/dist/worker/observe/static/run-processing.js +2 -0
- package/dist/worker/observe/static/shell-chrome.js +36 -3
- package/dist/worker/observe/static/state.js +35 -2
- package/dist/worker/observe/static/styles.css +431 -39
- package/dist/worker/observe/static/task-failure-labels.d.ts +4 -0
- package/dist/worker/observe/static/task-failure-labels.js +67 -0
- package/dist/worker/observe/static/task-history.js +12 -0
- package/dist/worker/observe/static/views/batch.js +6 -13
- package/dist/worker/observe/static/views/dag-graph.js +51 -4
- package/dist/worker/observe/static/views/dag-inspector.js +1069 -426
- package/dist/worker/observe/static/views/dag-trajectory.js +3 -0
- package/dist/worker/observe/static/views/dag.d.ts +6 -0
- package/dist/worker/observe/static/views/dag.js +64 -22
- package/dist/worker/observe/static/views/dags.js +2 -0
- package/dist/worker/observe/static/views/dashboard.js +21 -12
- package/dist/worker/observe/static/views/failures.js +21 -11
- package/dist/worker/observe/static/views/feature.js +11 -29
- package/dist/worker/observe/static/views/pool.js +37 -28
- package/dist/worker/observe/static/views/run.js +48 -5
- package/dist/worker/observe/static/views/session-timeline.js +194 -243
- package/dist/worker/observe/static/views/task.js +81 -62
- package/dist/workflows/dag/backend-test-plan-protocol.js +104 -0
- package/dist/workflows/dag/budget-enforcement.js +53 -3
- package/dist/workflows/dag/dag-retry-schema.js +3 -0
- package/dist/workflows/dag/frontend-capacity.js +9 -0
- package/dist/workflows/dag/frontend-contract-facts.js +130 -0
- package/dist/workflows/dag/frontend-design-policy.js +103 -17
- package/dist/workflows/dag/frontend-durable-tools.js +193 -0
- package/dist/workflows/dag/frontend-execution-groups.js +24 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +423 -41
- package/dist/workflows/dag/frontend-input-projection.js +76 -0
- package/dist/workflows/dag/frontend-plan-completeness.js +186 -0
- package/dist/workflows/dag/frontend-plan-recovery-policy.js +18 -0
- package/dist/workflows/dag/frontend-plan-render.js +21 -3
- package/dist/workflows/dag/frontend-prewrite-gate.js +1 -1
- package/dist/workflows/dag/frontend-recovery-controller.js +32 -8
- package/dist/workflows/dag/frontend-recovery-lineage.js +13 -0
- package/dist/workflows/dag/frontend-recovery-plan.js +4 -1
- package/dist/workflows/dag/frontend-recovery-run.js +146 -16
- package/dist/workflows/dag/frontend-review-scopes.js +139 -0
- package/dist/workflows/dag/frontend-risk.js +2 -0
- package/dist/workflows/dag/frontend-session-budget.js +249 -0
- package/dist/workflows/dag/frontend-shadow-dual-write.js +37 -3
- package/dist/workflows/dag/frontend-shape-facts.js +12 -2
- package/dist/workflows/dag/frontend-shape.js +46 -7
- package/dist/workflows/dag/frontend-test-execution-evidence.js +49 -0
- package/dist/workflows/dag/frontend-typed-event-store.js +14 -0
- package/dist/workflows/dag/frontend-verification-trace.js +38 -4
- package/dist/workflows/dag/frontend-writer-admission.js +2 -2
- package/dist/workflows/dag/init-hybrid.js +70 -35
- package/dist/workflows/dag/node-execution.js +225 -226
- package/dist/workflows/dag/prompt.js +4 -0
- package/dist/workflows/dag/recovery-lease.js +106 -16
- package/dist/workflows/dag/rerun-feedback.js +1 -0
- package/dist/workflows/dag/rerun-plan.js +96 -4
- package/dist/workflows/dag/rerun-run.js +28 -6
- package/dist/workflows/dag/rerun-task.js +221 -18
- package/dist/workflows/dag/retry-policy.js +27 -10
- package/dist/workflows/dag/runner-exit-diagnostics.js +125 -0
- package/dist/workflows/dag/runner.js +246 -19
- package/dist/workflows/dag/structured-output-repair.js +4 -1
- package/dist/workflows/dag/types.js +10 -3
- package/dist/workflows/dag/validate.js +10 -8
- package/dist/workflows/dag/workspace-checkpoint.js +66 -0
- package/docs/operations/README.md +2 -0
- package/docs/templates/agent-dag.schema.json +2 -2
- package/docs/templates/backend-test-dag.json +6 -4
- package/docs/templates/frontend-design-contract.md +4 -4
- package/docs/templates/frontend-implementation-contract.schema.json +68 -4
- package/docs/templates/frontend-implementation-dag.json +5 -5
- package/harness.json +1 -1
- package/package.json +8 -6
- package/skills/frontend-contract/SKILL.md +2 -1
- package/skills/frontend-contract/references/contract-protocol.md +19 -3
- package/skills/frontend-design-review/SKILL.md +12 -11
- package/skills/frontend-plan/SKILL.md +22 -2
- package/skills/frontend-plan/references/decision-contract.md +114 -5
- package/skills/frontend-plan/references/design-decisions.md +32 -0
- package/skills/frontend-review/SKILL.md +10 -11
- package/skills/frontend-scout/references/scout-evidence.md +4 -0
- package/dist/worker/console/static/assets/channel-DsZxrgqe.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-CuToPeGV.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-CuToPeGV.js +0 -1
- package/dist/worker/console/static/assets/index-D9Sc0f0y.js +0 -449
- package/dist/worker/console/static/assets/index-DDQc5a50.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-yPoT19ft.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-VWdarchG.js +0 -8
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Insert the optimistic user bubble and assistant slot exactly once.
|
|
2
|
+
*
|
|
3
|
+
* The optimistic user row is the ONLY row carrying `pending: true`. Every
|
|
4
|
+
* rollback path deletes pending rows by clientRequestId; a durable row that
|
|
5
|
+
* already replaced it must survive (2026-09-08). */
|
|
6
|
+
export function appendPendingSubmission(messages, pending) {
|
|
7
|
+
if (messages.some((message) => message.clientRequestId === pending.clientRequestId))
|
|
8
|
+
return [...messages];
|
|
9
|
+
return [...messages, { ...pending.user, pending: true }, pending.assistant];
|
|
10
|
+
}
|
|
11
|
+
/** Merge a durable user message into its optimistic or snapshot row in place.
|
|
12
|
+
*
|
|
13
|
+
* The replacement drops the `pending` marker: the row is server truth from now
|
|
14
|
+
* on, so a later adopt/rejected/connect rollback for the same clientRequestId
|
|
15
|
+
* must not delete it. Matching the durable id also dedupes retained SSE replay
|
|
16
|
+
* against a session snapshot whose stored row predates clientRequestId. */
|
|
17
|
+
export function replacePendingUserMessage(messages, clientRequestId, durable) {
|
|
18
|
+
let replaced = false;
|
|
19
|
+
const next = [];
|
|
20
|
+
for (const message of messages) {
|
|
21
|
+
const matches = message.clientRequestId === clientRequestId || message.id === durable.id;
|
|
22
|
+
if (!matches) {
|
|
23
|
+
next.push(message);
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (replaced)
|
|
27
|
+
continue;
|
|
28
|
+
replaced = true;
|
|
29
|
+
const { pending: _pending, ...rest } = message;
|
|
30
|
+
next.push({ ...rest, ...durable, clientRequestId });
|
|
31
|
+
}
|
|
32
|
+
return replaced ? next : [...next, durable];
|
|
33
|
+
}
|
|
34
|
+
/** Keep an unconfirmed optimistic bubble when a recovery snapshot lacks it. */
|
|
35
|
+
export function mergePendingSubmissionOnHydration(canonical, pending) {
|
|
36
|
+
if (!pending || canonical.some((message) => message.clientRequestId === pending.clientRequestId))
|
|
37
|
+
return [...canonical];
|
|
38
|
+
return appendPendingSubmission(canonical, pending);
|
|
39
|
+
}
|
|
40
|
+
/** Drop only the unconfirmed optimistic rows for a submission. A durable row
|
|
41
|
+
* that already replaced the optimistic one is left untouched. */
|
|
42
|
+
export function removePendingSubmission(messages, clientRequestId) {
|
|
43
|
+
return messages.filter((message) => message.clientRequestId !== clientRequestId || message.pending !== true);
|
|
44
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { isSubagentTool } from "../../chat/tool-preview.js";
|
|
2
|
+
/**
|
|
3
|
+
* Count the collapsed-turn disclosure buckets exactly like DSH
|
|
4
|
+
* `TurnProcessNodeView`: tool calls, process messages and subagents, in that
|
|
5
|
+
* order. Built-in subagent delegations (`Agent` tool) count as subagents, not
|
|
6
|
+
* as ordinary tool calls (2026-09-08 alignment).
|
|
7
|
+
*
|
|
8
|
+
* @param items - ordered process nodes of one turn.
|
|
9
|
+
* @returns the three bucket counts.
|
|
10
|
+
*/
|
|
11
|
+
export function countTurnProcessItems(items) {
|
|
12
|
+
let toolCalls = 0;
|
|
13
|
+
let messages = 0;
|
|
14
|
+
let subagents = 0;
|
|
15
|
+
for (const item of items) {
|
|
16
|
+
if (item.kind === "tool") {
|
|
17
|
+
if (isSubagentTool(item.tool.toolName))
|
|
18
|
+
subagents += 1;
|
|
19
|
+
else
|
|
20
|
+
toolCalls += 1;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
messages += 1;
|
|
24
|
+
}
|
|
25
|
+
return { toolCalls, messages, subagents };
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Summary label for a completed turn's process disclosure.
|
|
29
|
+
*
|
|
30
|
+
* @param counts - bucket counts from {@link countTurnProcessItems}.
|
|
31
|
+
* @returns `N 次工具调用 · M 条消息 · K 个 subagent`, or「已思考」when empty.
|
|
32
|
+
*/
|
|
33
|
+
export function buildTurnProcessLabel(counts) {
|
|
34
|
+
return ([
|
|
35
|
+
counts.toolCalls > 0 ? `${counts.toolCalls} 次工具调用` : "",
|
|
36
|
+
counts.messages > 0 ? `${counts.messages} 条消息` : "",
|
|
37
|
+
counts.subagents > 0 ? `${counts.subagents} 个 subagent` : "",
|
|
38
|
+
]
|
|
39
|
+
.filter(Boolean)
|
|
40
|
+
.join(" · ") || "已思考");
|
|
41
|
+
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ADR 0011: full Pi tools + optional safe-* explore surface.
|
|
3
|
+
* The `Agent` (Explore) entry is injected from capabilities.builtinSubagents
|
|
4
|
+
* (ADR 0022 server truth); the static entry below is only a fallback for
|
|
5
|
+
* older backends that do not send the field yet.
|
|
3
6
|
*/
|
|
4
|
-
const
|
|
7
|
+
const STATIC_EXPLORE_TOOLS = [
|
|
5
8
|
{
|
|
6
9
|
toolId: "read",
|
|
7
10
|
kind: "explore",
|
|
@@ -58,6 +61,12 @@ const EXPLORE_TOOLS = [
|
|
|
58
61
|
description: "固定参数查看 git diff;敏感路径不可查看且结果自动脱敏。",
|
|
59
62
|
},
|
|
60
63
|
];
|
|
64
|
+
/** Fallback when the backend does not advertise builtinSubagents yet. */
|
|
65
|
+
const FALLBACK_AGENT_ENTRY = {
|
|
66
|
+
toolId: "Agent",
|
|
67
|
+
kind: "explore",
|
|
68
|
+
description: "启动内置只读 Explore 子 Agent,执行代码库搜索、符号查找和调用链追踪。",
|
|
69
|
+
};
|
|
61
70
|
export function buildToolCatalog(capabilities) {
|
|
62
71
|
if (!capabilities)
|
|
63
72
|
return null;
|
|
@@ -68,7 +77,17 @@ export function buildToolCatalog(capabilities) {
|
|
|
68
77
|
kind: t.kind ?? "read",
|
|
69
78
|
cli: t.cli,
|
|
70
79
|
}));
|
|
71
|
-
|
|
80
|
+
// ADR 0022: subagents come from the server's builtinSubagents field;
|
|
81
|
+
// static entries never duplicate a server-advertised toolId.
|
|
82
|
+
const subagentTools = (capabilities.builtinSubagents ?? []).map((s) => ({
|
|
83
|
+
toolId: s.toolId,
|
|
84
|
+
description: s.description,
|
|
85
|
+
kind: "explore",
|
|
86
|
+
}));
|
|
87
|
+
const staticTools = subagentTools.length > 0
|
|
88
|
+
? STATIC_EXPLORE_TOOLS
|
|
89
|
+
: [...STATIC_EXPLORE_TOOLS, FALLBACK_AGENT_ENTRY];
|
|
90
|
+
const explore = [...staticTools, ...subagentTools];
|
|
72
91
|
return {
|
|
73
92
|
explore,
|
|
74
93
|
operator: operatorTools,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
function sameReferences(previous, next) {
|
|
2
|
+
return (previous.length === next.length &&
|
|
3
|
+
previous.every((value, index) => value === next[index]));
|
|
4
|
+
}
|
|
5
|
+
/** A completed group's rendered projection is reusable only while every source
|
|
6
|
+
* object that feeds grouping and process-item derivation remains unchanged. */
|
|
7
|
+
export function sameTurnGroupInputs(previous, next) {
|
|
8
|
+
return (previous.key === next.key &&
|
|
9
|
+
previous.user === next.user &&
|
|
10
|
+
previous.live === next.live &&
|
|
11
|
+
sameReferences(previous.assistants, next.assistants) &&
|
|
12
|
+
sameReferences(previous.tools, next.tools));
|
|
13
|
+
}
|
|
@@ -224,6 +224,7 @@ export function applyAdoptActiveOwnership(bag, input) {
|
|
|
224
224
|
releaseSubmitOwnership(bag, input.pending.clientRequestId);
|
|
225
225
|
if (!input.current)
|
|
226
226
|
return;
|
|
227
|
+
input.ui?.removePendingMessage?.(input.pending.clientRequestId);
|
|
227
228
|
input.ui?.clearEmptyAssistant(input.assistantId);
|
|
228
229
|
input.ui?.setInput(input.text);
|
|
229
230
|
input.ui?.setPendingImages(input.images);
|
|
@@ -253,6 +254,7 @@ export function applyRejectedOwnership(bag, input) {
|
|
|
253
254
|
input.ui?.setStreaming(false);
|
|
254
255
|
input.ui?.setStreamingAssistantId(null);
|
|
255
256
|
input.ui?.clearEmptyAssistant(input.assistantId);
|
|
257
|
+
input.ui?.removePendingMessage?.(input.pending.clientRequestId);
|
|
256
258
|
input.ui?.setInput(input.text);
|
|
257
259
|
input.ui?.setPendingImages(input.images);
|
|
258
260
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { useEffect, useLayoutEffect, useRef } from "react";
|
|
2
|
+
/** Whether this browser can reveal `hidden="until-found"` content through
|
|
3
|
+
* `beforematch` (Chrome 102+, Firefox 148+, Safari: not supported at all).
|
|
4
|
+
* `content-visibility` support does NOT imply this — Safari 18+ and
|
|
5
|
+
* Firefox 125–147 have the former without the latter. */
|
|
6
|
+
function supportsUntilFound() {
|
|
7
|
+
return typeof document !== "undefined" && "onbeforematch" in document.body;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Collapse content without removing it from the document: the element gets
|
|
11
|
+
* `hidden="until-found"` so browser find-in-page can still reach the text, and
|
|
12
|
+
* a match fires `beforematch`, which reveals (expands) the disclosure again.
|
|
13
|
+
*
|
|
14
|
+
* Mirrors DSH `useSearchableHidden` (2026-09-08 alignment). Browsers without
|
|
15
|
+
* `until-found` support get a plain `hidden=""` element, which keeps the
|
|
16
|
+
* pre-alignment behavior (collapsed and not searchable).
|
|
17
|
+
*
|
|
18
|
+
* @param hidden - whether the content is collapsed.
|
|
19
|
+
* @param reveal - expand callback invoked on `beforematch` and when the
|
|
20
|
+
* collapsed subtree already owns focus.
|
|
21
|
+
* @returns ref for the collapsible container.
|
|
22
|
+
*/
|
|
23
|
+
export function useSearchableHidden(hidden, reveal) {
|
|
24
|
+
const ref = useRef(null);
|
|
25
|
+
useLayoutEffect(() => {
|
|
26
|
+
const element = ref.current;
|
|
27
|
+
if (element === null)
|
|
28
|
+
return;
|
|
29
|
+
// Focus would otherwise be lost inside a hidden subtree.
|
|
30
|
+
if (hidden && element.contains(element.ownerDocument.activeElement)) {
|
|
31
|
+
reveal();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (hidden) {
|
|
35
|
+
element.setAttribute("hidden", supportsUntilFound() ? "until-found" : "");
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
element.removeAttribute("hidden");
|
|
39
|
+
}
|
|
40
|
+
}, [hidden, reveal]);
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
const element = ref.current;
|
|
43
|
+
if (element === null)
|
|
44
|
+
return;
|
|
45
|
+
element.addEventListener("beforematch", reveal);
|
|
46
|
+
return () => {
|
|
47
|
+
element.removeEventListener("beforematch", reveal);
|
|
48
|
+
};
|
|
49
|
+
}, [reveal]);
|
|
50
|
+
return ref;
|
|
51
|
+
}
|
|
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
|
|
2
2
|
import { runtimeSelectionFromRecord } from "../../chat/runtime-selection.js";
|
|
3
3
|
import { hydrateSessionThread } from "../../chat/turn-process.js";
|
|
4
4
|
import { forgetActiveChatSession, readActiveChatSessionId, rememberActiveChatSession, } from "../../chat/workspace-landing.js";
|
|
5
|
+
import { DEFAULT_CONSOLE_CHAT_SESSION_MODE } from "../../chat/session-mode-view.js";
|
|
5
6
|
import { confirmationToken } from "./format.js";
|
|
6
7
|
import { isLostChatSessionError } from "./runtime-snapshot-store.js";
|
|
7
8
|
import { upsertCompactionMessage } from "./compaction-message.js";
|
|
@@ -67,6 +68,8 @@ function sameSessionSummary(a, b) {
|
|
|
67
68
|
a.model?.modelId === b.model?.modelId &&
|
|
68
69
|
a.thinkingLevel === b.thinkingLevel &&
|
|
69
70
|
a.isBlank === b.isBlank &&
|
|
71
|
+
(a.sessionMode ?? DEFAULT_CONSOLE_CHAT_SESSION_MODE) ===
|
|
72
|
+
(b.sessionMode ?? DEFAULT_CONSOLE_CHAT_SESSION_MODE) &&
|
|
70
73
|
a.optimisticState === b.optimisticState &&
|
|
71
74
|
sameQueueSummary(a, b));
|
|
72
75
|
}
|
|
@@ -83,8 +86,18 @@ export function stabilizeSessionList(previous, next) {
|
|
|
83
86
|
});
|
|
84
87
|
return changed ? stabilized : previous;
|
|
85
88
|
}
|
|
89
|
+
/** Project a session detail/list record into the active ChatSessionInfo. */
|
|
90
|
+
export function projectActiveChatSession(session, fallbackWorkspaceId) {
|
|
91
|
+
const workspaceId = session.workspaceId?.trim() || fallbackWorkspaceId?.trim() || "";
|
|
92
|
+
return {
|
|
93
|
+
sessionId: session.sessionId,
|
|
94
|
+
...(workspaceId ? { workspaceId } : {}),
|
|
95
|
+
sessionMode: session.sessionMode ?? DEFAULT_CONSOLE_CHAT_SESSION_MODE,
|
|
96
|
+
...runtimeSelectionFromRecord(session),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
86
99
|
/** Select a server-confirmed blank session owned by the requested workspace. */
|
|
87
|
-
export function findReusableBlankSession(list, workspaceId) {
|
|
100
|
+
export function findReusableBlankSession(list, workspaceId, sessionMode = DEFAULT_CONSOLE_CHAT_SESSION_MODE) {
|
|
88
101
|
const scope = workspaceId.trim();
|
|
89
102
|
if (!scope)
|
|
90
103
|
return undefined;
|
|
@@ -93,7 +106,8 @@ export function findReusableBlankSession(list, workspaceId) {
|
|
|
93
106
|
item.isBlank === true &&
|
|
94
107
|
item.turnPhase === "idle" &&
|
|
95
108
|
!item.activeTurnId &&
|
|
96
|
-
!item.optimisticState
|
|
109
|
+
!item.optimisticState &&
|
|
110
|
+
(item.sessionMode ?? DEFAULT_CONSOLE_CHAT_SESSION_MODE) === sessionMode);
|
|
97
111
|
}
|
|
98
112
|
export async function createSessionAndPrimeTitleWatcher(input) {
|
|
99
113
|
if (input.creatingRef?.current) {
|
|
@@ -127,7 +141,13 @@ export async function createSessionAndPrimeTitleWatcher(input) {
|
|
|
127
141
|
const body = (await res.json());
|
|
128
142
|
if (!body.session)
|
|
129
143
|
throw new Error("创建会话响应缺少 durable session");
|
|
130
|
-
const durable = {
|
|
144
|
+
const durable = {
|
|
145
|
+
...body.session,
|
|
146
|
+
isBlank: true,
|
|
147
|
+
sessionMode: body.session.sessionMode ??
|
|
148
|
+
input.options?.sessionMode ??
|
|
149
|
+
DEFAULT_CONSOLE_CHAT_SESSION_MODE,
|
|
150
|
+
};
|
|
131
151
|
const created = {
|
|
132
152
|
sessionId: durable.sessionId,
|
|
133
153
|
...(durable.workspaceId || input.options?.workspaceId
|
|
@@ -135,6 +155,7 @@ export async function createSessionAndPrimeTitleWatcher(input) {
|
|
|
135
155
|
workspaceId: durable.workspaceId ?? input.options?.workspaceId,
|
|
136
156
|
}
|
|
137
157
|
: {}),
|
|
158
|
+
sessionMode: durable.sessionMode,
|
|
138
159
|
...runtimeSelectionFromRecord(durable),
|
|
139
160
|
};
|
|
140
161
|
const nextList = replaceOptimisticSession(input.sessionListRef.current, input.options?.optimisticSessionId, durable);
|
|
@@ -402,11 +423,7 @@ export function useChatSessions(params) {
|
|
|
402
423
|
}
|
|
403
424
|
if (selectionVersion !== sessionSelectionVersionRef.current)
|
|
404
425
|
return false;
|
|
405
|
-
const next =
|
|
406
|
-
sessionId: body.session.sessionId,
|
|
407
|
-
...(body.session.workspaceId ? { workspaceId: body.session.workspaceId } : {}),
|
|
408
|
-
...runtimeSelectionFromRecord(body.session),
|
|
409
|
-
};
|
|
426
|
+
const next = projectActiveChatSession(body.session);
|
|
410
427
|
// AC-002: session switch (same or cross workspace) never aborts other
|
|
411
428
|
// sessions' background request controllers; the single /events owner is
|
|
412
429
|
// re-bound by useChatStream's session effect.
|
|
@@ -449,6 +466,7 @@ export function useChatSessions(params) {
|
|
|
449
466
|
pendingQuestion: body.session.pendingQuestion ?? null,
|
|
450
467
|
todos: body.session.todos ?? [],
|
|
451
468
|
goal: body.session.goal ?? null,
|
|
469
|
+
history: body.history,
|
|
452
470
|
});
|
|
453
471
|
rememberActiveChatSession(window.localStorage, body.session.workspaceId ?? ownerWorkspaceId ?? workspaceId, sessionId);
|
|
454
472
|
setActivePreviewPath(null);
|
|
@@ -537,15 +555,7 @@ export function useChatSessions(params) {
|
|
|
537
555
|
selectionVersion !== sessionSelectionVersionRef.current ||
|
|
538
556
|
!body.session)
|
|
539
557
|
return;
|
|
540
|
-
const restored =
|
|
541
|
-
sessionId: body.session.sessionId,
|
|
542
|
-
...(body.session.workspaceId
|
|
543
|
-
? { workspaceId: body.session.workspaceId }
|
|
544
|
-
: workspaceId
|
|
545
|
-
? { workspaceId }
|
|
546
|
-
: {}),
|
|
547
|
-
...runtimeSelectionFromRecord(body.session),
|
|
548
|
-
};
|
|
558
|
+
const restored = projectActiveChatSession(body.session, workspaceId);
|
|
549
559
|
refs.sessionGenerationRef.current += 1;
|
|
550
560
|
refs.sessionRef.current = restored;
|
|
551
561
|
// Do not seed Last-Event-ID here: session messages are restored
|
|
@@ -585,6 +595,7 @@ export function useChatSessions(params) {
|
|
|
585
595
|
pendingQuestion: body.session.pendingQuestion ?? null,
|
|
586
596
|
todos: body.session.todos ?? [],
|
|
587
597
|
goal: body.session.goal ?? null,
|
|
598
|
+
history: body.history,
|
|
588
599
|
});
|
|
589
600
|
}
|
|
590
601
|
catch {
|
|
@@ -617,6 +628,7 @@ export function useChatSessions(params) {
|
|
|
617
628
|
setError("创建会话需要选择一个有效的工作区(不会回退默认工作区)");
|
|
618
629
|
return Promise.reject(new Error("WORKSPACE_REQUIRED"));
|
|
619
630
|
}
|
|
631
|
+
const sessionMode = options?.sessionMode ?? DEFAULT_CONSOLE_CHAT_SESSION_MODE;
|
|
620
632
|
const optimisticSessionId = `optimistic-chat-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
621
633
|
const now = new Date().toISOString();
|
|
622
634
|
const optimisticRow = {
|
|
@@ -625,6 +637,7 @@ export function useChatSessions(params) {
|
|
|
625
637
|
state: "active",
|
|
626
638
|
updatedAt: now,
|
|
627
639
|
turnPhase: "idle",
|
|
640
|
+
sessionMode,
|
|
628
641
|
optimisticState: "creating",
|
|
629
642
|
};
|
|
630
643
|
pendingCreatedRowsRef.current.set(optimisticSessionId, optimisticRow);
|
|
@@ -667,9 +680,10 @@ export function useChatSessions(params) {
|
|
|
667
680
|
}
|
|
668
681
|
// A caller-provided snapshot is an explicit new-session boundary; reusing
|
|
669
682
|
// an existing shell would silently drop that context suffix.
|
|
683
|
+
const sessionMode = options?.sessionMode ?? DEFAULT_CONSOLE_CHAT_SESSION_MODE;
|
|
670
684
|
const reusable = options?.snapshot
|
|
671
685
|
? undefined
|
|
672
|
-
: findReusableBlankSession(sessionListRef.current, scope);
|
|
686
|
+
: findReusableBlankSession(sessionListRef.current, scope, sessionMode);
|
|
673
687
|
if (reusable) {
|
|
674
688
|
if (previousSession?.sessionId === reusable.sessionId)
|
|
675
689
|
return previousSession;
|
|
@@ -679,6 +693,7 @@ export function useChatSessions(params) {
|
|
|
679
693
|
return {
|
|
680
694
|
sessionId: reusable.sessionId,
|
|
681
695
|
...(reusable.workspaceId ? { workspaceId: reusable.workspaceId } : {}),
|
|
696
|
+
sessionMode: reusable.sessionMode ?? sessionMode,
|
|
682
697
|
...runtimeSelectionFromRecord(reusable),
|
|
683
698
|
};
|
|
684
699
|
}
|
|
@@ -3,6 +3,7 @@ import { formatOperatorUserError } from "../../operator-user-error.js";
|
|
|
3
3
|
import { confirmationToken, requestId, sleep } from "./format.js";
|
|
4
4
|
import { classifyCreateTurnNetworkFailure, classifyCreateTurnResponse, createPendingSubmission, decideSubmissionRoute, isRuntimeBusyUnion, isSubmissionCurrent, isTerminalTurnState, keepAmbiguousAfterRetry, reconcileDeferredSubmissionFromState, reconcilePendingSubmissionFromState, resolveAmbiguousFromState, } from "./turn-submission.js";
|
|
5
5
|
import { applyAbortDecision, applyPendingReconcileDecision, classifyAbortResponse, markStopRequested, operatorChatAbortUrl, operatorChatEventsUrl, operatorChatStateUrl, operatorChatTurnsUrl, releaseOwnershipOnTerminalEvidence, releaseSubmitOwnership as releaseOwnershipBag, requestStopOnOwnership, settleCreateTurnOutcome, turnPhaseFromOwnership, visualTurnPhaseFromOwnership, } from "./turn-stream-controller.js";
|
|
6
|
+
import { appendPendingSubmission, removePendingSubmission } from "./pending-user-message.js";
|
|
6
7
|
/** Project ChatRefs into the executable ownership bag used by the controller. */
|
|
7
8
|
function ownershipBagFromRefs(refs) {
|
|
8
9
|
return {
|
|
@@ -52,7 +53,11 @@ function clearEmptyAssistant(setMessages, assistantId) {
|
|
|
52
53
|
return;
|
|
53
54
|
setMessages((messages) => messages.filter((message) => !(message.id === assistantId &&
|
|
54
55
|
message.role === "assistant" &&
|
|
55
|
-
!message.text.trim()
|
|
56
|
+
!message.text.trim() &&
|
|
57
|
+
// A row carrying only thinking/tool blocks is real content. Deleting
|
|
58
|
+
// it here dropped the turn's process nodes while the settled path in
|
|
59
|
+
// chat-sse-events deliberately keeps them (2026-09-08).
|
|
60
|
+
!(message.content && message.content.length > 0))));
|
|
56
61
|
}
|
|
57
62
|
/** One GET /events stream owns all Chat events. Sending a prompt creates a
|
|
58
63
|
* detached Turn resource only; it never opens a second inline SSE consumer. */
|
|
@@ -139,7 +144,10 @@ export function useChatStream(params) {
|
|
|
139
144
|
current.generation === generation)
|
|
140
145
|
current.ready = true;
|
|
141
146
|
replay = false;
|
|
142
|
-
|
|
147
|
+
// Fall through: the applier owns the buffered-replay flush and
|
|
148
|
+
// returns immediately for this event. `continue` here silently
|
|
149
|
+
// dropped that flush, so a reconnect that landed after the
|
|
150
|
+
// turn settled never restored the answer (2026-09-08).
|
|
143
151
|
}
|
|
144
152
|
const assistantId = refs.assistantIdRef.current ?? fallbackAssistantId;
|
|
145
153
|
const eventId = applySseBlock(block, assistantId, {
|
|
@@ -364,6 +372,7 @@ export function useChatStream(params) {
|
|
|
364
372
|
setPendingImages: setPendingImagesUi,
|
|
365
373
|
setStreaming,
|
|
366
374
|
setStreamingAssistantId,
|
|
375
|
+
removePendingMessage: (requestId) => setMessages((messages) => removePendingSubmission(messages, requestId)),
|
|
367
376
|
},
|
|
368
377
|
rejectedUi: {
|
|
369
378
|
setError,
|
|
@@ -373,6 +382,7 @@ export function useChatStream(params) {
|
|
|
373
382
|
setInput,
|
|
374
383
|
setPendingImages: setPendingImagesUi,
|
|
375
384
|
formatError: formatOperatorUserError,
|
|
385
|
+
removePendingMessage: (requestId) => setMessages((messages) => removePendingSubmission(messages, requestId)),
|
|
376
386
|
},
|
|
377
387
|
// AC-R5-001: durable stopRequested + accepted/adopt yields one abort.
|
|
378
388
|
onAutoAbort: (turnId) => {
|
|
@@ -669,6 +679,29 @@ export function useChatStream(params) {
|
|
|
669
679
|
userMessageId,
|
|
670
680
|
});
|
|
671
681
|
refs.pendingSubmissionRef.current = pending;
|
|
682
|
+
const userMsg = {
|
|
683
|
+
id: userMessageId,
|
|
684
|
+
role: "user",
|
|
685
|
+
text: draftText,
|
|
686
|
+
createdAt: Date.now(),
|
|
687
|
+
clientRequestId,
|
|
688
|
+
};
|
|
689
|
+
const assistantMsg = {
|
|
690
|
+
id: assistantId,
|
|
691
|
+
role: "assistant",
|
|
692
|
+
text: "",
|
|
693
|
+
createdAt: Date.now(),
|
|
694
|
+
};
|
|
695
|
+
// React must receive the optimistic projection before the first await so
|
|
696
|
+
// SSE readiness latency cannot hide the user's submitted message.
|
|
697
|
+
setMessages((messages) => appendPendingSubmission(messages, {
|
|
698
|
+
clientRequestId,
|
|
699
|
+
user: userMsg,
|
|
700
|
+
assistant: assistantMsg,
|
|
701
|
+
}));
|
|
702
|
+
setInput("");
|
|
703
|
+
setAutoScroll(true);
|
|
704
|
+
setError(null);
|
|
672
705
|
try {
|
|
673
706
|
await ensureEventsConnected(activeSession.sessionId, generation);
|
|
674
707
|
}
|
|
@@ -678,28 +711,27 @@ export function useChatStream(params) {
|
|
|
678
711
|
setError(formatOperatorUserError(error instanceof Error ? error.message : String(error)));
|
|
679
712
|
}
|
|
680
713
|
releaseSubmitOwnership(clientRequestId);
|
|
714
|
+
setMessages((messages) => removePendingSubmission(messages, clientRequestId));
|
|
715
|
+
clearEmptyAssistant(setMessages, assistantId);
|
|
716
|
+
setInput(draftText);
|
|
717
|
+
setPendingImages(draftImages);
|
|
681
718
|
return;
|
|
682
719
|
}
|
|
683
720
|
if (refs.sessionRef.current?.sessionId !== activeSession.sessionId ||
|
|
684
721
|
refs.sessionGenerationRef.current !== generation) {
|
|
685
722
|
releaseSubmitOwnership(clientRequestId);
|
|
723
|
+
if (refs.sessionRef.current?.sessionId === activeSession.sessionId) {
|
|
724
|
+
// Same session, newer generation: the activation snapshot replaced
|
|
725
|
+
// the thread, so the optimistic row is gone. Restore the draft
|
|
726
|
+
// instead of losing the user's text silently (2026-09-08).
|
|
727
|
+
setMessages((messages) => removePendingSubmission(messages, clientRequestId));
|
|
728
|
+
clearEmptyAssistant(setMessages, assistantId);
|
|
729
|
+
setInput(draftText);
|
|
730
|
+
setPendingImages(draftImages);
|
|
731
|
+
}
|
|
686
732
|
return;
|
|
687
733
|
}
|
|
688
|
-
const userMsg = {
|
|
689
|
-
id: userMessageId,
|
|
690
|
-
role: "user",
|
|
691
|
-
text: draftText,
|
|
692
|
-
createdAt: Date.now(),
|
|
693
|
-
};
|
|
694
|
-
setMessages((messages) => [
|
|
695
|
-
...messages,
|
|
696
|
-
userMsg,
|
|
697
|
-
{ id: assistantId, role: "assistant", text: "", createdAt: Date.now() },
|
|
698
|
-
]);
|
|
699
|
-
setInput("");
|
|
700
734
|
// Attachments stay until accepted (AC/UIS-accepted).
|
|
701
|
-
setAutoScroll(true);
|
|
702
|
-
setError(null);
|
|
703
735
|
refs.streamingRef.current = true;
|
|
704
736
|
setTurnPhase("running");
|
|
705
737
|
refs.assistantIdRef.current = assistantId;
|