@tea-agent/loop-agent 0.40.0-next.4 → 0.40.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 (132) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/build-stamp.json +3 -3
  3. package/dist/infrastructure/harness/atomic-write.js +23 -0
  4. package/dist/worker/console/chat/chat-event-store.js +62 -4
  5. package/dist/worker/console/chat/context-panel.js +7 -0
  6. package/dist/worker/console/chat/deferred-turn.js +12 -0
  7. package/dist/worker/console/chat/pi-runtime.js +264 -16
  8. package/dist/worker/console/chat/provider-error.js +98 -0
  9. package/dist/worker/console/chat/routes.js +292 -29
  10. package/dist/worker/console/chat/session-stats.js +179 -0
  11. package/dist/worker/console/chat/session-store.js +93 -63
  12. package/dist/worker/console/chat/todos.js +115 -0
  13. package/dist/worker/console/chat/tool-preview.js +42 -0
  14. package/dist/worker/console/chat/turn-process.js +20 -55
  15. package/dist/worker/console/chat/user-questions.js +266 -0
  16. package/dist/worker/console/chat/workspace-landing.js +14 -0
  17. package/dist/worker/console/native-directory-picker.js +13 -0
  18. package/dist/worker/console/routes.js +309 -1
  19. package/dist/worker/console/security.js +4 -4
  20. package/dist/worker/console/server.js +529 -186
  21. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-CTx1TrNa.js → abnfDiagram-N423BO3Z-CGZrpj0K.js} +1 -1
  22. package/dist/worker/console/static/assets/{arc-B4i9cVxX.js → arc-CwEyA8iq.js} +1 -1
  23. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BAXE8_Rl.js → architectureDiagram-T3A2C74G-DLTkrEQ3.js} +1 -1
  24. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-C5Xh59at.js → blockDiagram-VBNYF7ZC-4w12zR3t.js} +1 -1
  25. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B_asOzAu.js → c4Diagram-5PPSVZJV-BppqLAjk.js} +1 -1
  26. package/dist/worker/console/static/assets/channel-TzuwQha5.js +1 -0
  27. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-wRO_iRNc.js → chunk-2GRJ4B5K-DO6U26Pm.js} +1 -1
  28. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-ByOaSh_G.js → chunk-2Q5K7J3B-DC7YEdOy.js} +1 -1
  29. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLzudan.js → chunk-5RXB4S5H-DtMYVQBw.js} +1 -1
  30. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-JCNGkXYT.js → chunk-5VM5RSS4-C5Ed-57N.js} +1 -1
  31. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BhrPXXMZ.js → chunk-6Q2QTUOP-BUY4oIzT.js} +1 -1
  32. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-TJ5JuEBT.js → chunk-GF5L2VYU-VnWHALIV.js} +1 -1
  33. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-VVthn_mD.js → chunk-JWPE2WC7-COV-RQk4.js} +1 -1
  34. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-D60-Mny4.js → chunk-KBJHAD2P-dSnLtUiz.js} +1 -1
  35. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-zXv7aVye.js → chunk-RYQCIY6F-CX6FDzWh.js} +1 -1
  36. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-By9VQoi1.js → chunk-XXDRQBXY-D3mEgb6W.js} +1 -1
  37. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DCu69b3S.js +1 -0
  38. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DCu69b3S.js +1 -0
  39. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-C4zQLe-M.js → cose-bilkent-JH36ORCC-QQBMUnAf.js} +1 -1
  40. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-wh5N2ReN.js → cynefin-VYW2F7L2-BxSD-LVV.js} +1 -1
  41. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Dlu5wIqs.js → cynefinDiagram-MW4NZA55-DxHNiNbk.js} +1 -1
  42. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CrDgthc6.js → dagre-VZM6K2ZE-C83bYy8E.js} +1 -1
  43. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-zN1clg27.js → diagram-7IWD3JNH-BhtZu2YY.js} +1 -1
  44. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BN-o7J7-.js → diagram-B4RE2ZJO-K0P_ntRE.js} +1 -1
  45. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-C8SuPt0d.js → diagram-LBJQPF4R-CXYy1Nib.js} +1 -1
  46. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BoizuHbZ.js → diagram-Q27KOJAE-D-CtIFkU.js} +1 -1
  47. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BzUzXixG.js → diagram-UB23O5K3-BEK9-_7c.js} +1 -1
  48. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Ev3lA4Cf.js → ebnfDiagram-BXEA7PRR-L0EX-9wn.js} +1 -1
  49. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-GALUzRbs.js → erDiagram-JOGREHBK-9pt0fc_5.js} +1 -1
  50. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CZJN65Sb.js → flowDiagram-UKHOOZJN-CZVeM_n1.js} +1 -1
  51. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-JLB9OESl.js → ganttDiagram-PKOTCBZU-DqUKkAZP.js} +1 -1
  52. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CcvCr4GD.js → gitGraphDiagram-DS77QQ5N-BYUuXEgp.js} +1 -1
  53. package/dist/worker/console/static/assets/index-B0jGzQj6.js +407 -0
  54. package/dist/worker/console/static/assets/index-BiYbPiTw.css +1 -0
  55. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-HG5z7aVg.js → infoDiagram-6WML65LV-DNzKk7ah.js} +1 -1
  56. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BHEu9chR.js → ishikawaDiagram-WSZJBQD7-Clbyg5IP.js} +1 -1
  57. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-Cp_Up8-a.js → journeyDiagram-NVQOT4AX-zDO_gZNO.js} +1 -1
  58. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-DvVeY-em.js → kanban-definition-27J2QSJJ-x3LpRYek.js} +1 -1
  59. package/dist/worker/console/static/assets/{linear-aGmcjgTn.js → linear-CTpwZqKR.js} +1 -1
  60. package/dist/worker/console/static/assets/{mermaid.core-Biyo-tqA.js → mermaid.core-D_WXK6C7.js} +5 -5
  61. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DyxY2pIb.js → mindmap-definition-FAOFIHXS-CWK-OYP5.js} +1 -1
  62. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CkdGuc50.js → pegDiagram-VL7TDLO6-B9gtUPsM.js} +1 -1
  63. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-NNQI9xVR.js → pieDiagram-7S7Q4E2Y-Dj2lzceR.js} +1 -1
  64. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-PAJkuz_G.js → quadrantDiagram-CIZ2JOQS-Nwljut7m.js} +1 -1
  65. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-ZwrQod0O.js → railroadDiagram-AXF67PYL-BY-KNt93.js} +1 -1
  66. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D3ZfrzM2.js → requirementDiagram-LRYGKXZP-Vx0wvpPy.js} +1 -1
  67. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-DDGFmWk5.js → sankeyDiagram-W5VNT64P-B6grNXUk.js} +1 -1
  68. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DO6mmXTA.js → sequenceDiagram-SI44F4Z6-CteidBvh.js} +1 -1
  69. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DMbCQiJt.js → sizeCapture-X5ZJPWSS-Du4W8mFK.js} +1 -1
  70. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Di_YUEfC.js → stateDiagram-OKZ733FA-jyI6Yh81.js} +1 -1
  71. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DyQblISl.js +1 -0
  72. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-Bn2PphNJ.js → swimlanes-SLNWSIFB-DYWs_fRv.js} +2 -2
  73. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DiygmgYP.js +8 -0
  74. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DPh8pRsp.js → timeline-definition-Z64GVDOM-DO5LLy47.js} +1 -1
  75. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-GMfl26pW.js → vennDiagram-T6HMQDX7-CLF2p1_L.js} +1 -1
  76. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DTlmVrmF.js → wardleyDiagram-T6FBY63Y-AwTaYBxA.js} +1 -1
  77. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-YGBjKAS5.js → xychartDiagram-ELKLHX3M-DzbblQRw.js} +1 -1
  78. package/dist/worker/console/static/index.html +2 -2
  79. package/dist/worker/console/static-src/app/console-types.js +29 -4
  80. package/dist/worker/console/static-src/app/useConsoleShell.js +30 -12
  81. package/dist/worker/console/static-src/app/useOperatorActions.js +36 -6
  82. package/dist/worker/console/static-src/app/useRecoveryConsole.js +10 -11
  83. package/dist/worker/console/static-src/app/useRunProgress.js +2 -1
  84. package/dist/worker/console/static-src/app/useTaskWizard.js +2 -2
  85. package/dist/worker/console/static-src/night/useNightBoard.js +8 -5
  86. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +140 -8
  87. package/dist/worker/console/static-src/operator-chat/details-dag-actions.js +122 -0
  88. package/dist/worker/console/static-src/operator-chat/refs.js +3 -0
  89. package/dist/worker/console/static-src/operator-chat/runtime-selection-labels.js +27 -0
  90. package/dist/worker/console/static-src/operator-chat/turn-submission.js +3 -2
  91. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +168 -72
  92. package/dist/worker/console/static-src/operator-chat/useChatStream.js +78 -17
  93. package/dist/worker/console/static-src/operator-chat/useChatThread.js +184 -4
  94. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +63 -9
  95. package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
  96. package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -3
  97. package/dist/worker/console/static-src/shell/useWorkspaces.js +299 -0
  98. package/dist/worker/console/static-src/shell/workspace-route.js +337 -0
  99. package/dist/worker/console/workspace-context.js +234 -0
  100. package/dist/worker/console/workspace-registry.js +214 -0
  101. package/dist/worker/observe/server.js +4 -0
  102. package/dist/worker/observe/static/api.js +26 -6
  103. package/dist/worker/observe/static/app.js +18 -70
  104. package/dist/worker/observe/static/index.html +1 -1
  105. package/dist/worker/observe/static/inspect-workspace.js +307 -0
  106. package/dist/worker/observe/static/operator-chrome.css +145 -20
  107. package/dist/worker/observe/static/operator-chrome.d.ts +20 -3
  108. package/dist/worker/observe/static/operator-chrome.js +330 -100
  109. package/dist/worker/observe/static/router.d.ts +46 -0
  110. package/dist/worker/observe/static/router.js +25 -1
  111. package/dist/worker/observe/static/state.d.ts +50 -0
  112. package/dist/worker/observe/static/state.js +172 -0
  113. package/dist/worker/observe/static/views/dag-inspector.js +3 -2
  114. package/dist/worker/observe/static/views/dag.js +2 -1
  115. package/dist/worker/observe/static/views/dashboard.js +6 -4
  116. package/dist/worker/observe/static/views/night.js +2 -2
  117. package/dist/worker/observe/static/views/pool.js +1 -0
  118. package/dist/worker/observe/static/views/run.js +3 -1
  119. package/dist/worker/observe/static/views/task.js +1 -1
  120. package/dist/workflows/dag/interrupt-request.js +3 -3
  121. package/dist/workflows/dag/lifecycle.js +3 -3
  122. package/docs/README.md +1 -0
  123. package/docs/operations/local-development-environment.md +21 -0
  124. package/harness.json +3 -3
  125. package/package.json +3 -1
  126. package/dist/worker/console/static/assets/channel-Dj_4Moei.js +0 -1
  127. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C0YYAyCa.js +0 -1
  128. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C0YYAyCa.js +0 -1
  129. package/dist/worker/console/static/assets/index-CW88Fjbw.js +0 -406
  130. package/dist/worker/console/static/assets/index-DP0ZJl39.css +0 -1
  131. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DrdDvela.js +0 -1
  132. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DPkEAULl.js +0 -8
@@ -1,5 +1,6 @@
1
1
  import { useCallback, useEffect, useMemo, useState } from "react";
2
2
  import { buildTurnProcessGroups, hydrateSessionThread, selectRecentTurnGroups, } from "../../chat/turn-process.js";
3
+ import { mergeOperationCardState, } from "../../chat/operation-card.js";
3
4
  import { createChatSseEventApplier, } from "./chat-sse-events.js";
4
5
  import { taskContextUnchanged } from "./format.js";
5
6
  const EMPTY_USAGE = {
@@ -9,13 +10,125 @@ const EMPTY_USAGE = {
9
10
  cacheWriteTokens: 0,
10
11
  totalTokens: 0,
11
12
  };
13
+ const EMPTY_SESSION_STATS = {
14
+ turns: 0,
15
+ steps: 0,
16
+ llmMs: 0,
17
+ toolMs: 0,
18
+ ttftMs: 0,
19
+ ttftSteps: 0,
20
+ decodeMs: 0,
21
+ decodeTokens: 0,
22
+ };
23
+ /** R8 AC-R8-003: project a raw GET /operations/:id fact (the same shape the
24
+ * server persists) into a browser OperationCardState. Field-validated; any
25
+ * malformed fact returns undefined (no fabricated cards). */
26
+ function operationCardFromOperationFact(op) {
27
+ const str = (key) => {
28
+ const value = op[key];
29
+ return typeof value === "string" && value ? value : undefined;
30
+ };
31
+ const operationId = str("operationId");
32
+ const action = str("action");
33
+ const state = str("state");
34
+ const createdAt = str("createdAt");
35
+ if (!operationId || !action || !state || !createdAt)
36
+ return undefined;
37
+ const updatedAt = str("updatedAt") ?? str("finishedAt") ?? str("startedAt") ?? createdAt;
38
+ const dagRunId = str("dagRunId");
39
+ return {
40
+ schemaVersion: 1,
41
+ operationId,
42
+ action,
43
+ state,
44
+ createdAt,
45
+ updatedAt,
46
+ inspectHref: `/inspect/?operationId=${encodeURIComponent(operationId)}#/`,
47
+ ...(str("taskId") ? { taskId: str("taskId") } : {}),
48
+ ...(dagRunId ? { dagRunId } : {}),
49
+ ...(str("errorCode") ? { errorCode: str("errorCode") } : {}),
50
+ activityRef: `operation:${operationId}`,
51
+ previewHash: `${operationId}:${state}:${updatedAt}`,
52
+ };
53
+ }
54
+ /**
55
+ * Apply one settled operation-link mutation to the visible card projection.
56
+ *
57
+ * The mutation identity is captured before the request starts. Both the
58
+ * session and workspace are fenced before and after the follow-up read so a
59
+ * late response can never leak into a newly selected workspace/session.
60
+ */
61
+ export async function applyOperationLinkMutationProjection(input) {
62
+ const { identity } = input;
63
+ const stillOwnsProjection = () => {
64
+ const current = input.currentOwner();
65
+ return (current.sessionId === identity.sessionId &&
66
+ current.workspaceId === identity.workspaceId);
67
+ };
68
+ if (!identity.workspaceId || !stillOwnsProjection())
69
+ return false;
70
+ if (identity.action === "remove") {
71
+ input.setOperationCards((current) => current.filter((card) => card.operationId !== identity.operationId));
72
+ return true;
73
+ }
74
+ try {
75
+ const workspaceQuery = new URLSearchParams({
76
+ workspaceId: identity.workspaceId,
77
+ });
78
+ const fetchImpl = input.fetchFn ?? fetch;
79
+ const res = await fetchImpl(`${input.origin}/api/operator/v1/operations/${encodeURIComponent(identity.operationId)}?${workspaceQuery.toString()}`, { credentials: "include" });
80
+ if (!res.ok || !stillOwnsProjection())
81
+ return false;
82
+ const body = (await res.json());
83
+ const operation = body.operation;
84
+ if (body.workspaceId !== identity.workspaceId ||
85
+ !operation ||
86
+ operation.workspaceId !== identity.workspaceId ||
87
+ operation.operationId !== identity.operationId ||
88
+ !stillOwnsProjection())
89
+ return false;
90
+ const card = operationCardFromOperationFact(operation);
91
+ if (!card)
92
+ return false;
93
+ input.setOperationCards((current) => stillOwnsProjection()
94
+ ? mergeOperationCardState(current, card)
95
+ : current);
96
+ return stillOwnsProjection();
97
+ }
98
+ catch {
99
+ return false;
100
+ }
101
+ }
102
+ function mergeCompactionMessages(messages, compactions) {
103
+ const result = [...messages];
104
+ for (const item of compactions ?? []) {
105
+ const data = item.data && typeof item.data === "object" ? item.data : {};
106
+ const eventId = typeof item.eventId === "string" ? item.eventId : undefined;
107
+ const id = eventId ? `compaction-${eventId}` : `compaction-${item.at ?? Date.now()}`;
108
+ if (result.some((message) => message.id === id))
109
+ continue;
110
+ result.push({
111
+ id,
112
+ role: "assistant",
113
+ kind: "compaction",
114
+ text: typeof data.summary === "string" ? data.summary : "历史上下文已精简,关键决策与当前任务已保留。",
115
+ createdAt: Date.parse(item.at ?? "") || Date.now(),
116
+ compaction: {
117
+ ...(typeof data.tokensBefore === "number" ? { tokensBefore: data.tokensBefore } : {}),
118
+ ...(typeof data.firstKeptEntryId === "string" ? { firstKeptEntryId: data.firstKeptEntryId } : {}),
119
+ ...(eventId ? { eventId } : {}),
120
+ },
121
+ });
122
+ }
123
+ return result.sort((a, b) => a.createdAt - b.createdAt);
124
+ }
12
125
  /**
13
126
  * Thread state + SSE block parsing. The event-application logic itself lives
14
127
  * in chat-sse-events.ts (createChatSseEventApplier); this hook owns the state
15
128
  * slices it patches and the derived timeline projections.
16
129
  */
17
130
  export function useChatThread(params) {
18
- const { origin, refs, sessionId, setError, onReconcile } = params;
131
+ const { origin, refs, sessionId, workspaceId, setError, onReconcile } = params;
19
132
  // setProcessOpen remains on the hook API for session lifecycle close/reset
20
133
  // (UI-08); the SSE applier must not open the process panel.
21
134
  void params.setProcessOpen;
@@ -30,12 +143,36 @@ export function useChatThread(params) {
30
143
  const [taskContext, setTaskContext] = useState(null);
31
144
  const [contextLoading, setContextLoading] = useState(false);
32
145
  const [usage, setUsage] = useState(EMPTY_USAGE);
146
+ const [sessionStats, setSessionStats] = useState(EMPTY_SESSION_STATS);
33
147
  const [streaming, setStreaming] = useState(false);
34
148
  const [turnPhase, setTurnPhase] = useState("idle");
35
149
  const [deferredTurn, setDeferredTurn] = useState(null);
150
+ const [deferredTurns, setDeferredTurns] = useState([]);
151
+ const [pendingQuestion, setPendingQuestion] = useState(null);
152
+ const [todos, setTodos] = useState([]);
36
153
  /** Assistant bubble id for the in-flight turn; only this row shows the streaming cursor. */
37
154
  const [streamingAssistantId, setStreamingAssistantId] = useState(null);
38
155
  const [showSystemInMessages] = useState(false);
156
+ const refreshSessionTelemetry = useCallback(async () => {
157
+ const targetSessionId = refs.sessionRef.current?.sessionId;
158
+ if (!targetSessionId)
159
+ return;
160
+ try {
161
+ const res = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(targetSessionId)}`, { credentials: "include" });
162
+ if (!res.ok)
163
+ return;
164
+ const body = (await res.json());
165
+ if (refs.sessionRef.current?.sessionId !== targetSessionId)
166
+ return;
167
+ if (body.sessionStats)
168
+ setSessionStats(body.sessionStats);
169
+ if (body.usage)
170
+ setUsage(body.usage);
171
+ }
172
+ catch {
173
+ // Telemetry is supplementary; the normal event stream remains authoritative.
174
+ }
175
+ }, [origin, refs]);
39
176
  // All inputs are stable (state setters + the memoized refs bag), so the
40
177
  // applier identity is stable across renders.
41
178
  const applyChatSseEvent = useMemo(() => createChatSseEventApplier({
@@ -48,13 +185,17 @@ export function useChatThread(params) {
48
185
  setStreaming,
49
186
  setTurnPhase,
50
187
  setDeferredTurn,
188
+ setDeferredTurns,
189
+ setPendingQuestion,
190
+ setTodos,
51
191
  setStreamingAssistantId,
52
192
  setUnread,
53
193
  setOperationCards,
54
194
  setUsage,
195
+ onTelemetryRefresh: () => void refreshSessionTelemetry(),
55
196
  setMessages,
56
197
  setToolCalls,
57
- }), [refs, onReconcile, setError]);
198
+ }), [refs, onReconcile, setError, refreshSessionTelemetry]);
58
199
  /** Parse + apply one raw SSE block (frames separated by \n\n). Returns the
59
200
  * eventId if the block carried an `id:` line (for Last-Event-ID tracking). */
60
201
  const applySseBlock = useCallback((block, assistantId, options) => {
@@ -124,6 +265,21 @@ export function useChatThread(params) {
124
265
  }, 350);
125
266
  return () => window.clearTimeout(timer);
126
267
  }, [sessionId, operationCards, humanGateCards, interview, loadTaskContext]);
268
+ /** R8 AC-R8-003: deterministic mutation-driven operationCards updates.
269
+ * Binds to the identity captured when the real POST/DELETE was issued:
270
+ * - add → consume server truth for that operation (GET operation) and
271
+ * upsert the card when the captured session still matches.
272
+ * - remove → drop the captured operation's card immediately (the server
273
+ * tombstone guarantees it never resurrects on replay). */
274
+ const applyOperationLinkMutation = useCallback((identity) => applyOperationLinkMutationProjection({
275
+ origin,
276
+ identity,
277
+ currentOwner: () => ({
278
+ sessionId: refs.sessionRef.current?.sessionId,
279
+ workspaceId,
280
+ }),
281
+ setOperationCards,
282
+ }), [origin, refs, workspaceId]);
127
283
  const timelineMessages = useMemo(() => messages.filter((m) => m.role !== "system" || showSystemInMessages), [messages, showSystemInMessages]);
128
284
  useEffect(() => {
129
285
  const onVisibility = () => {
@@ -193,9 +349,13 @@ export function useChatThread(params) {
193
349
  setInterview(null);
194
350
  setTaskContext(null);
195
351
  setUsage(EMPTY_USAGE);
352
+ setSessionStats(EMPTY_SESSION_STATS);
196
353
  setStreaming(false);
197
354
  setTurnPhase("idle");
198
355
  setDeferredTurn(null);
356
+ setDeferredTurns([]);
357
+ setPendingQuestion(null);
358
+ setTodos([]);
199
359
  setStreamingAssistantId(null);
200
360
  setUnread(0);
201
361
  setHistoryLimit(50);
@@ -210,11 +370,15 @@ export function useChatThread(params) {
210
370
  setArtifactCards([]);
211
371
  setTaskContext(null);
212
372
  setInterview(null);
213
- setUsage(EMPTY_USAGE);
373
+ setUsage(next.usage ?? EMPTY_USAGE);
374
+ setSessionStats(next.sessionStats ?? EMPTY_SESSION_STATS);
214
375
  refs.usageResponseKeysRef.current = new Set();
215
376
  setStreaming(next.streaming);
216
377
  setTurnPhase(next.turnPhase ?? (next.streaming ? "running" : "idle"));
217
378
  setDeferredTurn(next.deferredTurn ?? null);
379
+ setDeferredTurns(next.deferredTurns ?? (next.deferredTurn ? [next.deferredTurn] : []));
380
+ setPendingQuestion(next.pendingQuestion ?? null);
381
+ setTodos(next.todos ?? []);
218
382
  setStreamingAssistantId(null);
219
383
  setUnread(0);
220
384
  setHistoryLimit(50);
@@ -238,6 +402,13 @@ export function useChatThread(params) {
238
402
  refs.streamingRef.current = Boolean(activeTurnId);
239
403
  setTurnPhase(body.session.turnPhase ?? (activeTurnId ? "running" : "idle"));
240
404
  setDeferredTurn(body.session.deferredTurn ?? null);
405
+ setDeferredTurns(body.session.deferredTurns ?? (body.session.deferredTurn ? [body.session.deferredTurn] : []));
406
+ setPendingQuestion(body.session.pendingQuestion ?? null);
407
+ setTodos(body.session.todos ?? []);
408
+ if (body.sessionStats)
409
+ setSessionStats(body.sessionStats);
410
+ if (body.usage)
411
+ setUsage(body.usage);
241
412
  // Resume the events stream from the snapshot tail so the next
242
413
  // reconnect never immediately re-expires (AC-003). The refs below
243
414
  // are reset so any follow-up tail replay re-applies cleanly.
@@ -246,7 +417,7 @@ export function useChatThread(params) {
246
417
  refs.turnAssistantIdsRef.current = new Map();
247
418
  refs.sealedAssistantIdsRef.current = new Set();
248
419
  const hydrated = hydrateSessionThread(body.session.messages ?? []);
249
- setMessages(hydrated.messages);
420
+ setMessages(mergeCompactionMessages(hydrated.messages, body.compactions));
250
421
  setToolCalls(hydrated.toolCalls);
251
422
  setStreamingAssistantId(null);
252
423
  return true;
@@ -264,6 +435,8 @@ export function useChatThread(params) {
264
435
  setStreamingAssistantId,
265
436
  setTurnPhase,
266
437
  setDeferredTurn,
438
+ setPendingQuestion,
439
+ setTodos,
267
440
  ]);
268
441
  return {
269
442
  messages,
@@ -281,12 +454,18 @@ export function useChatThread(params) {
281
454
  taskContext,
282
455
  contextLoading,
283
456
  usage,
457
+ sessionStats,
284
458
  streaming,
285
459
  setStreaming,
286
460
  turnPhase,
287
461
  setTurnPhase,
288
462
  deferredTurn,
289
463
  setDeferredTurn,
464
+ deferredTurns,
465
+ setDeferredTurns,
466
+ pendingQuestion,
467
+ setPendingQuestion,
468
+ todos,
290
469
  streamingAssistantId,
291
470
  setStreamingAssistantId,
292
471
  visibleMessages,
@@ -295,6 +474,7 @@ export function useChatThread(params) {
295
474
  applyChatSseEvent,
296
475
  applySseBlock,
297
476
  loadTaskContext,
477
+ applyOperationLinkMutation,
298
478
  resetForNewSession,
299
479
  applyActivatedSession,
300
480
  recoverSession,
@@ -1,21 +1,32 @@
1
1
  import { useCallback, useEffect, useRef, useState } from "react";
2
2
  import { ACTIVE_CHAT_SESSION_STORAGE_KEY } from "../../chat/workspace-landing.js";
3
- /** Left-rail repo tree + file preview tabs state. */
3
+ /** Details-rail repo tree + file preview tabs state. */
4
4
  export function useRepoBrowser(params) {
5
- const { origin, refs, sessionId, setError } = params;
5
+ const { origin, refs, sessionId, workspaceId, blocked, setError } = params;
6
6
  const [tree, setTree] = useState({});
7
7
  const [expandedDirs, setExpandedDirs] = useState(new Set());
8
8
  const [treeError, setTreeError] = useState(null);
9
9
  const [previews, setPreviews] = useState([]);
10
10
  const [activePreviewPath, setActivePreviewPath] = useState(null);
11
11
  const [previewLoading, setPreviewLoading] = useState(false);
12
+ const workspaceScopeRef = useRef(workspaceId ?? null);
13
+ workspaceScopeRef.current = workspaceId ?? null;
14
+ const autoPreviewedWorkspaceRef = useRef(new Set());
12
15
  const expandedDirsRef = useRef(new Set());
13
16
  expandedDirsRef.current = expandedDirs;
17
+ const workspaceQuery = workspaceId
18
+ ? `&workspaceId=${encodeURIComponent(workspaceId)}`
19
+ : "";
14
20
  const loadTree = useCallback(async (directory = "") => {
21
+ if (blocked)
22
+ return; // AC-R5-002: fail-closed, zero repo requests
23
+ const requestWorkspaceId = workspaceId ?? null;
15
24
  try {
16
25
  setTreeError(null);
17
- const res = await fetch(`${origin}/api/operator/v1/chat/repo/tree?path=${encodeURIComponent(directory)}`, { credentials: "include" });
26
+ const res = await fetch(`${origin}/api/operator/v1/chat/repo/tree?path=${encodeURIComponent(directory)}${workspaceQuery}`, { credentials: "include" });
18
27
  const body = (await res.json());
28
+ if (workspaceScopeRef.current !== requestWorkspaceId)
29
+ return;
19
30
  if (!res.ok)
20
31
  throw new Error(body.error?.message ?? "无法读取仓库目录");
21
32
  setTree((current) => ({
@@ -24,9 +35,11 @@ export function useRepoBrowser(params) {
24
35
  }));
25
36
  }
26
37
  catch (error) {
38
+ if (workspaceScopeRef.current !== requestWorkspaceId)
39
+ return;
27
40
  setTreeError(error instanceof Error ? error.message : String(error));
28
41
  }
29
- }, [origin]);
42
+ }, [origin, workspaceQuery, workspaceId, blocked]);
30
43
  useEffect(() => {
31
44
  void loadTree();
32
45
  }, [loadTree]);
@@ -53,10 +66,15 @@ export function useRepoBrowser(params) {
53
66
  return () => document.removeEventListener("visibilitychange", onVisibility);
54
67
  }, [refreshTree]);
55
68
  const openPreview = useCallback(async (file) => {
69
+ if (blocked)
70
+ return; // AC-R5-002: fail-closed
71
+ const requestWorkspaceId = workspaceId ?? null;
56
72
  setPreviewLoading(true);
57
73
  try {
58
- const res = await fetch(`${origin}/api/operator/v1/chat/repo/file?path=${encodeURIComponent(file)}`, { credentials: "include" });
74
+ const res = await fetch(`${origin}/api/operator/v1/chat/repo/file?path=${encodeURIComponent(file)}${workspaceQuery}`, { credentials: "include" });
59
75
  const body = (await res.json());
76
+ if (workspaceScopeRef.current !== requestWorkspaceId)
77
+ return;
60
78
  if (!res.ok || !body.data)
61
79
  throw new Error(body.error?.message ?? "无法预览文件");
62
80
  setPreviews((current) => {
@@ -72,12 +90,48 @@ export function useRepoBrowser(params) {
72
90
  setActivePreviewPath(body.data.path);
73
91
  }
74
92
  catch (error) {
93
+ if (workspaceScopeRef.current !== requestWorkspaceId)
94
+ return;
75
95
  setError(error instanceof Error ? error.message : String(error));
76
96
  }
77
97
  finally {
78
- setPreviewLoading(false);
98
+ if (workspaceScopeRef.current === requestWorkspaceId)
99
+ setPreviewLoading(false);
79
100
  }
80
- }, [origin, refs, setError]);
101
+ }, [origin, workspaceQuery, workspaceId, refs, setError, blocked]);
102
+ // A workspace switch must immediately remove the previous workspace's tree
103
+ // and file contents while the newly scoped requests are in flight.
104
+ useEffect(() => {
105
+ setTree({});
106
+ setExpandedDirs(new Set());
107
+ setTreeError(null);
108
+ setPreviews([]);
109
+ setActivePreviewPath(null);
110
+ setPreviewLoading(false);
111
+ }, [workspaceId]);
112
+ // DSH parity: Details opens as a real tree + preview split. Select a real
113
+ // root file once per workspace (README first), never placeholder content.
114
+ useEffect(() => {
115
+ if (blocked || activePreviewPath || previews.length > 0)
116
+ return;
117
+ const workspaceKey = workspaceId ?? "__default__";
118
+ if (autoPreviewedWorkspaceRef.current.has(workspaceKey))
119
+ return;
120
+ const rootFiles = (tree[""] ?? []).filter((entry) => entry.kind === "file");
121
+ if (rootFiles.length === 0)
122
+ return;
123
+ const initial = rootFiles.find((entry) => /^readme(?:\.[^.]+)?$/i.test(entry.name)) ??
124
+ rootFiles[0];
125
+ autoPreviewedWorkspaceRef.current.add(workspaceKey);
126
+ void openPreview(initial.path);
127
+ }, [
128
+ activePreviewPath,
129
+ blocked,
130
+ openPreview,
131
+ previews.length,
132
+ tree,
133
+ workspaceId,
134
+ ]);
81
135
  useEffect(() => {
82
136
  if (!sessionId)
83
137
  return;
@@ -95,7 +149,7 @@ export function useRepoBrowser(params) {
95
149
  if (!paths.length)
96
150
  return;
97
151
  void Promise.all(paths.map(async (file) => {
98
- const res = await fetch(`${origin}/api/operator/v1/chat/repo/file?path=${encodeURIComponent(file)}`, { credentials: "include" });
152
+ const res = await fetch(`${origin}/api/operator/v1/chat/repo/file?path=${encodeURIComponent(file)}${workspaceQuery}`, { credentials: "include" });
99
153
  if (!res.ok)
100
154
  return undefined;
101
155
  return (await res.json()).data;
@@ -106,7 +160,7 @@ export function useRepoBrowser(params) {
106
160
  setPreviews(available);
107
161
  setActivePreviewPath(available.at(-1)?.path ?? null);
108
162
  });
109
- }, [origin, sessionId, refs]);
163
+ }, [origin, workspaceQuery, sessionId, refs]);
110
164
  const toggleDir = useCallback((dir) => {
111
165
  setExpandedDirs((current) => {
112
166
  const next = new Set(current);
@@ -0,0 +1,4 @@
1
+ export const WORKSPACE_BROWSER_OK = { status: "ok" };
2
+ export function workspaceBrowserGateActive(gate) {
3
+ return gate.status !== "ok";
4
+ }
@@ -1,12 +1,12 @@
1
1
  /** Workspace layout modes resolved from measured Chat container width. */
2
2
  /** Width thresholds in CSS px against `.oc-workspace` (not window/UA). */
3
3
  export const WORKSPACE_LAYOUT_THRESHOLDS = {
4
- comfortableMin: 1450,
5
- compactMin: 1100,
4
+ comfortableMin: 1200,
5
+ compactMin: 720,
6
6
  };
7
7
  /**
8
8
  * Resolve the three-band workspace mode from a measured container width.
9
- * Boundaries: comfortable >=1450, compact 1100-1449, narrow <1100.
9
+ * Boundaries: comfortable >=1200, compact 720-1199, narrow <720.
10
10
  */
11
11
  export function resolveWorkspaceLayoutMode(widthPx) {
12
12
  if (!Number.isFinite(widthPx) || widthPx < 0)