@tea-agent/loop-agent 0.44.0-next.5 → 0.44.0-next.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +299 -445
- package/dist/build-stamp.json +3 -3
- package/dist/executors/pi-sdk-executor.js +7 -7
- package/dist/infrastructure/console/operation-store.js +46 -14
- package/dist/shared/git-workspace-status.js +2 -0
- package/dist/shared/wizard-local-telemetry.js +2 -1
- package/dist/worker/console/chat/assistant-content.js +5 -34
- package/dist/worker/console/chat/chat-event-store.js +27 -10
- package/dist/worker/console/chat/db-connections.js +72 -8
- package/dist/worker/console/chat/pi-runtime/custom-tools/sql-tools.js +52 -24
- package/dist/worker/console/chat/pi-runtime.js +251 -18
- package/dist/worker/console/chat/repo-browser.js +49 -0
- package/dist/worker/console/chat/routes.js +215 -66
- package/dist/worker/console/chat/session-store.js +147 -19
- package/dist/worker/console/chat/turn-process.js +69 -23
- package/dist/worker/console/chat/user-turn-outline.js +19 -0
- package/dist/worker/console/console-update-runtime.js +2 -2
- package/dist/worker/console/operator-user-error.js +28 -1
- package/dist/worker/console/pi-readiness.js +4 -4
- package/dist/worker/console/recovery-cta.js +14 -14
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-BHuhGM1e.js → abnfDiagram-N423BO3Z-DTuku_-7.js} +1 -1
- package/dist/worker/console/static/assets/{arc-CKh5E_sr.js → arc-CQbCsSqb.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DizhiBEv.js → architectureDiagram-T3A2C74G-C-xUntTR.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-3Neea-Z-.js → blockDiagram-VBNYF7ZC-DhNlwGAl.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BCP_Qrgi.js → c4Diagram-5PPSVZJV-B7j3nx8o.js} +1 -1
- package/dist/worker/console/static/assets/channel-5_os7t_x.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-C_wydftK.js → chunk-2GRJ4B5K-BCrcxEh2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-jRPXZbzx.js → chunk-2Q5K7J3B-CsaePDb3.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BLQf2OYO.js → chunk-5RXB4S5H--SRQc1rN.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-0qRq64rI.js → chunk-5VM5RSS4-C5BFY2Po.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-ClMg8xmV.js → chunk-6Q2QTUOP-Caso6uN7.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-Coop2FlL.js → chunk-GF5L2VYU-AY2ZSAx2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CDdO-jPJ.js → chunk-JWPE2WC7-DiMqZQ_8.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-D8Gkppjl.js → chunk-KBJHAD2P-gpvvriOp.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DA9HIySf.js → chunk-RYQCIY6F-CUdyoJhI.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DZNDRX1k.js → chunk-XXDRQBXY-Bp1xW3T4.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-loz0n18W.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-loz0n18W.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CZ1zGyjq.js → cose-bilkent-JH36ORCC-D-JIcOIT.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-B8u1_FtM.js → cynefin-VYW2F7L2-C-NKFflx.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-CPBB6lfo.js → cynefinDiagram-MW4NZA55-CSOcvOAA.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CG3Eov3E.js → dagre-VZM6K2ZE-BQSBltMy.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-ycI-dOjh.js → diagram-7IWD3JNH-B0LVC45G.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Bx_GyQ7Y.js → diagram-B4RE2ZJO-Cypg3nqy.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-m0G6mP4d.js → diagram-LBJQPF4R-BBDU88XJ.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DTDJkqcJ.js → diagram-Q27KOJAE-BZusLqSb.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BCfFrG-p.js → diagram-UB23O5K3-Cfolotcr.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BIkTNPpp.js → ebnfDiagram-BXEA7PRR-CjfyUy0m.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DTcMWrd2.js → erDiagram-JOGREHBK-DJdz7txE.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-BqzDaNQ7.js → flowDiagram-UKHOOZJN-CQqgPO6y.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-BSl_vGFe.js → ganttDiagram-PKOTCBZU-BOpGfS2m.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DCA5HXM6.js → gitGraphDiagram-DS77QQ5N-C6UCu0qx.js} +1 -1
- package/dist/worker/console/static/assets/index-CUT440A8.css +1 -0
- package/dist/worker/console/static/assets/index-DWKejNVJ.js +486 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DvqT18ge.js → infoDiagram-6WML65LV-BWeaVEUB.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-B6zGKrQ3.js → ishikawaDiagram-WSZJBQD7-C8gmlBbk.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BpLQPI9C.js → journeyDiagram-NVQOT4AX-DF3bm8Bp.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CzIz_-Lt.js → kanban-definition-27J2QSJJ-D2fG2Vxi.js} +1 -1
- package/dist/worker/console/static/assets/{linear-CMQ8Jpp-.js → linear-BGKgMc2T.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-BdAMrTf7.js → mermaid.core-DF6mMPJ9.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-hSwF6sOq.js → mindmap-definition-FAOFIHXS-AMhePheC.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DZdv509B.js → pegDiagram-VL7TDLO6-DN1Cl0W0.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CmD4-b0K.js → pieDiagram-7S7Q4E2Y-Cx9ngUeN.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-DtKxNxlS.js → quadrantDiagram-CIZ2JOQS-BcYPTQJb.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-D48iIoiS.js → railroadDiagram-AXF67PYL-BqHUZWnH.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-C3N0K-2p.js → requirementDiagram-LRYGKXZP-CmqWZ0-B.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-CACWR-Xs.js → sankeyDiagram-W5VNT64P--4YetH1y.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-g_QgugvZ.js → sequenceDiagram-SI44F4Z6-DGD4VMfD.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DqLoRPGK.js → sizeCapture-X5ZJPWSS-QVw9q7rh.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-ES59VgMG.js → stateDiagram-OKZ733FA-CuemV9pe.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BdlGdIJw.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-fjyLVNWo.js → swimlanes-SLNWSIFB-C7oKzZKs.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DjjbHbde.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Cd5-biKv.js → timeline-definition-Z64GVDOM-C2Of60nT.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-CZTu6mPf.js → vennDiagram-T6HMQDX7-DHtpQHyL.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DR01jjbe.js → wardleyDiagram-T6FBY63Y-COi9OhSy.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-iZcuGVGa.js → xychartDiagram-ELKLHX3M-DPHQfRrt.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +9 -1
- package/dist/worker/console/static-src/operator-chat/chat-link.js +20 -0
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +58 -3
- package/dist/worker/console/static-src/operator-chat/details-rail-surfaces.js +98 -0
- package/dist/worker/console/static-src/operator-chat/file-palette-nav.js +81 -0
- package/dist/worker/console/static-src/operator-chat/process-label.js +19 -6
- package/dist/worker/console/static-src/operator-chat/transcript-mode.js +21 -0
- package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +2 -0
- package/dist/worker/console/static-src/operator-chat/turn-process-disclosure.js +13 -5
- package/dist/worker/console/static-src/operator-chat/turn-state-projection.js +166 -0
- package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +56 -2
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +202 -41
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +78 -23
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +55 -18
- package/dist/worker/console/static-src/operator-chat/useComposer.js +80 -6
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +13 -3
- package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +36 -17
- package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +18 -0
- package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
- package/dist/worker/console/static-src/shell/console-update-reload.js +1 -1
- package/dist/worker/console/workspace-initialization.js +3 -3
- package/dist/worker/observability/read-model.js +23 -2
- package/dist/worker/scheduler/scheduled-goal-dispatch.js +3 -2
- package/dist/worker/scheduler/scheduled-goal-evidence.js +6 -1
- package/docs/templates/backend-test-dag.json +1 -0
- package/package.json +4 -4
- package/dist/worker/console/static/assets/channel-DxgzA1GF.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BZcm7fs6.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BZcm7fs6.js +0 -1
- package/dist/worker/console/static/assets/index-BAEDjTbD.css +0 -1
- package/dist/worker/console/static/assets/index-wKEJUaP5.js +0 -482
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DyCIq6jx.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CsKtO34q.js +0 -8
|
@@ -9,6 +9,7 @@ import { taskContextUnchanged } from "./format.js";
|
|
|
9
9
|
import { upsertCompactionMessage } from "./compaction-message.js";
|
|
10
10
|
import { sameTurnGroupInputs } from "./turn-group-equality.js";
|
|
11
11
|
import { conversationNodesFromEvents, } from "../../chat/conversation-nodes.js";
|
|
12
|
+
import { mergeTurnStateSummaries, setProjectedTurnState, } from "./turn-state-projection.js";
|
|
12
13
|
const EMPTY_USAGE = {
|
|
13
14
|
inputTokens: 0,
|
|
14
15
|
outputTokens: 0,
|
|
@@ -141,8 +142,13 @@ export function useChatThread(params) {
|
|
|
141
142
|
const [usage, setUsage] = useState(EMPTY_USAGE);
|
|
142
143
|
const [sessionStats, setSessionStats] = useState(EMPTY_SESSION_STATS);
|
|
143
144
|
const [turnTelemetry, setTurnTelemetry] = useState([]);
|
|
145
|
+
const [userTurnOutline, setUserTurnOutline] = useState([]);
|
|
144
146
|
const [operatorActions, setOperatorActions] = useState([]);
|
|
145
147
|
const [conversationNodes, setConversationNodes] = useState([]);
|
|
148
|
+
const [turnStatesById, setTurnStatesById] = useState({});
|
|
149
|
+
const reconcileTurnStates = useCallback((summaries) => {
|
|
150
|
+
setTurnStatesById((current) => mergeTurnStateSummaries(current, summaries));
|
|
151
|
+
}, []);
|
|
146
152
|
const [sddSessionIdentity, setSddSessionIdentity] = useState(null);
|
|
147
153
|
const [streaming, setStreaming] = useState(false);
|
|
148
154
|
const [turnPhase, setTurnPhase] = useState("idle");
|
|
@@ -172,11 +178,12 @@ export function useChatThread(params) {
|
|
|
172
178
|
setUsage(body.usage);
|
|
173
179
|
setTurnTelemetry(body.turnTelemetry ?? []);
|
|
174
180
|
setSddSessionIdentity(body.sddDataAlignment ?? null);
|
|
181
|
+
reconcileTurnStates(body.session?.turnStates);
|
|
175
182
|
}
|
|
176
183
|
catch {
|
|
177
184
|
// Telemetry is supplementary; the normal event stream remains authoritative.
|
|
178
185
|
}
|
|
179
|
-
}, [origin, refs]);
|
|
186
|
+
}, [origin, refs, reconcileTurnStates]);
|
|
180
187
|
// All inputs are stable (state setters + the memoized refs bag), so the
|
|
181
188
|
// applier identity is stable across renders.
|
|
182
189
|
const applyChatSseEvent = useMemo(() => createChatSseEventApplier({
|
|
@@ -195,6 +202,7 @@ export function useChatThread(params) {
|
|
|
195
202
|
setGoal,
|
|
196
203
|
setOperatorActions,
|
|
197
204
|
setConversationNodes,
|
|
205
|
+
setTurnState: (turnId, state) => setTurnStatesById((current) => setProjectedTurnState(current, turnId, state)),
|
|
198
206
|
setSddSessionIdentity,
|
|
199
207
|
setStreamingAssistantId,
|
|
200
208
|
setUnread,
|
|
@@ -334,6 +342,7 @@ export function useChatThread(params) {
|
|
|
334
342
|
streaming,
|
|
335
343
|
activeAssistantId: streamingAssistantId,
|
|
336
344
|
conversationNodes,
|
|
345
|
+
turnStatesById,
|
|
337
346
|
});
|
|
338
347
|
// Preserve completed group objects across streaming tokens. Stable
|
|
339
348
|
// historical groups keep their references so
|
|
@@ -353,11 +362,12 @@ export function useChatThread(params) {
|
|
|
353
362
|
streaming,
|
|
354
363
|
streamingAssistantId,
|
|
355
364
|
conversationNodes,
|
|
365
|
+
turnStatesById,
|
|
356
366
|
historyLimit,
|
|
357
367
|
]);
|
|
358
|
-
const
|
|
368
|
+
const loadHistoryPages = useCallback(async (input) => {
|
|
359
369
|
const targetSessionId = refs.sessionRef.current?.sessionId;
|
|
360
|
-
|
|
370
|
+
let beforeId = historyNextCursor;
|
|
361
371
|
if (!targetSessionId || !historyHasMore || !beforeId || historyLoadingRef.current)
|
|
362
372
|
return false;
|
|
363
373
|
const generation = refs.sessionGenerationRef.current;
|
|
@@ -365,17 +375,29 @@ export function useChatThread(params) {
|
|
|
365
375
|
historyLoadingRef.current = request;
|
|
366
376
|
setHistoryLoading(true);
|
|
367
377
|
try {
|
|
368
|
-
const
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
378
|
+
const collectedMessages = [];
|
|
379
|
+
const collectedTurnStates = [];
|
|
380
|
+
let hasMore = historyHasMore;
|
|
381
|
+
let found = false;
|
|
382
|
+
do {
|
|
383
|
+
const query = new URLSearchParams({ limit: "50", beforeId });
|
|
384
|
+
const res = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(targetSessionId)}/messages?${query.toString()}`, { credentials: "include" });
|
|
385
|
+
if (!res.ok || refs.sessionRef.current?.sessionId !== targetSessionId)
|
|
386
|
+
return false;
|
|
387
|
+
const body = (await res.json());
|
|
388
|
+
if (refs.sessionRef.current?.sessionId !== targetSessionId ||
|
|
389
|
+
refs.sessionGenerationRef.current !== generation ||
|
|
390
|
+
historyLoadingRef.current !== request)
|
|
391
|
+
return false;
|
|
392
|
+
const pageMessages = body.messages ?? [];
|
|
393
|
+
collectedMessages.unshift(...pageMessages);
|
|
394
|
+
collectedTurnStates.push(...(body.turnStates ?? []));
|
|
395
|
+
found = Boolean(input?.untilMessageId && pageMessages.some((message) => message.id === input.untilMessageId));
|
|
396
|
+
hasMore = body.hasMore === true;
|
|
397
|
+
beforeId = body.nextCursor ?? "";
|
|
398
|
+
} while (input?.untilMessageId && !found && hasMore && beforeId);
|
|
399
|
+
const page = hydrateSessionThread(collectedMessages);
|
|
400
|
+
input?.beforePrepend?.();
|
|
379
401
|
setMessages((current) => {
|
|
380
402
|
const seen = new Set();
|
|
381
403
|
const merged = [...page.messages, ...current].filter((message) => {
|
|
@@ -392,10 +414,11 @@ export function useChatThread(params) {
|
|
|
392
414
|
byId.set(tool.id, tool);
|
|
393
415
|
return [...byId.values()].sort((a, b) => a.createdAt - b.createdAt);
|
|
394
416
|
});
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
417
|
+
setTurnStatesById((current) => mergeTurnStateSummaries(current, collectedTurnStates));
|
|
418
|
+
setHistoryHasMore(hasMore);
|
|
419
|
+
setHistoryNextCursor(beforeId || null);
|
|
420
|
+
setHistoryLimit((value) => value + collectedMessages.length);
|
|
421
|
+
return input?.untilMessageId ? found : true;
|
|
399
422
|
}
|
|
400
423
|
catch (error) {
|
|
401
424
|
if (refs.sessionRef.current?.sessionId === targetSessionId && refs.sessionGenerationRef.current === generation) {
|
|
@@ -410,6 +433,10 @@ export function useChatThread(params) {
|
|
|
410
433
|
}
|
|
411
434
|
}
|
|
412
435
|
}, [historyHasMore, historyNextCursor, origin, refs, setError]);
|
|
436
|
+
const loadEarlierMessages = useCallback((beforePrepend) => loadHistoryPages({ beforePrepend }), [loadHistoryPages]);
|
|
437
|
+
const loadThroughMessage = useCallback((messageId) => messagesRef.current.some((message) => message.id === messageId)
|
|
438
|
+
? Promise.resolve(true)
|
|
439
|
+
: loadHistoryPages({ untilMessageId: messageId }), [loadHistoryPages]);
|
|
413
440
|
const visibleMessages = useMemo(() => {
|
|
414
441
|
const next = [];
|
|
415
442
|
for (const group of turnGroups) {
|
|
@@ -431,8 +458,10 @@ export function useChatThread(params) {
|
|
|
431
458
|
setUsage(EMPTY_USAGE);
|
|
432
459
|
setSessionStats(EMPTY_SESSION_STATS);
|
|
433
460
|
setTurnTelemetry([]);
|
|
461
|
+
setUserTurnOutline([]);
|
|
434
462
|
setOperatorActions([]);
|
|
435
463
|
setConversationNodes([]);
|
|
464
|
+
setTurnStatesById({});
|
|
436
465
|
setSddSessionIdentity(null);
|
|
437
466
|
setStreaming(false);
|
|
438
467
|
setTurnPhase("idle");
|
|
@@ -483,6 +512,7 @@ export function useChatThread(params) {
|
|
|
483
512
|
: undefined));
|
|
484
513
|
setToolCalls(next.toolCalls);
|
|
485
514
|
setConversationNodes(conversationNodesFromEvents(next.conversationEvents ?? []));
|
|
515
|
+
setTurnStatesById(mergeTurnStateSummaries({}, next.turnStates));
|
|
486
516
|
setOperationCards([]);
|
|
487
517
|
setHumanGateCards([]);
|
|
488
518
|
setArtifactCards([]);
|
|
@@ -491,6 +521,7 @@ export function useChatThread(params) {
|
|
|
491
521
|
setUsage(next.usage ?? EMPTY_USAGE);
|
|
492
522
|
setSessionStats(next.sessionStats ?? EMPTY_SESSION_STATS);
|
|
493
523
|
setTurnTelemetry(next.turnTelemetry ?? []);
|
|
524
|
+
setUserTurnOutline(next.userTurnOutline ?? []);
|
|
494
525
|
setOperatorActions([]);
|
|
495
526
|
setSddSessionIdentity(next.sddSessionIdentity ?? null);
|
|
496
527
|
refs.usageResponseKeysRef.current = new Set();
|
|
@@ -544,6 +575,7 @@ export function useChatThread(params) {
|
|
|
544
575
|
if (body.usage)
|
|
545
576
|
setUsage(body.usage);
|
|
546
577
|
setTurnTelemetry(body.turnTelemetry ?? []);
|
|
578
|
+
setUserTurnOutline(body.userTurnOutline ?? []);
|
|
547
579
|
setSddSessionIdentity(body.sddDataAlignment ?? null);
|
|
548
580
|
// Resume the events stream from the snapshot tail so the next
|
|
549
581
|
// reconnect never immediately re-expires (AC-003). The refs below
|
|
@@ -554,6 +586,7 @@ export function useChatThread(params) {
|
|
|
554
586
|
refs.sealedAssistantIdsRef.current = new Set();
|
|
555
587
|
const hydrated = hydrateSessionThread(body.session.messages ?? []);
|
|
556
588
|
setConversationNodes(conversationNodesFromEvents(body.conversationEvents ?? []));
|
|
589
|
+
setTurnStatesById((current) => mergeTurnStateSummaries(current, body.session?.turnStates));
|
|
557
590
|
// Recovery is addressed to targetSessionId, so an optimistic
|
|
558
591
|
// submission from another session must never be projected into
|
|
559
592
|
// this snapshot (the activation path enforces the same owner gate).
|
|
@@ -616,6 +649,7 @@ export function useChatThread(params) {
|
|
|
616
649
|
historyNextCursor,
|
|
617
650
|
historyLoading,
|
|
618
651
|
loadEarlierMessages,
|
|
652
|
+
loadThroughMessage,
|
|
619
653
|
unread,
|
|
620
654
|
humanGateCards,
|
|
621
655
|
interview,
|
|
@@ -625,8 +659,11 @@ export function useChatThread(params) {
|
|
|
625
659
|
usage,
|
|
626
660
|
sessionStats,
|
|
627
661
|
turnTelemetry,
|
|
662
|
+
userTurnOutline,
|
|
628
663
|
operatorActions,
|
|
629
664
|
conversationNodes,
|
|
665
|
+
turnStatesById,
|
|
666
|
+
reconcileTurnStates,
|
|
630
667
|
sddSessionIdentity,
|
|
631
668
|
setSddSessionIdentity,
|
|
632
669
|
streaming,
|
|
@@ -43,6 +43,9 @@ export function useComposer(params) {
|
|
|
43
43
|
const input = activeSnapshot.draft;
|
|
44
44
|
const pendingImages = activeSnapshot.images;
|
|
45
45
|
const [fileOptions, setFileOptions] = useState([]);
|
|
46
|
+
const [fileIndex, setFileIndex] = useState(0);
|
|
47
|
+
const [fileDrilled, setFileDrilled] = useState(false);
|
|
48
|
+
const [fileDismissedQuery, setFileDismissedQuery] = useState(null);
|
|
46
49
|
const [shortcutOpen, setShortcutOpen] = useState(false);
|
|
47
50
|
/** Any composer text that starts with `/` should reveal the palette,
|
|
48
51
|
* including draft restore. Escape can still close it without clearing. */
|
|
@@ -123,6 +126,10 @@ export function useComposer(params) {
|
|
|
123
126
|
forceRender();
|
|
124
127
|
setShortcutOpen(false);
|
|
125
128
|
setShortcutIndex(0);
|
|
129
|
+
setFileOptions([]);
|
|
130
|
+
setFileDrilled(false);
|
|
131
|
+
setFileDismissedQuery(null);
|
|
132
|
+
setFileIndex(0);
|
|
126
133
|
return attempt;
|
|
127
134
|
}, [ledger, refs]);
|
|
128
135
|
const claimSessionSubmission = useCallback((identity, text, images = []) => {
|
|
@@ -259,6 +266,14 @@ export function useComposer(params) {
|
|
|
259
266
|
.slice(0, textareaRef.current?.selectionStart ?? input.length)
|
|
260
267
|
.match(/(?:^|\s)@([^\s@]*)$/);
|
|
261
268
|
if (!match) {
|
|
269
|
+
setFileOptions([]);
|
|
270
|
+
setFileDrilled(false);
|
|
271
|
+
setFileDismissedQuery(null);
|
|
272
|
+
setFileIndex(0);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const query = match[1] ?? "";
|
|
276
|
+
if (fileDismissedQuery === query) {
|
|
262
277
|
setFileOptions([]);
|
|
263
278
|
return;
|
|
264
279
|
}
|
|
@@ -267,7 +282,7 @@ export function useComposer(params) {
|
|
|
267
282
|
const generation = refs.sessionGenerationRef.current;
|
|
268
283
|
const controller = new AbortController();
|
|
269
284
|
const timer = window.setTimeout(() => {
|
|
270
|
-
void fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(sessionId)}/files?q=${encodeURIComponent(
|
|
285
|
+
void fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(sessionId)}/files?q=${encodeURIComponent(query)}`, { signal: controller.signal })
|
|
271
286
|
.then((r) => (r.ok ? r.json() : null))
|
|
272
287
|
.then((body) => {
|
|
273
288
|
const active = activeIdentityRef.current;
|
|
@@ -276,7 +291,14 @@ export function useComposer(params) {
|
|
|
276
291
|
composerSessionKey(active) !== ownerKey ||
|
|
277
292
|
refs.sessionGenerationRef.current !== generation)
|
|
278
293
|
return;
|
|
279
|
-
|
|
294
|
+
const files = (body?.files ?? []);
|
|
295
|
+
setFileOptions(files
|
|
296
|
+
.filter((item) => typeof item.path === "string" && item.path.length > 0)
|
|
297
|
+
.map((item) => ({
|
|
298
|
+
path: item.path,
|
|
299
|
+
isDir: item.isDir === true || item.path.endsWith("/"),
|
|
300
|
+
})));
|
|
301
|
+
setFileIndex(0);
|
|
280
302
|
})
|
|
281
303
|
.catch(() => undefined);
|
|
282
304
|
}, 150);
|
|
@@ -284,16 +306,58 @@ export function useComposer(params) {
|
|
|
284
306
|
window.clearTimeout(timer);
|
|
285
307
|
controller.abort();
|
|
286
308
|
};
|
|
287
|
-
}, [
|
|
288
|
-
|
|
309
|
+
}, [
|
|
310
|
+
origin,
|
|
311
|
+
sessionId,
|
|
312
|
+
sessionIdentity,
|
|
313
|
+
sessionReady,
|
|
314
|
+
input,
|
|
315
|
+
refs,
|
|
316
|
+
fileDismissedQuery,
|
|
317
|
+
]);
|
|
318
|
+
const replaceAtToken = useCallback((nextPath, close) => {
|
|
289
319
|
const caret = textareaRef.current?.selectionStart ?? input.length;
|
|
290
320
|
const before = input.slice(0, caret);
|
|
291
321
|
const start = before.search(/@[^\s@]*$/);
|
|
292
322
|
if (start < 0)
|
|
323
|
+
return false;
|
|
324
|
+
const suffix = close ? " " : "";
|
|
325
|
+
setInput(`${input.slice(0, start)}@${nextPath}${suffix}${input.slice(caret)}`);
|
|
326
|
+
return true;
|
|
327
|
+
}, [input, setInput]);
|
|
328
|
+
const insertFileReference = useCallback((file) => {
|
|
329
|
+
if (!replaceAtToken(file, true))
|
|
293
330
|
return;
|
|
294
|
-
setInput(`${input.slice(0, start)}@${file} ${input.slice(caret)}`);
|
|
295
331
|
setFileOptions([]);
|
|
296
|
-
|
|
332
|
+
setFileDrilled(false);
|
|
333
|
+
setFileDismissedQuery(null);
|
|
334
|
+
setFileIndex(0);
|
|
335
|
+
}, [replaceAtToken]);
|
|
336
|
+
const drillFileReference = useCallback((file) => {
|
|
337
|
+
if (!replaceAtToken(file, false))
|
|
338
|
+
return;
|
|
339
|
+
setFileDrilled(true);
|
|
340
|
+
setFileDismissedQuery(null);
|
|
341
|
+
setFileIndex(0);
|
|
342
|
+
}, [replaceAtToken]);
|
|
343
|
+
const pickFileCrumb = useCallback((value) => {
|
|
344
|
+
if (!replaceAtToken(value, false))
|
|
345
|
+
return;
|
|
346
|
+
setFileDrilled(true);
|
|
347
|
+
setFileDismissedQuery(null);
|
|
348
|
+
setFileIndex(0);
|
|
349
|
+
}, [replaceAtToken]);
|
|
350
|
+
const dismissFilePalette = useCallback(() => {
|
|
351
|
+
const query = input
|
|
352
|
+
.slice(0, textareaRef.current?.selectionStart ?? input.length)
|
|
353
|
+
.match(/(?:^|\s)@([^\s@]*)$/)?.[1];
|
|
354
|
+
if (query === undefined) {
|
|
355
|
+
setFileOptions([]);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
setFileDismissedQuery(query);
|
|
359
|
+
setFileOptions([]);
|
|
360
|
+
}, [input]);
|
|
297
361
|
const addImagesFromFiles = useCallback((files) => {
|
|
298
362
|
const owner = activeIdentityRef.current;
|
|
299
363
|
if (!owner)
|
|
@@ -364,6 +428,9 @@ export function useComposer(params) {
|
|
|
364
428
|
useEffect(() => {
|
|
365
429
|
setShortcutIndex((index) => clampShortcutIndex(index, filteredShortcuts.length));
|
|
366
430
|
}, [filteredShortcuts.length]);
|
|
431
|
+
useEffect(() => {
|
|
432
|
+
setFileIndex((index) => clampShortcutIndex(index, fileOptions.length));
|
|
433
|
+
}, [fileOptions.length]);
|
|
367
434
|
const onCompositionStart = useCallback(() => {
|
|
368
435
|
refs.isComposingRef.current = true;
|
|
369
436
|
}, [refs]);
|
|
@@ -388,6 +455,7 @@ export function useComposer(params) {
|
|
|
388
455
|
sessionId: sessionReady ? sessionId : undefined,
|
|
389
456
|
refs,
|
|
390
457
|
onUnrecoverableSession,
|
|
458
|
+
deferInitialFetch: true,
|
|
391
459
|
});
|
|
392
460
|
}, [origin, sessionId, sessionReady, refs, onUnrecoverableSession]);
|
|
393
461
|
return {
|
|
@@ -404,6 +472,9 @@ export function useComposer(params) {
|
|
|
404
472
|
replaceSessionDraft,
|
|
405
473
|
releaseSession,
|
|
406
474
|
fileOptions,
|
|
475
|
+
fileIndex,
|
|
476
|
+
setFileIndex,
|
|
477
|
+
fileDrilled,
|
|
407
478
|
pendingImages,
|
|
408
479
|
setPendingImages,
|
|
409
480
|
addImagesFromFiles,
|
|
@@ -417,6 +488,9 @@ export function useComposer(params) {
|
|
|
417
488
|
dynamicCommandsError,
|
|
418
489
|
retryDynamicCommands,
|
|
419
490
|
insertFileReference,
|
|
491
|
+
drillFileReference,
|
|
492
|
+
pickFileCrumb,
|
|
493
|
+
dismissFilePalette,
|
|
420
494
|
textareaRef,
|
|
421
495
|
onCompositionStart,
|
|
422
496
|
onCompositionEnd,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
2
|
import { ACTIVE_CHAT_SESSION_STORAGE_KEY } from "../../chat/workspace-landing.js";
|
|
3
|
+
import { workspaceScopeReady } from "./useWorkspaceBrowserGate.js";
|
|
3
4
|
const REPO_HTML_PREVIEW_ROUTE_PREFIX = "/api/operator/v1/chat/repo/html/";
|
|
4
5
|
/** DSH-style path-encoded preview URL: relative HTML assets resolve under the
|
|
5
6
|
* same workspace-scoped route instead of losing a query-string workspaceId. */
|
|
@@ -46,6 +47,8 @@ export function useRepoBrowser(params) {
|
|
|
46
47
|
const loadTree = useCallback(async (directory = "") => {
|
|
47
48
|
if (blocked)
|
|
48
49
|
return; // AC-R5-002: fail-closed, zero repo requests
|
|
50
|
+
if (!workspaceScopeReady(workspaceId))
|
|
51
|
+
return;
|
|
49
52
|
const requestWorkspaceId = workspaceId ?? null;
|
|
50
53
|
try {
|
|
51
54
|
setTreeError(null);
|
|
@@ -91,9 +94,11 @@ export function useRepoBrowser(params) {
|
|
|
91
94
|
document.addEventListener("visibilitychange", onVisibility);
|
|
92
95
|
return () => document.removeEventListener("visibilitychange", onVisibility);
|
|
93
96
|
}, [refreshTree]);
|
|
94
|
-
const openPreview = useCallback(async (file) => {
|
|
97
|
+
const openPreview = useCallback(async (file, focusLine) => {
|
|
95
98
|
if (blocked)
|
|
96
99
|
return; // AC-R5-002: fail-closed
|
|
100
|
+
if (!workspaceScopeReady(workspaceId))
|
|
101
|
+
return;
|
|
97
102
|
const requestWorkspaceId = workspaceId ?? null;
|
|
98
103
|
setPreviewLoading(true);
|
|
99
104
|
try {
|
|
@@ -103,7 +108,10 @@ export function useRepoBrowser(params) {
|
|
|
103
108
|
return;
|
|
104
109
|
if (!res.ok || !body.data)
|
|
105
110
|
throw new Error(body.error?.message ?? "无法预览文件");
|
|
106
|
-
const loaded =
|
|
111
|
+
const loaded = {
|
|
112
|
+
...withHtmlPreviewUrl(body.data, origin, workspaceId),
|
|
113
|
+
...(focusLine ? { focusLine } : {}),
|
|
114
|
+
};
|
|
107
115
|
setPreviews((current) => {
|
|
108
116
|
const next = [
|
|
109
117
|
...current.filter((item) => item.path !== loaded.path),
|
|
@@ -177,6 +185,8 @@ export function useRepoBrowser(params) {
|
|
|
177
185
|
}
|
|
178
186
|
if (!paths.length)
|
|
179
187
|
return;
|
|
188
|
+
if (!workspaceScopeReady(workspaceId))
|
|
189
|
+
return;
|
|
180
190
|
void Promise.all(paths.map(async (file) => {
|
|
181
191
|
const res = await fetch(`${origin}/api/operator/v1/chat/repo/file?path=${encodeURIComponent(file)}${workspaceQuery}`, { credentials: "include" });
|
|
182
192
|
if (!res.ok)
|
|
@@ -192,7 +202,7 @@ export function useRepoBrowser(params) {
|
|
|
192
202
|
setPreviews(available);
|
|
193
203
|
setActivePreviewPath(available.at(-1)?.path ?? null);
|
|
194
204
|
});
|
|
195
|
-
}, [origin, workspaceQuery, sessionId, refs]);
|
|
205
|
+
}, [origin, workspaceQuery, workspaceId, sessionId, refs]);
|
|
196
206
|
const toggleDir = useCallback((dir) => {
|
|
197
207
|
setExpandedDirs((current) => {
|
|
198
208
|
const next = new Set(current);
|
|
@@ -3,7 +3,8 @@ import { formatModelRef, mergeSessionRuntime, sessionRuntimeMatches, } from "../
|
|
|
3
3
|
import { confirmationToken } from "./format.js";
|
|
4
4
|
import { isLostChatSessionError, mergeRuntimeContextWithSnapshot, runtimeSelectionFromSnapshot, } from "./runtime-snapshot-store.js";
|
|
5
5
|
import { buildToolCatalog } from "./tools-catalog.js";
|
|
6
|
-
import { adoptMutationSnapshot, dispatchRuntimeSnapshot, getRuntimeSnapshotState, refreshRuntimeSnapshot, settleMutationLifecycle, useRuntimeSnapshot, } from "./useRuntimeSnapshot.js";
|
|
6
|
+
import { adoptMutationSnapshot, dispatchRuntimeSnapshot, getRuntimeSnapshotState, refreshRuntimeSnapshot, releaseDeferredRuntimeSnapshotFetch, settleMutationLifecycle, useRuntimeSnapshot, } from "./useRuntimeSnapshot.js";
|
|
7
|
+
import { workspaceScopeReady } from "./useWorkspaceBrowserGate.js";
|
|
7
8
|
/**
|
|
8
9
|
* Monotonic mutation sequence shared across switchRuntime calls (RF-04). Each
|
|
9
10
|
* mutation captures its own token; only the LATEST started token may settle
|
|
@@ -19,6 +20,7 @@ export function useRuntimeControls(params) {
|
|
|
19
20
|
const [models, setModels] = useState([]);
|
|
20
21
|
const [modelsStatus, setModelsStatus] = useState("idle");
|
|
21
22
|
const [modelsError, setModelsError] = useState(null);
|
|
23
|
+
const [modelsForSessionId, setModelsForSessionId] = useState(null);
|
|
22
24
|
const [thinkingLevels, setThinkingLevels] = useState([]);
|
|
23
25
|
const [runtimeContext, setRuntimeContext] = useState(null);
|
|
24
26
|
const [runtimeContextError, setRuntimeContextError] = useState(null);
|
|
@@ -44,13 +46,14 @@ export function useRuntimeControls(params) {
|
|
|
44
46
|
const loadCapabilities = useCallback(async () => {
|
|
45
47
|
try {
|
|
46
48
|
// Workspace-scoped: the server rejects a bare capabilities call with
|
|
47
|
-
// WORKSPACE_REQUIRED
|
|
48
|
-
//
|
|
49
|
-
const scope = session?.workspaceId ??
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
// WORKSPACE_REQUIRED. Wait for a selected workspace instead of firing
|
|
50
|
+
// an unscoped request that shows up as a 400 in the browser Network panel.
|
|
51
|
+
const scope = (session?.workspaceId ??
|
|
52
|
+
params.workspaceId ??
|
|
53
|
+
"").trim();
|
|
54
|
+
if (!workspaceScopeReady(scope))
|
|
55
|
+
return;
|
|
56
|
+
const res = await fetch(`${origin}/api/operator/v1/chat/capabilities?workspaceId=${encodeURIComponent(scope)}`);
|
|
54
57
|
if (!res.ok)
|
|
55
58
|
return;
|
|
56
59
|
const body = (await res.json());
|
|
@@ -63,16 +66,17 @@ export function useRuntimeControls(params) {
|
|
|
63
66
|
useEffect(() => {
|
|
64
67
|
void loadCapabilities();
|
|
65
68
|
}, [loadCapabilities]);
|
|
66
|
-
// Models + thinking levels:
|
|
67
|
-
// session
|
|
68
|
-
// never silently folded into an empty list.
|
|
69
|
+
// Models + thinking levels: GET lists the workspace catalog and does not
|
|
70
|
+
// wait for historical Pi session reopen. Failure is a STRUCTURED error
|
|
71
|
+
// (with retry), never silently folded into an empty list.
|
|
72
|
+
const sessionId = session?.sessionId ?? null;
|
|
69
73
|
const loadModels = useCallback(async () => {
|
|
70
|
-
if (!
|
|
74
|
+
if (!sessionId) {
|
|
71
75
|
setModels([]);
|
|
72
76
|
setModelsStatus("idle");
|
|
77
|
+
setModelsForSessionId(null);
|
|
73
78
|
return;
|
|
74
79
|
}
|
|
75
|
-
const sessionId = session.sessionId;
|
|
76
80
|
setModelsStatus("loading");
|
|
77
81
|
setModelsError(null);
|
|
78
82
|
try {
|
|
@@ -93,27 +97,36 @@ export function useRuntimeControls(params) {
|
|
|
93
97
|
})) {
|
|
94
98
|
setModelsStatus("idle");
|
|
95
99
|
setModelsError(null);
|
|
100
|
+
setModelsForSessionId(sessionId);
|
|
96
101
|
onUnrecoverableSession?.(sessionId);
|
|
97
102
|
return;
|
|
98
103
|
}
|
|
99
104
|
setModelsStatus("error");
|
|
100
105
|
setModelsError(error?.message ?? `模型列表读取失败(HTTP ${res.status})`);
|
|
106
|
+
setModelsForSessionId(sessionId);
|
|
101
107
|
return;
|
|
102
108
|
}
|
|
103
109
|
setModels(body.models ?? []);
|
|
104
110
|
setThinkingLevels(body.thinkingLevels ?? []);
|
|
105
111
|
setModelsStatus("ready");
|
|
112
|
+
setModelsForSessionId(sessionId);
|
|
106
113
|
}
|
|
107
114
|
catch (reason) {
|
|
108
115
|
if (refs.sessionRef.current?.sessionId !== sessionId)
|
|
109
116
|
return;
|
|
110
117
|
setModelsStatus("error");
|
|
111
118
|
setModelsError(reason instanceof Error ? reason.message : String(reason));
|
|
119
|
+
setModelsForSessionId(sessionId);
|
|
112
120
|
}
|
|
113
|
-
}, [origin,
|
|
121
|
+
}, [origin, sessionId, refs, onUnrecoverableSession]);
|
|
114
122
|
useEffect(() => {
|
|
115
123
|
void loadModels();
|
|
116
124
|
}, [loadModels]);
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
if (!session || modelsForSessionId !== session.sessionId)
|
|
127
|
+
return;
|
|
128
|
+
releaseDeferredRuntimeSnapshotFetch(session.sessionId);
|
|
129
|
+
}, [session, modelsForSessionId]);
|
|
117
130
|
useEffect(() => {
|
|
118
131
|
if (!session) {
|
|
119
132
|
setRuntimeContext(null);
|
|
@@ -121,6 +134,12 @@ export function useRuntimeControls(params) {
|
|
|
121
134
|
setRuntimeContextLoading(false);
|
|
122
135
|
return;
|
|
123
136
|
}
|
|
137
|
+
if (modelsForSessionId !== session.sessionId) {
|
|
138
|
+
setRuntimeContext(null);
|
|
139
|
+
setRuntimeContextError(null);
|
|
140
|
+
setRuntimeContextLoading(true);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
124
143
|
const sessionId = session.sessionId;
|
|
125
144
|
const base = `${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(sessionId)}`;
|
|
126
145
|
setRuntimeContextError(null);
|
|
@@ -181,7 +200,7 @@ export function useRuntimeControls(params) {
|
|
|
181
200
|
return;
|
|
182
201
|
setRuntimeContextError("注入上下文读取失败,请重试");
|
|
183
202
|
});
|
|
184
|
-
}, [origin, session?.sessionId, refs, onUnrecoverableSession]);
|
|
203
|
+
}, [origin, session?.sessionId, modelsForSessionId, refs, onUnrecoverableSession]);
|
|
185
204
|
// AC-01/RF3-01: hydrate the Composer from the unified snapshot — real
|
|
186
205
|
// model and thinking selection; `auto` displays the Pi actual effective
|
|
187
206
|
// level. Derived session/runtimeContext state ONLY comes from a snapshot
|
|
@@ -348,8 +367,8 @@ export function useRuntimeControls(params) {
|
|
|
348
367
|
capabilities,
|
|
349
368
|
]);
|
|
350
369
|
// Snapshot is the single source of truth when ready; the /models fetch
|
|
351
|
-
// result is the fallback
|
|
352
|
-
const effectiveModels = snapshot
|
|
370
|
+
// result is the fallback so the picker is usable before Pi session reopen.
|
|
371
|
+
const effectiveModels = snapshot && snapshotState.status === "ready"
|
|
353
372
|
? (snapshot.models ?? []).map((model) => ({
|
|
354
373
|
provider: model.provider,
|
|
355
374
|
id: model.id,
|
|
@@ -14,6 +14,7 @@ import { confirmationToken } from "./format.js";
|
|
|
14
14
|
import { createRuntimeSnapshotStore, isLostChatSessionError, parseRuntimeSnapshotError, } from "./runtime-snapshot-store.js";
|
|
15
15
|
const store = createRuntimeSnapshotStore();
|
|
16
16
|
let controller;
|
|
17
|
+
let snapshotFetchStartedFor;
|
|
17
18
|
function snapshotUrl(sessionId) {
|
|
18
19
|
return `/api/operator/v1/chat/sessions/${encodeURIComponent(sessionId)}`;
|
|
19
20
|
}
|
|
@@ -36,12 +37,28 @@ export function attachRuntimeSnapshotController(input) {
|
|
|
36
37
|
store.dispatch({ type: "mutation-reset" });
|
|
37
38
|
}
|
|
38
39
|
if (!input.sessionId) {
|
|
40
|
+
snapshotFetchStartedFor = undefined;
|
|
39
41
|
store.dispatch({ type: "reset" });
|
|
40
42
|
return;
|
|
41
43
|
}
|
|
42
44
|
store.dispatch({ type: "load-start", sessionId: input.sessionId });
|
|
45
|
+
if (input.deferInitialFetch) {
|
|
46
|
+
snapshotFetchStartedFor = undefined;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
snapshotFetchStartedFor = input.sessionId;
|
|
43
50
|
void fetchSnapshot(input.sessionId, input.origin);
|
|
44
51
|
}
|
|
52
|
+
/** Start the deferred GET /runtime-snapshot after the workspace model catalog settles. */
|
|
53
|
+
export function releaseDeferredRuntimeSnapshotFetch(sessionId) {
|
|
54
|
+
const ctrl = controller;
|
|
55
|
+
if (!ctrl?.sessionId || ctrl.sessionId !== sessionId)
|
|
56
|
+
return;
|
|
57
|
+
if (snapshotFetchStartedFor === sessionId)
|
|
58
|
+
return;
|
|
59
|
+
snapshotFetchStartedFor = sessionId;
|
|
60
|
+
void fetchSnapshot(sessionId, ctrl.origin);
|
|
61
|
+
}
|
|
45
62
|
async function fetchSnapshot(sessionId, origin) {
|
|
46
63
|
try {
|
|
47
64
|
const res = await fetch(`${origin}${snapshotUrl(sessionId)}/runtime-snapshot`, { credentials: "include" });
|
|
@@ -144,6 +161,7 @@ export async function refreshRuntimeSnapshot() {
|
|
|
144
161
|
return false;
|
|
145
162
|
const sessionId = ctrl.sessionId;
|
|
146
163
|
store.dispatch({ type: "load-start", sessionId });
|
|
164
|
+
snapshotFetchStartedFor = sessionId;
|
|
147
165
|
void fetchSnapshot(sessionId, ctrl.origin);
|
|
148
166
|
return true;
|
|
149
167
|
}
|
|
@@ -2,3 +2,7 @@ export const WORKSPACE_BROWSER_OK = { status: "ok" };
|
|
|
2
2
|
export function workspaceBrowserGateActive(gate) {
|
|
3
3
|
return gate.status !== "ok";
|
|
4
4
|
}
|
|
5
|
+
/** Non-session chat APIs require a selected workspace; empty/whitespace is unset. */
|
|
6
|
+
export function workspaceScopeReady(workspaceId) {
|
|
7
|
+
return Boolean(workspaceId?.trim());
|
|
8
|
+
}
|
|
@@ -22,7 +22,7 @@ export async function reloadWhenConsoleReady(options = {}) {
|
|
|
22
22
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
23
23
|
const reload = options.reload ?? (() => window.location.reload());
|
|
24
24
|
const started = Date.now();
|
|
25
|
-
let previousConsoleInstanceId = null;
|
|
25
|
+
let previousConsoleInstanceId = options.previousConsoleInstanceId?.trim() || null;
|
|
26
26
|
while (Date.now() - started < timeoutMs) {
|
|
27
27
|
try {
|
|
28
28
|
const res = await fetchImpl(`/api/health?ts=${Date.now()}`, {
|
|
@@ -2,7 +2,7 @@ import { lstat, readFile } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { acquireUpdateLock, updateLockPath } from "../../shared/update/state.js";
|
|
4
4
|
import os from "node:os";
|
|
5
|
-
import {
|
|
5
|
+
import { isOperationOccupied } from "../../infrastructure/console/operation-store.js";
|
|
6
6
|
import { readInitRuntimeActivity } from "../observability/init-runtime-activity.js";
|
|
7
7
|
const record = (value) => {
|
|
8
8
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
@@ -138,7 +138,7 @@ export class WorkspaceInitializationService {
|
|
|
138
138
|
}
|
|
139
139
|
async check(refresh = false) {
|
|
140
140
|
const latest = (await this.options.operations.list()).filter(op => op.action === "workspaceInitialize").at(-1);
|
|
141
|
-
const running = latest &&
|
|
141
|
+
const running = latest && isOperationOccupied(latest.state);
|
|
142
142
|
if (!running && (refresh || !this.cached || Date.now() - this.cached.at > 30_000)) {
|
|
143
143
|
const generation = this.generation;
|
|
144
144
|
this.pending ??= this.inspect().then(value => { if (generation === this.generation)
|
|
@@ -174,7 +174,7 @@ export class WorkspaceInitializationService {
|
|
|
174
174
|
if (activity.workerProjectionError || activity.activeDagRunIds.length || activity.activeWorkerTasks || activity.activeWorkerBatches || this.options.activeChatTurnCount())
|
|
175
175
|
throw new Error("当前工作区有任务或对话正在运行,请结束后重试");
|
|
176
176
|
const operations = await this.options.operations.list();
|
|
177
|
-
if (operations.some(op => op.operationId !== operationId &&
|
|
177
|
+
if (operations.some(op => op.operationId !== operationId && isOperationOccupied(op.state)))
|
|
178
178
|
throw new Error("当前工作区有其他操作正在运行,请稍后重试");
|
|
179
179
|
const before = await this.inspect();
|
|
180
180
|
if (before.state === "error")
|