@tea-agent/loop-agent 0.40.0-next.7 → 0.40.0-next.9
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 +106 -140
- package/dist/build-stamp.json +3 -3
- package/dist/executors/dag-pi-executor.js +3 -0
- package/dist/executors/pi-executor.js +4 -2
- package/dist/executors/pi-sdk-executor.js +46 -1
- package/dist/worker/console/chat/chat-event-store.js +13 -1
- package/dist/worker/console/chat/pi-runtime.js +70 -5
- package/dist/worker/console/chat/routes.js +99 -29
- package/dist/worker/console/chat/workspace-landing.js +65 -0
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DGt5TRsk.js → abnfDiagram-N423BO3Z-Bs-CDAXM.js} +1 -1
- package/dist/worker/console/static/assets/{arc-DKBSJsXm.js → arc-CAkA3We3.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BnhooRBs.js → architectureDiagram-T3A2C74G-DEmI_zqr.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-Bm9qkrJP.js → blockDiagram-VBNYF7ZC-uDHANebE.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B2cFh4bO.js → c4Diagram-5PPSVZJV-Bv97b4_B.js} +1 -1
- package/dist/worker/console/static/assets/channel-amxUpk7o.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-DhvWRI03.js → chunk-2GRJ4B5K-shmQKsFz.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-chzTP_2w.js → chunk-2Q5K7J3B-BAraNvcN.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-SdWIR12m.js → chunk-5RXB4S5H-BirhxUop.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-C2-8Eo_A.js → chunk-5VM5RSS4-C9LgUJpR.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DvbE1yQW.js → chunk-6Q2QTUOP-DrU-s-t7.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CEenyPTz.js → chunk-GF5L2VYU-Tx-H_FA2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CcaZrq0k.js → chunk-JWPE2WC7-BjLtNFrR.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-FGngOTto.js → chunk-KBJHAD2P-UNLVwSsy.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-jT_yX3sf.js → chunk-RYQCIY6F-CBEUX8XD.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CjE9LQOM.js → chunk-XXDRQBXY-WTL7IZOq.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-bejwBVIz.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-bejwBVIz.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-GQWns1tb.js → cose-bilkent-JH36ORCC-CkHG6W3s.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-CGRpxWNJ.js → cynefin-VYW2F7L2-BRzFqTaL.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Cd3uFGE4.js → cynefinDiagram-MW4NZA55-2_CFp_i7.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-DmDHuUW0.js → dagre-VZM6K2ZE-BJtsmdkE.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-kAr--hF3.js → diagram-7IWD3JNH-DNEFFo6w.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-woahL2Oh.js → diagram-B4RE2ZJO-B_IWV7eo.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-7syNVaNf.js → diagram-LBJQPF4R-DvvRx2dz.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BELPXiaY.js → diagram-Q27KOJAE-CsibsWLg.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-CrPa4aXi.js → diagram-UB23O5K3-les_djBQ.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-B48u2VVe.js → ebnfDiagram-BXEA7PRR-BEm2n7qJ.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DhBTfwTF.js → erDiagram-JOGREHBK-BI7vHHVm.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CAeZ5nDU.js → flowDiagram-UKHOOZJN-CUFCERrB.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CkIFeFLN.js → ganttDiagram-PKOTCBZU-wxyLiGWt.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-t0HGYDel.js → gitGraphDiagram-DS77QQ5N-Bt897K8Z.js} +1 -1
- package/dist/worker/console/static/assets/index-BHhUOTri.css +1 -0
- package/dist/worker/console/static/assets/index-DqJbO3-p.js +407 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-WM0HZhe4.js → infoDiagram-6WML65LV-DYNx_IPv.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-CV5byHe0.js → ishikawaDiagram-WSZJBQD7-CzpLNEu6.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-lIyWkgnw.js → journeyDiagram-NVQOT4AX-97f9owNU.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BIH4YNRY.js → kanban-definition-27J2QSJJ-BxM9hyBn.js} +1 -1
- package/dist/worker/console/static/assets/{linear-lQ0ZGrWc.js → linear-ByuxcHvp.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-BYdXtke8.js → mermaid.core-DwPGvpCv.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-k93vkIa4.js → mindmap-definition-FAOFIHXS-DCoqG9RT.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-Bm6AqvSy.js → pegDiagram-VL7TDLO6-BLSBmBMx.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DnJW6Xpf.js → pieDiagram-7S7Q4E2Y-B0t_0oJW.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CMzm4ObL.js → quadrantDiagram-CIZ2JOQS-sTzDk7Nt.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-fwFF5dFQ.js → railroadDiagram-AXF67PYL-B3jRdATC.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-tzl9VDUc.js → requirementDiagram-LRYGKXZP-D-cTtXEN.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-B26V09DD.js → sankeyDiagram-W5VNT64P-B25igJRE.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-D2npyCeP.js → sequenceDiagram-SI44F4Z6-jW_lM1_D.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-CTJtkRU2.js → sizeCapture-X5ZJPWSS-CrzTgsHE.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-B8lk4Lzu.js → stateDiagram-OKZ733FA-B01MvlqE.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BvFKEtm2.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-C3yP53hH.js → swimlanes-SLNWSIFB-YcF12FiU.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DIB2eVg-.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Bck7xjhe.js → timeline-definition-Z64GVDOM-ClnRcg2s.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-CCntr1w7.js → vennDiagram-T6HMQDX7-B6gr_z-q.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DsCqCBZz.js → wardleyDiagram-T6FBY63Y-CvSgguGH.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-Br2l90OM.js → xychartDiagram-ELKLHX3M-23OtSZCb.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +33 -32
- package/dist/worker/console/static-src/operator-chat/compaction-message.js +79 -0
- package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +1 -2
- package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +25 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +54 -56
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +81 -27
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +14 -25
- package/dist/worker/console/static-src/shell/workspace-route.js +7 -5
- package/dist/workflows/dag/backend-test-writer-completeness.js +7 -12
- package/dist/workflows/dag/init-hybrid.js +27 -5
- package/dist/workflows/dag/types.js +13 -0
- package/docs/templates/agent-dag.schema.json +18 -0
- package/docs/templates/backend-test-dag.json +22 -5
- package/package.json +1 -1
- package/dist/worker/console/static/assets/channel-DSgFX07x.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-unJObRls.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-unJObRls.js +0 -1
- package/dist/worker/console/static/assets/index-B4l5ag79.js +0 -407
- package/dist/worker/console/static/assets/index-CwVHWB71.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BmOcMTJE.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Beah-dq0.js +0 -8
- package/dist/worker/console/static-src/operator-chat/activity-rail-presentation.js +0 -73
- package/dist/worker/console/static-src/operator-chat/useActivityRailTransition.js +0 -59
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
2
|
import { runtimeSelectionFromRecord } from "../../chat/runtime-selection.js";
|
|
3
3
|
import { hydrateSessionThread } from "../../chat/turn-process.js";
|
|
4
|
-
import {
|
|
4
|
+
import { forgetActiveChatSession, readActiveChatSessionId, rememberActiveChatSession, } from "../../chat/workspace-landing.js";
|
|
5
5
|
import { CHAT_SESSION_LIST_LIMIT } from "../chat-view-types.js";
|
|
6
6
|
import { confirmationToken } from "./format.js";
|
|
7
|
+
import { upsertCompactionMessage } from "./compaction-message.js";
|
|
7
8
|
import { describeMutationGateFailure, isMutationGateFailureCode, isStaleTokenFailure, } from "./mutation-gate.js";
|
|
8
9
|
import { SessionTitleWatcher } from "./session-title-watcher.js";
|
|
9
10
|
export async function createSessionAndPrimeTitleWatcher(input) {
|
|
@@ -64,28 +65,7 @@ export async function createSessionAndPrimeTitleWatcher(input) {
|
|
|
64
65
|
}
|
|
65
66
|
}
|
|
66
67
|
function mergeCompactionMessages(messages, compactions) {
|
|
67
|
-
|
|
68
|
-
for (const item of compactions ?? []) {
|
|
69
|
-
const data = item.data && typeof item.data === "object" ? item.data : {};
|
|
70
|
-
const eventId = typeof item.eventId === "string" ? item.eventId : undefined;
|
|
71
|
-
const id = eventId ? `compaction-${eventId}` : `compaction-${item.at ?? Date.now()}`;
|
|
72
|
-
if (result.some((message) => message.id === id))
|
|
73
|
-
continue;
|
|
74
|
-
const summary = typeof data.summary === "string" ? data.summary : "历史上下文已精简,关键决策与当前任务已保留。";
|
|
75
|
-
result.push({
|
|
76
|
-
id,
|
|
77
|
-
role: "assistant",
|
|
78
|
-
kind: "compaction",
|
|
79
|
-
text: summary,
|
|
80
|
-
createdAt: Date.parse(item.at ?? "") || Date.now(),
|
|
81
|
-
compaction: {
|
|
82
|
-
...(typeof data.tokensBefore === "number" ? { tokensBefore: data.tokensBefore } : {}),
|
|
83
|
-
...(typeof data.firstKeptEntryId === "string" ? { firstKeptEntryId: data.firstKeptEntryId } : {}),
|
|
84
|
-
...(eventId ? { eventId } : {}),
|
|
85
|
-
},
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
return result.sort((a, b) => a.createdAt - b.createdAt);
|
|
68
|
+
return (compactions ?? []).reduce((result, item) => upsertCompactionMessage(result, item), messages);
|
|
89
69
|
}
|
|
90
70
|
/** Session lifecycle: list / activate / create / delete / rename-archive /
|
|
91
71
|
* compact, plus browser-refresh recovery from the durable session store. */
|
|
@@ -206,11 +186,12 @@ export function useChatSessions(params) {
|
|
|
206
186
|
}
|
|
207
187
|
titleWatcherRef.current?.cancel(sessionId);
|
|
208
188
|
if (refs.sessionRef.current?.sessionId === sessionId) {
|
|
189
|
+
const activeWorkspaceId = refs.sessionRef.current.workspaceId ?? workspaceId;
|
|
209
190
|
sessionSelectionVersionRef.current += 1;
|
|
210
191
|
refs.sessionRef.current = null;
|
|
211
192
|
resetActiveProjection();
|
|
212
193
|
setSession(null);
|
|
213
|
-
window.localStorage
|
|
194
|
+
forgetActiveChatSession(window.localStorage, activeWorkspaceId, sessionId);
|
|
214
195
|
}
|
|
215
196
|
await refreshSessionList();
|
|
216
197
|
return true;
|
|
@@ -227,6 +208,7 @@ export function useChatSessions(params) {
|
|
|
227
208
|
refreshSessionList,
|
|
228
209
|
setError,
|
|
229
210
|
sessionOwnerQuery,
|
|
211
|
+
workspaceId,
|
|
230
212
|
]);
|
|
231
213
|
const activateSession = useCallback(async (sessionId, ownerWorkspaceId) => {
|
|
232
214
|
const selectionVersion = ++sessionSelectionVersionRef.current;
|
|
@@ -251,9 +233,15 @@ export function useChatSessions(params) {
|
|
|
251
233
|
// re-bound by useChatStream's session effect.
|
|
252
234
|
resetActiveProjection({ keepStreams: true });
|
|
253
235
|
refs.sessionRef.current = next;
|
|
254
|
-
refs.streamingRef.current = Boolean(body.session.activeTurnId);
|
|
255
236
|
refs.activeTurnIdRef.current = body.session.activeTurnId ?? null;
|
|
256
|
-
|
|
237
|
+
const visuallyStopped = body.session.turnPhase === "stopping";
|
|
238
|
+
refs.streamingRef.current = visuallyStopped
|
|
239
|
+
? false
|
|
240
|
+
: Boolean(body.session.activeTurnId);
|
|
241
|
+
refs.stoppingTurnIdRef.current = visuallyStopped
|
|
242
|
+
? (body.session.activeTurnId ?? null)
|
|
243
|
+
: null;
|
|
244
|
+
refs.stopRequestedRef.current = visuallyStopped;
|
|
257
245
|
refs.seenEventIdsRef.current = new Set();
|
|
258
246
|
refs.turnAssistantIdsRef.current = new Map();
|
|
259
247
|
refs.sealedAssistantIdsRef.current = new Set();
|
|
@@ -273,7 +261,7 @@ export function useChatSessions(params) {
|
|
|
273
261
|
toolCalls: hydrated.toolCalls,
|
|
274
262
|
sessionStats: body.sessionStats,
|
|
275
263
|
usage: body.usage,
|
|
276
|
-
streaming: Boolean(body.session.activeTurnId),
|
|
264
|
+
streaming: visuallyStopped ? false : Boolean(body.session.activeTurnId),
|
|
277
265
|
turnPhase: body.session.turnPhase ??
|
|
278
266
|
(body.session.activeTurnId ? "running" : "idle"),
|
|
279
267
|
deferredTurn: body.session.deferredTurn ?? null,
|
|
@@ -281,7 +269,7 @@ export function useChatSessions(params) {
|
|
|
281
269
|
pendingQuestion: body.session.pendingQuestion ?? null,
|
|
282
270
|
todos: body.session.todos ?? [],
|
|
283
271
|
});
|
|
284
|
-
window.localStorage.
|
|
272
|
+
rememberActiveChatSession(window.localStorage, body.session.workspaceId ?? ownerWorkspaceId ?? workspaceId, sessionId);
|
|
285
273
|
setActivePreviewPath(null);
|
|
286
274
|
}
|
|
287
275
|
catch (error) {
|
|
@@ -301,6 +289,7 @@ export function useChatSessions(params) {
|
|
|
301
289
|
setActivePreviewPath,
|
|
302
290
|
setError,
|
|
303
291
|
sessionOwnerQuery,
|
|
292
|
+
workspaceId,
|
|
304
293
|
]);
|
|
305
294
|
useEffect(() => {
|
|
306
295
|
void refreshSessionList();
|
|
@@ -331,21 +320,30 @@ export function useChatSessions(params) {
|
|
|
331
320
|
.map((item) => item.sessionId));
|
|
332
321
|
}, [sessionList]);
|
|
333
322
|
useEffect(() => () => titleWatcherRef.current?.dispose(), []);
|
|
334
|
-
// Browser refresh recovery:
|
|
335
|
-
//
|
|
323
|
+
// Browser refresh/workspace-switch recovery: selection is client state keyed
|
|
324
|
+
// by workspace; messages and operation refs/facts remain server-owned.
|
|
336
325
|
useEffect(() => {
|
|
337
326
|
let cancelled = false;
|
|
338
327
|
if (blocked)
|
|
339
328
|
return; // AC-R5-002: unknown workspace — no restore GET
|
|
340
|
-
const savedSessionId = window.localStorage.getItem(ACTIVE_CHAT_SESSION_STORAGE_KEY);
|
|
341
|
-
if (!savedSessionId)
|
|
342
|
-
return;
|
|
343
329
|
const selectionVersion = ++sessionSelectionVersionRef.current;
|
|
330
|
+
// Remove the old workspace projection immediately. Background session
|
|
331
|
+
// streams remain alive; only the visible selection is rebound below.
|
|
332
|
+
resetActiveProjection({ keepStreams: true });
|
|
333
|
+
refs.sessionRef.current = null;
|
|
334
|
+
setSession(null);
|
|
335
|
+
const savedSessionId = readActiveChatSessionId(window.localStorage, workspaceId);
|
|
336
|
+
if (!savedSessionId) {
|
|
337
|
+
// Keep the per-workspace map intact, but clear the legacy global pointer
|
|
338
|
+
// so Operate/Models cannot link a session from another workspace.
|
|
339
|
+
forgetActiveChatSession(window.localStorage, workspaceId);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
344
342
|
void (async () => {
|
|
345
343
|
try {
|
|
346
344
|
const res = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(savedSessionId)}${workspaceQuery}`, { credentials: "include" });
|
|
347
345
|
if (!res.ok) {
|
|
348
|
-
window.localStorage
|
|
346
|
+
forgetActiveChatSession(window.localStorage, workspaceId, savedSessionId);
|
|
349
347
|
return;
|
|
350
348
|
}
|
|
351
349
|
const body = (await res.json());
|
|
@@ -356,6 +354,11 @@ export function useChatSessions(params) {
|
|
|
356
354
|
return;
|
|
357
355
|
const restored = {
|
|
358
356
|
sessionId: body.session.sessionId,
|
|
357
|
+
...(body.session.workspaceId
|
|
358
|
+
? { workspaceId: body.session.workspaceId }
|
|
359
|
+
: workspaceId
|
|
360
|
+
? { workspaceId }
|
|
361
|
+
: {}),
|
|
359
362
|
...runtimeSelectionFromRecord(body.session),
|
|
360
363
|
};
|
|
361
364
|
refs.sessionGenerationRef.current += 1;
|
|
@@ -365,12 +368,21 @@ export function useChatSessions(params) {
|
|
|
365
368
|
// in the event ring and must replay once. applySseBlock dedups by
|
|
366
369
|
// eventId and marks replay so usage/unread/sound do not double-count.
|
|
367
370
|
refs.activeTurnIdRef.current = body.session.activeTurnId ?? null;
|
|
371
|
+
const visuallyStopped = body.session.turnPhase === "stopping";
|
|
372
|
+
refs.streamingRef.current = visuallyStopped
|
|
373
|
+
? false
|
|
374
|
+
: Boolean(body.session.activeTurnId);
|
|
375
|
+
refs.stoppingTurnIdRef.current = visuallyStopped
|
|
376
|
+
? (body.session.activeTurnId ?? null)
|
|
377
|
+
: null;
|
|
378
|
+
refs.stopRequestedRef.current = visuallyStopped;
|
|
368
379
|
refs.seenEventIdsRef.current = new Set();
|
|
369
380
|
refs.turnAssistantIdsRef.current = new Map();
|
|
370
381
|
refs.sealedAssistantIdsRef.current = new Set();
|
|
371
382
|
refs.settledTurnIdsRef.current = new Set();
|
|
372
383
|
refs.lastEventIdRef.current = null;
|
|
373
384
|
setSession(restored);
|
|
385
|
+
rememberActiveChatSession(window.localStorage, restored.workspaceId ?? workspaceId, restored.sessionId);
|
|
374
386
|
setInput(body.session.composerDraft ?? "");
|
|
375
387
|
const hydrated = hydrateSessionThread(body.session.messages ?? []);
|
|
376
388
|
hydrated.messages = mergeCompactionMessages(hydrated.messages, body.compactions);
|
|
@@ -379,7 +391,7 @@ export function useChatSessions(params) {
|
|
|
379
391
|
toolCalls: hydrated.toolCalls,
|
|
380
392
|
sessionStats: body.sessionStats,
|
|
381
393
|
usage: body.usage,
|
|
382
|
-
streaming: Boolean(body.session.activeTurnId),
|
|
394
|
+
streaming: visuallyStopped ? false : Boolean(body.session.activeTurnId),
|
|
383
395
|
turnPhase: body.session.turnPhase ??
|
|
384
396
|
(body.session.activeTurnId ? "running" : "idle"),
|
|
385
397
|
deferredTurn: body.session.deferredTurn ?? null,
|
|
@@ -403,6 +415,8 @@ export function useChatSessions(params) {
|
|
|
403
415
|
setInput,
|
|
404
416
|
applyActivatedSession,
|
|
405
417
|
blocked,
|
|
418
|
+
resetActiveProjection,
|
|
419
|
+
workspaceId,
|
|
406
420
|
]);
|
|
407
421
|
// The sole production create path delegates to the exported orchestration
|
|
408
422
|
// helper so its durable commit order is directly traceable and testable.
|
|
@@ -444,7 +458,7 @@ export function useChatSessions(params) {
|
|
|
444
458
|
// createSession().then(sendPrompt) reads the new session even
|
|
445
459
|
// though sendPrompt's closure captured the stale (null) value.
|
|
446
460
|
refs.sessionRef.current = created;
|
|
447
|
-
window.localStorage.
|
|
461
|
+
rememberActiveChatSession(window.localStorage, created.workspaceId ?? options?.workspaceId ?? workspaceId, created.sessionId);
|
|
448
462
|
setSession(created);
|
|
449
463
|
return created;
|
|
450
464
|
}
|
|
@@ -464,6 +478,7 @@ export function useChatSessions(params) {
|
|
|
464
478
|
resetActiveProjection,
|
|
465
479
|
createSessionAndPrimeTitleWatcherForHook,
|
|
466
480
|
blocked,
|
|
481
|
+
workspaceId,
|
|
467
482
|
]);
|
|
468
483
|
// Phase-1: fork / in-session branch / mainline switch UI deferred; API routes remain.
|
|
469
484
|
/** Single compact helper for toolbar + `/compact <instructions>` (AC-1). */
|
|
@@ -510,26 +525,8 @@ export function useChatSessions(params) {
|
|
|
510
525
|
// pi-web parity: reload the compacted session, then leave a distinct
|
|
511
526
|
// compaction summary card in the timeline plus a transient toast.
|
|
512
527
|
const summary = body.snapshot?.summary?.trim();
|
|
513
|
-
const tokensBefore = body.snapshot?.tokensBefore;
|
|
514
528
|
const summaryText = summary || "历史上下文已精简,关键决策与当前任务已保留。";
|
|
515
529
|
await activateSession(current.sessionId);
|
|
516
|
-
setMessages((messages) => {
|
|
517
|
-
const eventId = typeof body.eventId === "string" ? body.eventId : undefined;
|
|
518
|
-
const id = eventId ? `compaction-${eventId}` : `compaction-${Date.now()}`;
|
|
519
|
-
if (messages.some((message) => message.id === id))
|
|
520
|
-
return messages;
|
|
521
|
-
return [...messages, {
|
|
522
|
-
id,
|
|
523
|
-
role: "assistant",
|
|
524
|
-
kind: "compaction",
|
|
525
|
-
text: summaryText,
|
|
526
|
-
createdAt: Date.now(),
|
|
527
|
-
compaction: {
|
|
528
|
-
...(tokensBefore === undefined ? {} : { tokensBefore }),
|
|
529
|
-
...(eventId ? { eventId } : {}),
|
|
530
|
-
},
|
|
531
|
-
}];
|
|
532
|
-
});
|
|
533
530
|
setNotice({
|
|
534
531
|
id: Date.now(),
|
|
535
532
|
message: `已压缩上下文:${summaryText.slice(0, 72)}${summaryText.length > 72 ? "…" : ""}`,
|
|
@@ -546,7 +543,6 @@ export function useChatSessions(params) {
|
|
|
546
543
|
refs,
|
|
547
544
|
setError,
|
|
548
545
|
setNotice,
|
|
549
|
-
setMessages,
|
|
550
546
|
onCompactionChange,
|
|
551
547
|
activateSession,
|
|
552
548
|
sessionOwnerQuery,
|
|
@@ -573,11 +569,13 @@ export function useChatSessions(params) {
|
|
|
573
569
|
}
|
|
574
570
|
if (patch.state === "archived" &&
|
|
575
571
|
refs.sessionRef.current?.sessionId === sessionId) {
|
|
572
|
+
const activeWorkspaceId = refs.sessionRef.current.workspaceId ?? workspaceId;
|
|
573
|
+
const activeSessionId = refs.sessionRef.current.sessionId;
|
|
576
574
|
sessionSelectionVersionRef.current += 1;
|
|
577
575
|
refs.sessionRef.current = null;
|
|
578
576
|
resetActiveProjection();
|
|
579
577
|
setSession(null);
|
|
580
|
-
window.localStorage
|
|
578
|
+
forgetActiveChatSession(window.localStorage, activeWorkspaceId, activeSessionId);
|
|
581
579
|
}
|
|
582
580
|
};
|
|
583
581
|
return {
|
|
@@ -2,7 +2,7 @@ import { useCallback, useEffect } from "react";
|
|
|
2
2
|
import { formatOperatorUserError } from "../../operator-user-error.js";
|
|
3
3
|
import { confirmationToken, requestId, sleep } from "./format.js";
|
|
4
4
|
import { classifyCreateTurnNetworkFailure, classifyCreateTurnResponse, createPendingSubmission, decideSubmissionRoute, isRuntimeBusyUnion, isSubmissionCurrent, isTerminalTurnState, keepAmbiguousAfterRetry, reconcileDeferredSubmissionFromState, reconcilePendingSubmissionFromState, resolveAmbiguousFromState, } from "./turn-submission.js";
|
|
5
|
-
import { applyAbortDecision, applyPendingReconcileDecision, classifyAbortResponse, markStopRequested, operatorChatAbortUrl, operatorChatEventsUrl, operatorChatStateUrl, operatorChatTurnsUrl, releaseOwnershipOnTerminalEvidence, releaseSubmitOwnership as releaseOwnershipBag, requestStopOnOwnership, settleCreateTurnOutcome, turnPhaseFromOwnership, } from "./turn-stream-controller.js";
|
|
5
|
+
import { applyAbortDecision, applyPendingReconcileDecision, classifyAbortResponse, markStopRequested, operatorChatAbortUrl, operatorChatEventsUrl, operatorChatStateUrl, operatorChatTurnsUrl, releaseOwnershipOnTerminalEvidence, releaseSubmitOwnership as releaseOwnershipBag, requestStopOnOwnership, settleCreateTurnOutcome, turnPhaseFromOwnership, visualTurnPhaseFromOwnership, } from "./turn-stream-controller.js";
|
|
6
6
|
/** Project ChatRefs into the executable ownership bag used by the controller. */
|
|
7
7
|
function ownershipBagFromRefs(refs) {
|
|
8
8
|
return {
|
|
@@ -46,6 +46,7 @@ function sseEventName(block) {
|
|
|
46
46
|
function sleepBriefly() {
|
|
47
47
|
return new Promise((resolve) => window.setTimeout(resolve, 25));
|
|
48
48
|
}
|
|
49
|
+
const STOP_RECONCILE_DELAYS_MS = [100, 250, 500, 1_000, 2_000];
|
|
49
50
|
function clearEmptyAssistant(setMessages, assistantId) {
|
|
50
51
|
if (!assistantId)
|
|
51
52
|
return;
|
|
@@ -73,15 +74,26 @@ export function useChatStream(params) {
|
|
|
73
74
|
if (refs.sessionRef.current?.sessionId !== sessionId ||
|
|
74
75
|
refs.sessionGenerationRef.current !== generation)
|
|
75
76
|
return;
|
|
77
|
+
const pendingClientRequestId = refs.pendingSubmissionRef.current?.clientRequestId;
|
|
78
|
+
if (pendingClientRequestId)
|
|
79
|
+
releaseSubmitOwnership(pendingClientRequestId);
|
|
76
80
|
refs.streamingRef.current = false;
|
|
77
81
|
refs.activeTurnIdRef.current = null;
|
|
78
82
|
refs.assistantIdRef.current = null;
|
|
79
83
|
refs.stoppingTurnIdRef.current = null;
|
|
84
|
+
refs.stopRequestedRef.current = false;
|
|
80
85
|
setStreaming(false);
|
|
81
86
|
setTurnPhase("idle");
|
|
82
87
|
setStreamingAssistantId(null);
|
|
83
88
|
clearEmptyAssistant(setMessages, assistantId);
|
|
84
|
-
}, [
|
|
89
|
+
}, [
|
|
90
|
+
refs,
|
|
91
|
+
releaseSubmitOwnership,
|
|
92
|
+
setStreaming,
|
|
93
|
+
setTurnPhase,
|
|
94
|
+
setStreamingAssistantId,
|
|
95
|
+
setMessages,
|
|
96
|
+
]);
|
|
85
97
|
/** Follow one session's event stream until it is explicitly aborted. The
|
|
86
98
|
* server's `stream_ready` marker separates replay from live events; this
|
|
87
99
|
* means replay cannot render duplicate messages while live text remains
|
|
@@ -243,12 +255,12 @@ export function useChatStream(params) {
|
|
|
243
255
|
/** Issue real POST /abort for a known turnId (Stop never disposes Session). */
|
|
244
256
|
const postAbortForTurn = useCallback(async (sessionId, generation, turnId) => {
|
|
245
257
|
refs.stoppingTurnIdRef.current = turnId;
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
//
|
|
258
|
+
// DSH-style visual stop: the composer becomes idle immediately. Hidden
|
|
259
|
+
// ownership remains stopping so a new message routes to the durable queue
|
|
260
|
+
// until terminal evidence proves the old Turn is gone.
|
|
249
261
|
refs.streamingRef.current = false;
|
|
250
262
|
setStreaming(false);
|
|
251
|
-
setTurnPhase("
|
|
263
|
+
setTurnPhase("idle");
|
|
252
264
|
setStreamingAssistantId(null);
|
|
253
265
|
try {
|
|
254
266
|
const res = await fetch(operatorChatAbortUrl(origin, sessionId, turnId), {
|
|
@@ -277,13 +289,17 @@ export function useChatStream(params) {
|
|
|
277
289
|
const state = await fetchSessionState(sessionId);
|
|
278
290
|
const stillCurrent = refs.sessionRef.current?.sessionId === sessionId &&
|
|
279
291
|
refs.sessionGenerationRef.current === generation;
|
|
292
|
+
// Terminal SSE may settle the Turn while /state is in flight. Do not
|
|
293
|
+
// write the pre-request ownership snapshot back after that newer fact.
|
|
294
|
+
if (!stillCurrent || refs.stoppingTurnIdRef.current !== turnId)
|
|
295
|
+
return;
|
|
280
296
|
applyAbortDecision(bag, decision, noticeSink(), {
|
|
281
|
-
sessionStillCurrent:
|
|
297
|
+
sessionStillCurrent: true,
|
|
282
298
|
runtimeBusy: isRuntimeBusyUnion(state ?? undefined),
|
|
283
299
|
markIdle: () => markIdleIfCurrent(sessionId, generation, refs.assistantIdRef.current),
|
|
284
300
|
});
|
|
285
301
|
applyOwnershipBagToRefs(refs, bag);
|
|
286
|
-
setTurnPhase(
|
|
302
|
+
setTurnPhase(visualTurnPhaseFromOwnership(bag));
|
|
287
303
|
return;
|
|
288
304
|
}
|
|
289
305
|
applyAbortDecision(bag, decision, noticeSink(), {
|
|
@@ -291,11 +307,7 @@ export function useChatStream(params) {
|
|
|
291
307
|
markIdle: () => markIdleIfCurrent(sessionId, generation, refs.assistantIdRef.current),
|
|
292
308
|
});
|
|
293
309
|
applyOwnershipBagToRefs(refs, bag);
|
|
294
|
-
setTurnPhase(
|
|
295
|
-
// Sync the post-decision busy state back to React: optimistic stop
|
|
296
|
-
// flipped it idle up front; pending/keep-busy outcomes restore it.
|
|
297
|
-
if (bag.streaming)
|
|
298
|
-
setStreaming(true);
|
|
310
|
+
setTurnPhase(visualTurnPhaseFromOwnership(bag));
|
|
299
311
|
}
|
|
300
312
|
catch {
|
|
301
313
|
// Abort network failure: keep busy (UIS-abort-network-fail).
|
|
@@ -308,11 +320,11 @@ export function useChatStream(params) {
|
|
|
308
320
|
networkError: true,
|
|
309
321
|
}), noticeSink());
|
|
310
322
|
applyOwnershipBagToRefs(refs, bag);
|
|
311
|
-
//
|
|
312
|
-
//
|
|
313
|
-
refs.streamingRef.current =
|
|
314
|
-
setStreaming(
|
|
315
|
-
setTurnPhase("
|
|
323
|
+
// Transport failure is ambiguous: keep the visual composer available,
|
|
324
|
+
// retain hidden ownership, and let the short /state backoff converge.
|
|
325
|
+
refs.streamingRef.current = false;
|
|
326
|
+
setStreaming(false);
|
|
327
|
+
setTurnPhase("idle");
|
|
316
328
|
}
|
|
317
329
|
}, [
|
|
318
330
|
origin,
|
|
@@ -368,7 +380,13 @@ export function useChatStream(params) {
|
|
|
368
380
|
},
|
|
369
381
|
});
|
|
370
382
|
applyOwnershipBagToRefs(refs, bag);
|
|
371
|
-
|
|
383
|
+
const ownershipPhase = turnPhaseFromOwnership(bag);
|
|
384
|
+
setTurnPhase(visualTurnPhaseFromOwnership(bag));
|
|
385
|
+
if (ownershipPhase === "stopping") {
|
|
386
|
+
refs.streamingRef.current = false;
|
|
387
|
+
setStreaming(false);
|
|
388
|
+
setStreamingAssistantId(null);
|
|
389
|
+
}
|
|
372
390
|
}, [
|
|
373
391
|
refs,
|
|
374
392
|
noticeSink,
|
|
@@ -806,10 +824,12 @@ export function useChatStream(params) {
|
|
|
806
824
|
// Decide + mark intent/coalesce only; POST is issued once below.
|
|
807
825
|
const stopResult = requestStopOnOwnership(bag, {
|
|
808
826
|
postAbort: () => { },
|
|
809
|
-
notice: noticeSink(),
|
|
810
827
|
});
|
|
811
828
|
applyOwnershipBagToRefs(refs, bag);
|
|
812
|
-
|
|
829
|
+
refs.streamingRef.current = false;
|
|
830
|
+
setStreaming(false);
|
|
831
|
+
setStreamingAssistantId(null);
|
|
832
|
+
setTurnPhase("idle");
|
|
813
833
|
// Immediate abort already scheduled for a known turnId — coalesce via
|
|
814
834
|
// abortPostedTurnIds; stopInFlight only gates concurrent in-flight POSTs.
|
|
815
835
|
if (stopResult.abortedTurnId) {
|
|
@@ -859,7 +879,6 @@ export function useChatStream(params) {
|
|
|
859
879
|
markStopRequested(lateBag);
|
|
860
880
|
const late = requestStopOnOwnership(lateBag, {
|
|
861
881
|
postAbort: () => { },
|
|
862
|
-
notice: noticeSink(),
|
|
863
882
|
});
|
|
864
883
|
applyOwnershipBagToRefs(refs, lateBag);
|
|
865
884
|
if (late.abortedTurnId) {
|
|
@@ -869,7 +888,13 @@ export function useChatStream(params) {
|
|
|
869
888
|
finally {
|
|
870
889
|
refs.stopInFlightRef.current = false;
|
|
871
890
|
}
|
|
872
|
-
}, [
|
|
891
|
+
}, [
|
|
892
|
+
refs,
|
|
893
|
+
postAbortForTurn,
|
|
894
|
+
setStreaming,
|
|
895
|
+
setStreamingAssistantId,
|
|
896
|
+
setTurnPhase,
|
|
897
|
+
]);
|
|
873
898
|
// State reconciliation is only a status fence: it never consumes/creates
|
|
874
899
|
// message events, and a generation check discards late responses (M5.3).
|
|
875
900
|
// Busy truth = durable activeTurn union runtime busy facts (AC-003/AC-006).
|
|
@@ -889,7 +914,8 @@ export function useChatStream(params) {
|
|
|
889
914
|
const sessionId = session.sessionId;
|
|
890
915
|
const generation = refs.sessionGenerationRef.current;
|
|
891
916
|
let cancelled = false;
|
|
892
|
-
|
|
917
|
+
let syncInFlight = null;
|
|
918
|
+
const runSync = async () => {
|
|
893
919
|
try {
|
|
894
920
|
const res = await fetch(operatorChatStateUrl(origin, sessionId), {
|
|
895
921
|
credentials: "include",
|
|
@@ -918,10 +944,16 @@ export function useChatStream(params) {
|
|
|
918
944
|
},
|
|
919
945
|
});
|
|
920
946
|
applyOwnershipBagToRefs(refs, bag);
|
|
921
|
-
|
|
947
|
+
const ownershipPhase = turnPhaseFromOwnership(bag);
|
|
948
|
+
setTurnPhase(visualTurnPhaseFromOwnership(bag));
|
|
949
|
+
if (ownershipPhase === "stopping") {
|
|
950
|
+
refs.streamingRef.current = false;
|
|
951
|
+
setStreaming(false);
|
|
952
|
+
setStreamingAssistantId(null);
|
|
953
|
+
}
|
|
922
954
|
if (state && "deferredTurn" in state)
|
|
923
955
|
setDeferredTurn(state.deferredTurn ?? null);
|
|
924
|
-
if (result === "bound") {
|
|
956
|
+
if (result === "bound" && ownershipPhase !== "stopping") {
|
|
925
957
|
// US-BUSY-ACTIVE-BOUND: keep streaming/busy with same id.
|
|
926
958
|
setStreaming(true);
|
|
927
959
|
return;
|
|
@@ -999,19 +1031,41 @@ export function useChatStream(params) {
|
|
|
999
1031
|
// offline/background case: interval, visibility and online retry.
|
|
1000
1032
|
}
|
|
1001
1033
|
};
|
|
1034
|
+
const sync = () => {
|
|
1035
|
+
if (syncInFlight)
|
|
1036
|
+
return syncInFlight;
|
|
1037
|
+
syncInFlight = runSync().finally(() => {
|
|
1038
|
+
syncInFlight = null;
|
|
1039
|
+
});
|
|
1040
|
+
return syncInFlight;
|
|
1041
|
+
};
|
|
1002
1042
|
const onVisible = () => {
|
|
1003
1043
|
if (document.visibilityState === "visible")
|
|
1004
1044
|
void sync();
|
|
1005
1045
|
};
|
|
1006
1046
|
const onOnline = () => void sync();
|
|
1007
1047
|
const interval = window.setInterval(() => void sync(), 15_000);
|
|
1048
|
+
let stopRetryTimer;
|
|
1049
|
+
let stopRetryIndex = 0;
|
|
1050
|
+
const scheduleStopRetry = () => {
|
|
1051
|
+
if (cancelled ||
|
|
1052
|
+
stopRetryIndex >= STOP_RECONCILE_DELAYS_MS.length ||
|
|
1053
|
+
(!refs.stopRequestedRef.current && !refs.stoppingTurnIdRef.current))
|
|
1054
|
+
return;
|
|
1055
|
+
const delay = STOP_RECONCILE_DELAYS_MS[stopRetryIndex++];
|
|
1056
|
+
stopRetryTimer = window.setTimeout(() => {
|
|
1057
|
+
void sync().finally(scheduleStopRetry);
|
|
1058
|
+
}, delay);
|
|
1059
|
+
};
|
|
1008
1060
|
document.addEventListener("visibilitychange", onVisible);
|
|
1009
1061
|
window.addEventListener("online", onOnline);
|
|
1010
1062
|
// Immediate tick so terminal-before-streaming-false races converge promptly.
|
|
1011
|
-
void sync();
|
|
1063
|
+
void sync().finally(scheduleStopRetry);
|
|
1012
1064
|
return () => {
|
|
1013
1065
|
cancelled = true;
|
|
1014
1066
|
window.clearInterval(interval);
|
|
1067
|
+
if (stopRetryTimer !== undefined)
|
|
1068
|
+
window.clearTimeout(stopRetryTimer);
|
|
1015
1069
|
document.removeEventListener("visibilitychange", onVisible);
|
|
1016
1070
|
window.removeEventListener("online", onOnline);
|
|
1017
1071
|
};
|
|
@@ -3,6 +3,7 @@ import { buildTurnProcessGroups, hydrateSessionThread, selectRecentTurnGroups, }
|
|
|
3
3
|
import { mergeOperationCardState, } from "../../chat/operation-card.js";
|
|
4
4
|
import { createChatSseEventApplier, } from "./chat-sse-events.js";
|
|
5
5
|
import { taskContextUnchanged } from "./format.js";
|
|
6
|
+
import { upsertCompactionMessage } from "./compaction-message.js";
|
|
6
7
|
const EMPTY_USAGE = {
|
|
7
8
|
inputTokens: 0,
|
|
8
9
|
outputTokens: 0,
|
|
@@ -100,27 +101,7 @@ export async function applyOperationLinkMutationProjection(input) {
|
|
|
100
101
|
}
|
|
101
102
|
}
|
|
102
103
|
function mergeCompactionMessages(messages, compactions) {
|
|
103
|
-
|
|
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);
|
|
104
|
+
return (compactions ?? []).reduce((result, item) => upsertCompactionMessage(result, item), messages);
|
|
124
105
|
}
|
|
125
106
|
/**
|
|
126
107
|
* Thread state + SSE block parsing. The event-application logic itself lives
|
|
@@ -363,6 +344,7 @@ export function useChatThread(params) {
|
|
|
363
344
|
}, [refs]);
|
|
364
345
|
/** Adopt a freshly activated session's hydrated thread + streaming flags. */
|
|
365
346
|
const applyActivatedSession = useCallback((next) => {
|
|
347
|
+
const visuallyStopped = next.turnPhase === "stopping";
|
|
366
348
|
setMessages(next.messages);
|
|
367
349
|
setToolCalls(next.toolCalls);
|
|
368
350
|
setOperationCards([]);
|
|
@@ -373,8 +355,10 @@ export function useChatThread(params) {
|
|
|
373
355
|
setUsage(next.usage ?? EMPTY_USAGE);
|
|
374
356
|
setSessionStats(next.sessionStats ?? EMPTY_SESSION_STATS);
|
|
375
357
|
refs.usageResponseKeysRef.current = new Set();
|
|
376
|
-
setStreaming(next.streaming);
|
|
377
|
-
setTurnPhase(
|
|
358
|
+
setStreaming(visuallyStopped ? false : next.streaming);
|
|
359
|
+
setTurnPhase(visuallyStopped
|
|
360
|
+
? "idle"
|
|
361
|
+
: (next.turnPhase ?? (next.streaming ? "running" : "idle")));
|
|
378
362
|
setDeferredTurn(next.deferredTurn ?? null);
|
|
379
363
|
setDeferredTurns(next.deferredTurns ?? (next.deferredTurn ? [next.deferredTurn] : []));
|
|
380
364
|
setPendingQuestion(next.pendingQuestion ?? null);
|
|
@@ -398,9 +382,13 @@ export function useChatThread(params) {
|
|
|
398
382
|
refs.sessionRef.current?.sessionId !== targetSessionId)
|
|
399
383
|
return false;
|
|
400
384
|
const activeTurnId = body.session.activeTurnId ?? null;
|
|
385
|
+
const visuallyStopped = body.session.turnPhase === "stopping";
|
|
401
386
|
refs.activeTurnIdRef.current = activeTurnId;
|
|
402
|
-
refs.streamingRef.current = Boolean(activeTurnId);
|
|
403
|
-
|
|
387
|
+
refs.streamingRef.current = visuallyStopped ? false : Boolean(activeTurnId);
|
|
388
|
+
setStreaming(visuallyStopped ? false : Boolean(activeTurnId));
|
|
389
|
+
setTurnPhase(visuallyStopped
|
|
390
|
+
? "idle"
|
|
391
|
+
: (body.session.turnPhase ?? (activeTurnId ? "running" : "idle")));
|
|
404
392
|
setDeferredTurn(body.session.deferredTurn ?? null);
|
|
405
393
|
setDeferredTurns(body.session.deferredTurns ?? (body.session.deferredTurn ? [body.session.deferredTurn] : []));
|
|
406
394
|
setPendingQuestion(body.session.pendingQuestion ?? null);
|
|
@@ -433,6 +421,7 @@ export function useChatThread(params) {
|
|
|
433
421
|
setMessages,
|
|
434
422
|
setToolCalls,
|
|
435
423
|
setStreamingAssistantId,
|
|
424
|
+
setStreaming,
|
|
436
425
|
setTurnPhase,
|
|
437
426
|
setDeferredTurn,
|
|
438
427
|
setPendingQuestion,
|
|
@@ -145,9 +145,9 @@ export function buildInspectHref(inspectPath = "/", query) {
|
|
|
145
145
|
: query?.toString() ?? "";
|
|
146
146
|
return `/#${path}${qs ? `?${qs}` : ""}`;
|
|
147
147
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
return
|
|
148
|
+
/** Chat selection is client state, like DSH: keep the browser route stable. */
|
|
149
|
+
export function buildChatHref(_params = {}) {
|
|
150
|
+
return CHAT_HREF;
|
|
151
151
|
}
|
|
152
152
|
/** Keep the selected Workspace on any canonical Console destination without
|
|
153
153
|
* changing its page-specific path or query state. */
|
|
@@ -158,6 +158,8 @@ export function withWorkspaceId(href, workspaceId) {
|
|
|
158
158
|
? `/${href}`
|
|
159
159
|
: rewriteInspectHrefToCanonical(href);
|
|
160
160
|
const { path, query } = splitHash(canonical.slice(2));
|
|
161
|
+
if (path === "/chat")
|
|
162
|
+
return CHAT_HREF;
|
|
161
163
|
const id = workspaceId?.trim();
|
|
162
164
|
if (id)
|
|
163
165
|
query.set("workspaceId", id);
|
|
@@ -201,14 +203,14 @@ function finishOperate(tab, query, replace) {
|
|
|
201
203
|
}
|
|
202
204
|
function finishChat(replace, params) {
|
|
203
205
|
const effective = params ?? {};
|
|
204
|
-
const canonicalHref =
|
|
206
|
+
const canonicalHref = CHAT_HREF;
|
|
205
207
|
return {
|
|
206
208
|
workspace: "chat",
|
|
207
209
|
operateTab: DEFAULT_OPERATE_TAB,
|
|
208
210
|
inspectPath: "/",
|
|
209
211
|
canonicalHash: canonicalHref.slice(2),
|
|
210
212
|
canonicalHref,
|
|
211
|
-
replace,
|
|
213
|
+
replace: replace || paramsQuery(effective).length > 0,
|
|
212
214
|
...effective,
|
|
213
215
|
};
|
|
214
216
|
}
|
|
@@ -884,28 +884,23 @@ export function isBackendTestShardChildTask(task) {
|
|
|
884
884
|
task.id?.startsWith("generate-backend-pytest-case-"));
|
|
885
885
|
}
|
|
886
886
|
/**
|
|
887
|
-
*
|
|
888
|
-
*
|
|
889
|
-
*
|
|
890
|
-
* must NOT require module files to exist at plan time.
|
|
887
|
+
* Historical classifier for the Markdown plan node. Current backend-test N2
|
|
888
|
+
* is read-only and persists a run-owned plan.md, so writer completeness never
|
|
889
|
+
* invokes the workspace README assessment below for newly generated DAGs.
|
|
891
890
|
*/
|
|
892
891
|
export function isBackendTestMdPlanTask(task) {
|
|
893
892
|
return task.id === "generate-backend-md-plan-pi";
|
|
894
893
|
}
|
|
895
894
|
/**
|
|
896
|
-
*
|
|
897
|
-
*
|
|
898
|
-
* scripts are produced by the downstream map_agent children, so the Gate
|
|
899
|
-
* must NOT require mapped pytest scripts to exist at plan time.
|
|
895
|
+
* Historical classifier for the former shared-resources pytest writer. Current
|
|
896
|
+
* N7 is static and every map child writes one self-contained pytest module.
|
|
900
897
|
*/
|
|
901
898
|
export function isBackendTestPytestPlanTask(task) {
|
|
902
899
|
return task.id === "generate-backend-pytest-plan-pi";
|
|
903
900
|
}
|
|
904
901
|
/**
|
|
905
|
-
*
|
|
906
|
-
*
|
|
907
|
-
* index), WITHOUT requiring the indexed module files to exist yet — those are
|
|
908
|
-
* written by the downstream map_agent shard children.
|
|
902
|
+
* Legacy workspace-plan completeness helper retained for isolated historical
|
|
903
|
+
* fixtures. New DAGs validate the run-owned plan artifact in N3 instead.
|
|
909
904
|
*/
|
|
910
905
|
export async function assessBackendTestMdPlanCompleteness(workspaceRoot, layout) {
|
|
911
906
|
const resolvedPlan = layout ?? resolveBackendTestLayout(undefined);
|