@tea-agent/loop-agent 0.43.0-next.1 → 0.43.0-next.11
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 +144 -9
- package/dist/application/dag/run-dag.js +5 -0
- package/dist/application/evaluation/budget.js +19 -1
- package/dist/application/task-lifecycle/advance.js +17 -4
- 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/executors/dag-pi-executor.js +1237 -586
- package/dist/executors/pi-executor.js +22 -1
- package/dist/executors/pi-sdk-executor.js +140 -39
- package/dist/executors/shell-executor.js +8 -4
- package/dist/shared/frontend-execution-policy.js +23 -0
- package/dist/task/config-types.js +4 -0
- 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/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 +113 -0
- package/dist/worker/console/chat/browser-routes.js +140 -0
- package/dist/worker/console/chat/explore-tools.js +13 -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/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 +86 -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/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 +207 -1178
- package/dist/worker/console/chat/routes.js +208 -71
- 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 +65 -9
- package/dist/worker/console/chat/session-catalog.js +32 -0
- package/dist/worker/console/chat/session-mode-view.js +49 -0
- package/dist/worker/console/chat/session-mode.js +219 -0
- package/dist/worker/console/chat/session-store.js +22 -6
- package/dist/worker/console/chat/shortcuts.js +6 -0
- package/dist/worker/console/chat/terminal-routes.js +216 -0
- package/dist/worker/console/chat/terminal-sessions.js +348 -0
- package/dist/worker/console/chat/terminal-tools.js +211 -0
- package/dist/worker/console/chat/tool-preview.js +11 -0
- package/dist/worker/console/chat/tools.js +2 -0
- package/dist/worker/console/server.js +2 -28
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-B0Q0nClf.js → abnfDiagram-N423BO3Z-CTSK0xbU.js} +1 -1
- package/dist/worker/console/static/assets/{arc-DCPjC19G.js → arc-_2MfctFP.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CP5n9jmG.js → architectureDiagram-T3A2C74G-DsDe2wTg.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CmaWi_Wg.js → blockDiagram-VBNYF7ZC-Dtz_779U.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-Ct5tcMfZ.js → c4Diagram-5PPSVZJV-LaULi-gw.js} +1 -1
- package/dist/worker/console/static/assets/channel-CqS4yBWT.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-C0osm1Zf.js → chunk-2GRJ4B5K-DuHBqqX8.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CDWviED7.js → chunk-2Q5K7J3B-BZgZs1dn.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-mtSk1-j7.js → chunk-5RXB4S5H-tzwmlIO_.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-D5J9PHC7.js → chunk-5VM5RSS4-CeYrLkSn.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Begg4WAa.js → chunk-6Q2QTUOP-C92cgilm.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-sF1AEy7T.js → chunk-GF5L2VYU-XOXpGC0F.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BqbsWXZb.js → chunk-JWPE2WC7-CGHbJyCH.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BKJrPcJ6.js → chunk-KBJHAD2P-Bs2-LTzf.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CcwNMRho.js → chunk-RYQCIY6F-rN7up7E4.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BEJi3iIs.js → chunk-XXDRQBXY-B_fWjKpR.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DVOxNOq0.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DVOxNOq0.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-DRLgTvVu.js → cose-bilkent-JH36ORCC-DE3SdJvB.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BoAVG3cs.js → cynefin-VYW2F7L2-B-4yuRAX.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bzol06hR.js → cynefinDiagram-MW4NZA55-TiCPFuO8.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-UmliJM77.js → dagre-VZM6K2ZE-7crzLMzA.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-BWLcJkFo.js → diagram-7IWD3JNH-Cvr-x6Fd.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CgApOm-O.js → diagram-B4RE2ZJO-BzcRHUjH.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-Bh3RgTLs.js → diagram-LBJQPF4R-BjkAXilh.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-Dsh-D5nE.js → diagram-Q27KOJAE-BkzyATK7.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-Dkbbpcpb.js → diagram-UB23O5K3-rZuTarYP.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-3EBaA3t3.js → ebnfDiagram-BXEA7PRR-CAYENaN1.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-Bq2Dc5ok.js → erDiagram-JOGREHBK-DCEam0Uq.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-De7y55ha.js → flowDiagram-UKHOOZJN-e_dH4wVO.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-fLiDbQRh.js → ganttDiagram-PKOTCBZU-CvZa2zOK.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Du617mp1.js → gitGraphDiagram-DS77QQ5N-CbT5TuKp.js} +1 -1
- package/dist/worker/console/static/assets/index-CUPeoE8W.js +468 -0
- package/dist/worker/console/static/assets/index-SAANIK8g.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BwmlF-XP.js → infoDiagram-6WML65LV-BWzHQa_Z.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-zsGtS59u.js → ishikawaDiagram-WSZJBQD7-BTjlovtU.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DTTPTe3f.js → journeyDiagram-NVQOT4AX-DkvQoCc1.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BkXg9aP-.js → kanban-definition-27J2QSJJ-CHSzGi3W.js} +1 -1
- package/dist/worker/console/static/assets/{linear-7U2ue5IE.js → linear-DZMkTCWH.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-BUuGHmWO.js → mermaid.core-BIMhty7A.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-7q6fX0Sv.js → mindmap-definition-FAOFIHXS-Dx-tyMl3.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CkQPJN55.js → pegDiagram-VL7TDLO6-C6G34rbY.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CCal_tfX.js → pieDiagram-7S7Q4E2Y-CHK9XOfm.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-B7bpxyAZ.js → quadrantDiagram-CIZ2JOQS-CDARkI0w.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-CJmww7D-.js → railroadDiagram-AXF67PYL-DiuUe0ZQ.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D6ldWJEv.js → requirementDiagram-LRYGKXZP-RE61UR56.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-VGh33I9e.js → sankeyDiagram-W5VNT64P-KDNLHMEG.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-CCkDrIjU.js → sequenceDiagram-SI44F4Z6-CeVgXnD9.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-NC8f5Otb.js → sizeCapture-X5ZJPWSS-CXwGyjjG.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-_92ezZdF.js → stateDiagram-OKZ733FA-BjTfozab.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BXoYsyr2.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BdUCGtbP.js → swimlanes-SLNWSIFB-BaXOB8GT.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DfKliS4e.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CdD1H8ia.js → timeline-definition-Z64GVDOM-C3r2hqUU.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-Dkdk3oFo.js → vennDiagram-T6HMQDX7-MWKgl1S9.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CKu2uPPY.js → wardleyDiagram-T6FBY63Y-C9iFcZPk.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-ClqAnuG8.js → xychartDiagram-ELKLHX3M-DqnM24Jp.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/active-turn-tail.js +10 -0
- 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 +122 -10
- package/dist/worker/console/static-src/operator-chat/compaction-message.js +7 -0
- package/dist/worker/console/static-src/operator-chat/pending-user-message.js +27 -15
- package/dist/worker/console/static-src/operator-chat/process-label.js +41 -0
- package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +13 -0
- package/dist/worker/console/static-src/operator-chat/turn-process-disclosure.js +10 -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 +44 -24
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +18 -2
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +41 -8
- package/dist/worker/console/static-src/operator-chat/useComposer.js +26 -11
- package/dist/worker/console/static-src/operator-chat/user-turn-anchor-equality.js +21 -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/dag-inspector-humanize.js +3 -0
- 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/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/relations.js +2 -0
- 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 +263 -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 +50 -3
- package/dist/worker/observe/static/views/dag-inspector.js +742 -268
- 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 +49 -11
- 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 +189 -240
- package/dist/worker/observe/static/views/task.js +81 -62
- package/dist/workflows/dag/budget-enforcement.js +53 -3
- package/dist/workflows/dag/frontend-capacity.js +9 -0
- package/dist/workflows/dag/frontend-closeout.js +1 -1
- package/dist/workflows/dag/frontend-design-policy.js +2 -1
- 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 +13 -0
- 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 +10 -3
- package/dist/workflows/dag/frontend-prewrite-gate.js +3 -3
- package/dist/workflows/dag/frontend-recovery-controller.js +7 -7
- package/dist/workflows/dag/frontend-recovery-lineage.js +13 -0
- package/dist/workflows/dag/frontend-recovery-run.js +4 -0
- package/dist/workflows/dag/frontend-review-context.js +2 -1
- package/dist/workflows/dag/frontend-review-scopes.js +139 -0
- package/dist/workflows/dag/frontend-session-budget.js +249 -0
- package/dist/workflows/dag/frontend-shadow-dual-write.js +20 -2
- package/dist/workflows/dag/frontend-shape-facts.js +12 -2
- package/dist/workflows/dag/frontend-shape.js +30 -1
- package/dist/workflows/dag/frontend-test-execution-evidence.js +3 -2
- package/dist/workflows/dag/frontend-typed-event-store.js +11 -0
- package/dist/workflows/dag/frontend-verification-trace.js +1 -1
- package/dist/workflows/dag/init-hybrid.js +65 -167
- package/dist/workflows/dag/node-execution.js +38 -155
- package/dist/workflows/dag/prompt.js +4 -0
- package/dist/workflows/dag/rerun-plan.js +7 -1
- package/dist/workflows/dag/runner.js +26 -1
- package/dist/workflows/dag/types.js +6 -0
- package/docs/operations/README.md +1 -0
- package/docs/templates/frontend-design-contract.md +4 -4
- package/docs/templates/frontend-implementation-contract.schema.json +34 -2
- package/docs/templates/frontend-implementation-dag.json +14 -13
- package/package.json +6 -3
- package/skills/frontend-bounded-implement/SKILL.md +3 -3
- package/skills/frontend-bounded-implement/references/code-standards.md +3 -3
- package/skills/frontend-contract/SKILL.md +2 -1
- package/skills/frontend-contract/references/contract-protocol.md +22 -4
- package/skills/frontend-design-review/SKILL.md +22 -16
- package/skills/frontend-design-review/references/review-checklist.md +7 -7
- package/skills/frontend-plan/SKILL.md +21 -2
- package/skills/frontend-plan/references/decision-contract.md +42 -23
- package/skills/frontend-review/SKILL.md +22 -29
- package/skills/frontend-review/references/review-findings.md +24 -43
- package/skills/frontend-scout/references/scout-evidence.md +4 -0
- package/dist/worker/console/static/assets/channel-DAS07MdS.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Cwk-5jiW.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Cwk-5jiW.js +0 -1
- package/dist/worker/console/static/assets/index-C0O48S_P.js +0 -449
- package/dist/worker/console/static/assets/index-CzKf4U8P.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DyJg5gzW.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B1cw_0Uy.js +0 -8
|
@@ -1,25 +1,35 @@
|
|
|
1
|
-
/** Insert the optimistic user bubble and assistant slot exactly once.
|
|
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). */
|
|
2
6
|
export function appendPendingSubmission(messages, pending) {
|
|
3
7
|
if (messages.some((message) => message.clientRequestId === pending.clientRequestId))
|
|
4
8
|
return [...messages];
|
|
5
|
-
return [...messages, pending.user, pending.assistant];
|
|
9
|
+
return [...messages, { ...pending.user, pending: true }, pending.assistant];
|
|
6
10
|
}
|
|
7
|
-
/**
|
|
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. */
|
|
8
17
|
export function replacePendingUserMessage(messages, clientRequestId, durable) {
|
|
9
18
|
let replaced = false;
|
|
10
|
-
const next =
|
|
11
|
-
|
|
12
|
-
|
|
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
|
+
}
|
|
13
26
|
if (replaced)
|
|
14
|
-
|
|
27
|
+
continue;
|
|
15
28
|
replaced = true;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
? { ...message, ...durable, clientRequestId }
|
|
21
|
-
: message)
|
|
22
|
-
: [...next, durable];
|
|
29
|
+
const { pending: _pending, ...rest } = message;
|
|
30
|
+
next.push({ ...rest, ...durable, clientRequestId });
|
|
31
|
+
}
|
|
32
|
+
return replaced ? next : [...next, durable];
|
|
23
33
|
}
|
|
24
34
|
/** Keep an unconfirmed optimistic bubble when a recovery snapshot lacks it. */
|
|
25
35
|
export function mergePendingSubmissionOnHydration(canonical, pending) {
|
|
@@ -27,6 +37,8 @@ export function mergePendingSubmissionOnHydration(canonical, pending) {
|
|
|
27
37
|
return [...canonical];
|
|
28
38
|
return appendPendingSubmission(canonical, pending);
|
|
29
39
|
}
|
|
40
|
+
/** Drop only the unconfirmed optimistic rows for a submission. A durable row
|
|
41
|
+
* that already replaced the optimistic one is left untouched. */
|
|
30
42
|
export function removePendingSubmission(messages, clientRequestId) {
|
|
31
|
-
return messages.filter((message) => message.clientRequestId !== clientRequestId);
|
|
43
|
+
return messages.filter((message) => message.clientRequestId !== clientRequestId || message.pending !== true);
|
|
32
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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function turnProcessDisclosureKey(groupKey, answerId) {
|
|
2
|
+
return `${groupKey}:generation-${answerId ?? "none"}`;
|
|
3
|
+
}
|
|
4
|
+
export function isTurnProcessCompact(input) {
|
|
5
|
+
return (!input.historyHasMore &&
|
|
6
|
+
!input.live &&
|
|
7
|
+
!input.hasError &&
|
|
8
|
+
input.processItemCount > 0 &&
|
|
9
|
+
!input.expanded);
|
|
10
|
+
}
|
|
@@ -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,9 +2,10 @@ 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
|
-
import { upsertCompactionMessage } from "./compaction-message.js";
|
|
8
|
+
import { upsertCompactionMessage, } from "./compaction-message.js";
|
|
8
9
|
import { describeMutationGateFailure, isMutationGateFailureCode, isStaleTokenFailure, } from "./mutation-gate.js";
|
|
9
10
|
import { SessionTitleWatcher } from "./session-title-watcher.js";
|
|
10
11
|
/** Pure list mutations keep optimistic UI deterministic and easy to verify. */
|
|
@@ -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);
|
|
@@ -158,7 +179,7 @@ function mergeCompactionMessages(messages, compactions) {
|
|
|
158
179
|
* compact, plus browser-refresh recovery from the durable session store. */
|
|
159
180
|
export function useChatSessions(params) {
|
|
160
181
|
const { origin, refs, setSession, thread, composer, browser, setError, setNotice, onCompactionChange, setProcessOpen, setAutoScroll, } = params;
|
|
161
|
-
const { applyActivatedSession, resetForNewSession } = thread;
|
|
182
|
+
const { applyActivatedSession, resetForNewSession, setMessages } = thread;
|
|
162
183
|
const { setInput, setPendingImages } = composer;
|
|
163
184
|
const { setPreviews, setActivePreviewPath } = browser;
|
|
164
185
|
const { workspaceId, blocked } = params;
|
|
@@ -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.
|
|
@@ -538,15 +555,7 @@ export function useChatSessions(params) {
|
|
|
538
555
|
selectionVersion !== sessionSelectionVersionRef.current ||
|
|
539
556
|
!body.session)
|
|
540
557
|
return;
|
|
541
|
-
const restored =
|
|
542
|
-
sessionId: body.session.sessionId,
|
|
543
|
-
...(body.session.workspaceId
|
|
544
|
-
? { workspaceId: body.session.workspaceId }
|
|
545
|
-
: workspaceId
|
|
546
|
-
? { workspaceId }
|
|
547
|
-
: {}),
|
|
548
|
-
...runtimeSelectionFromRecord(body.session),
|
|
549
|
-
};
|
|
558
|
+
const restored = projectActiveChatSession(body.session, workspaceId);
|
|
550
559
|
refs.sessionGenerationRef.current += 1;
|
|
551
560
|
refs.sessionRef.current = restored;
|
|
552
561
|
// Do not seed Last-Event-ID here: session messages are restored
|
|
@@ -619,6 +628,7 @@ export function useChatSessions(params) {
|
|
|
619
628
|
setError("创建会话需要选择一个有效的工作区(不会回退默认工作区)");
|
|
620
629
|
return Promise.reject(new Error("WORKSPACE_REQUIRED"));
|
|
621
630
|
}
|
|
631
|
+
const sessionMode = options?.sessionMode ?? DEFAULT_CONSOLE_CHAT_SESSION_MODE;
|
|
622
632
|
const optimisticSessionId = `optimistic-chat-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
623
633
|
const now = new Date().toISOString();
|
|
624
634
|
const optimisticRow = {
|
|
@@ -627,6 +637,7 @@ export function useChatSessions(params) {
|
|
|
627
637
|
state: "active",
|
|
628
638
|
updatedAt: now,
|
|
629
639
|
turnPhase: "idle",
|
|
640
|
+
sessionMode,
|
|
630
641
|
optimisticState: "creating",
|
|
631
642
|
};
|
|
632
643
|
pendingCreatedRowsRef.current.set(optimisticSessionId, optimisticRow);
|
|
@@ -669,9 +680,10 @@ export function useChatSessions(params) {
|
|
|
669
680
|
}
|
|
670
681
|
// A caller-provided snapshot is an explicit new-session boundary; reusing
|
|
671
682
|
// an existing shell would silently drop that context suffix.
|
|
683
|
+
const sessionMode = options?.sessionMode ?? DEFAULT_CONSOLE_CHAT_SESSION_MODE;
|
|
672
684
|
const reusable = options?.snapshot
|
|
673
685
|
? undefined
|
|
674
|
-
: findReusableBlankSession(sessionListRef.current, scope);
|
|
686
|
+
: findReusableBlankSession(sessionListRef.current, scope, sessionMode);
|
|
675
687
|
if (reusable) {
|
|
676
688
|
if (previousSession?.sessionId === reusable.sessionId)
|
|
677
689
|
return previousSession;
|
|
@@ -681,6 +693,7 @@ export function useChatSessions(params) {
|
|
|
681
693
|
return {
|
|
682
694
|
sessionId: reusable.sessionId,
|
|
683
695
|
...(reusable.workspaceId ? { workspaceId: reusable.workspaceId } : {}),
|
|
696
|
+
sessionMode: reusable.sessionMode ?? sessionMode,
|
|
684
697
|
...runtimeSelectionFromRecord(reusable),
|
|
685
698
|
};
|
|
686
699
|
}
|
|
@@ -763,11 +776,18 @@ export function useChatSessions(params) {
|
|
|
763
776
|
return;
|
|
764
777
|
}
|
|
765
778
|
setError(null);
|
|
766
|
-
//
|
|
767
|
-
//
|
|
779
|
+
// Keep the current projection and converge the lifecycle row in place.
|
|
780
|
+
// Re-activating here would clear the timeline and replay the full event ring.
|
|
768
781
|
const summary = body.snapshot?.summary?.trim();
|
|
769
782
|
const summaryText = summary || "历史上下文已精简,关键决策与当前任务已保留。";
|
|
770
|
-
|
|
783
|
+
const compaction = body.compaction;
|
|
784
|
+
if (compaction &&
|
|
785
|
+
refs.sessionRef.current?.sessionId === current.sessionId) {
|
|
786
|
+
if (compaction.eventId) {
|
|
787
|
+
refs.seenEventIdsRef.current.add(compaction.eventId);
|
|
788
|
+
}
|
|
789
|
+
setMessages((messages) => upsertCompactionMessage(messages, compaction));
|
|
790
|
+
}
|
|
771
791
|
setNotice({
|
|
772
792
|
id: Date.now(),
|
|
773
793
|
message: `已压缩上下文:${summaryText.slice(0, 72)}${summaryText.length > 72 ? "…" : ""}`,
|
|
@@ -785,7 +805,7 @@ export function useChatSessions(params) {
|
|
|
785
805
|
setError,
|
|
786
806
|
setNotice,
|
|
787
807
|
onCompactionChange,
|
|
788
|
-
|
|
808
|
+
setMessages,
|
|
789
809
|
sessionOwnerQuery,
|
|
790
810
|
]);
|
|
791
811
|
const updateSession = async (sessionId, patch) => {
|
|
@@ -53,7 +53,11 @@ function clearEmptyAssistant(setMessages, assistantId) {
|
|
|
53
53
|
return;
|
|
54
54
|
setMessages((messages) => messages.filter((message) => !(message.id === assistantId &&
|
|
55
55
|
message.role === "assistant" &&
|
|
56
|
-
!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))));
|
|
57
61
|
}
|
|
58
62
|
/** One GET /events stream owns all Chat events. Sending a prompt creates a
|
|
59
63
|
* detached Turn resource only; it never opens a second inline SSE consumer. */
|
|
@@ -140,7 +144,10 @@ export function useChatStream(params) {
|
|
|
140
144
|
current.generation === generation)
|
|
141
145
|
current.ready = true;
|
|
142
146
|
replay = false;
|
|
143
|
-
|
|
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).
|
|
144
151
|
}
|
|
145
152
|
const assistantId = refs.assistantIdRef.current ?? fallbackAssistantId;
|
|
146
153
|
const eventId = applySseBlock(block, assistantId, {
|
|
@@ -713,6 +720,15 @@ export function useChatStream(params) {
|
|
|
713
720
|
if (refs.sessionRef.current?.sessionId !== activeSession.sessionId ||
|
|
714
721
|
refs.sessionGenerationRef.current !== generation) {
|
|
715
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
|
+
}
|
|
716
732
|
return;
|
|
717
733
|
}
|
|
718
734
|
// Attachments stay until accepted (AC/UIS-accepted).
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
1
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from "react";
|
|
2
2
|
import { buildTurnProcessGroups, hydrateSessionThread, selectRecentTurnGroups, } from "../../chat/turn-process.js";
|
|
3
|
+
import { isToolFailure } from "../../chat/tool-preview.js";
|
|
3
4
|
import { mergeOperationCardState, } from "../../chat/operation-card.js";
|
|
4
5
|
import { createChatSseEventApplier, } from "./chat-sse-events.js";
|
|
5
6
|
import { mergePendingSubmissionOnHydration } from "./pending-user-message.js";
|
|
6
7
|
import { taskContextUnchanged } from "./format.js";
|
|
7
8
|
import { upsertCompactionMessage } from "./compaction-message.js";
|
|
9
|
+
import { sameTurnGroupInputs } from "./turn-group-equality.js";
|
|
10
|
+
import { selectActiveTurnTail } from "./active-turn-tail.js";
|
|
8
11
|
const EMPTY_USAGE = {
|
|
9
12
|
inputTokens: 0,
|
|
10
13
|
outputTokens: 0,
|
|
@@ -115,6 +118,12 @@ export function useChatThread(params) {
|
|
|
115
118
|
// (UI-08); the SSE applier must not open the process panel.
|
|
116
119
|
void params.setProcessOpen;
|
|
117
120
|
const [messages, setMessages] = useState([]);
|
|
121
|
+
/** Rendered-messages mirror for the replay flush: it must resolve its target
|
|
122
|
+
* row synchronously, before React commits the queued updater (2026-09-08). */
|
|
123
|
+
const messagesRef = useRef([]);
|
|
124
|
+
useLayoutEffect(() => {
|
|
125
|
+
messagesRef.current = messages;
|
|
126
|
+
}, [messages]);
|
|
118
127
|
const [toolCalls, setToolCalls] = useState([]);
|
|
119
128
|
const [operationCards, setOperationCards] = useState([]);
|
|
120
129
|
const [artifactCards, setArtifactCards] = useState([]);
|
|
@@ -187,6 +196,7 @@ export function useChatThread(params) {
|
|
|
187
196
|
onTelemetryRefresh: () => void refreshSessionTelemetry(),
|
|
188
197
|
setMessages,
|
|
189
198
|
setToolCalls,
|
|
199
|
+
getMessages: () => messagesRef.current,
|
|
190
200
|
}), [refs, onReconcile, setError, refreshSessionTelemetry]);
|
|
191
201
|
/** Parse + apply one raw SSE block (frames separated by \n\n). Returns the
|
|
192
202
|
* eventId if the block carried an `id:` line (for Last-Event-ID tracking). */
|
|
@@ -285,7 +295,7 @@ export function useChatThread(params) {
|
|
|
285
295
|
if (toolCalls.length === 0)
|
|
286
296
|
return null;
|
|
287
297
|
const running = toolCalls.filter((t) => t.status === "running");
|
|
288
|
-
const errored = toolCalls.filter((t) => t
|
|
298
|
+
const errored = toolCalls.filter((t) => isToolFailure(t));
|
|
289
299
|
const current = running[running.length - 1];
|
|
290
300
|
let statusLabel = "完成";
|
|
291
301
|
let statusKind = "ok";
|
|
@@ -317,10 +327,7 @@ export function useChatThread(params) {
|
|
|
317
327
|
previousTimeline?.activeAssistantId === streamingAssistantId &&
|
|
318
328
|
(previousTimeline?.messages.every((message, index) => message.id === streamingAssistantId || timelineMessages[index]?.id === message.id) ?? false);
|
|
319
329
|
if (canBuildActiveTail && previousTimeline) {
|
|
320
|
-
|
|
321
|
-
while (tailStart > 0 && timelineMessages[tailStart - 1]?.role !== "user")
|
|
322
|
-
tailStart -= 1;
|
|
323
|
-
const tailMessages = timelineMessages.slice(tailStart);
|
|
330
|
+
const tailMessages = selectActiveTurnTail(timelineMessages, activeIndex);
|
|
324
331
|
const tailIds = new Set(tailMessages.map((message) => message.id));
|
|
325
332
|
const tailTurnIds = new Set(tailMessages.map((message) => message.turnId).filter(Boolean));
|
|
326
333
|
const tailTools = toolCalls.filter((tool) => tailIds.has(tool.assistantMessageId ?? "") ||
|
|
@@ -348,7 +355,9 @@ export function useChatThread(params) {
|
|
|
348
355
|
const previous = new Map(completedGroupsRef.current.map((group) => [group.key, group]));
|
|
349
356
|
const all = built.map((group) => {
|
|
350
357
|
const cached = previous.get(group.key);
|
|
351
|
-
return cached && !group.live
|
|
358
|
+
return cached && !group.live && sameTurnGroupInputs(cached, group)
|
|
359
|
+
? cached
|
|
360
|
+
: group;
|
|
352
361
|
});
|
|
353
362
|
completedGroupsRef.current = all.filter((group) => !group.live);
|
|
354
363
|
previousTimelineRef.current = {
|
|
@@ -450,7 +459,31 @@ export function useChatThread(params) {
|
|
|
450
459
|
/** Adopt a freshly activated session's hydrated thread + streaming flags. */
|
|
451
460
|
const applyActivatedSession = useCallback((next) => {
|
|
452
461
|
const visuallyStopped = next.turnPhase === "stopping";
|
|
453
|
-
|
|
462
|
+
// A submission still owned for THIS session keeps its optimistic bubble
|
|
463
|
+
// across an activation snapshot (refresh / reopen); a lease owned by
|
|
464
|
+
// another session must never leak into this thread (2026-09-08).
|
|
465
|
+
const ownedPending = refs.pendingSubmissionRef.current?.sessionId ===
|
|
466
|
+
refs.sessionRef.current?.sessionId
|
|
467
|
+
? refs.pendingSubmissionRef.current
|
|
468
|
+
: undefined;
|
|
469
|
+
setMessages(mergePendingSubmissionOnHydration(next.messages, ownedPending
|
|
470
|
+
? {
|
|
471
|
+
clientRequestId: ownedPending.clientRequestId,
|
|
472
|
+
user: {
|
|
473
|
+
id: ownedPending.userMessageId,
|
|
474
|
+
role: "user",
|
|
475
|
+
text: ownedPending.text,
|
|
476
|
+
createdAt: Date.now(),
|
|
477
|
+
clientRequestId: ownedPending.clientRequestId,
|
|
478
|
+
},
|
|
479
|
+
assistant: {
|
|
480
|
+
id: ownedPending.assistantId,
|
|
481
|
+
role: "assistant",
|
|
482
|
+
text: "",
|
|
483
|
+
createdAt: Date.now(),
|
|
484
|
+
},
|
|
485
|
+
}
|
|
486
|
+
: undefined));
|
|
454
487
|
setToolCalls(next.toolCalls);
|
|
455
488
|
setOperationCards([]);
|
|
456
489
|
setHumanGateCards([]);
|
|
@@ -7,7 +7,7 @@ import { attachRuntimeSnapshotController } from "./useRuntimeSnapshot.js";
|
|
|
7
7
|
/** Composer state: input text, @file palette, image attachments, /shortcuts,
|
|
8
8
|
* IME composition guards, auto-grow and server-side draft sync. */
|
|
9
9
|
export function useComposer(params) {
|
|
10
|
-
const { origin, sessionId, refs, setError, onUnrecoverableSession } = params;
|
|
10
|
+
const { origin, sessionId, sessionMode, refs, setError, onUnrecoverableSession, } = params;
|
|
11
11
|
const [input, setInputState] = useState("");
|
|
12
12
|
const [fileOptions, setFileOptions] = useState([]);
|
|
13
13
|
const [pendingImages, setPendingImages] = useState([]);
|
|
@@ -28,7 +28,10 @@ export function useComposer(params) {
|
|
|
28
28
|
}, []);
|
|
29
29
|
const [shortcutIndex, setShortcutIndex] = useState(0);
|
|
30
30
|
const [dynamicCommands, setDynamicCommands] = useState([]);
|
|
31
|
-
const [commandCapabilities, setCommandCapabilities] = useState({
|
|
31
|
+
const [commandCapabilities, setCommandCapabilities] = useState({
|
|
32
|
+
sddBingo: false,
|
|
33
|
+
sessionMode: undefined,
|
|
34
|
+
});
|
|
32
35
|
const [dynamicCommandsLoading, setDynamicCommandsLoading] = useState(false);
|
|
33
36
|
const [dynamicCommandsError, setDynamicCommandsError] = useState(null);
|
|
34
37
|
const [commandsEpoch, setCommandsEpoch] = useState(0);
|
|
@@ -75,10 +78,10 @@ export function useComposer(params) {
|
|
|
75
78
|
commandsSessionRef.current = sessionId;
|
|
76
79
|
commandsGenerationRef.current += 1;
|
|
77
80
|
setDynamicCommands([]);
|
|
78
|
-
setCommandCapabilities({ sddBingo: false });
|
|
81
|
+
setCommandCapabilities({ sddBingo: false, sessionMode });
|
|
79
82
|
setDynamicCommandsError(null);
|
|
80
83
|
setDynamicCommandsLoading(false);
|
|
81
|
-
}, [sessionId]);
|
|
84
|
+
}, [sessionId, sessionMode]);
|
|
82
85
|
const loadDynamicCommands = useCallback(async () => {
|
|
83
86
|
if (!sessionId) {
|
|
84
87
|
setDynamicCommands([]);
|
|
@@ -124,7 +127,10 @@ export function useComposer(params) {
|
|
|
124
127
|
if (!isCurrent())
|
|
125
128
|
return;
|
|
126
129
|
setDynamicCommands(items);
|
|
127
|
-
setCommandCapabilities({
|
|
130
|
+
setCommandCapabilities({
|
|
131
|
+
sddBingo: body?.capabilities?.sddBingo === true,
|
|
132
|
+
sessionMode: body?.capabilities?.sessionMode ?? sessionMode,
|
|
133
|
+
});
|
|
128
134
|
// Compact server fail-soft / partial-source warning; local Operator/Web still merge.
|
|
129
135
|
const warning = typeof body?.warning === "string" ? body.warning.trim() : "";
|
|
130
136
|
setDynamicCommandsError(warning ? warning.slice(0, 160) : null);
|
|
@@ -134,14 +140,14 @@ export function useComposer(params) {
|
|
|
134
140
|
return;
|
|
135
141
|
setDynamicCommandsError("动态命令加载失败");
|
|
136
142
|
setDynamicCommands([]);
|
|
137
|
-
setCommandCapabilities({ sddBingo: false });
|
|
143
|
+
setCommandCapabilities({ sddBingo: false, sessionMode });
|
|
138
144
|
}
|
|
139
145
|
finally {
|
|
140
146
|
if (isCurrent()) {
|
|
141
147
|
setDynamicCommandsLoading(false);
|
|
142
148
|
}
|
|
143
149
|
}
|
|
144
|
-
}, [origin, sessionId]);
|
|
150
|
+
}, [origin, sessionId, sessionMode]);
|
|
145
151
|
// Lazy-load dynamic commands when slash palette opens or session changes.
|
|
146
152
|
useEffect(() => {
|
|
147
153
|
if (!sessionId)
|
|
@@ -206,13 +212,22 @@ export function useComposer(params) {
|
|
|
206
212
|
const removeImage = useCallback((image) => {
|
|
207
213
|
setPendingImages((items) => items.filter((item) => item !== image));
|
|
208
214
|
}, []);
|
|
209
|
-
//
|
|
210
|
-
|
|
215
|
+
// Chromium handles growth in CSS without forcing a layout read on every key.
|
|
216
|
+
// Keep a deferred fallback for browsers that do not support field-sizing.
|
|
217
|
+
useEffect(() => {
|
|
211
218
|
const ta = textareaRef.current;
|
|
212
219
|
if (!ta)
|
|
213
220
|
return;
|
|
214
|
-
|
|
215
|
-
|
|
221
|
+
if (typeof CSS !== "undefined" &&
|
|
222
|
+
CSS.supports("field-sizing", "content")) {
|
|
223
|
+
ta.style.removeProperty("height");
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const frame = window.requestAnimationFrame(() => {
|
|
227
|
+
ta.style.height = "auto";
|
|
228
|
+
ta.style.height = `${Math.min(ta.scrollHeight, 280)}px`;
|
|
229
|
+
});
|
|
230
|
+
return () => window.cancelAnimationFrame(frame);
|
|
216
231
|
}, [input]);
|
|
217
232
|
// Debounced server-side composer draft sync.
|
|
218
233
|
useEffect(() => {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** User anchors only depend on the ordered user-message objects, not on
|
|
2
|
+
* assistant/process updates that rebuild their containing turn groups. */
|
|
3
|
+
export function sameUserMessages(previous, next) {
|
|
4
|
+
let previousIndex = 0;
|
|
5
|
+
let nextIndex = 0;
|
|
6
|
+
for (;;) {
|
|
7
|
+
while (previousIndex < previous.length && !previous[previousIndex].user) {
|
|
8
|
+
previousIndex += 1;
|
|
9
|
+
}
|
|
10
|
+
while (nextIndex < next.length && !next[nextIndex].user) {
|
|
11
|
+
nextIndex += 1;
|
|
12
|
+
}
|
|
13
|
+
if (previousIndex === previous.length || nextIndex === next.length) {
|
|
14
|
+
return previousIndex === previous.length && nextIndex === next.length;
|
|
15
|
+
}
|
|
16
|
+
if (previous[previousIndex].user !== next[nextIndex].user)
|
|
17
|
+
return false;
|
|
18
|
+
previousIndex += 1;
|
|
19
|
+
nextIndex += 1;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -37,6 +37,8 @@ import { UserQuestionRegistry } from "./chat/user-questions.js";
|
|
|
37
37
|
import { TodoListRegistry } from "./chat/todos.js";
|
|
38
38
|
import { GoalRegistry } from "./chat/goals.js";
|
|
39
39
|
import { GoalRoundDriver } from "./chat/goal-round-driver.js";
|
|
40
|
+
import { TerminalSessionRegistry } from "./chat/terminal-sessions.js";
|
|
41
|
+
import { ScmService } from "./chat/scm-service.js";
|
|
40
42
|
import { driveGoalRound, recoverDeferredTurns } from "./chat/routes.js";
|
|
41
43
|
import { createOperationEventStore, } from "./operation-sse.js";
|
|
42
44
|
import { OperationStore } from "../../infrastructure/console/operation-store.js";
|
|
@@ -254,7 +256,9 @@ export async function createWorkspaceContext(record, deps) {
|
|
|
254
256
|
}
|
|
255
257
|
},
|
|
256
258
|
});
|
|
257
|
-
const
|
|
259
|
+
const aimaxAuth = new AimaxAuthReader();
|
|
260
|
+
await aimaxAuth.getToken();
|
|
261
|
+
const sddDataAlignment = new SddDataAlignmentService(chatStore, aimaxAuth, deps.fetchImpl);
|
|
258
262
|
const chatEventEpoch = randomBytes(12).toString("hex");
|
|
259
263
|
const chatEvents = createChatEventStore({
|
|
260
264
|
appData,
|
|
@@ -285,6 +289,12 @@ export async function createWorkspaceContext(record, deps) {
|
|
|
285
289
|
operations,
|
|
286
290
|
operationEvents: events,
|
|
287
291
|
});
|
|
292
|
+
// Persistent PTY terminal sessions shared by the model tool (terminal) and
|
|
293
|
+
// the Console UI terminal panel (HTTP API), keyed per chat session owner.
|
|
294
|
+
const terminalRegistry = new TerminalSessionRegistry();
|
|
295
|
+
// Workspace-scoped git source control shared by the model tool (scm) and
|
|
296
|
+
// the Console UI SCM panel (HTTP API).
|
|
297
|
+
const scmService = new ScmService(repoRoot);
|
|
288
298
|
const chatRuntime = new ConsolePiRuntime({
|
|
289
299
|
cwd: repoRoot,
|
|
290
300
|
sessionDir: appData.chatSessions,
|
|
@@ -294,6 +304,8 @@ export async function createWorkspaceContext(record, deps) {
|
|
|
294
304
|
todoRegistry,
|
|
295
305
|
goalRegistry,
|
|
296
306
|
sddDataAlignment,
|
|
307
|
+
terminalRegistry,
|
|
308
|
+
scmService,
|
|
297
309
|
});
|
|
298
310
|
let chatRouteDeps;
|
|
299
311
|
const goalRoundDriver = new GoalRoundDriver((sessionId) => driveGoalRound(chatRouteDeps, sessionId));
|
|
@@ -308,6 +320,8 @@ export async function createWorkspaceContext(record, deps) {
|
|
|
308
320
|
sddDataAlignment,
|
|
309
321
|
goalRoundDriver,
|
|
310
322
|
operationLinker: chatOperationLinker,
|
|
323
|
+
terminals: terminalRegistry,
|
|
324
|
+
scm: scmService,
|
|
311
325
|
appData,
|
|
312
326
|
bootToken: deps.bootToken,
|
|
313
327
|
getConsoleOrigin: deps.getConsoleOrigin,
|