@tea-agent/loop-agent 0.40.0-next.4 → 0.40.0-next.6
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 +20 -0
- package/dist/build-stamp.json +3 -3
- package/dist/executors/shell-executor.js +4 -1
- package/dist/infrastructure/harness/atomic-write.js +23 -0
- package/dist/worker/console/chat/chat-event-store.js +62 -4
- package/dist/worker/console/chat/context-panel.js +7 -0
- package/dist/worker/console/chat/deferred-turn.js +12 -0
- package/dist/worker/console/chat/pi-runtime.js +264 -16
- package/dist/worker/console/chat/provider-error.js +98 -0
- package/dist/worker/console/chat/routes.js +292 -29
- package/dist/worker/console/chat/session-stats.js +179 -0
- package/dist/worker/console/chat/session-store.js +93 -63
- package/dist/worker/console/chat/todos.js +115 -0
- package/dist/worker/console/chat/tool-preview.js +42 -0
- package/dist/worker/console/chat/turn-process.js +20 -55
- package/dist/worker/console/chat/user-questions.js +266 -0
- package/dist/worker/console/chat/workspace-landing.js +14 -0
- package/dist/worker/console/native-directory-picker.js +13 -0
- package/dist/worker/console/routes.js +309 -1
- package/dist/worker/console/security.js +4 -4
- package/dist/worker/console/server.js +529 -186
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-CTx1TrNa.js → abnfDiagram-N423BO3Z-DGt5TRsk.js} +1 -1
- package/dist/worker/console/static/assets/{arc-B4i9cVxX.js → arc-DKBSJsXm.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BAXE8_Rl.js → architectureDiagram-T3A2C74G-BnhooRBs.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-C5Xh59at.js → blockDiagram-VBNYF7ZC-Bm9qkrJP.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B_asOzAu.js → c4Diagram-5PPSVZJV-B2cFh4bO.js} +1 -1
- package/dist/worker/console/static/assets/channel-DSgFX07x.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-wRO_iRNc.js → chunk-2GRJ4B5K-DhvWRI03.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-ByOaSh_G.js → chunk-2Q5K7J3B-chzTP_2w.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLzudan.js → chunk-5RXB4S5H-SdWIR12m.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-JCNGkXYT.js → chunk-5VM5RSS4-C2-8Eo_A.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BhrPXXMZ.js → chunk-6Q2QTUOP-DvbE1yQW.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-TJ5JuEBT.js → chunk-GF5L2VYU-CEenyPTz.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-VVthn_mD.js → chunk-JWPE2WC7-CcaZrq0k.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-D60-Mny4.js → chunk-KBJHAD2P-FGngOTto.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-zXv7aVye.js → chunk-RYQCIY6F-jT_yX3sf.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-By9VQoi1.js → chunk-XXDRQBXY-CjE9LQOM.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-unJObRls.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-unJObRls.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-C4zQLe-M.js → cose-bilkent-JH36ORCC-GQWns1tb.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-wh5N2ReN.js → cynefin-VYW2F7L2-CGRpxWNJ.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Dlu5wIqs.js → cynefinDiagram-MW4NZA55-Cd3uFGE4.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CrDgthc6.js → dagre-VZM6K2ZE-DmDHuUW0.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-zN1clg27.js → diagram-7IWD3JNH-kAr--hF3.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BN-o7J7-.js → diagram-B4RE2ZJO-woahL2Oh.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-C8SuPt0d.js → diagram-LBJQPF4R-7syNVaNf.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BoizuHbZ.js → diagram-Q27KOJAE-BELPXiaY.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BzUzXixG.js → diagram-UB23O5K3-CrPa4aXi.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Ev3lA4Cf.js → ebnfDiagram-BXEA7PRR-B48u2VVe.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-GALUzRbs.js → erDiagram-JOGREHBK-DhBTfwTF.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CZJN65Sb.js → flowDiagram-UKHOOZJN-CAeZ5nDU.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-JLB9OESl.js → ganttDiagram-PKOTCBZU-CkIFeFLN.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CcvCr4GD.js → gitGraphDiagram-DS77QQ5N-t0HGYDel.js} +1 -1
- package/dist/worker/console/static/assets/index-B4l5ag79.js +407 -0
- package/dist/worker/console/static/assets/index-CwVHWB71.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-HG5z7aVg.js → infoDiagram-6WML65LV-WM0HZhe4.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BHEu9chR.js → ishikawaDiagram-WSZJBQD7-CV5byHe0.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-Cp_Up8-a.js → journeyDiagram-NVQOT4AX-lIyWkgnw.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-DvVeY-em.js → kanban-definition-27J2QSJJ-BIH4YNRY.js} +1 -1
- package/dist/worker/console/static/assets/{linear-aGmcjgTn.js → linear-lQ0ZGrWc.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-Biyo-tqA.js → mermaid.core-BYdXtke8.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DyxY2pIb.js → mindmap-definition-FAOFIHXS-k93vkIa4.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CkdGuc50.js → pegDiagram-VL7TDLO6-Bm6AqvSy.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-NNQI9xVR.js → pieDiagram-7S7Q4E2Y-DnJW6Xpf.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-PAJkuz_G.js → quadrantDiagram-CIZ2JOQS-CMzm4ObL.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-ZwrQod0O.js → railroadDiagram-AXF67PYL-fwFF5dFQ.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D3ZfrzM2.js → requirementDiagram-LRYGKXZP-tzl9VDUc.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-DDGFmWk5.js → sankeyDiagram-W5VNT64P-B26V09DD.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DO6mmXTA.js → sequenceDiagram-SI44F4Z6-D2npyCeP.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DMbCQiJt.js → sizeCapture-X5ZJPWSS-CTJtkRU2.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Di_YUEfC.js → stateDiagram-OKZ733FA-B8lk4Lzu.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BmOcMTJE.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-Bn2PphNJ.js → swimlanes-SLNWSIFB-C3yP53hH.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Beah-dq0.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DPh8pRsp.js → timeline-definition-Z64GVDOM-Bck7xjhe.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-GMfl26pW.js → vennDiagram-T6HMQDX7-CCntr1w7.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DTlmVrmF.js → wardleyDiagram-T6FBY63Y-DsCqCBZz.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-YGBjKAS5.js → xychartDiagram-ELKLHX3M-Br2l90OM.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +29 -4
- package/dist/worker/console/static-src/app/useConsoleShell.js +30 -12
- package/dist/worker/console/static-src/app/useOperatorActions.js +36 -6
- package/dist/worker/console/static-src/app/useRecoveryConsole.js +10 -11
- package/dist/worker/console/static-src/app/useRunProgress.js +2 -1
- package/dist/worker/console/static-src/app/useTaskWizard.js +2 -2
- package/dist/worker/console/static-src/night/useNightBoard.js +8 -5
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +140 -8
- package/dist/worker/console/static-src/operator-chat/details-dag-actions.js +122 -0
- package/dist/worker/console/static-src/operator-chat/refs.js +3 -0
- package/dist/worker/console/static-src/operator-chat/runtime-selection-labels.js +27 -0
- package/dist/worker/console/static-src/operator-chat/turn-submission.js +3 -2
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +168 -72
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +78 -17
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +184 -4
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +63 -9
- package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
- package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -3
- package/dist/worker/console/static-src/shell/useWorkspaces.js +299 -0
- package/dist/worker/console/static-src/shell/workspace-route.js +337 -0
- package/dist/worker/console/workspace-context.js +234 -0
- package/dist/worker/console/workspace-registry.js +214 -0
- package/dist/worker/observe/server.js +4 -0
- package/dist/worker/observe/static/api.js +26 -6
- package/dist/worker/observe/static/app.js +18 -70
- package/dist/worker/observe/static/index.html +1 -1
- package/dist/worker/observe/static/inspect-workspace.js +307 -0
- package/dist/worker/observe/static/operator-chrome.css +145 -20
- package/dist/worker/observe/static/operator-chrome.d.ts +20 -3
- package/dist/worker/observe/static/operator-chrome.js +330 -100
- package/dist/worker/observe/static/router.d.ts +46 -0
- package/dist/worker/observe/static/router.js +25 -1
- package/dist/worker/observe/static/state.d.ts +50 -0
- package/dist/worker/observe/static/state.js +172 -0
- package/dist/worker/observe/static/views/dag-inspector.js +3 -2
- package/dist/worker/observe/static/views/dag.js +2 -1
- package/dist/worker/observe/static/views/dashboard.js +6 -4
- package/dist/worker/observe/static/views/night.js +2 -2
- package/dist/worker/observe/static/views/pool.js +1 -0
- package/dist/worker/observe/static/views/run.js +3 -1
- package/dist/worker/observe/static/views/task.js +1 -1
- package/dist/workflows/dag/backend-test-pytest-collection.js +11 -10
- package/dist/workflows/dag/interrupt-request.js +3 -3
- package/dist/workflows/dag/lifecycle.js +3 -3
- package/docs/README.md +1 -0
- package/docs/operations/local-development-environment.md +21 -0
- package/harness.json +3 -3
- package/package.json +3 -1
- package/dist/worker/console/static/assets/channel-Dj_4Moei.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C0YYAyCa.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C0YYAyCa.js +0 -1
- package/dist/worker/console/static/assets/index-CW88Fjbw.js +0 -406
- package/dist/worker/console/static/assets/index-DP0ZJl39.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DrdDvela.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DPkEAULl.js +0 -8
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
-
import { parseAssistantContent } from "../../chat/assistant-content.js";
|
|
3
2
|
import { runtimeSelectionFromRecord } from "../../chat/runtime-selection.js";
|
|
4
3
|
import { hydrateSessionThread } from "../../chat/turn-process.js";
|
|
5
4
|
import { ACTIVE_CHAT_SESSION_STORAGE_KEY } from "../../chat/workspace-landing.js";
|
|
@@ -28,6 +27,9 @@ export async function createSessionAndPrimeTitleWatcher(input) {
|
|
|
28
27
|
});
|
|
29
28
|
if (!res.ok) {
|
|
30
29
|
const body = (await res.json().catch(() => ({})));
|
|
30
|
+
if (isMutationGateFailureCode(body.error?.code)) {
|
|
31
|
+
throw new Error(describeMutationGateFailure(body.error.code));
|
|
32
|
+
}
|
|
31
33
|
throw new Error(body.error?.message ?? `HTTP ${res.status}`);
|
|
32
34
|
}
|
|
33
35
|
const body = (await res.json());
|
|
@@ -36,6 +38,11 @@ export async function createSessionAndPrimeTitleWatcher(input) {
|
|
|
36
38
|
const durable = body.session;
|
|
37
39
|
const created = {
|
|
38
40
|
sessionId: durable.sessionId,
|
|
41
|
+
...(durable.workspaceId || input.options?.workspaceId
|
|
42
|
+
? {
|
|
43
|
+
workspaceId: durable.workspaceId ?? input.options?.workspaceId,
|
|
44
|
+
}
|
|
45
|
+
: {}),
|
|
39
46
|
...runtimeSelectionFromRecord(durable),
|
|
40
47
|
};
|
|
41
48
|
const nextList = [
|
|
@@ -56,6 +63,30 @@ export async function createSessionAndPrimeTitleWatcher(input) {
|
|
|
56
63
|
input.setCreatingSession?.(false);
|
|
57
64
|
}
|
|
58
65
|
}
|
|
66
|
+
function mergeCompactionMessages(messages, compactions) {
|
|
67
|
+
const result = [...messages];
|
|
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);
|
|
89
|
+
}
|
|
59
90
|
/** Session lifecycle: list / activate / create / delete / rename-archive /
|
|
60
91
|
* compact, plus browser-refresh recovery from the durable session store. */
|
|
61
92
|
export function useChatSessions(params) {
|
|
@@ -63,6 +94,21 @@ export function useChatSessions(params) {
|
|
|
63
94
|
const { applyActivatedSession, resetForNewSession, setMessages } = thread;
|
|
64
95
|
const { setInput, setPendingImages } = composer;
|
|
65
96
|
const { setPreviews, setActivePreviewPath } = browser;
|
|
97
|
+
const { workspaceId, blocked } = params;
|
|
98
|
+
const workspaceQuery = workspaceId
|
|
99
|
+
? `?workspaceId=${encodeURIComponent(workspaceId)}`
|
|
100
|
+
: "";
|
|
101
|
+
/** AC-002 (2026-08-26): session requests follow the session's DURABLE
|
|
102
|
+
* owning workspace, not the global active workspace. The sidebar list
|
|
103
|
+
* items carry workspaceId; once a session is activated its owner is kept
|
|
104
|
+
* on the session record itself. Switching the Operate/Inspect active
|
|
105
|
+
* workspace therefore never retargets an open session's requests. */
|
|
106
|
+
const sessionOwnerQuery = useCallback((ownerWorkspaceId) => {
|
|
107
|
+
const owner = ownerWorkspaceId?.trim() ||
|
|
108
|
+
refs.sessionRef.current?.workspaceId?.trim() ||
|
|
109
|
+
"";
|
|
110
|
+
return owner ? `?workspaceId=${encodeURIComponent(owner)}` : "";
|
|
111
|
+
}, [refs]);
|
|
66
112
|
const [sessionList, setSessionList] = useState([]);
|
|
67
113
|
const sessionListRef = useRef([]);
|
|
68
114
|
sessionListRef.current = sessionList;
|
|
@@ -76,11 +122,15 @@ export function useChatSessions(params) {
|
|
|
76
122
|
// state. This prevents slow responses from an older selection overwriting a
|
|
77
123
|
// newer user choice.
|
|
78
124
|
const sessionSelectionVersionRef = useRef(0);
|
|
79
|
-
const resetActiveProjection = useCallback(() => {
|
|
80
|
-
// M4.5/M5.2: selection is a hard ownership boundary
|
|
81
|
-
//
|
|
125
|
+
const resetActiveProjection = useCallback((options) => {
|
|
126
|
+
// M4.5/M5.2: selection is a hard ownership boundary for THIS projection.
|
|
127
|
+
// Multi-workspace (AC-002): keepStreams=true (workspace switch) aborts
|
|
128
|
+
// only projection-local requests and leaves background SSE streams of
|
|
129
|
+
// other sessions/workspaces running.
|
|
130
|
+
const keepStreams = options?.keepStreams === true;
|
|
82
131
|
refs.sessionGenerationRef.current += 1;
|
|
83
|
-
|
|
132
|
+
if (!keepStreams)
|
|
133
|
+
refs.abortRef.current?.abort();
|
|
84
134
|
refs.reconnectAbortRef.current?.abort();
|
|
85
135
|
for (const controller of refs.sessionAbortControllersRef.current.values())
|
|
86
136
|
controller.abort();
|
|
@@ -114,43 +164,36 @@ export function useChatSessions(params) {
|
|
|
114
164
|
setAutoScroll,
|
|
115
165
|
]);
|
|
116
166
|
const refreshSessionList = useCallback(async () => {
|
|
167
|
+
if (blocked)
|
|
168
|
+
return; // AC-R5-002: fail-closed, zero session requests
|
|
117
169
|
try {
|
|
118
|
-
const res = await fetch(`${origin}/api/operator/v1/chat/sessions`, {
|
|
170
|
+
const res = await fetch(`${origin}/api/operator/v1/chat/sessions${workspaceQuery}`, {
|
|
119
171
|
credentials: "include",
|
|
120
172
|
});
|
|
121
173
|
if (!res.ok)
|
|
122
174
|
return;
|
|
123
|
-
const sessions = (await res.json())
|
|
124
|
-
[];
|
|
175
|
+
const sessions = (await res.json())
|
|
176
|
+
.sessions ?? [];
|
|
177
|
+
// Multi-workspace (AC-002): list only this workspace's sessions when a
|
|
178
|
+
// scope is active; unscoped (legacy) keeps the full list.
|
|
179
|
+
const scoped = workspaceId
|
|
180
|
+
? sessions.filter((item) => !item.workspaceId || item.workspaceId === workspaceId)
|
|
181
|
+
: sessions;
|
|
125
182
|
// API returns newest-first; keep only the recent window in the sidebar.
|
|
126
|
-
const keep =
|
|
127
|
-
const excess = sessions.slice(CHAT_SESSION_LIST_LIMIT);
|
|
183
|
+
const keep = scoped.slice(0, CHAT_SESSION_LIST_LIMIT);
|
|
128
184
|
setSessionList(keep);
|
|
129
|
-
if (excess.length === 0)
|
|
130
|
-
return;
|
|
131
|
-
await Promise.all(excess.map((item) => fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(item.sessionId)}`, {
|
|
132
|
-
method: "DELETE",
|
|
133
|
-
credentials: "include",
|
|
134
|
-
headers: {
|
|
135
|
-
"x-loop-console-confirmation": confirmationToken(),
|
|
136
|
-
},
|
|
137
|
-
}).catch(() => undefined)));
|
|
138
|
-
const activeId = refs.sessionRef.current?.sessionId;
|
|
139
|
-
if (activeId && excess.some((item) => item.sessionId === activeId)) {
|
|
140
|
-
sessionSelectionVersionRef.current += 1;
|
|
141
|
-
refs.sessionRef.current = null;
|
|
142
|
-
resetActiveProjection();
|
|
143
|
-
setSession(null);
|
|
144
|
-
window.localStorage.removeItem(ACTIVE_CHAT_SESSION_STORAGE_KEY);
|
|
145
|
-
}
|
|
146
185
|
}
|
|
147
186
|
catch {
|
|
148
187
|
/* Chat remains usable without the sidebar index. */
|
|
149
188
|
}
|
|
150
|
-
}, [origin,
|
|
189
|
+
}, [origin, workspaceQuery, workspaceId, blocked]);
|
|
151
190
|
const deleteSession = useCallback(async (sessionId) => {
|
|
152
191
|
try {
|
|
153
|
-
|
|
192
|
+
// AC-002: delete follows the session's durable owner, not the
|
|
193
|
+
// global active workspace.
|
|
194
|
+
const ownerQuery = sessionOwnerQuery(sessionListRef.current.find((item) => item.sessionId === sessionId)
|
|
195
|
+
?.workspaceId ?? null);
|
|
196
|
+
const res = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(sessionId)}${ownerQuery}`, {
|
|
154
197
|
method: "DELETE",
|
|
155
198
|
credentials: "include",
|
|
156
199
|
headers: {
|
|
@@ -183,11 +226,16 @@ export function useChatSessions(params) {
|
|
|
183
226
|
resetActiveProjection,
|
|
184
227
|
refreshSessionList,
|
|
185
228
|
setError,
|
|
229
|
+
sessionOwnerQuery,
|
|
186
230
|
]);
|
|
187
|
-
const activateSession = useCallback(async (sessionId) => {
|
|
231
|
+
const activateSession = useCallback(async (sessionId, ownerWorkspaceId) => {
|
|
188
232
|
const selectionVersion = ++sessionSelectionVersionRef.current;
|
|
233
|
+
// AC-002: activation targets the session's durable owning workspace —
|
|
234
|
+
// never the global active workspace (no cross-workspace leak when
|
|
235
|
+
// Operate/Inspect switch their scope while a session stays open).
|
|
236
|
+
const ownerQuery = sessionOwnerQuery(ownerWorkspaceId);
|
|
189
237
|
try {
|
|
190
|
-
const res = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(sessionId)}`, { credentials: "include" });
|
|
238
|
+
const res = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(sessionId)}${ownerQuery}`, { credentials: "include" });
|
|
191
239
|
const body = (await res.json());
|
|
192
240
|
if (!res.ok || !body.session)
|
|
193
241
|
throw new Error(body.error?.message ?? "会话不可用");
|
|
@@ -195,12 +243,17 @@ export function useChatSessions(params) {
|
|
|
195
243
|
return;
|
|
196
244
|
const next = {
|
|
197
245
|
sessionId: body.session.sessionId,
|
|
246
|
+
...(body.session.workspaceId ? { workspaceId: body.session.workspaceId } : {}),
|
|
198
247
|
...runtimeSelectionFromRecord(body.session),
|
|
199
248
|
};
|
|
200
|
-
|
|
249
|
+
// AC-002: session switch (same or cross workspace) never aborts other
|
|
250
|
+
// sessions' background request controllers; the single /events owner is
|
|
251
|
+
// re-bound by useChatStream's session effect.
|
|
252
|
+
resetActiveProjection({ keepStreams: true });
|
|
201
253
|
refs.sessionRef.current = next;
|
|
202
254
|
refs.streamingRef.current = Boolean(body.session.activeTurnId);
|
|
203
255
|
refs.activeTurnIdRef.current = body.session.activeTurnId ?? null;
|
|
256
|
+
refs.streamingRef.current = Boolean(body.session.activeTurnId);
|
|
204
257
|
refs.seenEventIdsRef.current = new Set();
|
|
205
258
|
refs.turnAssistantIdsRef.current = new Map();
|
|
206
259
|
refs.sealedAssistantIdsRef.current = new Set();
|
|
@@ -214,13 +267,19 @@ export function useChatSessions(params) {
|
|
|
214
267
|
setPreviews([]);
|
|
215
268
|
setInput(body.session.composerDraft ?? "");
|
|
216
269
|
const hydrated = hydrateSessionThread(body.session.messages ?? []);
|
|
270
|
+
hydrated.messages = mergeCompactionMessages(hydrated.messages, body.compactions);
|
|
217
271
|
applyActivatedSession({
|
|
218
272
|
messages: hydrated.messages,
|
|
219
273
|
toolCalls: hydrated.toolCalls,
|
|
274
|
+
sessionStats: body.sessionStats,
|
|
275
|
+
usage: body.usage,
|
|
220
276
|
streaming: Boolean(body.session.activeTurnId),
|
|
221
277
|
turnPhase: body.session.turnPhase ??
|
|
222
278
|
(body.session.activeTurnId ? "running" : "idle"),
|
|
223
279
|
deferredTurn: body.session.deferredTurn ?? null,
|
|
280
|
+
deferredTurns: body.session.deferredTurns ?? (body.session.deferredTurn ? [body.session.deferredTurn] : []),
|
|
281
|
+
pendingQuestion: body.session.pendingQuestion ?? null,
|
|
282
|
+
todos: body.session.todos ?? [],
|
|
224
283
|
});
|
|
225
284
|
window.localStorage.setItem(ACTIVE_CHAT_SESSION_STORAGE_KEY, sessionId);
|
|
226
285
|
setActivePreviewPath(null);
|
|
@@ -241,6 +300,7 @@ export function useChatSessions(params) {
|
|
|
241
300
|
resetActiveProjection,
|
|
242
301
|
setActivePreviewPath,
|
|
243
302
|
setError,
|
|
303
|
+
sessionOwnerQuery,
|
|
244
304
|
]);
|
|
245
305
|
useEffect(() => {
|
|
246
306
|
void refreshSessionList();
|
|
@@ -275,19 +335,22 @@ export function useChatSessions(params) {
|
|
|
275
335
|
// re-read messages and operation refs/facts from the server-owned stores.
|
|
276
336
|
useEffect(() => {
|
|
277
337
|
let cancelled = false;
|
|
338
|
+
if (blocked)
|
|
339
|
+
return; // AC-R5-002: unknown workspace — no restore GET
|
|
278
340
|
const savedSessionId = window.localStorage.getItem(ACTIVE_CHAT_SESSION_STORAGE_KEY);
|
|
279
341
|
if (!savedSessionId)
|
|
280
342
|
return;
|
|
281
343
|
const selectionVersion = ++sessionSelectionVersionRef.current;
|
|
282
344
|
void (async () => {
|
|
283
345
|
try {
|
|
284
|
-
const res = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(savedSessionId)}`, { credentials: "include" });
|
|
346
|
+
const res = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(savedSessionId)}${workspaceQuery}`, { credentials: "include" });
|
|
285
347
|
if (!res.ok) {
|
|
286
348
|
window.localStorage.removeItem(ACTIVE_CHAT_SESSION_STORAGE_KEY);
|
|
287
349
|
return;
|
|
288
350
|
}
|
|
289
351
|
const body = (await res.json());
|
|
290
352
|
if (cancelled ||
|
|
353
|
+
blocked ||
|
|
291
354
|
selectionVersion !== sessionSelectionVersionRef.current ||
|
|
292
355
|
!body.session)
|
|
293
356
|
return;
|
|
@@ -309,18 +372,21 @@ export function useChatSessions(params) {
|
|
|
309
372
|
refs.lastEventIdRef.current = null;
|
|
310
373
|
setSession(restored);
|
|
311
374
|
setInput(body.session.composerDraft ?? "");
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
375
|
+
const hydrated = hydrateSessionThread(body.session.messages ?? []);
|
|
376
|
+
hydrated.messages = mergeCompactionMessages(hydrated.messages, body.compactions);
|
|
377
|
+
applyActivatedSession({
|
|
378
|
+
messages: hydrated.messages,
|
|
379
|
+
toolCalls: hydrated.toolCalls,
|
|
380
|
+
sessionStats: body.sessionStats,
|
|
381
|
+
usage: body.usage,
|
|
382
|
+
streaming: Boolean(body.session.activeTurnId),
|
|
383
|
+
turnPhase: body.session.turnPhase ??
|
|
384
|
+
(body.session.activeTurnId ? "running" : "idle"),
|
|
385
|
+
deferredTurn: body.session.deferredTurn ?? null,
|
|
386
|
+
deferredTurns: body.session.deferredTurns ?? (body.session.deferredTurn ? [body.session.deferredTurn] : []),
|
|
387
|
+
pendingQuestion: body.session.pendingQuestion ?? null,
|
|
388
|
+
todos: body.session.todos ?? [],
|
|
389
|
+
});
|
|
324
390
|
}
|
|
325
391
|
catch {
|
|
326
392
|
// Recovery is best-effort; a new session remains available.
|
|
@@ -329,30 +395,51 @@ export function useChatSessions(params) {
|
|
|
329
395
|
return () => {
|
|
330
396
|
cancelled = true;
|
|
331
397
|
};
|
|
332
|
-
}, [
|
|
398
|
+
}, [
|
|
399
|
+
origin,
|
|
400
|
+
workspaceQuery,
|
|
401
|
+
refs,
|
|
402
|
+
setSession,
|
|
403
|
+
setInput,
|
|
404
|
+
applyActivatedSession,
|
|
405
|
+
blocked,
|
|
406
|
+
]);
|
|
333
407
|
// The sole production create path delegates to the exported orchestration
|
|
334
408
|
// helper so its durable commit order is directly traceable and testable.
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
409
|
+
// AC-003 (2026-08-26): session creation REQUIRES a known workspaceId —
|
|
410
|
+
// either explicit (sidebar create-in-workspace) or the current valid
|
|
411
|
+
// selection. No scope → fail closed client-side (the server would reject
|
|
412
|
+
// with WORKSPACE_REQUIRED anyway); never a default-workspace fallback.
|
|
413
|
+
const createSessionAndPrimeTitleWatcherForHook = useCallback((options) => {
|
|
414
|
+
const scope = options?.workspaceId?.trim() ?? workspaceId?.trim();
|
|
415
|
+
if (!scope) {
|
|
416
|
+
setError("创建会话需要选择一个有效的工作区(不会回退默认工作区)");
|
|
417
|
+
return Promise.reject(new Error("WORKSPACE_REQUIRED"));
|
|
418
|
+
}
|
|
419
|
+
return createSessionAndPrimeTitleWatcher({
|
|
420
|
+
origin,
|
|
421
|
+
sessionListRef,
|
|
422
|
+
setSessionList,
|
|
423
|
+
titleWatcher: titleWatcherRef.current,
|
|
424
|
+
refreshSessionList,
|
|
425
|
+
options: { ...options, workspaceId: scope },
|
|
426
|
+
setCreatingSession,
|
|
427
|
+
creatingRef: creatingSessionRef,
|
|
428
|
+
});
|
|
429
|
+
}, [origin, refreshSessionList, workspaceId, setError]);
|
|
345
430
|
const createSession = useCallback(async (options) => {
|
|
346
431
|
// Suppress concurrent create from any entry (sidebar / Landing / chip).
|
|
347
432
|
if (creatingSessionRef.current)
|
|
348
433
|
return null;
|
|
434
|
+
if (blocked)
|
|
435
|
+
return null; // AC-R5-002: unknown workspace — no POST
|
|
349
436
|
const selectionVersion = ++sessionSelectionVersionRef.current;
|
|
350
437
|
setError(null);
|
|
351
438
|
try {
|
|
352
439
|
const created = await createSessionAndPrimeTitleWatcherForHook(options);
|
|
353
440
|
if (selectionVersion !== sessionSelectionVersionRef.current)
|
|
354
441
|
return null;
|
|
355
|
-
resetActiveProjection();
|
|
442
|
+
resetActiveProjection({ keepStreams: true });
|
|
356
443
|
// Set ref synchronously BEFORE setState so a chained
|
|
357
444
|
// createSession().then(sendPrompt) reads the new session even
|
|
358
445
|
// though sendPrompt's closure captured the stale (null) value.
|
|
@@ -376,6 +463,7 @@ export function useChatSessions(params) {
|
|
|
376
463
|
setError,
|
|
377
464
|
resetActiveProjection,
|
|
378
465
|
createSessionAndPrimeTitleWatcherForHook,
|
|
466
|
+
blocked,
|
|
379
467
|
]);
|
|
380
468
|
// Phase-1: fork / in-session branch / mainline switch UI deferred; API routes remain.
|
|
381
469
|
/** Single compact helper for toolbar + `/compact <instructions>` (AC-1). */
|
|
@@ -392,7 +480,8 @@ export function useChatSessions(params) {
|
|
|
392
480
|
if (trimmed) {
|
|
393
481
|
headers["content-type"] = "application/json";
|
|
394
482
|
}
|
|
395
|
-
|
|
483
|
+
// AC-002: compaction follows the session's durable owner.
|
|
484
|
+
const response = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(current.sessionId)}/compact${sessionOwnerQuery()}`, {
|
|
396
485
|
method: "POST",
|
|
397
486
|
credentials: "include",
|
|
398
487
|
headers,
|
|
@@ -424,19 +513,23 @@ export function useChatSessions(params) {
|
|
|
424
513
|
const tokensBefore = body.snapshot?.tokensBefore;
|
|
425
514
|
const summaryText = summary || "历史上下文已精简,关键决策与当前任务已保留。";
|
|
426
515
|
await activateSession(current.sessionId);
|
|
427
|
-
setMessages((messages) =>
|
|
428
|
-
|
|
429
|
-
{
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
:
|
|
438
|
-
|
|
439
|
-
|
|
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
|
+
});
|
|
440
533
|
setNotice({
|
|
441
534
|
id: Date.now(),
|
|
442
535
|
message: `已压缩上下文:${summaryText.slice(0, 72)}${summaryText.length > 72 ? "…" : ""}`,
|
|
@@ -456,9 +549,12 @@ export function useChatSessions(params) {
|
|
|
456
549
|
setMessages,
|
|
457
550
|
onCompactionChange,
|
|
458
551
|
activateSession,
|
|
552
|
+
sessionOwnerQuery,
|
|
459
553
|
]);
|
|
460
554
|
const updateSession = async (sessionId, patch) => {
|
|
461
|
-
|
|
555
|
+
// AC-002: rename/archive follows the session's durable owner.
|
|
556
|
+
const res = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(sessionId)}${sessionOwnerQuery(sessionListRef.current.find((item) => item.sessionId === sessionId)
|
|
557
|
+
?.workspaceId ?? null)}`, {
|
|
462
558
|
method: "PATCH",
|
|
463
559
|
credentials: "include",
|
|
464
560
|
headers: {
|
|
@@ -57,7 +57,7 @@ function clearEmptyAssistant(setMessages, assistantId) {
|
|
|
57
57
|
* detached Turn resource only; it never opens a second inline SSE consumer. */
|
|
58
58
|
export function useChatStream(params) {
|
|
59
59
|
const { origin, refs, session, thread, composer, setError, setAutoScroll, setNotice, recoveryVersion = 0, watchSessionTitle, } = params;
|
|
60
|
-
const { applySseBlock, setMessages, setStreaming, setStreamingAssistantId, streaming, setTurnPhase, setDeferredTurn, } = thread;
|
|
60
|
+
const { applySseBlock, setMessages, setStreaming, setStreamingAssistantId, streaming, setTurnPhase, setDeferredTurn, setDeferredTurns, } = thread;
|
|
61
61
|
const { setInput, setPendingImages, recordHistory } = composer;
|
|
62
62
|
const showNotice = useCallback((message) => {
|
|
63
63
|
if (!setNotice)
|
|
@@ -386,17 +386,18 @@ export function useChatStream(params) {
|
|
|
386
386
|
const activeSession = refs.sessionRef.current;
|
|
387
387
|
if (!activeSession || (!input.text.trim() && input.images.length === 0))
|
|
388
388
|
return { action: "handled" };
|
|
389
|
-
const existing = thread.deferredTurn;
|
|
390
389
|
showNotice("正在排队…");
|
|
391
|
-
const acceptDeferred = (deferredTurn) => {
|
|
392
|
-
setDeferredTurn(deferredTurn);
|
|
390
|
+
const acceptDeferred = (deferredTurn, deferredTurns) => {
|
|
391
|
+
setDeferredTurn(deferredTurns?.[0] ?? deferredTurn);
|
|
392
|
+
setDeferredTurns(deferredTurns ?? [deferredTurn]);
|
|
393
393
|
setInput("");
|
|
394
394
|
setPendingImages([]);
|
|
395
395
|
setError(null);
|
|
396
|
-
showNotice(
|
|
396
|
+
showNotice("已排队,将在当前回合结束后自动发送");
|
|
397
397
|
};
|
|
398
398
|
const acceptDispatched = () => {
|
|
399
399
|
setDeferredTurn(null);
|
|
400
|
+
setDeferredTurns([]);
|
|
400
401
|
setInput("");
|
|
401
402
|
setPendingImages([]);
|
|
402
403
|
setError(null);
|
|
@@ -415,7 +416,6 @@ export function useChatStream(params) {
|
|
|
415
416
|
blockedByTurnId: input.blockedByTurnId,
|
|
416
417
|
text: input.text,
|
|
417
418
|
images: input.images,
|
|
418
|
-
...(existing ? { expectedRevision: existing.revision } : {}),
|
|
419
419
|
}),
|
|
420
420
|
});
|
|
421
421
|
const body = (await response.json().catch(() => ({})));
|
|
@@ -425,9 +425,11 @@ export function useChatStream(params) {
|
|
|
425
425
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
426
426
|
const { response, body } = await putOnce();
|
|
427
427
|
if (response.ok && body.accepted && body.deferredTurn) {
|
|
428
|
-
acceptDeferred(body.deferredTurn);
|
|
428
|
+
acceptDeferred(body.deferredTurn, body.deferredTurns);
|
|
429
429
|
return { action: "handled" };
|
|
430
430
|
}
|
|
431
|
+
if (body.deferredTurns)
|
|
432
|
+
setDeferredTurns(body.deferredTurns);
|
|
431
433
|
if (body.deferredTurn)
|
|
432
434
|
setDeferredTurn(body.deferredTurn);
|
|
433
435
|
if (body.error?.code !== "TURN_NOT_ACTIVE") {
|
|
@@ -442,7 +444,7 @@ export function useChatStream(params) {
|
|
|
442
444
|
state: canonical ?? undefined,
|
|
443
445
|
});
|
|
444
446
|
if (decision.action === "deferred") {
|
|
445
|
-
acceptDeferred(decision.deferredTurn);
|
|
447
|
+
acceptDeferred(decision.deferredTurn, canonical?.deferredTurns);
|
|
446
448
|
return { action: "handled" };
|
|
447
449
|
}
|
|
448
450
|
if (decision.action === "accepted") {
|
|
@@ -451,7 +453,7 @@ export function useChatStream(params) {
|
|
|
451
453
|
}
|
|
452
454
|
if (decision.action === "retry-deferred" && attempt === 0)
|
|
453
455
|
continue;
|
|
454
|
-
if (decision.action === "send-now"
|
|
456
|
+
if (decision.action === "send-now") {
|
|
455
457
|
showNotice("上一轮已停止,正在发送当前消息…");
|
|
456
458
|
return { action: "send-now" };
|
|
457
459
|
}
|
|
@@ -468,14 +470,14 @@ export function useChatStream(params) {
|
|
|
468
470
|
state: canonical ?? undefined,
|
|
469
471
|
});
|
|
470
472
|
if (decision.action === "deferred") {
|
|
471
|
-
acceptDeferred(decision.deferredTurn);
|
|
473
|
+
acceptDeferred(decision.deferredTurn, canonical?.deferredTurns);
|
|
472
474
|
return { action: "handled" };
|
|
473
475
|
}
|
|
474
476
|
if (decision.action === "accepted") {
|
|
475
477
|
acceptDispatched();
|
|
476
478
|
return { action: "handled" };
|
|
477
479
|
}
|
|
478
|
-
if (decision.action === "send-now"
|
|
480
|
+
if (decision.action === "send-now") {
|
|
479
481
|
showNotice("上一轮已停止,正在发送当前消息…");
|
|
480
482
|
return { action: "send-now" };
|
|
481
483
|
}
|
|
@@ -489,6 +491,7 @@ export function useChatStream(params) {
|
|
|
489
491
|
refs,
|
|
490
492
|
thread.deferredTurn,
|
|
491
493
|
setDeferredTurn,
|
|
494
|
+
setDeferredTurns,
|
|
492
495
|
setInput,
|
|
493
496
|
setPendingImages,
|
|
494
497
|
setError,
|
|
@@ -497,9 +500,11 @@ export function useChatStream(params) {
|
|
|
497
500
|
]);
|
|
498
501
|
/** DELETE the queued turn; "restore" moves its text back into the composer,
|
|
499
502
|
* "discard" drops it entirely (2026-08-23 queue chip actions). */
|
|
500
|
-
const requestDeferredTurnDelete = useCallback(async (mode) => {
|
|
503
|
+
const requestDeferredTurnDelete = useCallback(async (mode, target) => {
|
|
501
504
|
const activeSession = refs.sessionRef.current;
|
|
502
|
-
const deferred =
|
|
505
|
+
const deferred = target
|
|
506
|
+
? thread.deferredTurns.find((item) => item.clientRequestId === target.clientRequestId)
|
|
507
|
+
: thread.deferredTurns[0] ?? thread.deferredTurn;
|
|
503
508
|
if (!activeSession || !deferred)
|
|
504
509
|
return;
|
|
505
510
|
try {
|
|
@@ -520,10 +525,12 @@ export function useChatStream(params) {
|
|
|
520
525
|
setError(body.error?.message ?? "待发送消息撤回失败");
|
|
521
526
|
return;
|
|
522
527
|
}
|
|
523
|
-
|
|
528
|
+
const remaining = thread.deferredTurns.filter((item) => item.clientRequestId !== deferred.clientRequestId);
|
|
529
|
+
setDeferredTurns(remaining);
|
|
530
|
+
setDeferredTurn(remaining[0] ?? null);
|
|
524
531
|
if (mode === "restore") {
|
|
525
|
-
setInput(body.deferredTurn?.text ?? "");
|
|
526
|
-
setPendingImages(body.deferredTurn?.images ?? []);
|
|
532
|
+
setInput(deferred.text ?? body.deferredTurn?.text ?? "");
|
|
533
|
+
setPendingImages(deferred.images ?? body.deferredTurn?.images ?? []);
|
|
527
534
|
showNotice("待发送消息已移回输入框");
|
|
528
535
|
}
|
|
529
536
|
else {
|
|
@@ -538,14 +545,66 @@ export function useChatStream(params) {
|
|
|
538
545
|
origin,
|
|
539
546
|
refs,
|
|
540
547
|
thread.deferredTurn,
|
|
548
|
+
thread.deferredTurns,
|
|
541
549
|
setDeferredTurn,
|
|
550
|
+
setDeferredTurns,
|
|
542
551
|
setInput,
|
|
543
552
|
setPendingImages,
|
|
544
553
|
setError,
|
|
545
554
|
showNotice,
|
|
546
555
|
]);
|
|
547
556
|
const cancelDeferredTurn = useCallback(() => requestDeferredTurnDelete("restore"), [requestDeferredTurnDelete]);
|
|
548
|
-
const discardDeferredTurn = useCallback(() => requestDeferredTurnDelete("discard"), [requestDeferredTurnDelete]);
|
|
557
|
+
const discardDeferredTurn = useCallback((target) => requestDeferredTurnDelete("discard", target), [requestDeferredTurnDelete]);
|
|
558
|
+
const editDeferredTurn = useCallback(async (target, text) => {
|
|
559
|
+
const activeSession = refs.sessionRef.current;
|
|
560
|
+
if (!activeSession)
|
|
561
|
+
return;
|
|
562
|
+
const item = thread.deferredTurns.find((entry) => entry.clientRequestId === target.clientRequestId);
|
|
563
|
+
if (!item)
|
|
564
|
+
return;
|
|
565
|
+
try {
|
|
566
|
+
const response = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(activeSession.sessionId)}/deferred-turn`, {
|
|
567
|
+
method: "PUT",
|
|
568
|
+
headers: { "Content-Type": "application/json", "x-loop-console-confirmation": confirmationToken() },
|
|
569
|
+
credentials: "include",
|
|
570
|
+
body: JSON.stringify({ clientRequestId: target.clientRequestId, blockedByTurnId: item.blockedByTurnId, text, images: item.images ?? [], expectedRevision: target.revision }),
|
|
571
|
+
});
|
|
572
|
+
const body = (await response.json().catch(() => ({})));
|
|
573
|
+
if (!response.ok)
|
|
574
|
+
throw new Error(body.error?.message ?? "编辑排队消息失败");
|
|
575
|
+
if (body.deferredTurns)
|
|
576
|
+
setDeferredTurns(body.deferredTurns);
|
|
577
|
+
if (body.deferredTurn)
|
|
578
|
+
setDeferredTurn(body.deferredTurn);
|
|
579
|
+
setError(null);
|
|
580
|
+
}
|
|
581
|
+
catch (error) {
|
|
582
|
+
setError(error instanceof Error ? error.message : "编辑排队消息失败");
|
|
583
|
+
}
|
|
584
|
+
}, [origin, refs, thread.deferredTurns, setDeferredTurn, setDeferredTurns, setError]);
|
|
585
|
+
const steerDeferredTurn = useCallback(async (target) => {
|
|
586
|
+
const activeSession = refs.sessionRef.current;
|
|
587
|
+
if (!activeSession)
|
|
588
|
+
return;
|
|
589
|
+
try {
|
|
590
|
+
const response = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(activeSession.sessionId)}/deferred-turn/${encodeURIComponent(target.clientRequestId)}/steer`, {
|
|
591
|
+
method: "POST",
|
|
592
|
+
headers: { "Content-Type": "application/json", "x-loop-console-confirmation": confirmationToken() },
|
|
593
|
+
credentials: "include",
|
|
594
|
+
body: JSON.stringify({ expectedRevision: target.revision }),
|
|
595
|
+
});
|
|
596
|
+
const body = (await response.json().catch(() => ({})));
|
|
597
|
+
if (!response.ok)
|
|
598
|
+
throw new Error(body.error?.message ?? "插话发送失败");
|
|
599
|
+
const remaining = body.deferredTurns ?? thread.deferredTurns.filter((item) => item.clientRequestId !== target.clientRequestId);
|
|
600
|
+
setDeferredTurns(remaining);
|
|
601
|
+
setDeferredTurn(remaining[0] ?? null);
|
|
602
|
+
setError(null);
|
|
603
|
+
}
|
|
604
|
+
catch (error) {
|
|
605
|
+
setError(error instanceof Error ? error.message : "插话发送失败");
|
|
606
|
+
}
|
|
607
|
+
}, [origin, refs, thread.deferredTurns, setDeferredTurn, setDeferredTurns, setError]);
|
|
549
608
|
const sendPrompt = useCallback(async (text, images) => {
|
|
550
609
|
const initialSession = refs.sessionRef.current;
|
|
551
610
|
if (!initialSession || (!text.trim() && images.length === 0))
|
|
@@ -988,6 +1047,8 @@ export function useChatStream(params) {
|
|
|
988
1047
|
handleStop,
|
|
989
1048
|
cancelDeferredTurn,
|
|
990
1049
|
discardDeferredTurn,
|
|
1050
|
+
editDeferredTurn,
|
|
1051
|
+
steerDeferredTurn,
|
|
991
1052
|
openEventsStream,
|
|
992
1053
|
};
|
|
993
1054
|
}
|