@tea-agent/loop-agent 0.43.0-next.3 → 0.43.0-next.5

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.
Files changed (109) hide show
  1. package/CHANGELOG.md +81 -9
  2. package/dist/application/task-lifecycle/advance.js +17 -4
  3. package/dist/application/task-lifecycle/observe.js +43 -29
  4. package/dist/application/task-lifecycle/plan-transitions.js +5 -4
  5. package/dist/build-stamp.json +3 -3
  6. package/dist/executors/dag-pi-executor.js +244 -33
  7. package/dist/executors/pi-executor.js +2 -0
  8. package/dist/executors/pi-sdk-executor.js +75 -39
  9. package/dist/task/source-prepare/source-fidelity-pi.js +2 -2
  10. package/dist/task/source-references.js +48 -23
  11. package/dist/worker/console/chat/assistant-content.js +23 -2
  12. package/dist/worker/console/chat/explore-tools.js +13 -0
  13. package/dist/worker/console/chat/pi-runtime.js +131 -95
  14. package/dist/worker/console/chat/routes.js +175 -54
  15. package/dist/worker/console/chat/scm-routes.js +193 -0
  16. package/dist/worker/console/chat/scm-service.js +253 -0
  17. package/dist/worker/console/chat/scm-tools.js +111 -0
  18. package/dist/worker/console/chat/sdd-data-alignment.js +65 -9
  19. package/dist/worker/console/chat/session-catalog.js +1 -0
  20. package/dist/worker/console/chat/session-mode-view.js +48 -0
  21. package/dist/worker/console/chat/session-mode.js +218 -0
  22. package/dist/worker/console/chat/session-store.js +22 -6
  23. package/dist/worker/console/chat/shortcuts.js +6 -0
  24. package/dist/worker/console/chat/terminal-routes.js +216 -0
  25. package/dist/worker/console/chat/terminal-sessions.js +284 -0
  26. package/dist/worker/console/chat/terminal-tools.js +199 -0
  27. package/dist/worker/console/chat/tool-preview.js +11 -0
  28. package/dist/worker/console/chat/tools.js +2 -0
  29. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-hUmM_GRf.js → abnfDiagram-N423BO3Z-BuyrlONK.js} +1 -1
  30. package/dist/worker/console/static/assets/{arc-Bt-xuieW.js → arc-BvCl_wpi.js} +1 -1
  31. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DTqkL4DJ.js → architectureDiagram-T3A2C74G-BkBpBaRq.js} +1 -1
  32. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-Cy0MNnzV.js → blockDiagram-VBNYF7ZC-yve4Mcb_.js} +1 -1
  33. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DDdlT0Pd.js → c4Diagram-5PPSVZJV-9yLGboBx.js} +1 -1
  34. package/dist/worker/console/static/assets/channel-BRahIhjw.js +1 -0
  35. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-3JawwNZ6.js → chunk-2GRJ4B5K-BKP69b8r.js} +1 -1
  36. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DoTp7Cri.js → chunk-2Q5K7J3B-NWFRnXju.js} +1 -1
  37. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-CcirhZaS.js → chunk-5RXB4S5H-BiweqLEc.js} +1 -1
  38. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-VrG21gnP.js → chunk-5VM5RSS4-CP9GHmRY.js} +1 -1
  39. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DDOgIS70.js → chunk-6Q2QTUOP-BiGDkBtj.js} +1 -1
  40. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CBjmgvJ_.js → chunk-GF5L2VYU-DkJ4ZjQS.js} +1 -1
  41. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CCQSht8M.js → chunk-JWPE2WC7-B5jjn7zq.js} +1 -1
  42. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BpGMkUUV.js → chunk-KBJHAD2P-C6KoK9O0.js} +1 -1
  43. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DNDT41QL.js → chunk-RYQCIY6F-BvNIfyn-.js} +1 -1
  44. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-euXnDFbn.js → chunk-XXDRQBXY-BZ33iX4W.js} +1 -1
  45. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-GOC80mhq.js +1 -0
  46. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-GOC80mhq.js +1 -0
  47. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BJ6NAeql.js → cose-bilkent-JH36ORCC-CP8eIuZd.js} +1 -1
  48. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-F5JKIvIq.js → cynefin-VYW2F7L2-DZECjpOe.js} +1 -1
  49. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Cwmkl_Gi.js → cynefinDiagram-MW4NZA55-CvVJoFqf.js} +1 -1
  50. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BmCZ8815.js → dagre-VZM6K2ZE-D_JTqdAn.js} +1 -1
  51. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-zWA3hUnX.js → diagram-7IWD3JNH-Db7AQ0z0.js} +1 -1
  52. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BzA-888F.js → diagram-B4RE2ZJO-AZPVo5CZ.js} +1 -1
  53. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-tPNheoch.js → diagram-LBJQPF4R-BgXqI3E6.js} +1 -1
  54. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-Cdi7Fyqp.js → diagram-Q27KOJAE-BYjFn3Ba.js} +1 -1
  55. package/dist/worker/console/static/assets/{diagram-UB23O5K3-DFK9VBf1.js → diagram-UB23O5K3-B4rJgGGO.js} +1 -1
  56. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-xCUr1kpg.js → ebnfDiagram-BXEA7PRR-C6XWZhpw.js} +1 -1
  57. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BANQHY_A.js → erDiagram-JOGREHBK-1YdQRIQW.js} +1 -1
  58. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-VHJYEMkP.js → flowDiagram-UKHOOZJN-l3a0AwN0.js} +1 -1
  59. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DiyZQ0C6.js → ganttDiagram-PKOTCBZU-CQL96F2Z.js} +1 -1
  60. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CZtIeBBa.js → gitGraphDiagram-DS77QQ5N-BxCvafRb.js} +1 -1
  61. package/dist/worker/console/static/assets/index-4wCGsXuU.js +465 -0
  62. package/dist/worker/console/static/assets/index-CiF9WBoR.css +1 -0
  63. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BuSspC0X.js → infoDiagram-6WML65LV-BUipIeDO.js} +1 -1
  64. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-DRJZyPFy.js → ishikawaDiagram-WSZJBQD7-CRfomVMX.js} +1 -1
  65. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DIfXIVuE.js → journeyDiagram-NVQOT4AX-_UFvyviX.js} +1 -1
  66. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CT10_7Lt.js → kanban-definition-27J2QSJJ-CKIRoqKv.js} +1 -1
  67. package/dist/worker/console/static/assets/{linear-ufemj74H.js → linear-CjttJDCM.js} +1 -1
  68. package/dist/worker/console/static/assets/{mermaid.core-DEQe8-_m.js → mermaid.core-DPvHX155.js} +5 -5
  69. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BRLUrbrC.js → mindmap-definition-FAOFIHXS-Qmj-qwBe.js} +1 -1
  70. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BgibKtiF.js → pegDiagram-VL7TDLO6-Auw6DRgq.js} +1 -1
  71. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-D3eyfdV9.js → pieDiagram-7S7Q4E2Y-CqMrfD4c.js} +1 -1
  72. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-DXeTk5Px.js → quadrantDiagram-CIZ2JOQS-BAddeggH.js} +1 -1
  73. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BgXdFEe7.js → railroadDiagram-AXF67PYL-DG1JuOu2.js} +1 -1
  74. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CseC2A7R.js → requirementDiagram-LRYGKXZP-DLYGMKlC.js} +1 -1
  75. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-D80PFzfJ.js → sankeyDiagram-W5VNT64P-g68SbLM9.js} +1 -1
  76. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BWzHW7LS.js → sequenceDiagram-SI44F4Z6-CmszLKv8.js} +1 -1
  77. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-xWQYTLAn.js → sizeCapture-X5ZJPWSS-D2tEKUCj.js} +1 -1
  78. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-CLxtpZeL.js → stateDiagram-OKZ733FA-Syc5E_Jf.js} +1 -1
  79. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-CwWwuPI-.js +1 -0
  80. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DwugtmWV.js → swimlanes-SLNWSIFB-Ibn0IRCa.js} +2 -2
  81. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-A7fqemr6.js +8 -0
  82. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DHEOwHIe.js → timeline-definition-Z64GVDOM-2ZW9W3iS.js} +1 -1
  83. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-pmbZKlBK.js → vennDiagram-T6HMQDX7-v50HsKUh.js} +1 -1
  84. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CivHoM5q.js → wardleyDiagram-T6FBY63Y-DqCXEoDH.js} +1 -1
  85. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-BfHIE7o6.js → xychartDiagram-ELKLHX3M-vIP8Vo9d.js} +1 -1
  86. package/dist/worker/console/static/index.html +2 -2
  87. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +122 -10
  88. package/dist/worker/console/static-src/operator-chat/pending-user-message.js +27 -15
  89. package/dist/worker/console/static-src/operator-chat/process-label.js +41 -0
  90. package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +13 -0
  91. package/dist/worker/console/static-src/operator-chat/use-searchable-hidden.js +51 -0
  92. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +20 -4
  93. package/dist/worker/console/static-src/operator-chat/useChatStream.js +18 -2
  94. package/dist/worker/console/static-src/operator-chat/useChatThread.js +39 -4
  95. package/dist/worker/console/static-src/operator-chat/useComposer.js +13 -7
  96. package/dist/worker/console/static-src/operator-chat/user-turn-anchor-equality.js +21 -0
  97. package/dist/worker/console/workspace-context.js +15 -1
  98. package/dist/workflows/dag/frontend-capacity.js +9 -0
  99. package/dist/workflows/dag/frontend-design-policy.js +2 -1
  100. package/dist/workflows/dag/frontend-shape-facts.js +12 -2
  101. package/package.json +6 -3
  102. package/skills/frontend-plan/SKILL.md +7 -0
  103. package/dist/worker/console/static/assets/channel-BpzRnZmU.js +0 -1
  104. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BhlMVqCc.js +0 -1
  105. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BhlMVqCc.js +0 -1
  106. package/dist/worker/console/static/assets/index-BlN6ytLr.css +0 -1
  107. package/dist/worker/console/static/assets/index-CA-jfWlE.js +0 -449
  108. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-CiVoq4G-.js +0 -1
  109. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BkdgdpfR.js +0 -8
@@ -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
  }
@@ -84,7 +87,7 @@ export function stabilizeSessionList(previous, next) {
84
87
  return changed ? stabilized : previous;
85
88
  }
86
89
  /** Select a server-confirmed blank session owned by the requested workspace. */
87
- export function findReusableBlankSession(list, workspaceId) {
90
+ export function findReusableBlankSession(list, workspaceId, sessionMode = DEFAULT_CONSOLE_CHAT_SESSION_MODE) {
88
91
  const scope = workspaceId.trim();
89
92
  if (!scope)
90
93
  return undefined;
@@ -93,7 +96,8 @@ export function findReusableBlankSession(list, workspaceId) {
93
96
  item.isBlank === true &&
94
97
  item.turnPhase === "idle" &&
95
98
  !item.activeTurnId &&
96
- !item.optimisticState);
99
+ !item.optimisticState &&
100
+ (item.sessionMode ?? DEFAULT_CONSOLE_CHAT_SESSION_MODE) === sessionMode);
97
101
  }
98
102
  export async function createSessionAndPrimeTitleWatcher(input) {
99
103
  if (input.creatingRef?.current) {
@@ -127,7 +131,13 @@ export async function createSessionAndPrimeTitleWatcher(input) {
127
131
  const body = (await res.json());
128
132
  if (!body.session)
129
133
  throw new Error("创建会话响应缺少 durable session");
130
- const durable = { ...body.session, isBlank: true };
134
+ const durable = {
135
+ ...body.session,
136
+ isBlank: true,
137
+ sessionMode: body.session.sessionMode ??
138
+ input.options?.sessionMode ??
139
+ DEFAULT_CONSOLE_CHAT_SESSION_MODE,
140
+ };
131
141
  const created = {
132
142
  sessionId: durable.sessionId,
133
143
  ...(durable.workspaceId || input.options?.workspaceId
@@ -135,6 +145,7 @@ export async function createSessionAndPrimeTitleWatcher(input) {
135
145
  workspaceId: durable.workspaceId ?? input.options?.workspaceId,
136
146
  }
137
147
  : {}),
148
+ sessionMode: durable.sessionMode,
138
149
  ...runtimeSelectionFromRecord(durable),
139
150
  };
140
151
  const nextList = replaceOptimisticSession(input.sessionListRef.current, input.options?.optimisticSessionId, durable);
@@ -545,6 +556,7 @@ export function useChatSessions(params) {
545
556
  : workspaceId
546
557
  ? { workspaceId }
547
558
  : {}),
559
+ sessionMode: body.session.sessionMode ?? DEFAULT_CONSOLE_CHAT_SESSION_MODE,
548
560
  ...runtimeSelectionFromRecord(body.session),
549
561
  };
550
562
  refs.sessionGenerationRef.current += 1;
@@ -619,6 +631,7 @@ export function useChatSessions(params) {
619
631
  setError("创建会话需要选择一个有效的工作区(不会回退默认工作区)");
620
632
  return Promise.reject(new Error("WORKSPACE_REQUIRED"));
621
633
  }
634
+ const sessionMode = options?.sessionMode ?? DEFAULT_CONSOLE_CHAT_SESSION_MODE;
622
635
  const optimisticSessionId = `optimistic-chat-${Date.now()}-${Math.random().toString(16).slice(2)}`;
623
636
  const now = new Date().toISOString();
624
637
  const optimisticRow = {
@@ -627,6 +640,7 @@ export function useChatSessions(params) {
627
640
  state: "active",
628
641
  updatedAt: now,
629
642
  turnPhase: "idle",
643
+ sessionMode,
630
644
  optimisticState: "creating",
631
645
  };
632
646
  pendingCreatedRowsRef.current.set(optimisticSessionId, optimisticRow);
@@ -669,9 +683,10 @@ export function useChatSessions(params) {
669
683
  }
670
684
  // A caller-provided snapshot is an explicit new-session boundary; reusing
671
685
  // an existing shell would silently drop that context suffix.
686
+ const sessionMode = options?.sessionMode ?? DEFAULT_CONSOLE_CHAT_SESSION_MODE;
672
687
  const reusable = options?.snapshot
673
688
  ? undefined
674
- : findReusableBlankSession(sessionListRef.current, scope);
689
+ : findReusableBlankSession(sessionListRef.current, scope, sessionMode);
675
690
  if (reusable) {
676
691
  if (previousSession?.sessionId === reusable.sessionId)
677
692
  return previousSession;
@@ -681,6 +696,7 @@ export function useChatSessions(params) {
681
696
  return {
682
697
  sessionId: reusable.sessionId,
683
698
  ...(reusable.workspaceId ? { workspaceId: reusable.workspaceId } : {}),
699
+ sessionMode: reusable.sessionMode ?? sessionMode,
684
700
  ...runtimeSelectionFromRecord(reusable),
685
701
  };
686
702
  }
@@ -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
- continue;
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,12 @@
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";
8
10
  const EMPTY_USAGE = {
9
11
  inputTokens: 0,
10
12
  outputTokens: 0,
@@ -115,6 +117,12 @@ export function useChatThread(params) {
115
117
  // (UI-08); the SSE applier must not open the process panel.
116
118
  void params.setProcessOpen;
117
119
  const [messages, setMessages] = useState([]);
120
+ /** Rendered-messages mirror for the replay flush: it must resolve its target
121
+ * row synchronously, before React commits the queued updater (2026-09-08). */
122
+ const messagesRef = useRef([]);
123
+ useLayoutEffect(() => {
124
+ messagesRef.current = messages;
125
+ }, [messages]);
118
126
  const [toolCalls, setToolCalls] = useState([]);
119
127
  const [operationCards, setOperationCards] = useState([]);
120
128
  const [artifactCards, setArtifactCards] = useState([]);
@@ -187,6 +195,7 @@ export function useChatThread(params) {
187
195
  onTelemetryRefresh: () => void refreshSessionTelemetry(),
188
196
  setMessages,
189
197
  setToolCalls,
198
+ getMessages: () => messagesRef.current,
190
199
  }), [refs, onReconcile, setError, refreshSessionTelemetry]);
191
200
  /** Parse + apply one raw SSE block (frames separated by \n\n). Returns the
192
201
  * eventId if the block carried an `id:` line (for Last-Event-ID tracking). */
@@ -285,7 +294,7 @@ export function useChatThread(params) {
285
294
  if (toolCalls.length === 0)
286
295
  return null;
287
296
  const running = toolCalls.filter((t) => t.status === "running");
288
- const errored = toolCalls.filter((t) => t.status === "error");
297
+ const errored = toolCalls.filter((t) => isToolFailure(t));
289
298
  const current = running[running.length - 1];
290
299
  let statusLabel = "完成";
291
300
  let statusKind = "ok";
@@ -348,7 +357,9 @@ export function useChatThread(params) {
348
357
  const previous = new Map(completedGroupsRef.current.map((group) => [group.key, group]));
349
358
  const all = built.map((group) => {
350
359
  const cached = previous.get(group.key);
351
- return cached && !group.live ? cached : group;
360
+ return cached && !group.live && sameTurnGroupInputs(cached, group)
361
+ ? cached
362
+ : group;
352
363
  });
353
364
  completedGroupsRef.current = all.filter((group) => !group.live);
354
365
  previousTimelineRef.current = {
@@ -450,7 +461,31 @@ export function useChatThread(params) {
450
461
  /** Adopt a freshly activated session's hydrated thread + streaming flags. */
451
462
  const applyActivatedSession = useCallback((next) => {
452
463
  const visuallyStopped = next.turnPhase === "stopping";
453
- setMessages(next.messages);
464
+ // A submission still owned for THIS session keeps its optimistic bubble
465
+ // across an activation snapshot (refresh / reopen); a lease owned by
466
+ // another session must never leak into this thread (2026-09-08).
467
+ const ownedPending = refs.pendingSubmissionRef.current?.sessionId ===
468
+ refs.sessionRef.current?.sessionId
469
+ ? refs.pendingSubmissionRef.current
470
+ : undefined;
471
+ setMessages(mergePendingSubmissionOnHydration(next.messages, ownedPending
472
+ ? {
473
+ clientRequestId: ownedPending.clientRequestId,
474
+ user: {
475
+ id: ownedPending.userMessageId,
476
+ role: "user",
477
+ text: ownedPending.text,
478
+ createdAt: Date.now(),
479
+ clientRequestId: ownedPending.clientRequestId,
480
+ },
481
+ assistant: {
482
+ id: ownedPending.assistantId,
483
+ role: "assistant",
484
+ text: "",
485
+ createdAt: Date.now(),
486
+ },
487
+ }
488
+ : undefined));
454
489
  setToolCalls(next.toolCalls);
455
490
  setOperationCards([]);
456
491
  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({ sddBingo: false });
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({ sddBingo: body?.capabilities?.sddBingo === true });
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)
@@ -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 sddDataAlignment = new SddDataAlignmentService(chatStore, new AimaxAuthReader(), deps.fetchImpl);
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,
@@ -0,0 +1,9 @@
1
+ /** Shared Contract/Plan bound. Permissions and directory globs are not files. */
2
+ export const FRONTEND_MAX_IMPLEMENTATION_FILES = 15;
3
+ export function frontendConcreteDeliveryFiles(files) {
4
+ if (!Array.isArray(files))
5
+ return [];
6
+ return [...new Set(files.filter((file) => typeof file === "string")
7
+ .map(file => file.trim().replace(/\\/g, "/").replace(/^(\.\/)+/, ""))
8
+ .filter(file => file.length > 0 && !/[*?{}]/.test(file) && !file.endsWith("/")))];
9
+ }
@@ -1,11 +1,12 @@
1
1
  import { deterministicSha256, } from "./frontend-implementation-contract.js";
2
2
  import { pathMatchesPattern } from "../../shared/git-progress.js";
3
+ import { FRONTEND_MAX_IMPLEMENTATION_FILES } from "./frontend-capacity.js";
3
4
  const SHA256_HEX = /^[a-f0-9]{64}$/;
4
5
  const NPM_PACKAGE_NAME = /^(?:@[a-zA-Z0-9_.-]+\/)?[a-zA-Z0-9][a-zA-Z0-9_.-]*$/;
5
6
  /** Max concrete implementation files a single writeSet may carry before the
6
7
  * plan is asked to split. Kept at 15: beyond this, a single implement node's
7
8
  * read/edit loop inflates provider context toward overflow. */
8
- const MAX_WRITE_SET_FILES = 15;
9
+ const MAX_WRITE_SET_FILES = FRONTEND_MAX_IMPLEMENTATION_FILES;
9
10
  function normalizeRelativePath(value) {
10
11
  return value.replace(/\\/g, "/").replace(/^\.\//, "");
11
12
  }
@@ -1,4 +1,5 @@
1
1
  import { sha256OfCanonicalJson } from "../../task/contract/hash.js";
2
+ import { FRONTEND_MAX_IMPLEMENTATION_FILES, frontendConcreteDeliveryFiles } from "./frontend-capacity.js";
2
3
  import { buildFrontendShapeTransitionCapsule, deriveFrontendRuntimeShapeFacts, evaluateFrontendShapeEscalation, parseFrontendShapeTransitionCapsule, } from "./frontend-shape.js";
3
4
  import { createTypedEventStore, readCommittedEvents, } from "./frontend-typed-event-store.js";
4
5
  import { adoptTypedEventFact, stageTypedEventFact, } from "./frontend-typed-event-transaction.js";
@@ -222,7 +223,13 @@ function extractRuntimeShapeEvidence(input) {
222
223
  !sourceText.includes(path) &&
223
224
  /(?:\.[A-Za-z0-9]+|\*\*)$/.test(path));
224
225
  const routes = canonicalStrings(text.match(/\/(?:[A-Za-z0-9_:[\]-]+\/?)+/g) ?? []);
225
- const splitRequired = /["']splitRequired["']\s*:\s*true|\bsplit-required\s+shape\b/i.test(text);
226
+ // The frozen delivery inventory is available before Plan. Do not spend
227
+ // coverage/UX sessions building a contract the same capacity gate will reject.
228
+ // allowedPaths and text mentions are permissions/evidence, never this count.
229
+ const skeleton = input.tasksById.get("frontend-plan-pi")?.structuredContractOutput?.skeleton;
230
+ const deliveryFiles = frontendConcreteDeliveryFiles(skeleton?.targets?.files);
231
+ const capacitySplit = deliveryFiles.length > FRONTEND_MAX_IMPLEMENTATION_FILES;
232
+ const splitRequired = capacitySplit || /["']splitRequired["']\s*:\s*true|\bsplit-required\s+shape\b/i.test(text);
226
233
  return {
227
234
  sourceNodeId: input.sourceNodeId,
228
235
  sourceNodeDigest: computeFrontendShapeNodeDigest(node),
@@ -232,7 +239,10 @@ function extractRuntimeShapeEvidence(input) {
232
239
  targetPaths,
233
240
  routes,
234
241
  splitRequired,
235
- evidenceRefs: [`node:${input.sourceNodeId}:stdout`],
242
+ evidenceRefs: [
243
+ ...(capacitySplit ? ["dag:frontend-plan-pi:structuredContractOutput.skeleton.targets.files"] : []),
244
+ `node:${input.sourceNodeId}:stdout`,
245
+ ],
236
246
  };
237
247
  }
238
248
  /** Commit a typed, structured runtime fact immediately after a canonical source
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tea-agent/loop-agent",
3
- "version": "0.43.0-next.3",
3
+ "version": "0.43.0-next.5",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "loop-agent": "bin/loop-agent.js",
@@ -78,9 +78,12 @@
78
78
  "@deepseek-ai/dsh-invariants": "0.1.1-rc.2",
79
79
  "@earendil-works/pi-ai": "0.83.0",
80
80
  "@earendil-works/pi-coding-agent": "0.83.0",
81
+ "@xterm/addon-fit": "^0.11.0",
82
+ "@xterm/xterm": "^6.0.0",
81
83
  "commander": "^12.1.0",
82
84
  "katex": "^0.16.47",
83
85
  "mermaid": "^11.16.1",
86
+ "node-pty": "^1.2.0-beta.15",
84
87
  "rehype-highlight": "^7.0.2",
85
88
  "rehype-katex": "^7.0.1",
86
89
  "remark-math": "^6.0.0",
@@ -90,8 +93,8 @@
90
93
  "zod": "^3.25.76"
91
94
  },
92
95
  "optionalDependencies": {
93
- "@cursor/sdk": "^1.0.7",
94
- "@agent-context/sift": "0.0.1-alpha.7"
96
+ "@agent-context/sift": "0.0.1-alpha.7",
97
+ "@cursor/sdk": "^1.0.7"
95
98
  },
96
99
  "devDependencies": {
97
100
  "@remixicon/react": "^4.9.0",
@@ -29,6 +29,13 @@ cite `evidencePath` of an existing file; greenfield is `decision=new`.
29
29
  Bind every file in the Contract-owned `requiredDeliverables` inventory. It records
30
30
  explicit source obligations, not every path mentioned by a requirement.
31
31
 
32
+ Large UX work runs in bounded scopes against that shared registry. Complete every
33
+ assigned requirement; preserve earlier scopes' shared bindings when correcting an
34
+ entry. Ledger previews are incomplete. Use `read_plan_facts` to retrieve full
35
+ fields, following `nextOffset` with `expectedRevision` pinned. For oversized
36
+ strings use `stringOffset` and follow `nextStringOffset`; restart pagination when
37
+ the revision changes. Never submit a truncated preview as a replacement payload.
38
+
32
39
  Cross-slice citations must close in the same session: when a requirement cites a
33
40
  verification target committed in another slice's session, re-commit that target
34
41
  here with the same id and `replace=true` (union backfill) before finalize. A
@@ -1 +0,0 @@
1
- import{ag as o,ah as n}from"./mermaid.core-DEQe8-_m.js";const t=(a,r)=>o.lang.round(n.parse(a)[r]);export{t as c};
@@ -1 +0,0 @@
1
- import{s as a,c as s,a as e,C as t}from"./chunk-GF5L2VYU-CBjmgvJ_.js";import{_ as i}from"./mermaid.core-DEQe8-_m.js";import"./chunk-5VM5RSS4-VrG21gnP.js";import"./chunk-XXDRQBXY-euXnDFbn.js";import"./chunk-KBJHAD2P-BpGMkUUV.js";import"./chunk-2GRJ4B5K-3JawwNZ6.js";import"./index-CA-jfWlE.js";var n={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{n as diagram};
@@ -1 +0,0 @@
1
- import{s as a,c as s,a as e,C as t}from"./chunk-GF5L2VYU-CBjmgvJ_.js";import{_ as i}from"./mermaid.core-DEQe8-_m.js";import"./chunk-5VM5RSS4-VrG21gnP.js";import"./chunk-XXDRQBXY-euXnDFbn.js";import"./chunk-KBJHAD2P-BpGMkUUV.js";import"./chunk-2GRJ4B5K-3JawwNZ6.js";import"./index-CA-jfWlE.js";var n={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{n as diagram};