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

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 (187) hide show
  1. package/CHANGELOG.md +9 -140
  2. package/dist/application/task-lifecycle/advance.js +4 -17
  3. package/dist/application/task-lifecycle/observe.js +29 -43
  4. package/dist/application/task-lifecycle/plan-transitions.js +4 -5
  5. package/dist/build-stamp.json +3 -3
  6. package/dist/commands/client-recovery.js +55 -20
  7. package/dist/commands/new-task.js +5 -0
  8. package/dist/commands/status.js +57 -0
  9. package/dist/commands/task-contract.js +269 -0
  10. package/dist/executors/dag-pi-executor.js +230 -269
  11. package/dist/executors/pi-executor.js +0 -2
  12. package/dist/executors/pi-sdk-executor.js +39 -77
  13. package/dist/executors/shell-executor.js +2 -3
  14. package/dist/executors/shell-write-guard.js +32 -27
  15. package/dist/shared/pi-context-pressure/extension.js +16 -26
  16. package/dist/shared/pi-retry-settings.js +0 -40
  17. package/dist/shared/prompts.js +26 -0
  18. package/dist/task/config.js +2 -0
  19. package/dist/task/contract/validate-draft.js +106 -0
  20. package/dist/task/goal.js +3 -0
  21. package/dist/task/index.js +12 -0
  22. package/dist/task/lifecycle.js +1 -0
  23. package/dist/task/operator/capabilities.js +6 -0
  24. package/dist/task/operator/envelope.js +2 -0
  25. package/dist/task/operator/index.js +5 -0
  26. package/dist/task/operator/registry.js +2 -0
  27. package/dist/task/operator/types.js +1 -0
  28. package/dist/task/paths.js +1 -0
  29. package/dist/task/source-prepare/source-fidelity-pi.js +2 -2
  30. package/dist/task/source-references.js +23 -48
  31. package/dist/task/source-state.js +1 -0
  32. package/dist/task/subagent-guidance.js +1 -0
  33. package/dist/worker/console/chat/assistant-content.js +2 -23
  34. package/dist/worker/console/chat/explore-tools.js +0 -13
  35. package/dist/worker/console/chat/pi-runtime.js +1182 -221
  36. package/dist/worker/console/chat/routes.js +71 -213
  37. package/dist/worker/console/chat/sdd-data-alignment.js +9 -65
  38. package/dist/worker/console/chat/session-catalog.js +0 -32
  39. package/dist/worker/console/chat/session-store.js +6 -22
  40. package/dist/worker/console/chat/shortcuts.js +0 -6
  41. package/dist/worker/console/chat/sift-bridge.js +1 -0
  42. package/dist/worker/console/chat/tool-preview.js +0 -11
  43. package/dist/worker/console/chat/tools.js +0 -6
  44. package/dist/worker/console/console-update-and-init.js +19 -1
  45. package/dist/worker/console/console-update-runtime.js +20 -5
  46. package/dist/worker/console/index.js +20 -0
  47. package/dist/worker/console/operator-actions.js +1 -13
  48. package/dist/worker/console/routes.js +0 -8
  49. package/dist/worker/console/server.js +29 -11
  50. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4NXyedy.js → abnfDiagram-N423BO3Z-hUmM_GRf.js} +1 -1
  51. package/dist/worker/console/static/assets/{arc-NZ3qXvBI.js → arc-Bt-xuieW.js} +1 -1
  52. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-COZSRvml.js → architectureDiagram-T3A2C74G-DTqkL4DJ.js} +1 -1
  53. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-Ck0Gxi9C.js → blockDiagram-VBNYF7ZC-Cy0MNnzV.js} +1 -1
  54. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BX5HQ4Ox.js → c4Diagram-5PPSVZJV-DDdlT0Pd.js} +1 -1
  55. package/dist/worker/console/static/assets/channel-BpzRnZmU.js +1 -0
  56. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-CoviemSL.js → chunk-2GRJ4B5K-3JawwNZ6.js} +1 -1
  57. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DbOdOkbZ.js → chunk-2Q5K7J3B-DoTp7Cri.js} +1 -1
  58. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BsG2pmdQ.js → chunk-5RXB4S5H-CcirhZaS.js} +1 -1
  59. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-DGdvrNm8.js → chunk-5VM5RSS4-VrG21gnP.js} +1 -1
  60. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BAy7Yd6y.js → chunk-6Q2QTUOP-DDOgIS70.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-D9UA7hUb.js → chunk-GF5L2VYU-CBjmgvJ_.js} +1 -1
  62. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-DPz7SZ8V.js → chunk-JWPE2WC7-CCQSht8M.js} +1 -1
  63. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BmuEOIh7.js → chunk-KBJHAD2P-BpGMkUUV.js} +1 -1
  64. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CIhBWnd8.js → chunk-RYQCIY6F-DNDT41QL.js} +1 -1
  65. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CScR6mDD.js → chunk-XXDRQBXY-euXnDFbn.js} +1 -1
  66. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BhlMVqCc.js +1 -0
  67. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BhlMVqCc.js +1 -0
  68. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BjpfWaUr.js → cose-bilkent-JH36ORCC-BJ6NAeql.js} +1 -1
  69. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BFQohJgT.js → cynefin-VYW2F7L2-F5JKIvIq.js} +1 -1
  70. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-D3gzRJnS.js → cynefinDiagram-MW4NZA55-Cwmkl_Gi.js} +1 -1
  71. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-5eojkhoA.js → dagre-VZM6K2ZE-BmCZ8815.js} +1 -1
  72. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CpD8LWu_.js → diagram-7IWD3JNH-zWA3hUnX.js} +1 -1
  73. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B8_4qJ2a.js → diagram-B4RE2ZJO-BzA-888F.js} +1 -1
  74. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-HOsXf0eu.js → diagram-LBJQPF4R-tPNheoch.js} +1 -1
  75. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BqwRwiWP.js → diagram-Q27KOJAE-Cdi7Fyqp.js} +1 -1
  76. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BRVCNZe1.js → diagram-UB23O5K3-DFK9VBf1.js} +1 -1
  77. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Cvb1TgGh.js → ebnfDiagram-BXEA7PRR-xCUr1kpg.js} +1 -1
  78. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DS0O0qG9.js → erDiagram-JOGREHBK-BANQHY_A.js} +1 -1
  79. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-BHIRvRr3.js → flowDiagram-UKHOOZJN-VHJYEMkP.js} +1 -1
  80. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DcCJ1JWx.js → ganttDiagram-PKOTCBZU-DiyZQ0C6.js} +1 -1
  81. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-B9ia5ony.js → gitGraphDiagram-DS77QQ5N-CZtIeBBa.js} +1 -1
  82. package/dist/worker/console/static/assets/index-BlN6ytLr.css +1 -0
  83. package/dist/worker/console/static/assets/index-CA-jfWlE.js +449 -0
  84. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-3ogpgegn.js → infoDiagram-6WML65LV-BuSspC0X.js} +1 -1
  85. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-4tgpQ5SL.js → ishikawaDiagram-WSZJBQD7-DRJZyPFy.js} +1 -1
  86. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DNfgHqFx.js → journeyDiagram-NVQOT4AX-DIfXIVuE.js} +1 -1
  87. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Dz7ngC8z.js → kanban-definition-27J2QSJJ-CT10_7Lt.js} +1 -1
  88. package/dist/worker/console/static/assets/{linear-DI7Jv62E.js → linear-ufemj74H.js} +1 -1
  89. package/dist/worker/console/static/assets/{mermaid.core-CQA4Nzmo.js → mermaid.core-DEQe8-_m.js} +5 -5
  90. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-CtsMc8Mf.js → mindmap-definition-FAOFIHXS-BRLUrbrC.js} +1 -1
  91. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-ByppE9yH.js → pegDiagram-VL7TDLO6-BgibKtiF.js} +1 -1
  92. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-B5PtgFgt.js → pieDiagram-7S7Q4E2Y-D3eyfdV9.js} +1 -1
  93. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Cq4p0k2K.js → quadrantDiagram-CIZ2JOQS-DXeTk5Px.js} +1 -1
  94. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-DjfkyljN.js → railroadDiagram-AXF67PYL-BgXdFEe7.js} +1 -1
  95. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DzynNRSl.js → requirementDiagram-LRYGKXZP-CseC2A7R.js} +1 -1
  96. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Cj7TCpTJ.js → sankeyDiagram-W5VNT64P-D80PFzfJ.js} +1 -1
  97. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-C73C6ykA.js → sequenceDiagram-SI44F4Z6-BWzHW7LS.js} +1 -1
  98. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-BiNLgN2x.js → sizeCapture-X5ZJPWSS-xWQYTLAn.js} +1 -1
  99. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-UwvjS64-.js → stateDiagram-OKZ733FA-CLxtpZeL.js} +1 -1
  100. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-CiVoq4G-.js +1 -0
  101. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-CC7DH98r.js → swimlanes-SLNWSIFB-DwugtmWV.js} +2 -2
  102. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BkdgdpfR.js +8 -0
  103. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-LSBXTR70.js → timeline-definition-Z64GVDOM-DHEOwHIe.js} +1 -1
  104. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BAuxWwGP.js → vennDiagram-T6HMQDX7-pmbZKlBK.js} +1 -1
  105. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y--E5_gjZx.js → wardleyDiagram-T6FBY63Y-CivHoM5q.js} +1 -1
  106. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-dJWNmyzF.js → xychartDiagram-ELKLHX3M-BfHIE7o6.js} +1 -1
  107. package/dist/worker/console/static/index.html +2 -2
  108. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +10 -122
  109. package/dist/worker/console/static-src/operator-chat/compaction-message.js +0 -7
  110. package/dist/worker/console/static-src/operator-chat/pending-user-message.js +15 -35
  111. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +62 -136
  112. package/dist/worker/console/static-src/operator-chat/useChatStream.js +68 -159
  113. package/dist/worker/console/static-src/operator-chat/useChatThread.js +56 -75
  114. package/dist/worker/console/static-src/operator-chat/useComposer.js +78 -206
  115. package/dist/worker/console/workspace-context.js +1 -21
  116. package/dist/worker/observe/static/router.js +1 -1
  117. package/dist/worker/observe/static/styles.css +4 -7
  118. package/dist/worker/observe/static/views/dag-inspector.js +19 -12
  119. package/dist/worker/observe/static/views/dag.js +2 -2
  120. package/dist/worker/scheduler/index.js +25 -0
  121. package/dist/workflows/dag/facts.js +4 -0
  122. package/dist/workflows/dag/frontend-closeout.js +1 -1
  123. package/dist/workflows/dag/frontend-design-policy.js +1 -2
  124. package/dist/workflows/dag/frontend-implementation-contract.js +4 -8
  125. package/dist/workflows/dag/frontend-prewrite-gate.js +3 -3
  126. package/dist/workflows/dag/frontend-repair.js +76 -55
  127. package/dist/workflows/dag/frontend-review-context.js +1 -2
  128. package/dist/workflows/dag/frontend-review-scopes.js +12 -34
  129. package/dist/workflows/dag/frontend-shape-facts.js +2 -12
  130. package/dist/workflows/dag/frontend-verification-trace.js +42 -8
  131. package/dist/workflows/dag/index.js +6 -0
  132. package/dist/workflows/dag/init-hybrid.js +160 -97
  133. package/dist/workflows/dynamic/index.js +7 -0
  134. package/docs/architecture/runtime-boundaries.md +3 -3
  135. package/docs/templates/frontend-implementation-dag.json +8 -9
  136. package/package.json +3 -7
  137. package/skills/frontend-bounded-implement/SKILL.md +3 -4
  138. package/skills/frontend-bounded-implement/references/code-standards.md +3 -3
  139. package/skills/frontend-contract/references/contract-protocol.md +1 -3
  140. package/skills/frontend-design-review/SKILL.md +7 -12
  141. package/skills/frontend-design-review/references/review-checklist.md +7 -7
  142. package/skills/frontend-plan/SKILL.md +0 -19
  143. package/skills/frontend-plan/references/decision-contract.md +19 -26
  144. package/skills/frontend-review/SKILL.md +18 -12
  145. package/skills/frontend-review/references/review-findings.md +43 -24
  146. package/dist/shared/pi-provider-recovery.js +0 -182
  147. package/dist/worker/console/chat/browser-automation.js +0 -363
  148. package/dist/worker/console/chat/browser-policy.js +0 -113
  149. package/dist/worker/console/chat/browser-routes.js +0 -162
  150. package/dist/worker/console/chat/pi-runtime/custom-tools/ask-user-question.js +0 -44
  151. package/dist/worker/console/chat/pi-runtime/custom-tools/browser-tools.js +0 -89
  152. package/dist/worker/console/chat/pi-runtime/custom-tools/explore-tools.js +0 -202
  153. package/dist/worker/console/chat/pi-runtime/custom-tools/goal-tools.js +0 -79
  154. package/dist/worker/console/chat/pi-runtime/custom-tools/operator-tools.js +0 -86
  155. package/dist/worker/console/chat/pi-runtime/custom-tools/todo-write.js +0 -38
  156. package/dist/worker/console/chat/pi-runtime/inspection-types.js +0 -28
  157. package/dist/worker/console/chat/pi-runtime/inspection.js +0 -439
  158. package/dist/worker/console/chat/pi-runtime/sdk-bindings.js +0 -153
  159. package/dist/worker/console/chat/pi-runtime/sdk-session.js +0 -5
  160. package/dist/worker/console/chat/scm-routes.js +0 -217
  161. package/dist/worker/console/chat/scm-service.js +0 -283
  162. package/dist/worker/console/chat/scm-tools.js +0 -111
  163. package/dist/worker/console/chat/session-mode-view.js +0 -49
  164. package/dist/worker/console/chat/session-mode.js +0 -220
  165. package/dist/worker/console/chat/terminal-routes.js +0 -216
  166. package/dist/worker/console/chat/terminal-sessions.js +0 -348
  167. package/dist/worker/console/chat/terminal-tools.js +0 -211
  168. package/dist/worker/console/static/assets/channel-BxdTlb_5.js +0 -1
  169. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-_ByA1Nf8.js +0 -1
  170. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-_ByA1Nf8.js +0 -1
  171. package/dist/worker/console/static/assets/index-B9goN3S5.css +0 -1
  172. package/dist/worker/console/static/assets/index-wXlQohNs.js +0 -469
  173. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-nLgMZ0D3.js +0 -1
  174. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BcYqTady.js +0 -8
  175. package/dist/worker/console/static-src/operator-chat/active-turn-tail.js +0 -10
  176. package/dist/worker/console/static-src/operator-chat/chat-link.js +0 -94
  177. package/dist/worker/console/static-src/operator-chat/chat-scroll-position.js +0 -24
  178. package/dist/worker/console/static-src/operator-chat/composer-session-ledger.js +0 -192
  179. package/dist/worker/console/static-src/operator-chat/process-label.js +0 -41
  180. package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +0 -13
  181. package/dist/worker/console/static-src/operator-chat/turn-process-disclosure.js +0 -10
  182. package/dist/worker/console/static-src/operator-chat/use-searchable-hidden.js +0 -51
  183. package/dist/worker/console/static-src/operator-chat/user-turn-anchor-equality.js +0 -21
  184. package/dist/worker/console/workspace-initialization.js +0 -209
  185. package/dist/workflows/dag/frontend-capacity.js +0 -9
  186. package/dist/workflows/dag/frontend-plan-completeness.js +0 -186
  187. package/dist/workflows/dag/frontend-plan-recovery-policy.js +0 -18
@@ -1,12 +1,10 @@
1
- import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from "react";
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
2
  import { buildTurnProcessGroups, hydrateSessionThread, selectRecentTurnGroups, } from "../../chat/turn-process.js";
3
- import { isToolFailure } from "../../chat/tool-preview.js";
4
3
  import { mergeOperationCardState, } from "../../chat/operation-card.js";
5
4
  import { createChatSseEventApplier, } from "./chat-sse-events.js";
6
5
  import { mergePendingSubmissionOnHydration } from "./pending-user-message.js";
7
6
  import { taskContextUnchanged } from "./format.js";
8
7
  import { upsertCompactionMessage } from "./compaction-message.js";
9
- import { sameTurnGroupInputs } from "./turn-group-equality.js";
10
8
  const EMPTY_USAGE = {
11
9
  inputTokens: 0,
12
10
  outputTokens: 0,
@@ -117,12 +115,6 @@ export function useChatThread(params) {
117
115
  // (UI-08); the SSE applier must not open the process panel.
118
116
  void params.setProcessOpen;
119
117
  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]);
126
118
  const [toolCalls, setToolCalls] = useState([]);
127
119
  const [operationCards, setOperationCards] = useState([]);
128
120
  const [artifactCards, setArtifactCards] = useState([]);
@@ -130,7 +122,7 @@ export function useChatThread(params) {
130
122
  const [historyHasMore, setHistoryHasMore] = useState(false);
131
123
  const [historyNextCursor, setHistoryNextCursor] = useState(null);
132
124
  const [historyLoading, setHistoryLoading] = useState(false);
133
- const historyLoadingRef = useRef(null);
125
+ const historyLoadingRef = useRef(false);
134
126
  const [unread, setUnread] = useState(0);
135
127
  const [humanGateCards, setHumanGateCards] = useState([]);
136
128
  const [interview, setInterview] = useState(null);
@@ -150,6 +142,7 @@ export function useChatThread(params) {
150
142
  const [streamingAssistantId, setStreamingAssistantId] = useState(null);
151
143
  const [showSystemInMessages] = useState(false);
152
144
  const completedGroupsRef = useRef([]);
145
+ const previousTimelineRef = useRef(null);
153
146
  const refreshSessionTelemetry = useCallback(async () => {
154
147
  const targetSessionId = refs.sessionRef.current?.sessionId;
155
148
  if (!targetSessionId)
@@ -194,7 +187,6 @@ export function useChatThread(params) {
194
187
  onTelemetryRefresh: () => void refreshSessionTelemetry(),
195
188
  setMessages,
196
189
  setToolCalls,
197
- getMessages: () => messagesRef.current,
198
190
  }), [refs, onReconcile, setError, refreshSessionTelemetry]);
199
191
  /** Parse + apply one raw SSE block (frames separated by \n\n). Returns the
200
192
  * eventId if the block carried an `id:` line (for Last-Event-ID tracking). */
@@ -293,7 +285,7 @@ export function useChatThread(params) {
293
285
  if (toolCalls.length === 0)
294
286
  return null;
295
287
  const running = toolCalls.filter((t) => t.status === "running");
296
- const errored = toolCalls.filter((t) => isToolFailure(t));
288
+ const errored = toolCalls.filter((t) => t.status === "error");
297
289
  const current = running[running.length - 1];
298
290
  let statusLabel = "完成";
299
291
  let statusKind = "ok";
@@ -316,26 +308,55 @@ export function useChatThread(params) {
316
308
  };
317
309
  }, [toolCalls]);
318
310
  const turnGroups = useMemo(() => {
319
- // Server confirmation can change ordering without changing message ids.
320
- // Rebuild membership from canonical messages; cache only finished row
321
- // objects below so rich historical content still avoids re-rendering.
322
- const built = buildTurnProcessGroups({
323
- messages: timelineMessages,
324
- toolCalls,
325
- streaming,
326
- activeAssistantId: streamingAssistantId,
327
- });
328
- // Preserve completed group objects across streaming tokens. Stable
329
- // historical groups keep their references so
311
+ const previousTimeline = previousTimelineRef.current;
312
+ let built;
313
+ const activeIndex = streamingAssistantId
314
+ ? timelineMessages.findIndex((message) => message.id === streamingAssistantId)
315
+ : -1;
316
+ const canBuildActiveTail = Boolean(previousTimeline && streaming && streamingAssistantId && activeIndex >= 0) &&
317
+ previousTimeline?.activeAssistantId === streamingAssistantId &&
318
+ (previousTimeline?.messages.every((message, index) => message.id === streamingAssistantId || timelineMessages[index]?.id === message.id) ?? false);
319
+ if (canBuildActiveTail && previousTimeline) {
320
+ let tailStart = activeIndex;
321
+ while (tailStart > 0 && timelineMessages[tailStart - 1]?.role !== "user")
322
+ tailStart -= 1;
323
+ const tailMessages = timelineMessages.slice(tailStart);
324
+ const tailIds = new Set(tailMessages.map((message) => message.id));
325
+ const tailTurnIds = new Set(tailMessages.map((message) => message.turnId).filter(Boolean));
326
+ const tailTools = toolCalls.filter((tool) => tailIds.has(tool.assistantMessageId ?? "") ||
327
+ (Boolean(tool.turnId) && tailTurnIds.has(tool.turnId)) ||
328
+ (tool.createdAt >= (tailMessages[0]?.createdAt ?? Number.POSITIVE_INFINITY)));
329
+ const activeTail = buildTurnProcessGroups({
330
+ messages: tailMessages,
331
+ toolCalls: tailTools,
332
+ streaming,
333
+ activeAssistantId: streamingAssistantId,
334
+ });
335
+ built = [...previousTimeline.groups.filter((group) => !group.live), ...activeTail];
336
+ }
337
+ else {
338
+ built = buildTurnProcessGroups({
339
+ messages: timelineMessages,
340
+ toolCalls,
341
+ streaming,
342
+ activeAssistantId: streamingAssistantId,
343
+ });
344
+ }
345
+ // Preserve completed group objects across streaming tokens. The active
346
+ // tail is rebuilt, while stable historical groups keep their references so
330
347
  // memoized rows do not re-render or re-parse the full conversation.
331
348
  const previous = new Map(completedGroupsRef.current.map((group) => [group.key, group]));
332
349
  const all = built.map((group) => {
333
350
  const cached = previous.get(group.key);
334
- return cached && !group.live && sameTurnGroupInputs(cached, group)
335
- ? cached
336
- : group;
351
+ return cached && !group.live ? cached : group;
337
352
  });
338
353
  completedGroupsRef.current = all.filter((group) => !group.live);
354
+ previousTimelineRef.current = {
355
+ messages: timelineMessages,
356
+ toolCalls,
357
+ activeAssistantId: streamingAssistantId,
358
+ groups: all,
359
+ };
339
360
  return selectRecentTurnGroups(all, historyLimit);
340
361
  }, [
341
362
  timelineMessages,
@@ -344,14 +365,12 @@ export function useChatThread(params) {
344
365
  streamingAssistantId,
345
366
  historyLimit,
346
367
  ]);
347
- const loadEarlierMessages = useCallback(async (beforePrepend) => {
368
+ const loadEarlierMessages = useCallback(async () => {
348
369
  const targetSessionId = refs.sessionRef.current?.sessionId;
349
370
  const beforeId = historyNextCursor;
350
371
  if (!targetSessionId || !historyHasMore || !beforeId || historyLoadingRef.current)
351
372
  return false;
352
- const generation = refs.sessionGenerationRef.current;
353
- const request = Symbol();
354
- historyLoadingRef.current = request;
373
+ historyLoadingRef.current = true;
355
374
  setHistoryLoading(true);
356
375
  try {
357
376
  const query = new URLSearchParams({ limit: "50", beforeId });
@@ -359,12 +378,7 @@ export function useChatThread(params) {
359
378
  if (!res.ok || refs.sessionRef.current?.sessionId !== targetSessionId)
360
379
  return false;
361
380
  const body = (await res.json());
362
- if (refs.sessionRef.current?.sessionId !== targetSessionId ||
363
- refs.sessionGenerationRef.current !== generation ||
364
- historyLoadingRef.current !== request)
365
- return false;
366
381
  const page = hydrateSessionThread(body.messages ?? []);
367
- beforePrepend?.();
368
382
  setMessages((current) => {
369
383
  const seen = new Set();
370
384
  const merged = [...page.messages, ...current].filter((message) => {
@@ -390,19 +404,11 @@ export function useChatThread(params) {
390
404
  setHistoryLimit((value) => value + 50);
391
405
  return true;
392
406
  }
393
- catch (error) {
394
- if (refs.sessionRef.current?.sessionId === targetSessionId && refs.sessionGenerationRef.current === generation) {
395
- setError(`加载历史消息失败:${error instanceof Error ? error.message : String(error)}`);
396
- }
397
- return false;
398
- }
399
407
  finally {
400
- if (historyLoadingRef.current === request) {
401
- historyLoadingRef.current = null;
402
- setHistoryLoading(false);
403
- }
408
+ historyLoadingRef.current = false;
409
+ setHistoryLoading(false);
404
410
  }
405
- }, [historyHasMore, historyNextCursor, origin, refs, setError]);
411
+ }, [historyHasMore, historyNextCursor, origin, refs]);
406
412
  const visibleMessages = useMemo(() => {
407
413
  const next = [];
408
414
  for (const group of turnGroups) {
@@ -436,41 +442,15 @@ export function useChatThread(params) {
436
442
  setHistoryLimit(50);
437
443
  setHistoryHasMore(false);
438
444
  setHistoryNextCursor(null);
439
- historyLoadingRef.current = null;
440
- setHistoryLoading(false);
445
+ historyLoadingRef.current = false;
441
446
  completedGroupsRef.current = [];
447
+ previousTimelineRef.current = null;
442
448
  refs.usageResponseKeysRef.current = new Set();
443
449
  }, [refs]);
444
450
  /** Adopt a freshly activated session's hydrated thread + streaming flags. */
445
451
  const applyActivatedSession = useCallback((next) => {
446
- historyLoadingRef.current = null;
447
- setHistoryLoading(false);
448
452
  const visuallyStopped = next.turnPhase === "stopping";
449
- // A submission still owned for THIS session keeps its optimistic bubble
450
- // across an activation snapshot (refresh / reopen); a lease owned by
451
- // another session must never leak into this thread (2026-09-08).
452
- const ownedPending = refs.pendingSubmissionRef.current?.sessionId ===
453
- refs.sessionRef.current?.sessionId
454
- ? refs.pendingSubmissionRef.current
455
- : undefined;
456
- setMessages(mergePendingSubmissionOnHydration(next.messages, ownedPending
457
- ? {
458
- clientRequestId: ownedPending.clientRequestId,
459
- user: {
460
- id: ownedPending.userMessageId,
461
- role: "user",
462
- text: ownedPending.text,
463
- createdAt: Date.now(),
464
- clientRequestId: ownedPending.clientRequestId,
465
- },
466
- assistant: {
467
- id: ownedPending.assistantId,
468
- role: "assistant",
469
- text: "",
470
- createdAt: Date.now(),
471
- },
472
- }
473
- : undefined));
453
+ setMessages(next.messages);
474
454
  setToolCalls(next.toolCalls);
475
455
  setOperationCards([]);
476
456
  setHumanGateCards([]);
@@ -496,6 +476,7 @@ export function useChatThread(params) {
496
476
  setHistoryHasMore(next.history?.hasMore === true);
497
477
  setHistoryNextCursor(next.history?.nextCursor ?? null);
498
478
  completedGroupsRef.current = [];
479
+ previousTimelineRef.current = null;
499
480
  }, [refs]);
500
481
  /** Reconcile recovery (AC-001/AC-002/AC-003): reload the canonical session
501
482
  * facts (session-store messages + event-ring tail cursor) and REPLACE the
@@ -1,103 +1,62 @@
1
- import { useCallback, useEffect, useLayoutEffect, useMemo, useReducer, useRef, useState, } from "react";
1
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from "react";
2
2
  import { filterSlashCommands, mergeSlashCommands, } from "../../chat/shortcuts.js";
3
- import { ComposerSessionLedger, composerSessionKey, } from "./composer-session-ledger.js";
4
- import { confirmationToken, requestId } from "./format.js";
3
+ import { confirmationToken } from "./format.js";
5
4
  import { clampShortcutIndex } from "./slash-palette-nav.js";
5
+ import { createInputHistoryState, navigateInputHistory, recordInputHistoryEntry, resetInputHistoryBrowsing, } from "./input-history.js";
6
6
  import { attachRuntimeSnapshotController } from "./useRuntimeSnapshot.js";
7
- function sameComposerSession(left, right) {
8
- if (!left || !right)
9
- return left === right;
10
- return composerSessionKey(left) === composerSessionKey(right);
11
- }
12
- function readPendingImage(file) {
13
- return new Promise((resolve, reject) => {
14
- const reader = new FileReader();
15
- reader.onerror = () => reject(reader.error ?? new Error("图片读取失败"));
16
- reader.onload = () => {
17
- const value = String(reader.result ?? "");
18
- resolve({
19
- name: file.name,
20
- type: "image",
21
- mimeType: file.type,
22
- data: value.slice(value.indexOf(",") + 1),
23
- });
24
- };
25
- reader.readAsDataURL(file);
26
- });
27
- }
28
7
  /** Composer state: input text, @file palette, image attachments, /shortcuts,
29
8
  * IME composition guards, auto-grow and server-side draft sync. */
30
9
  export function useComposer(params) {
31
- const { origin, sessionIdentity, sessionMode, sessionReady = true, refs, setError, onUnrecoverableSession, } = params;
32
- const sessionId = sessionIdentity?.sessionId;
33
- const ledgerRef = useRef(null);
34
- if (!ledgerRef.current)
35
- ledgerRef.current = new ComposerSessionLedger();
36
- const ledger = ledgerRef.current;
37
- const activeIdentityRef = useRef(sessionIdentity);
38
- activeIdentityRef.current = sessionIdentity;
39
- const [, forceRender] = useReducer((version) => version + 1, 0);
40
- const activeSnapshot = sessionIdentity
41
- ? ledger.snapshot(sessionIdentity)
42
- : { draft: "", images: [] };
43
- const input = activeSnapshot.draft;
44
- const pendingImages = activeSnapshot.images;
10
+ const { origin, sessionId, refs, setError, onUnrecoverableSession } = params;
11
+ const [input, setInputState] = useState("");
45
12
  const [fileOptions, setFileOptions] = useState([]);
13
+ const [pendingImages, setPendingImages] = useState([]);
46
14
  const [shortcutOpen, setShortcutOpen] = useState(false);
47
15
  /** Any composer text that starts with `/` should reveal the palette,
48
16
  * including draft restore. Escape can still close it without clearing. */
49
17
  const setInput = useCallback((value) => {
50
- const identity = activeIdentityRef.current;
51
- if (!identity)
52
- return;
53
- const previous = ledger.snapshot(identity).draft;
54
- const next = typeof value === "function" ? value(previous) : value;
55
- if (ledger.setDraft(identity, next))
56
- forceRender();
57
- setShortcutOpen(next.startsWith("/"));
58
- }, [ledger]);
59
- const setPendingImages = useCallback((value) => {
60
- const identity = activeIdentityRef.current;
61
- if (!identity)
18
+ if (typeof value === "function") {
19
+ setInputState((prev) => {
20
+ const next = value(prev);
21
+ setShortcutOpen(next.startsWith("/"));
22
+ return next;
23
+ });
62
24
  return;
63
- const previous = ledger.snapshot(identity).images;
64
- const next = typeof value === "function" ? value(previous) : value;
65
- ledger.setImages(identity, next);
66
- forceRender();
67
- }, [ledger]);
25
+ }
26
+ setInputState(value);
27
+ setShortcutOpen(value.startsWith("/"));
28
+ }, []);
68
29
  const [shortcutIndex, setShortcutIndex] = useState(0);
69
30
  const [dynamicCommands, setDynamicCommands] = useState([]);
70
- const [commandCapabilities, setCommandCapabilities] = useState({
71
- sddBingo: false,
72
- sessionMode: undefined,
73
- });
31
+ const [commandCapabilities, setCommandCapabilities] = useState({ sddBingo: false });
74
32
  const [dynamicCommandsLoading, setDynamicCommandsLoading] = useState(false);
75
33
  const [dynamicCommandsError, setDynamicCommandsError] = useState(null);
76
34
  const [commandsEpoch, setCommandsEpoch] = useState(0);
77
35
  const textareaRef = useRef(null);
36
+ /** Arrow-key input history; unsent drafts survive history browsing. */
37
+ const historyRef = useRef(createInputHistoryState());
78
38
  /** Active session generation for /commands; bumped immediately on switch (AC-2). */
79
39
  const commandsSessionRef = useRef(sessionId);
80
40
  const commandsGenerationRef = useRef(0);
81
41
  const handleInputChange = useCallback((value) => {
82
42
  setInput(value);
43
+ setShortcutOpen(value.startsWith("/"));
83
44
  setShortcutIndex(0);
84
- }, [setInput]);
45
+ // Manual typing edits a recalled entry in place → back to present draft.
46
+ historyRef.current = resetInputHistoryBrowsing(historyRef.current);
47
+ }, []);
85
48
  /** Recall an earlier input via ↑/↓; returns true when the key was consumed. */
86
49
  const navigateHistory = useCallback((direction) => {
87
- const identity = activeIdentityRef.current;
88
- if (!identity)
89
- return false;
90
- const current = ledger.snapshot(identity).draft;
91
50
  const textarea = textareaRef.current;
92
- const decision = ledger.navigateHistory(identity, direction, {
93
- text: current,
94
- caretStart: textarea?.selectionStart ?? current.length,
95
- caretEnd: textarea?.selectionEnd ?? current.length,
51
+ const decision = navigateInputHistory(historyRef.current, direction, {
52
+ text: input,
53
+ caretStart: textarea?.selectionStart ?? input.length,
54
+ caretEnd: textarea?.selectionEnd ?? input.length,
96
55
  });
97
56
  if (!decision.handled)
98
57
  return false;
99
- forceRender();
100
- setShortcutOpen(decision.text.startsWith("/"));
58
+ historyRef.current = decision.state;
59
+ setInput(decision.text);
101
60
  const caret = decision.text.length;
102
61
  queueMicrotask(() => {
103
62
  const el = textareaRef.current;
@@ -106,76 +65,22 @@ export function useComposer(params) {
106
65
  }
107
66
  });
108
67
  return true;
109
- }, [ledger]);
110
- /** Record a submitted text into the selected session's history. */
68
+ }, [input, setInput]);
69
+ /** Record a submitted text into history (consecutive duplicates collapse). */
111
70
  const recordHistory = useCallback((text) => {
112
- const identity = activeIdentityRef.current;
113
- if (identity)
114
- ledger.recordHistory(identity, text);
115
- }, [ledger]);
116
- const claimSubmission = useCallback(() => {
117
- const identity = activeIdentityRef.current;
118
- if (!identity)
119
- return null;
120
- const attempt = ledger.beginSubmission(identity, refs.sessionGenerationRef.current, requestId("chat-submit"));
121
- if (!attempt)
122
- return null;
123
- forceRender();
124
- setShortcutOpen(false);
125
- setShortcutIndex(0);
126
- return attempt;
127
- }, [ledger, refs]);
128
- const claimSessionSubmission = useCallback((identity, text, images = []) => {
129
- ledger.setDraft(identity, text);
130
- ledger.setImages(identity, images);
131
- const attempt = ledger.beginSubmission(identity, refs.sessionGenerationRef.current, requestId("chat-submit"));
132
- if (sameComposerSession(activeIdentityRef.current, identity))
133
- forceRender();
134
- return attempt;
135
- }, [ledger, refs]);
136
- const settleSubmission = useCallback((attempt, outcome) => {
137
- const result = ledger.settle(attempt, outcome);
138
- if (sameComposerSession(activeIdentityRef.current, attempt.identity))
139
- forceRender();
140
- return result;
141
- }, [ledger]);
142
- const settleSubmissionByRequest = useCallback((identity, clientRequestId, outcome) => {
143
- const result = ledger.settleByRequest(identity, clientRequestId, outcome);
144
- if (sameComposerSession(activeIdentityRef.current, identity))
145
- forceRender();
146
- return result;
147
- }, [ledger]);
148
- const seedSession = useCallback((identity, draft) => {
149
- const seeded = ledger.seedDraft(identity, draft);
150
- if (seeded && sameComposerSession(activeIdentityRef.current, identity))
151
- forceRender();
152
- return seeded;
153
- }, [ledger]);
154
- const replaceSessionDraft = useCallback((identity, draft, images) => {
155
- ledger.setDraft(identity, draft);
156
- ledger.setImages(identity, images);
157
- if (sameComposerSession(activeIdentityRef.current, identity))
158
- forceRender();
159
- }, [ledger]);
160
- const releaseSession = useCallback((identity) => {
161
- ledger.release(identity);
162
- if (sameComposerSession(activeIdentityRef.current, identity))
163
- forceRender();
164
- }, [ledger]);
71
+ historyRef.current = recordInputHistoryEntry(historyRef.current, text);
72
+ }, []);
165
73
  // Immediate invalidation on A→B even when B palette never opens (AC-2).
166
74
  useLayoutEffect(() => {
167
75
  commandsSessionRef.current = sessionId;
168
76
  commandsGenerationRef.current += 1;
169
- refs.isComposingRef.current = false;
170
- setFileOptions([]);
171
- setShortcutOpen(input.startsWith("/"));
172
77
  setDynamicCommands([]);
173
- setCommandCapabilities({ sddBingo: false, sessionMode });
78
+ setCommandCapabilities({ sddBingo: false });
174
79
  setDynamicCommandsError(null);
175
80
  setDynamicCommandsLoading(false);
176
- }, [sessionId, sessionMode]);
81
+ }, [sessionId]);
177
82
  const loadDynamicCommands = useCallback(async () => {
178
- if (!sessionId || !sessionReady) {
83
+ if (!sessionId) {
179
84
  setDynamicCommands([]);
180
85
  setDynamicCommandsError(null);
181
86
  setDynamicCommandsLoading(false);
@@ -219,10 +124,7 @@ export function useComposer(params) {
219
124
  if (!isCurrent())
220
125
  return;
221
126
  setDynamicCommands(items);
222
- setCommandCapabilities({
223
- sddBingo: body?.capabilities?.sddBingo === true,
224
- sessionMode: body?.capabilities?.sessionMode ?? sessionMode,
225
- });
127
+ setCommandCapabilities({ sddBingo: body?.capabilities?.sddBingo === true });
226
128
  // Compact server fail-soft / partial-source warning; local Operator/Web still merge.
227
129
  const warning = typeof body?.warning === "string" ? body.warning.trim() : "";
228
130
  setDynamicCommandsError(warning ? warning.slice(0, 160) : null);
@@ -232,28 +134,28 @@ export function useComposer(params) {
232
134
  return;
233
135
  setDynamicCommandsError("动态命令加载失败");
234
136
  setDynamicCommands([]);
235
- setCommandCapabilities({ sddBingo: false, sessionMode });
137
+ setCommandCapabilities({ sddBingo: false });
236
138
  }
237
139
  finally {
238
140
  if (isCurrent()) {
239
141
  setDynamicCommandsLoading(false);
240
142
  }
241
143
  }
242
- }, [origin, sessionId, sessionMode, sessionReady]);
144
+ }, [origin, sessionId]);
243
145
  // Lazy-load dynamic commands when slash palette opens or session changes.
244
146
  useEffect(() => {
245
- if (!sessionId || !sessionReady)
147
+ if (!sessionId)
246
148
  return;
247
149
  if (!shortcutOpen && commandsEpoch === 0)
248
150
  return;
249
151
  void loadDynamicCommands();
250
- }, [sessionId, sessionReady, shortcutOpen, commandsEpoch, loadDynamicCommands]);
152
+ }, [sessionId, shortcutOpen, commandsEpoch, loadDynamicCommands]);
251
153
  const retryDynamicCommands = useCallback(() => {
252
154
  setCommandsEpoch((n) => n + 1);
253
155
  void loadDynamicCommands();
254
156
  }, [loadDynamicCommands]);
255
157
  useEffect(() => {
256
- if (!sessionId || !sessionIdentity || !sessionReady)
158
+ if (!sessionId)
257
159
  return;
258
160
  const match = input
259
161
  .slice(0, textareaRef.current?.selectionStart ?? input.length)
@@ -262,29 +164,13 @@ export function useComposer(params) {
262
164
  setFileOptions([]);
263
165
  return;
264
166
  }
265
- const owner = { ...sessionIdentity };
266
- const ownerKey = composerSessionKey(owner);
267
- const generation = refs.sessionGenerationRef.current;
268
- const controller = new AbortController();
269
167
  const timer = window.setTimeout(() => {
270
- void fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(sessionId)}/files?q=${encodeURIComponent(match[1] ?? "")}`, { signal: controller.signal })
168
+ void fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(sessionId)}/files?q=${encodeURIComponent(match[1] ?? "")}`)
271
169
  .then((r) => (r.ok ? r.json() : null))
272
- .then((body) => {
273
- const active = activeIdentityRef.current;
274
- if (controller.signal.aborted ||
275
- !active ||
276
- composerSessionKey(active) !== ownerKey ||
277
- refs.sessionGenerationRef.current !== generation)
278
- return;
279
- setFileOptions((body?.files ?? []).map((item) => item.path));
280
- })
281
- .catch(() => undefined);
170
+ .then((body) => setFileOptions((body?.files ?? []).map((item) => item.path)));
282
171
  }, 150);
283
- return () => {
284
- window.clearTimeout(timer);
285
- controller.abort();
286
- };
287
- }, [origin, sessionId, sessionIdentity, sessionReady, input, refs]);
172
+ return () => window.clearTimeout(timer);
173
+ }, [origin, sessionId, input]);
288
174
  const insertFileReference = useCallback((file) => {
289
175
  const caret = textareaRef.current?.selectionStart ?? input.length;
290
176
  const before = input.slice(0, caret);
@@ -293,51 +179,44 @@ export function useComposer(params) {
293
179
  return;
294
180
  setInput(`${input.slice(0, start)}@${file} ${input.slice(caret)}`);
295
181
  setFileOptions([]);
296
- }, [input, setInput]);
182
+ }, [input]);
297
183
  const addImagesFromFiles = useCallback((files) => {
298
- const owner = activeIdentityRef.current;
299
- if (!owner)
300
- return;
301
- const ownerIdentity = { ...owner };
302
- const selected = Array.from(files).slice(0, 4);
303
- const valid = selected.filter((file) => file.type.startsWith("image/") && file.size <= 5 * 1024 * 1024);
304
- if (valid.length !== selected.length)
305
- setError("图片仅支持 PNG/JPEG/GIF/WebP,单张不超过 5 MiB");
306
- void Promise.all(valid.map(readPendingImage)).then((loaded) => {
307
- if (!ledger.has(ownerIdentity) || loaded.length === 0)
308
- return;
309
- const current = ledger.snapshot(ownerIdentity).images;
310
- ledger.setImages(ownerIdentity, [...current, ...loaded].slice(0, 4));
311
- if (sameComposerSession(activeIdentityRef.current, ownerIdentity))
312
- forceRender();
313
- }, (error) => {
314
- if (sameComposerSession(activeIdentityRef.current, ownerIdentity))
315
- setError(error instanceof Error ? error.message : "图片读取失败");
316
- });
317
- }, [ledger, setError]);
184
+ for (const file of Array.from(files).slice(0, 4)) {
185
+ if (!file.type.startsWith("image/") || file.size > 5 * 1024 * 1024) {
186
+ setError("图片仅支持 PNG/JPEG/GIF/WebP,单张不超过 5 MiB");
187
+ continue;
188
+ }
189
+ const reader = new FileReader();
190
+ reader.onload = () => {
191
+ const value = String(reader.result ?? "");
192
+ const data = value.slice(value.indexOf(",") + 1);
193
+ setPendingImages((items) => [
194
+ ...items,
195
+ {
196
+ name: file.name,
197
+ type: "image",
198
+ mimeType: file.type,
199
+ data,
200
+ },
201
+ ].slice(0, 4));
202
+ };
203
+ reader.readAsDataURL(file);
204
+ }
205
+ }, [setError]);
318
206
  const removeImage = useCallback((image) => {
319
207
  setPendingImages((items) => items.filter((item) => item !== image));
320
- }, [setPendingImages]);
321
- // Chromium handles growth in CSS without forcing a layout read on every key.
322
- // Keep a deferred fallback for browsers that do not support field-sizing.
323
- useEffect(() => {
208
+ }, []);
209
+ // Auto-grow textarea height.
210
+ useLayoutEffect(() => {
324
211
  const ta = textareaRef.current;
325
212
  if (!ta)
326
213
  return;
327
- if (typeof CSS !== "undefined" &&
328
- CSS.supports("field-sizing", "content")) {
329
- ta.style.removeProperty("height");
330
- return;
331
- }
332
- const frame = window.requestAnimationFrame(() => {
333
- ta.style.height = "auto";
334
- ta.style.height = `${Math.min(ta.scrollHeight, 280)}px`;
335
- });
336
- return () => window.cancelAnimationFrame(frame);
214
+ ta.style.height = "auto";
215
+ ta.style.height = `${Math.min(ta.scrollHeight, 280)}px`;
337
216
  }, [input]);
338
217
  // Debounced server-side composer draft sync.
339
218
  useEffect(() => {
340
- if (!sessionId || !sessionReady)
219
+ if (!sessionId)
341
220
  return;
342
221
  const timer = window.setTimeout(() => {
343
222
  void fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(sessionId)}/draft`, {
@@ -351,7 +230,7 @@ export function useComposer(params) {
351
230
  });
352
231
  }, 400);
353
232
  return () => window.clearTimeout(timer);
354
- }, [origin, sessionId, sessionReady, input]);
233
+ }, [origin, sessionId, input]);
355
234
  const mergedCommands = useMemo(() => mergeSlashCommands(dynamicCommands, commandCapabilities), [dynamicCommands, commandCapabilities]);
356
235
  const filteredShortcuts = useMemo(() => input.startsWith("/")
357
236
  ? filterSlashCommands(mergedCommands, input)
@@ -385,24 +264,17 @@ export function useComposer(params) {
385
264
  useEffect(() => {
386
265
  attachRuntimeSnapshotController({
387
266
  origin,
388
- sessionId: sessionReady ? sessionId : undefined,
267
+ sessionId,
389
268
  refs,
390
269
  onUnrecoverableSession,
391
270
  });
392
- }, [origin, sessionId, sessionReady, refs, onUnrecoverableSession]);
271
+ }, [origin, sessionId, refs, onUnrecoverableSession]);
393
272
  return {
394
273
  input,
395
274
  setInput,
396
275
  handleInputChange,
397
276
  navigateHistory,
398
277
  recordHistory,
399
- claimSubmission,
400
- claimSessionSubmission,
401
- settleSubmission,
402
- settleSubmissionByRequest,
403
- seedSession,
404
- replaceSessionDraft,
405
- releaseSession,
406
278
  fileOptions,
407
279
  pendingImages,
408
280
  setPendingImages,