@tea-agent/loop-agent 0.44.0-next.1 → 0.44.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 (99) hide show
  1. package/CHANGELOG.md +178 -175
  2. package/dist/build-stamp.json +3 -3
  3. package/dist/executors/pi-sdk-executor.js +29 -1
  4. package/dist/shared/wizard-local-telemetry.js +50 -0
  5. package/dist/worker/console/chat/chat-event-store.js +3 -0
  6. package/dist/worker/console/chat/db-connections.js +790 -0
  7. package/dist/worker/console/chat/pi-runtime/custom-tools/operator-tools.js +1 -1
  8. package/dist/worker/console/chat/pi-runtime/custom-tools/sql-tools.js +627 -0
  9. package/dist/worker/console/chat/pi-runtime.js +66 -6
  10. package/dist/worker/console/chat/routes.js +154 -3
  11. package/dist/worker/console/chat/sdd-data-alignment.js +8 -31
  12. package/dist/worker/console/chat/timeline-message-order.js +22 -0
  13. package/dist/worker/console/chat/tools.js +34 -0
  14. package/dist/worker/console/chat/turn-process.js +43 -39
  15. package/dist/worker/console/dag-confirmation.js +3 -0
  16. package/dist/worker/console/dag-execution-receipt.js +3 -0
  17. package/dist/worker/console/operator-actions.js +32 -2
  18. package/dist/worker/console/server.js +5 -1
  19. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-8-j6y-sd.js → abnfDiagram-N423BO3Z-COQlLTI7.js} +1 -1
  20. package/dist/worker/console/static/assets/{arc-eoQiMvuk.js → arc-zN3QkNaV.js} +1 -1
  21. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-C4A3uMcI.js → architectureDiagram-T3A2C74G-Pyoyj5f2.js} +1 -1
  22. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-D4zD2F-Q.js → blockDiagram-VBNYF7ZC-BNppA5WW.js} +1 -1
  23. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-j1RkJziL.js → c4Diagram-5PPSVZJV-lyxL8nTx.js} +1 -1
  24. package/dist/worker/console/static/assets/channel-CgLPkTw2.js +1 -0
  25. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-YBHmhik1.js → chunk-2GRJ4B5K-0hgjcdK4.js} +1 -1
  26. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-COfWyo9P.js → chunk-2Q5K7J3B-D63N9d4i.js} +1 -1
  27. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-I99OUkHY.js → chunk-5RXB4S5H-CUw2AyB5.js} +1 -1
  28. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-XKxoJNJ4.js → chunk-5VM5RSS4-Q_kwZT30.js} +1 -1
  29. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DLT_cYx1.js → chunk-6Q2QTUOP-C7vMiKcw.js} +1 -1
  30. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CxcKZ9mV.js → chunk-GF5L2VYU-T9GbV5Hz.js} +1 -1
  31. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-V1EqXdjY.js → chunk-JWPE2WC7-D8nUSUJI.js} +1 -1
  32. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DebTtdFp.js → chunk-KBJHAD2P-BCH2ewdp.js} +1 -1
  33. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-B-ivNRDf.js → chunk-RYQCIY6F-BTdDuDBN.js} +1 -1
  34. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DC_Ds11b.js → chunk-XXDRQBXY-Cqm2RcKF.js} +1 -1
  35. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-CuSD1Mhb.js +1 -0
  36. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-CuSD1Mhb.js +1 -0
  37. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-sWEqKwIb.js → cose-bilkent-JH36ORCC-pjwfpVu4.js} +1 -1
  38. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BXa_dcu4.js → cynefin-VYW2F7L2-BvalptKA.js} +1 -1
  39. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-C-Mle84F.js → cynefinDiagram-MW4NZA55-BU4qJ6d7.js} +1 -1
  40. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CXPDBITe.js → dagre-VZM6K2ZE-DVTpq8J9.js} +1 -1
  41. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CC-WJQfa.js → diagram-7IWD3JNH-B8s1W6Fc.js} +1 -1
  42. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CDAV5Vs4.js → diagram-B4RE2ZJO-DPrFk9yH.js} +1 -1
  43. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CmFiAcNz.js → diagram-LBJQPF4R-CnQIQsHC.js} +1 -1
  44. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DPBZHuyn.js → diagram-Q27KOJAE-op0DEpCv.js} +1 -1
  45. package/dist/worker/console/static/assets/{diagram-UB23O5K3-jUlm_Ds3.js → diagram-UB23O5K3-DXQamm4T.js} +1 -1
  46. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DWhQ3mfY.js → ebnfDiagram-BXEA7PRR-b1ad2C0r.js} +1 -1
  47. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-Tr2gMqet.js → erDiagram-JOGREHBK-B4cD-ZFJ.js} +1 -1
  48. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-DJjVQHPA.js → flowDiagram-UKHOOZJN-CrgGjlQ1.js} +1 -1
  49. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-D74dQ4u0.js → ganttDiagram-PKOTCBZU-DovmXQNV.js} +1 -1
  50. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DwW0tZ0X.js → gitGraphDiagram-DS77QQ5N-DpFyu2Jn.js} +1 -1
  51. package/dist/worker/console/static/assets/index-DHBWEwz_.css +1 -0
  52. package/dist/worker/console/static/assets/index-JRfTUgP7.js +478 -0
  53. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-ILCbxJyb.js → infoDiagram-6WML65LV-D3MoQZIl.js} +1 -1
  54. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-DeuWBQ89.js → ishikawaDiagram-WSZJBQD7-Ci6C1yjS.js} +1 -1
  55. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-CF5ih8Fk.js → journeyDiagram-NVQOT4AX-BWMo2lIP.js} +1 -1
  56. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-C7yOSuRO.js → kanban-definition-27J2QSJJ-BdthIAg3.js} +1 -1
  57. package/dist/worker/console/static/assets/{linear-BDZ9riWi.js → linear-DuFAlSOj.js} +1 -1
  58. package/dist/worker/console/static/assets/{mermaid.core-7pKqYtpZ.js → mermaid.core-Dqz3Slj9.js} +5 -5
  59. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-LeJDybSU.js → mindmap-definition-FAOFIHXS-DNVTrD09.js} +1 -1
  60. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BJvT3pMD.js → pegDiagram-VL7TDLO6-CgfZPfBh.js} +1 -1
  61. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-_rGqpMan.js → pieDiagram-7S7Q4E2Y-ClM9P7yo.js} +1 -1
  62. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-DPcSv5aA.js → quadrantDiagram-CIZ2JOQS-DwnEak2J.js} +1 -1
  63. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Drxx4hkJ.js → railroadDiagram-AXF67PYL-rK4hxt9P.js} +1 -1
  64. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BCTUdU4z.js → requirementDiagram-LRYGKXZP-Cjqa6vo3.js} +1 -1
  65. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-B6wzbZmB.js → sankeyDiagram-W5VNT64P-D3Fa250Z.js} +1 -1
  66. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BGt8d3QQ.js → sequenceDiagram-SI44F4Z6-2PdrNajj.js} +1 -1
  67. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-7nlhJKo7.js → sizeCapture-X5ZJPWSS-BBcuGeZn.js} +1 -1
  68. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Cv2stqMA.js → stateDiagram-OKZ733FA-CSJX9X6s.js} +1 -1
  69. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Cqskj0zt.js +1 -0
  70. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-qDAo4Yc1.js → swimlanes-SLNWSIFB-pOUU1bic.js} +2 -2
  71. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CM0ZIhNP.js +8 -0
  72. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CKbDNKTr.js → timeline-definition-Z64GVDOM-CAeqfQn0.js} +1 -1
  73. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DI-9EHic.js → vennDiagram-T6HMQDX7-DYPLOMVO.js} +1 -1
  74. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-BrzzRDpX.js → wardleyDiagram-T6FBY63Y-Dy2V0Cha.js} +1 -1
  75. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-BclH5hGh.js → xychartDiagram-ELKLHX3M-Dj9o7jrn.js} +1 -1
  76. package/dist/worker/console/static/index.html +2 -2
  77. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +33 -1
  78. package/dist/worker/console/static-src/operator-chat/chat-stream-continuity.js +101 -0
  79. package/dist/worker/console/static-src/operator-chat/compaction-message.js +18 -3
  80. package/dist/worker/console/static-src/operator-chat/pending-user-message.js +17 -3
  81. package/dist/worker/console/static-src/operator-chat/tools-catalog.js +16 -7
  82. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +15 -7
  83. package/dist/worker/console/static-src/operator-chat/useChatStream.js +47 -5
  84. package/dist/worker/console/static-src/operator-chat/useChatThread.js +24 -2
  85. package/dist/worker/console/workspace-context.js +2 -0
  86. package/dist/worker/observe/static/console-theme.css +174 -0
  87. package/dist/worker/observe/static/console-theme.js +36 -0
  88. package/dist/worker/observe/static/operator-chrome.css +85 -0
  89. package/dist/worker/observe/static/operator-chrome.js +51 -0
  90. package/dist/worker/scheduler/scheduled-goal-evidence.js +60 -2
  91. package/dist/worker/scheduler/scheduled-goal-store.js +10 -0
  92. package/package.json +3 -1
  93. package/dist/worker/console/static/assets/channel-ChE7y-cx.js +0 -1
  94. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C01TCf2X.js +0 -1
  95. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C01TCf2X.js +0 -1
  96. package/dist/worker/console/static/assets/index-BWkIfcrK.css +0 -1
  97. package/dist/worker/console/static/assets/index-Cdkvw_H6.js +0 -469
  98. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DC7V5vcq.js +0 -1
  99. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Bwy4QUTO.js +0 -8
@@ -1,4 +1,4 @@
1
- import { useCallback, useEffect, useRef, useState } from "react";
1
+ import { useCallback, useEffect, useMemo, 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";
@@ -179,10 +179,11 @@ function mergeCompactionMessages(messages, compactions) {
179
179
  * compact, plus browser-refresh recovery from the durable session store. */
180
180
  export function useChatSessions(params) {
181
181
  const { origin, refs, setSession, thread, composer, browser, setError, setNotice, onCompactionChange, setProcessOpen, setAutoScroll, } = params;
182
- const { applyActivatedSession, resetForNewSession, setMessages } = thread;
182
+ const { applyActivatedSession, resetForNewSession, setMessages, refreshSessionTelemetry, } = thread;
183
183
  const { seedSession, releaseSession } = composer;
184
184
  const { setPreviews, setActivePreviewPath } = browser;
185
185
  const { workspaceId, blocked } = params;
186
+ const visibleWorkspaceIds = useMemo(() => new Set(params.visibleWorkspaceIds), [params.visibleWorkspaceIds]);
186
187
  const workspaceQuery = workspaceId
187
188
  ? `?workspaceId=${encodeURIComponent(workspaceId)}`
188
189
  : "";
@@ -276,10 +277,13 @@ export function useChatSessions(params) {
276
277
  return;
277
278
  }
278
279
  const sessions = body.sessions ?? [];
279
- const durable = sessions.filter((item) => !pendingDeletedIdsRef.current.has(item.sessionId));
280
- const optimistic = [...pendingCreatedRowsRef.current.values()].filter((item) => !durable.some((session) => session.sessionId === item.sessionId));
281
- // Preserve every workspace's summaries. ChatWorkspaceSidebar applies the
282
- // visible recent-item limit independently inside each workspace group.
280
+ const belongsToVisibleWorkspace = (item) => !item.workspaceId || visibleWorkspaceIds.has(item.workspaceId);
281
+ const durable = sessions.filter((item) => belongsToVisibleWorkspace(item) &&
282
+ !pendingDeletedIdsRef.current.has(item.sessionId));
283
+ const optimistic = [...pendingCreatedRowsRef.current.values()].filter((item) => belongsToVisibleWorkspace(item) &&
284
+ !durable.some((session) => session.sessionId === item.sessionId));
285
+ // Preserve summaries for visible workspaces. ChatWorkspaceSidebar applies
286
+ // the recent-item limit independently inside each workspace group.
283
287
  const next = stabilizeSessionList(sessionListRef.current, [...optimistic, ...durable]);
284
288
  sessionListRef.current = next;
285
289
  setSessionList(next);
@@ -292,7 +296,7 @@ export function useChatSessions(params) {
292
296
  return;
293
297
  setError(`会话列表加载失败:${error instanceof Error ? error.message : String(error)}`);
294
298
  }
295
- }, [origin, blocked, setError]);
299
+ }, [origin, blocked, setError, visibleWorkspaceIds]);
296
300
  const deleteSession = useCallback(async (sessionId) => {
297
301
  if (pendingDeletedIdsRef.current.has(sessionId))
298
302
  return false;
@@ -500,6 +504,7 @@ export function useChatSessions(params) {
500
504
  sessionStats: body.sessionStats,
501
505
  usage: body.usage,
502
506
  turnTelemetry: body.turnTelemetry,
507
+ sddSessionIdentity: body.sddDataAlignment ?? null,
503
508
  streaming: visuallyStopped ? false : Boolean(body.session.activeTurnId),
504
509
  turnPhase: body.session.turnPhase ??
505
510
  (body.session.activeTurnId ? "running" : "idle"),
@@ -634,6 +639,7 @@ export function useChatSessions(params) {
634
639
  sessionStats: body.sessionStats,
635
640
  usage: body.usage,
636
641
  turnTelemetry: body.turnTelemetry,
642
+ sddSessionIdentity: body.sddDataAlignment ?? null,
637
643
  streaming: visuallyStopped ? false : Boolean(body.session.activeTurnId),
638
644
  turnPhase: body.session.turnPhase ??
639
645
  (body.session.activeTurnId ? "running" : "idle"),
@@ -760,6 +766,7 @@ export function useChatSessions(params) {
760
766
  setSelectedSession(created);
761
767
  rememberActiveChatSession(window.localStorage, created.workspaceId ?? options?.workspaceId ?? workspaceId, created.sessionId);
762
768
  setSession(created);
769
+ void refreshSessionTelemetry();
763
770
  return created;
764
771
  }
765
772
  catch (e) {
@@ -783,6 +790,7 @@ export function useChatSessions(params) {
783
790
  activateSession,
784
791
  blocked,
785
792
  workspaceId,
793
+ refreshSessionTelemetry,
786
794
  ]);
787
795
  // Phase-1: fork / in-session branch / mainline switch UI deferred; API routes remain.
788
796
  /** Single compact helper for toolbar + `/compact <instructions>` (AC-1). */
@@ -1,5 +1,6 @@
1
1
  import { useCallback, useEffect } from "react";
2
2
  import { formatOperatorUserError } from "../../operator-user-error.js";
3
+ import { inspectChatSseContinuity, readChatStreamReadyCursor, selectChatSseRepairBlocks, selectLaterChatEventId, } from "./chat-stream-continuity.js";
3
4
  import { confirmationToken, requestId, sleep } from "./format.js";
4
5
  import { classifyCreateTurnNetworkFailure, classifyCreateTurnResponse, createPendingSubmission, decideSubmissionRoute, isRuntimeBusyUnion, isSubmissionCurrent, isTerminalTurnState, keepAmbiguousAfterRetry, reconcileDeferredSubmissionFromState, reconcilePendingSubmissionFromState, resolveAmbiguousFromState, } from "./turn-submission.js";
5
6
  import { applyAbortDecision, applyPendingReconcileDecision, classifyAbortResponse, markStopRequested, operatorChatAbortUrl, operatorChatEventsUrl, operatorChatStateUrl, operatorChatTurnsUrl, releaseOwnershipOnTerminalEvidence, releaseSubmitOwnership as releaseOwnershipBag, requestStopOnOwnership, settleCreateTurnOutcome, turnPhaseFromOwnership, visualTurnPhaseFromOwnership, } from "./turn-stream-controller.js";
@@ -104,6 +105,7 @@ export function useChatStream(params) {
104
105
  * means replay cannot render duplicate messages while live text remains
105
106
  * smooth (M4.1/M4.3). */
106
107
  const openEventsStream = useCallback(async (sessionId, generation, signal) => {
108
+ const gapReplayBuffer = [];
107
109
  while (!signal.aborted) {
108
110
  const state = refs.eventsStreamStateRef.current;
109
111
  if (!state ||
@@ -129,21 +131,38 @@ export function useChatStream(params) {
129
131
  const decoder = new TextDecoder();
130
132
  let buffer = "";
131
133
  let replay = true;
134
+ let replayRepairNeeded = false;
135
+ let liveContinuityCursor = null;
132
136
  const fallbackAssistantId = requestId("a");
133
- for (;;) {
137
+ readLoop: for (;;) {
134
138
  const { done, value } = await reader.read();
135
139
  if (done)
136
140
  break;
137
141
  buffer += decoder.decode(value, { stream: true });
138
142
  const blocks = buffer.split("\n\n");
139
143
  buffer = blocks.pop() ?? "";
140
- for (const block of blocks) {
144
+ for (let blockIndex = 0; blockIndex < blocks.length; blockIndex += 1) {
145
+ const block = blocks[blockIndex];
146
+ if (block === undefined)
147
+ continue;
148
+ if (!replay) {
149
+ const continuity = inspectChatSseContinuity(block, liveContinuityCursor);
150
+ if (continuity.action === "reconnect") {
151
+ // Buffer this block and the rest of the decoded chunk before closing
152
+ // the reader. Retained replay repairs structure; the buffer preserves
153
+ // cumulative text frames that persistence may have coalesced away.
154
+ gapReplayBuffer.push(...blocks.slice(blockIndex));
155
+ replayRepairNeeded = true;
156
+ break readLoop;
157
+ }
158
+ }
141
159
  if (sseEventName(block) === "stream_ready") {
142
160
  const current = refs.eventsStreamStateRef.current;
143
161
  if (current?.sessionId === sessionId &&
144
162
  current.generation === generation)
145
163
  current.ready = true;
146
164
  replay = false;
165
+ liveContinuityCursor = readChatStreamReadyCursor(block);
147
166
  // Fall through: the applier owns the buffered-replay flush and
148
167
  // returns immediately for this event. `continue` here silently
149
168
  // dropped that flush, so a reconnect that landed after the
@@ -155,15 +174,38 @@ export function useChatStream(params) {
155
174
  targetSessionId: sessionId,
156
175
  generation,
157
176
  });
158
- if (eventId)
159
- refs.lastEventIdRef.current = eventId;
177
+ if (eventId) {
178
+ refs.lastEventIdRef.current = selectLaterChatEventId(refs.lastEventIdRef.current, eventId);
179
+ if (!replay)
180
+ liveContinuityCursor = selectLaterChatEventId(liveContinuityCursor, eventId);
181
+ }
182
+ if (sseEventName(block) === "stream_ready" &&
183
+ gapReplayBuffer.length > 0) {
184
+ const repairedBlocks = selectChatSseRepairBlocks(gapReplayBuffer.splice(0), liveContinuityCursor);
185
+ for (const repairedBlock of repairedBlocks) {
186
+ const repairedEventId = applySseBlock(repairedBlock, refs.assistantIdRef.current ?? fallbackAssistantId, {
187
+ replay: false,
188
+ targetSessionId: sessionId,
189
+ generation,
190
+ });
191
+ if (!repairedEventId)
192
+ continue;
193
+ refs.lastEventIdRef.current = selectLaterChatEventId(refs.lastEventIdRef.current, repairedEventId);
194
+ }
195
+ }
160
196
  }
161
197
  }
198
+ if (replayRepairNeeded) {
199
+ await reader.cancel().catch(() => undefined);
200
+ }
162
201
  const current = refs.eventsStreamStateRef.current;
163
202
  if (current?.sessionId === sessionId &&
164
203
  current.generation === generation)
165
204
  current.ready = false;
166
- await sleep(1_000, signal);
205
+ // A detected hole is expected to be immediately recoverable from the
206
+ // durable ring. Other EOFs retain the normal reconnect backoff.
207
+ if (!replayRepairNeeded)
208
+ await sleep(1_000, signal);
167
209
  }
168
210
  catch {
169
211
  if (signal.aborted)
@@ -140,6 +140,8 @@ export function useChatThread(params) {
140
140
  const [usage, setUsage] = useState(EMPTY_USAGE);
141
141
  const [sessionStats, setSessionStats] = useState(EMPTY_SESSION_STATS);
142
142
  const [turnTelemetry, setTurnTelemetry] = useState([]);
143
+ const [operatorActions, setOperatorActions] = useState([]);
144
+ const [sddSessionIdentity, setSddSessionIdentity] = useState(null);
143
145
  const [streaming, setStreaming] = useState(false);
144
146
  const [turnPhase, setTurnPhase] = useState("idle");
145
147
  const [deferredTurn, setDeferredTurn] = useState(null);
@@ -167,6 +169,7 @@ export function useChatThread(params) {
167
169
  if (body.usage)
168
170
  setUsage(body.usage);
169
171
  setTurnTelemetry(body.turnTelemetry ?? []);
172
+ setSddSessionIdentity(body.sddDataAlignment ?? null);
170
173
  }
171
174
  catch {
172
175
  // Telemetry is supplementary; the normal event stream remains authoritative.
@@ -188,6 +191,8 @@ export function useChatThread(params) {
188
191
  setPendingQuestion,
189
192
  setTodos,
190
193
  setGoal,
194
+ setOperatorActions,
195
+ setSddSessionIdentity,
191
196
  setStreamingAssistantId,
192
197
  setUnread,
193
198
  setOperationCards,
@@ -421,6 +426,8 @@ export function useChatThread(params) {
421
426
  setUsage(EMPTY_USAGE);
422
427
  setSessionStats(EMPTY_SESSION_STATS);
423
428
  setTurnTelemetry([]);
429
+ setOperatorActions([]);
430
+ setSddSessionIdentity(null);
424
431
  setStreaming(false);
425
432
  setTurnPhase("idle");
426
433
  setDeferredTurn(null);
@@ -477,6 +484,8 @@ export function useChatThread(params) {
477
484
  setUsage(next.usage ?? EMPTY_USAGE);
478
485
  setSessionStats(next.sessionStats ?? EMPTY_SESSION_STATS);
479
486
  setTurnTelemetry(next.turnTelemetry ?? []);
487
+ setOperatorActions([]);
488
+ setSddSessionIdentity(next.sddSessionIdentity ?? null);
480
489
  refs.usageResponseKeysRef.current = new Set();
481
490
  setStreaming(visuallyStopped ? false : next.streaming);
482
491
  setTurnPhase(visuallyStopped
@@ -500,13 +509,15 @@ export function useChatThread(params) {
500
509
  * operation/human-gate/artifact/interview cards, which the reconnected tail
501
510
  * stream will not replay (they live in the ring, already consumed). */
502
511
  const recoverSession = useCallback(async (targetSessionId) => {
512
+ const targetGeneration = refs.sessionGenerationRef.current;
503
513
  try {
504
514
  const res = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(targetSessionId)}`, { credentials: "include" });
505
515
  if (!res.ok)
506
516
  return false;
507
517
  const body = (await res.json());
508
518
  if (!body.session ||
509
- refs.sessionRef.current?.sessionId !== targetSessionId)
519
+ refs.sessionRef.current?.sessionId !== targetSessionId ||
520
+ refs.sessionGenerationRef.current !== targetGeneration)
510
521
  return false;
511
522
  const activeTurnId = body.session.activeTurnId ?? null;
512
523
  const visuallyStopped = body.session.turnPhase === "stopping";
@@ -526,6 +537,7 @@ export function useChatThread(params) {
526
537
  if (body.usage)
527
538
  setUsage(body.usage);
528
539
  setTurnTelemetry(body.turnTelemetry ?? []);
540
+ setSddSessionIdentity(body.sddDataAlignment ?? null);
529
541
  // Resume the events stream from the snapshot tail so the next
530
542
  // reconnect never immediately re-expires (AC-003). The refs below
531
543
  // are reset so any follow-up tail replay re-applies cleanly.
@@ -534,7 +546,13 @@ export function useChatThread(params) {
534
546
  refs.turnAssistantIdsRef.current = new Map();
535
547
  refs.sealedAssistantIdsRef.current = new Set();
536
548
  const hydrated = hydrateSessionThread(body.session.messages ?? []);
537
- const pending = refs.pendingSubmissionRef.current;
549
+ // Recovery is addressed to targetSessionId, so an optimistic
550
+ // submission from another session must never be projected into
551
+ // this snapshot (the activation path enforces the same owner gate).
552
+ const pending = refs.pendingSubmissionRef.current?.sessionId === targetSessionId &&
553
+ refs.pendingSubmissionRef.current.sessionGeneration === targetGeneration
554
+ ? refs.pendingSubmissionRef.current
555
+ : undefined;
538
556
  const recovered = mergePendingSubmissionOnHydration(hydrated.messages, pending
539
557
  ? {
540
558
  clientRequestId: pending.clientRequestId,
@@ -599,6 +617,9 @@ export function useChatThread(params) {
599
617
  usage,
600
618
  sessionStats,
601
619
  turnTelemetry,
620
+ operatorActions,
621
+ sddSessionIdentity,
622
+ setSddSessionIdentity,
602
623
  streaming,
603
624
  setStreaming,
604
625
  turnPhase,
@@ -621,6 +642,7 @@ export function useChatThread(params) {
621
642
  applySseBlock,
622
643
  loadTaskContext,
623
644
  applyOperationLinkMutation,
645
+ refreshSessionTelemetry,
624
646
  resetForNewSession,
625
647
  applyActivatedSession,
626
648
  recoverSession,
@@ -265,6 +265,7 @@ export async function createWorkspaceContext(record, deps) {
265
265
  const aimaxAuth = new AimaxAuthReader();
266
266
  await aimaxAuth.getToken();
267
267
  const sddDataAlignment = new SddDataAlignmentService(chatStore, aimaxAuth, deps.fetchImpl);
268
+ actionContext.sddDataAlignment = sddDataAlignment;
268
269
  const chatEventEpoch = randomBytes(12).toString("hex");
269
270
  const chatEvents = createChatEventStore({
270
271
  appData,
@@ -318,6 +319,7 @@ export async function createWorkspaceContext(record, deps) {
318
319
  terminalRegistry,
319
320
  scmService,
320
321
  browserAutomation,
322
+ globalAppDataRoot: appData.root,
321
323
  });
322
324
  let chatRouteDeps;
323
325
  const goalRoundDriver = new GoalRoundDriver((sessionId) => driveGoalRound(chatRouteDeps, sessionId));
@@ -0,0 +1,174 @@
1
+ /* Opt-in palettes. Repeat tokens at the Inspect boundary: it owns local defaults
2
+ * and is loaded after the Console stylesheet. Specificity keeps load order safe. */
3
+ :root[data-console-theme="midnight"],
4
+ :root[data-console-theme="midnight"] body[data-shell="observe-console"],
5
+ :root[data-console-theme="midnight"] .inspect-workspace-root {
6
+ color-scheme: dark;
7
+ --canvas: #1c222a;
8
+ --canvas-soft: #202730;
9
+ --surface: #252d37;
10
+ --surface-muted: #2d3641;
11
+ --ink: #dce2e9;
12
+ --surface-ink: #dce2e9;
13
+ --body: #bcc5d0;
14
+ --muted: #a1acba;
15
+ --muted-soft: #909eaf;
16
+ --muted-ink: #a1acba;
17
+ --hairline: #353f4b;
18
+ --hairline-strong: #526171;
19
+ --orange: #a0bcd3;
20
+ --orange-active: #b8cfe0;
21
+ --blue: #a0bcd3;
22
+ --brand: #b2aacb;
23
+ --theme-action: #4d6b86;
24
+ --theme-action-hover: #557590;
25
+ --theme-on-action: #ffffff;
26
+ --green: #99c5b0;
27
+ --amber: #d3b783;
28
+ --warn: #d3b783;
29
+ --red: #dea4ac;
30
+ --peach: #303e4d;
31
+ --mint: #2d403b;
32
+ --lavender: #393746;
33
+ --gold: #c3ac80;
34
+ --shadow-sm: 0 1px 2px rgb(0 0 0 / 12%);
35
+ --shadow-panel: 0 1px 2px rgb(0 0 0 / 12%), 0 6px 18px rgb(0 0 0 / 10%);
36
+ --shadow-soft: 0 8px 24px rgb(0 0 0 / 18%);
37
+ --shadow-lg: 0 16px 40px rgb(0 0 0 / 24%);
38
+ }
39
+
40
+ :root[data-console-theme="daylight"],
41
+ :root[data-console-theme="daylight"] body[data-shell="observe-console"],
42
+ :root[data-console-theme="daylight"] .inspect-workspace-root {
43
+ color-scheme: light;
44
+ --canvas: #eef0f2;
45
+ --canvas-soft: #f3f4f5;
46
+ --surface: #f9fafb;
47
+ --surface-muted: #e5e9ed;
48
+ --ink: #303b47;
49
+ --surface-ink: #303b47;
50
+ --body: #4b5867;
51
+ --muted: #5e6a78;
52
+ --muted-soft: #697583;
53
+ --muted-ink: #5e6a78;
54
+ --hairline: #dce1e6;
55
+ --hairline-strong: #b5bfc9;
56
+ --orange: #466581;
57
+ --orange-active: #365570;
58
+ --blue: #466581;
59
+ --brand: #706387;
60
+ --theme-action: #536f88;
61
+ --theme-action-hover: #466079;
62
+ --theme-on-action: #ffffff;
63
+ --green: #3e735d;
64
+ --amber: #85632d;
65
+ --warn: #85632d;
66
+ --red: #a04f5c;
67
+ --peach: #dfe7ee;
68
+ --mint: #e0eae4;
69
+ --lavender: #e7e3ed;
70
+ --gold: #927644;
71
+ --shadow-sm: 0 1px 2px rgb(35 45 58 / 4%);
72
+ --shadow-panel: 0 1px 2px rgb(35 45 58 / 3%), 0 6px 18px rgb(35 45 58 / 3%);
73
+ --shadow-soft: 0 8px 24px rgb(35 45 58 / 6%);
74
+ --shadow-lg: 0 16px 40px rgb(35 45 58 / 9%);
75
+ }
76
+
77
+ /* Only the two added themes override component literals; original warm paper
78
+ * remains byte-for-byte defined by its existing stylesheets. */
79
+ :root:is([data-console-theme="midnight"], [data-console-theme="daylight"]) {
80
+ --orange-soft: color-mix(in srgb, var(--orange) 10%, var(--surface));
81
+ background: var(--canvas);
82
+
83
+ body[data-shell="observe-console"], .inspect-workspace-root {
84
+ --orange-soft: color-mix(in srgb, var(--orange) 10%, var(--surface));
85
+ }
86
+
87
+ ::selection { background: color-mix(in srgb, var(--blue) 32%, var(--surface)); }
88
+
89
+ .oc-composer-shell, .console-update-channel, .console-update-button,
90
+ .panel-output { background: var(--surface); color: var(--ink); }
91
+ .console-update-channel, .console-update-button { border-color: var(--hairline-strong); }
92
+ .console-update-channel-label { color: var(--body); }
93
+ .console-update-error, .console-update-result-error { color: var(--red); }
94
+ .console-update-badge { background: var(--surface-muted); color: var(--amber); }
95
+
96
+ .oc-bubble-user { background: color-mix(in srgb, var(--blue) 9%, var(--surface)); }
97
+ .oc-composer-shell { box-shadow: var(--shadow-panel); }
98
+ .oc-bubble-assistant .oc-codeblock { background: var(--canvas-soft); }
99
+ .oc-session-scroll, .oc-tree-scroll { background: var(--canvas-soft); }
100
+ .oc-sidebar-new-session:is(:hover, :focus-visible),
101
+ .oc-hero-chip:is(:hover, :focus-visible, [aria-expanded="true"]) { background: var(--surface-muted); }
102
+
103
+ .night-health-pill, .models-status-pill[data-state="ready"],
104
+ .models-probe-item.is-ok .models-probe-badge {
105
+ background: color-mix(in srgb, var(--green) 14%, var(--surface));
106
+ }
107
+ .night-health-pill.is-bad, .models-status-pill[data-state="setup-required"],
108
+ .models-probe-item.is-bad .models-probe-badge, .dag-error-node {
109
+ background: color-mix(in srgb, var(--red) 12%, var(--surface));
110
+ }
111
+ .models-status-pill[data-state="degraded"], .models-degraded-note,
112
+ .dag-active-node { background: var(--orange-soft); }
113
+ .liveness-timeout-risk { background: color-mix(in srgb, var(--amber) 12%, var(--surface)); }
114
+ .night-strip-icon { color: var(--brand); }
115
+
116
+ /* Quiet selected surfaces keep navigation secondary to the content. */
117
+ .och-mode-thumb { background: var(--surface); box-shadow: var(--shadow-sm); }
118
+ .och-mode-btn.och-on { color: var(--ink); }
119
+ .dag-inspector-tab[aria-selected="true"],
120
+ .dag-graph-mode-tab[aria-selected="true"],
121
+ #dag-inspector-panel-prompt-restart button.prompt-restart-mode-option.is-selected {
122
+ background: var(--surface-muted);
123
+ color: var(--ink);
124
+ }
125
+
126
+ /* Accent text needs a different luminance from a white-labelled action. */
127
+ .btn-primary, button.btn-primary, .oc-send, .oc-question-button.is-primary,
128
+ .oc-scm-commitbtn, .skip-link, .dags-page-num.is-current,
129
+ .dag-action-primary, .context-export-submit,
130
+ #dag-inspector-panel-prompt-restart button.prompt-restart-submit:not(.is-secondary) {
131
+ background: var(--theme-action);
132
+ border-color: var(--theme-action);
133
+ color: var(--theme-on-action);
134
+ box-shadow: var(--shadow-sm);
135
+ }
136
+ .btn-primary:hover:not(:disabled), button.btn-primary:hover:not(:disabled),
137
+ .oc-send:hover:not(:disabled), .oc-question-button.is-primary:hover:not(:disabled),
138
+ .oc-scm-commitbtn:hover:not(:disabled), .dag-action-primary:hover:not(:disabled),
139
+ .context-export-submit:hover:not(:disabled),
140
+ #dag-inspector-panel-prompt-restart button.prompt-restart-submit:not(.is-secondary):hover {
141
+ background: var(--theme-action-hover);
142
+ border-color: var(--theme-action-hover);
143
+ }
144
+ .och-context-return, .process-timeline-autoscroll::after {
145
+ background: var(--surface-muted);
146
+ color: var(--ink);
147
+ }
148
+ .markdown-output pre, .output-block { background: var(--canvas-soft); color: var(--ink); }
149
+ .context-export-ticket { background: var(--surface-muted); color: var(--ink); }
150
+ .context-export-ticket-safety { color: var(--green); }
151
+ .context-export-ticket-safety.is-risk { color: var(--red); }
152
+ }
153
+
154
+ /* Syntax highlighting is independently hardcoded to a light palette. */
155
+ :root[data-console-theme="midnight"] {
156
+ /* The SVG brand uses dark strokes; provide a subtle light backing, not a filter. */
157
+ .och-brand-mark, .oc-hero img { background: #aebac7; border-radius: 6px; }
158
+ .hljs { color: var(--ink); background: var(--canvas-soft); }
159
+ .hljs-doctag, .hljs-keyword, .hljs-meta .hljs-keyword,
160
+ .hljs-template-tag, .hljs-template-variable, .hljs-type,
161
+ .hljs-variable.language_ { color: #c7afcf; }
162
+ .hljs-title, .hljs-title.class_, .hljs-title.class_.inherited__,
163
+ .hljs-title.function_ { color: #b9b3d1; }
164
+ .hljs-attr, .hljs-attribute, .hljs-literal, .hljs-meta, .hljs-number,
165
+ .hljs-operator, .hljs-selector-attr, .hljs-selector-class,
166
+ .hljs-selector-id, .hljs-variable, .hljs-section { color: var(--blue); }
167
+ .hljs-meta .hljs-string, .hljs-regexp, .hljs-string,
168
+ .hljs-name, .hljs-quote, .hljs-selector-pseudo, .hljs-selector-tag { color: var(--green); }
169
+ .hljs-built_in, .hljs-symbol, .hljs-bullet { color: var(--amber); }
170
+ .hljs-code, .hljs-comment, .hljs-formula { color: var(--muted); }
171
+ .hljs-subst, .hljs-emphasis, .hljs-strong { color: var(--ink); }
172
+ .hljs-addition { color: var(--green); background: var(--mint); }
173
+ .hljs-deletion { color: var(--red); background: color-mix(in srgb, var(--red) 12%, var(--surface)); }
174
+ }
@@ -0,0 +1,36 @@
1
+ /** Shared Console theme identities; the default leaves the original palette intact. */
2
+ export const CONSOLE_THEMES = Object.freeze([
3
+ { id: "default", label: "默认" },
4
+ { id: "midnight", label: "深色" },
5
+ { id: "daylight", label: "浅色" },
6
+ ]);
7
+
8
+ const STORAGE_KEY = "loop-console.theme.v1";
9
+
10
+ function normalizeTheme(value) {
11
+ return CONSOLE_THEMES.some((theme) => theme.id === value) ? value : "default";
12
+ }
13
+
14
+ export function applyStoredConsoleTheme() {
15
+ let value;
16
+ try {
17
+ value = window.localStorage.getItem(STORAGE_KEY);
18
+ } catch {
19
+ // Storage can be disabled, including the localStorage getter itself.
20
+ }
21
+ const theme = normalizeTheme(value);
22
+ document.documentElement.dataset.consoleTheme = theme;
23
+ return theme;
24
+ }
25
+
26
+ export function setConsoleTheme(value) {
27
+ const theme = normalizeTheme(value);
28
+ // Apply before persistence: a storage failure must not disable the picker.
29
+ document.documentElement.dataset.consoleTheme = theme;
30
+ try {
31
+ window.localStorage.setItem(STORAGE_KEY, theme);
32
+ } catch {
33
+ // The document attribute remains the in-memory preference.
34
+ }
35
+ return theme;
36
+ }
@@ -8,6 +8,84 @@
8
8
  * 的暖白 token 保持一致;宿主样式表已定义同名变量时以宿主为准。
9
9
  */
10
10
 
11
+ @import "./console-theme.css";
12
+
13
+ .och-theme {
14
+ display: flex;
15
+ align-items: center;
16
+ gap: 6px;
17
+ flex: none;
18
+ color: var(--och-muted);
19
+ font-size: 12px;
20
+ }
21
+
22
+ .och-theme-trigger {
23
+ display: inline-flex;
24
+ align-items: center;
25
+ justify-content: center;
26
+ width: 26px;
27
+ min-height: 26px;
28
+ padding: 0;
29
+ border: 1px solid transparent;
30
+ border-radius: 6px;
31
+ background: transparent;
32
+ color: var(--och-body);
33
+ font: inherit;
34
+ cursor: pointer;
35
+ }
36
+
37
+ .och-theme-trigger:hover,
38
+ .och-theme-trigger[aria-expanded="true"] {
39
+ background: var(--och-surface-muted);
40
+ color: var(--och-ink);
41
+ }
42
+
43
+ .och-theme-trigger:focus-visible,
44
+ .och-theme-option:focus-visible {
45
+ outline: 2px solid var(--och-orange);
46
+ outline-offset: 2px;
47
+ }
48
+
49
+ /* Native popover renders in the top layer, outside the narrow header's clip. */
50
+ .och-theme-menu {
51
+ position: fixed;
52
+ inset: auto;
53
+ box-sizing: border-box;
54
+ width: 112px;
55
+ margin: 0;
56
+ padding: 4px;
57
+ border: 1px solid var(--och-hairline);
58
+ border-radius: 8px;
59
+ background: var(--och-surface);
60
+ color: var(--och-ink);
61
+ box-shadow: var(--shadow-soft);
62
+ font: inherit;
63
+ }
64
+
65
+ .och-theme-option {
66
+ display: flex;
67
+ align-items: center;
68
+ justify-content: space-between;
69
+ width: 100%;
70
+ min-height: 28px;
71
+ padding: 4px 8px;
72
+ border: 0;
73
+ border-radius: 4px;
74
+ background: transparent;
75
+ color: inherit;
76
+ font: inherit;
77
+ cursor: pointer;
78
+ }
79
+
80
+ .och-theme-option:hover,
81
+ .och-theme-option[aria-pressed="true"] {
82
+ background: var(--och-surface-muted);
83
+ }
84
+
85
+ .och-theme-option[aria-pressed="true"]::after {
86
+ content: "✓";
87
+ }
88
+
11
89
  .och {
12
90
  --och-canvas: var(--canvas, #f3f4f6);
13
91
  --och-surface: var(--surface, #fbfcfd);
@@ -622,7 +700,14 @@
622
700
  .och-inner {
623
701
  padding-inline: 12px;
624
702
  }
703
+ .och-theme {
704
+ position: absolute;
705
+ top: 46px;
706
+ right: 0;
707
+ }
625
708
  .och-global {
709
+ height: 84px;
710
+ padding-bottom: 40px;
626
711
  gap: 6px;
627
712
  overflow-x: auto;
628
713
  scrollbar-width: none;
@@ -12,6 +12,8 @@
12
12
  */
13
13
 
14
14
  import { withWorkspaceQuery } from "./router.js";
15
+ import { CONSOLE_THEMES, applyStoredConsoleTheme, setConsoleTheme } from "./console-theme.js";
16
+ export { CONSOLE_THEMES } from "./console-theme.js";
15
17
 
16
18
  export const OPERATE_TABS = Object.freeze([
17
19
  { id: "tasks", label: "任务", href: "/#/operate/tasks" },
@@ -296,6 +298,13 @@ export function buildOperatorChromeModel(config) {
296
298
 
297
299
  const SVG_NS = "http:" + "//www.w3.org/2000/svg";
298
300
  const ICONS = Object.freeze({
301
+ theme: {
302
+ size: 16,
303
+ parts: [
304
+ ["circle", { cx: 12, cy: 12, r: 9 }],
305
+ ["path", { d: "M12 3a9 9 0 0 1 0 18Z", fill: "currentColor", stroke: "none" }],
306
+ ],
307
+ },
299
308
  radar: {
300
309
  size: 17,
301
310
  parts: [
@@ -426,6 +435,7 @@ function readWorkspaceCollapsedPref() {
426
435
  export function mountOperatorChrome(root, config = {}) {
427
436
  if (!root) throw new Error("mountOperatorChrome: root element is required");
428
437
  root.classList.add("och-host");
438
+ if (!document.documentElement.dataset.consoleTheme) applyStoredConsoleTheme();
429
439
 
430
440
  let currentConfig = { ...config };
431
441
  let popoverOpen = false;
@@ -701,6 +711,47 @@ export function mountOperatorChrome(root, config = {}) {
701
711
 
702
712
  global.appendChild(el("span", "och-spacer"));
703
713
 
714
+ const themePicker = el("div", "och-theme");
715
+ const trigger = el("button", "och-theme-trigger");
716
+ trigger.type = "button";
717
+ trigger.title = "主题";
718
+ trigger.setAttribute("aria-label", "主题");
719
+ trigger.appendChild(makeIcon("theme"));
720
+ trigger.setAttribute("aria-expanded", "false");
721
+ const menu = el("div", "och-theme-menu");
722
+ menu.id = `${popoverId}-theme`;
723
+ menu.popover = "auto";
724
+ menu.setAttribute("role", "group");
725
+ menu.setAttribute("aria-label", "选择主题");
726
+ trigger.setAttribute("aria-controls", menu.id);
727
+ trigger.popoverTargetElement = menu;
728
+ menu.addEventListener("beforetoggle", (event) => {
729
+ if (event.newState !== "open") return;
730
+ const rect = trigger.getBoundingClientRect();
731
+ menu.style.top = `${rect.bottom + 6}px`;
732
+ menu.style.left = `${Math.max(8, Math.min(rect.right - 112, window.innerWidth - 120))}px`;
733
+ });
734
+ menu.addEventListener("toggle", (event) => {
735
+ trigger.setAttribute("aria-expanded", String(event.newState === "open"));
736
+ });
737
+ for (const theme of CONSOLE_THEMES) {
738
+ const option = el("button", "och-theme-option", theme.label);
739
+ option.type = "button";
740
+ option.dataset.theme = theme.id;
741
+ option.setAttribute("aria-pressed", String(theme.id === (document.documentElement.dataset.consoleTheme ?? "default")));
742
+ option.addEventListener("click", () => {
743
+ setConsoleTheme(theme.id);
744
+ for (const item of menu.children) {
745
+ item.setAttribute("aria-pressed", String(item.dataset.theme === theme.id));
746
+ }
747
+ menu.hidePopover();
748
+ trigger.focus();
749
+ });
750
+ menu.appendChild(option);
751
+ }
752
+ themePicker.append(trigger, menu);
753
+ global.appendChild(themePicker);
754
+
704
755
  if (model.statusVisible && model.freshness) {
705
756
  const fresh = el("span", "och-fresh");
706
757
  fresh.setAttribute("aria-label", "数据新鲜度");