@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,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
2
|
import { buildTurnProcessGroups, hydrateSessionThread, selectRecentTurnGroups, } from "../../chat/turn-process.js";
|
|
3
|
+
import { mergeOperationCardState, } from "../../chat/operation-card.js";
|
|
3
4
|
import { createChatSseEventApplier, } from "./chat-sse-events.js";
|
|
4
5
|
import { taskContextUnchanged } from "./format.js";
|
|
5
6
|
const EMPTY_USAGE = {
|
|
@@ -9,13 +10,125 @@ const EMPTY_USAGE = {
|
|
|
9
10
|
cacheWriteTokens: 0,
|
|
10
11
|
totalTokens: 0,
|
|
11
12
|
};
|
|
13
|
+
const EMPTY_SESSION_STATS = {
|
|
14
|
+
turns: 0,
|
|
15
|
+
steps: 0,
|
|
16
|
+
llmMs: 0,
|
|
17
|
+
toolMs: 0,
|
|
18
|
+
ttftMs: 0,
|
|
19
|
+
ttftSteps: 0,
|
|
20
|
+
decodeMs: 0,
|
|
21
|
+
decodeTokens: 0,
|
|
22
|
+
};
|
|
23
|
+
/** R8 AC-R8-003: project a raw GET /operations/:id fact (the same shape the
|
|
24
|
+
* server persists) into a browser OperationCardState. Field-validated; any
|
|
25
|
+
* malformed fact returns undefined (no fabricated cards). */
|
|
26
|
+
function operationCardFromOperationFact(op) {
|
|
27
|
+
const str = (key) => {
|
|
28
|
+
const value = op[key];
|
|
29
|
+
return typeof value === "string" && value ? value : undefined;
|
|
30
|
+
};
|
|
31
|
+
const operationId = str("operationId");
|
|
32
|
+
const action = str("action");
|
|
33
|
+
const state = str("state");
|
|
34
|
+
const createdAt = str("createdAt");
|
|
35
|
+
if (!operationId || !action || !state || !createdAt)
|
|
36
|
+
return undefined;
|
|
37
|
+
const updatedAt = str("updatedAt") ?? str("finishedAt") ?? str("startedAt") ?? createdAt;
|
|
38
|
+
const dagRunId = str("dagRunId");
|
|
39
|
+
return {
|
|
40
|
+
schemaVersion: 1,
|
|
41
|
+
operationId,
|
|
42
|
+
action,
|
|
43
|
+
state,
|
|
44
|
+
createdAt,
|
|
45
|
+
updatedAt,
|
|
46
|
+
inspectHref: `/inspect/?operationId=${encodeURIComponent(operationId)}#/`,
|
|
47
|
+
...(str("taskId") ? { taskId: str("taskId") } : {}),
|
|
48
|
+
...(dagRunId ? { dagRunId } : {}),
|
|
49
|
+
...(str("errorCode") ? { errorCode: str("errorCode") } : {}),
|
|
50
|
+
activityRef: `operation:${operationId}`,
|
|
51
|
+
previewHash: `${operationId}:${state}:${updatedAt}`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Apply one settled operation-link mutation to the visible card projection.
|
|
56
|
+
*
|
|
57
|
+
* The mutation identity is captured before the request starts. Both the
|
|
58
|
+
* session and workspace are fenced before and after the follow-up read so a
|
|
59
|
+
* late response can never leak into a newly selected workspace/session.
|
|
60
|
+
*/
|
|
61
|
+
export async function applyOperationLinkMutationProjection(input) {
|
|
62
|
+
const { identity } = input;
|
|
63
|
+
const stillOwnsProjection = () => {
|
|
64
|
+
const current = input.currentOwner();
|
|
65
|
+
return (current.sessionId === identity.sessionId &&
|
|
66
|
+
current.workspaceId === identity.workspaceId);
|
|
67
|
+
};
|
|
68
|
+
if (!identity.workspaceId || !stillOwnsProjection())
|
|
69
|
+
return false;
|
|
70
|
+
if (identity.action === "remove") {
|
|
71
|
+
input.setOperationCards((current) => current.filter((card) => card.operationId !== identity.operationId));
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const workspaceQuery = new URLSearchParams({
|
|
76
|
+
workspaceId: identity.workspaceId,
|
|
77
|
+
});
|
|
78
|
+
const fetchImpl = input.fetchFn ?? fetch;
|
|
79
|
+
const res = await fetchImpl(`${input.origin}/api/operator/v1/operations/${encodeURIComponent(identity.operationId)}?${workspaceQuery.toString()}`, { credentials: "include" });
|
|
80
|
+
if (!res.ok || !stillOwnsProjection())
|
|
81
|
+
return false;
|
|
82
|
+
const body = (await res.json());
|
|
83
|
+
const operation = body.operation;
|
|
84
|
+
if (body.workspaceId !== identity.workspaceId ||
|
|
85
|
+
!operation ||
|
|
86
|
+
operation.workspaceId !== identity.workspaceId ||
|
|
87
|
+
operation.operationId !== identity.operationId ||
|
|
88
|
+
!stillOwnsProjection())
|
|
89
|
+
return false;
|
|
90
|
+
const card = operationCardFromOperationFact(operation);
|
|
91
|
+
if (!card)
|
|
92
|
+
return false;
|
|
93
|
+
input.setOperationCards((current) => stillOwnsProjection()
|
|
94
|
+
? mergeOperationCardState(current, card)
|
|
95
|
+
: current);
|
|
96
|
+
return stillOwnsProjection();
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function mergeCompactionMessages(messages, compactions) {
|
|
103
|
+
const result = [...messages];
|
|
104
|
+
for (const item of compactions ?? []) {
|
|
105
|
+
const data = item.data && typeof item.data === "object" ? item.data : {};
|
|
106
|
+
const eventId = typeof item.eventId === "string" ? item.eventId : undefined;
|
|
107
|
+
const id = eventId ? `compaction-${eventId}` : `compaction-${item.at ?? Date.now()}`;
|
|
108
|
+
if (result.some((message) => message.id === id))
|
|
109
|
+
continue;
|
|
110
|
+
result.push({
|
|
111
|
+
id,
|
|
112
|
+
role: "assistant",
|
|
113
|
+
kind: "compaction",
|
|
114
|
+
text: typeof data.summary === "string" ? data.summary : "历史上下文已精简,关键决策与当前任务已保留。",
|
|
115
|
+
createdAt: Date.parse(item.at ?? "") || Date.now(),
|
|
116
|
+
compaction: {
|
|
117
|
+
...(typeof data.tokensBefore === "number" ? { tokensBefore: data.tokensBefore } : {}),
|
|
118
|
+
...(typeof data.firstKeptEntryId === "string" ? { firstKeptEntryId: data.firstKeptEntryId } : {}),
|
|
119
|
+
...(eventId ? { eventId } : {}),
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
return result.sort((a, b) => a.createdAt - b.createdAt);
|
|
124
|
+
}
|
|
12
125
|
/**
|
|
13
126
|
* Thread state + SSE block parsing. The event-application logic itself lives
|
|
14
127
|
* in chat-sse-events.ts (createChatSseEventApplier); this hook owns the state
|
|
15
128
|
* slices it patches and the derived timeline projections.
|
|
16
129
|
*/
|
|
17
130
|
export function useChatThread(params) {
|
|
18
|
-
const { origin, refs, sessionId, setError, onReconcile } = params;
|
|
131
|
+
const { origin, refs, sessionId, workspaceId, setError, onReconcile } = params;
|
|
19
132
|
// setProcessOpen remains on the hook API for session lifecycle close/reset
|
|
20
133
|
// (UI-08); the SSE applier must not open the process panel.
|
|
21
134
|
void params.setProcessOpen;
|
|
@@ -30,12 +143,36 @@ export function useChatThread(params) {
|
|
|
30
143
|
const [taskContext, setTaskContext] = useState(null);
|
|
31
144
|
const [contextLoading, setContextLoading] = useState(false);
|
|
32
145
|
const [usage, setUsage] = useState(EMPTY_USAGE);
|
|
146
|
+
const [sessionStats, setSessionStats] = useState(EMPTY_SESSION_STATS);
|
|
33
147
|
const [streaming, setStreaming] = useState(false);
|
|
34
148
|
const [turnPhase, setTurnPhase] = useState("idle");
|
|
35
149
|
const [deferredTurn, setDeferredTurn] = useState(null);
|
|
150
|
+
const [deferredTurns, setDeferredTurns] = useState([]);
|
|
151
|
+
const [pendingQuestion, setPendingQuestion] = useState(null);
|
|
152
|
+
const [todos, setTodos] = useState([]);
|
|
36
153
|
/** Assistant bubble id for the in-flight turn; only this row shows the streaming cursor. */
|
|
37
154
|
const [streamingAssistantId, setStreamingAssistantId] = useState(null);
|
|
38
155
|
const [showSystemInMessages] = useState(false);
|
|
156
|
+
const refreshSessionTelemetry = useCallback(async () => {
|
|
157
|
+
const targetSessionId = refs.sessionRef.current?.sessionId;
|
|
158
|
+
if (!targetSessionId)
|
|
159
|
+
return;
|
|
160
|
+
try {
|
|
161
|
+
const res = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(targetSessionId)}`, { credentials: "include" });
|
|
162
|
+
if (!res.ok)
|
|
163
|
+
return;
|
|
164
|
+
const body = (await res.json());
|
|
165
|
+
if (refs.sessionRef.current?.sessionId !== targetSessionId)
|
|
166
|
+
return;
|
|
167
|
+
if (body.sessionStats)
|
|
168
|
+
setSessionStats(body.sessionStats);
|
|
169
|
+
if (body.usage)
|
|
170
|
+
setUsage(body.usage);
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
// Telemetry is supplementary; the normal event stream remains authoritative.
|
|
174
|
+
}
|
|
175
|
+
}, [origin, refs]);
|
|
39
176
|
// All inputs are stable (state setters + the memoized refs bag), so the
|
|
40
177
|
// applier identity is stable across renders.
|
|
41
178
|
const applyChatSseEvent = useMemo(() => createChatSseEventApplier({
|
|
@@ -48,13 +185,17 @@ export function useChatThread(params) {
|
|
|
48
185
|
setStreaming,
|
|
49
186
|
setTurnPhase,
|
|
50
187
|
setDeferredTurn,
|
|
188
|
+
setDeferredTurns,
|
|
189
|
+
setPendingQuestion,
|
|
190
|
+
setTodos,
|
|
51
191
|
setStreamingAssistantId,
|
|
52
192
|
setUnread,
|
|
53
193
|
setOperationCards,
|
|
54
194
|
setUsage,
|
|
195
|
+
onTelemetryRefresh: () => void refreshSessionTelemetry(),
|
|
55
196
|
setMessages,
|
|
56
197
|
setToolCalls,
|
|
57
|
-
}), [refs, onReconcile, setError]);
|
|
198
|
+
}), [refs, onReconcile, setError, refreshSessionTelemetry]);
|
|
58
199
|
/** Parse + apply one raw SSE block (frames separated by \n\n). Returns the
|
|
59
200
|
* eventId if the block carried an `id:` line (for Last-Event-ID tracking). */
|
|
60
201
|
const applySseBlock = useCallback((block, assistantId, options) => {
|
|
@@ -124,6 +265,21 @@ export function useChatThread(params) {
|
|
|
124
265
|
}, 350);
|
|
125
266
|
return () => window.clearTimeout(timer);
|
|
126
267
|
}, [sessionId, operationCards, humanGateCards, interview, loadTaskContext]);
|
|
268
|
+
/** R8 AC-R8-003: deterministic mutation-driven operationCards updates.
|
|
269
|
+
* Binds to the identity captured when the real POST/DELETE was issued:
|
|
270
|
+
* - add → consume server truth for that operation (GET operation) and
|
|
271
|
+
* upsert the card when the captured session still matches.
|
|
272
|
+
* - remove → drop the captured operation's card immediately (the server
|
|
273
|
+
* tombstone guarantees it never resurrects on replay). */
|
|
274
|
+
const applyOperationLinkMutation = useCallback((identity) => applyOperationLinkMutationProjection({
|
|
275
|
+
origin,
|
|
276
|
+
identity,
|
|
277
|
+
currentOwner: () => ({
|
|
278
|
+
sessionId: refs.sessionRef.current?.sessionId,
|
|
279
|
+
workspaceId,
|
|
280
|
+
}),
|
|
281
|
+
setOperationCards,
|
|
282
|
+
}), [origin, refs, workspaceId]);
|
|
127
283
|
const timelineMessages = useMemo(() => messages.filter((m) => m.role !== "system" || showSystemInMessages), [messages, showSystemInMessages]);
|
|
128
284
|
useEffect(() => {
|
|
129
285
|
const onVisibility = () => {
|
|
@@ -193,9 +349,13 @@ export function useChatThread(params) {
|
|
|
193
349
|
setInterview(null);
|
|
194
350
|
setTaskContext(null);
|
|
195
351
|
setUsage(EMPTY_USAGE);
|
|
352
|
+
setSessionStats(EMPTY_SESSION_STATS);
|
|
196
353
|
setStreaming(false);
|
|
197
354
|
setTurnPhase("idle");
|
|
198
355
|
setDeferredTurn(null);
|
|
356
|
+
setDeferredTurns([]);
|
|
357
|
+
setPendingQuestion(null);
|
|
358
|
+
setTodos([]);
|
|
199
359
|
setStreamingAssistantId(null);
|
|
200
360
|
setUnread(0);
|
|
201
361
|
setHistoryLimit(50);
|
|
@@ -210,11 +370,15 @@ export function useChatThread(params) {
|
|
|
210
370
|
setArtifactCards([]);
|
|
211
371
|
setTaskContext(null);
|
|
212
372
|
setInterview(null);
|
|
213
|
-
setUsage(EMPTY_USAGE);
|
|
373
|
+
setUsage(next.usage ?? EMPTY_USAGE);
|
|
374
|
+
setSessionStats(next.sessionStats ?? EMPTY_SESSION_STATS);
|
|
214
375
|
refs.usageResponseKeysRef.current = new Set();
|
|
215
376
|
setStreaming(next.streaming);
|
|
216
377
|
setTurnPhase(next.turnPhase ?? (next.streaming ? "running" : "idle"));
|
|
217
378
|
setDeferredTurn(next.deferredTurn ?? null);
|
|
379
|
+
setDeferredTurns(next.deferredTurns ?? (next.deferredTurn ? [next.deferredTurn] : []));
|
|
380
|
+
setPendingQuestion(next.pendingQuestion ?? null);
|
|
381
|
+
setTodos(next.todos ?? []);
|
|
218
382
|
setStreamingAssistantId(null);
|
|
219
383
|
setUnread(0);
|
|
220
384
|
setHistoryLimit(50);
|
|
@@ -238,6 +402,13 @@ export function useChatThread(params) {
|
|
|
238
402
|
refs.streamingRef.current = Boolean(activeTurnId);
|
|
239
403
|
setTurnPhase(body.session.turnPhase ?? (activeTurnId ? "running" : "idle"));
|
|
240
404
|
setDeferredTurn(body.session.deferredTurn ?? null);
|
|
405
|
+
setDeferredTurns(body.session.deferredTurns ?? (body.session.deferredTurn ? [body.session.deferredTurn] : []));
|
|
406
|
+
setPendingQuestion(body.session.pendingQuestion ?? null);
|
|
407
|
+
setTodos(body.session.todos ?? []);
|
|
408
|
+
if (body.sessionStats)
|
|
409
|
+
setSessionStats(body.sessionStats);
|
|
410
|
+
if (body.usage)
|
|
411
|
+
setUsage(body.usage);
|
|
241
412
|
// Resume the events stream from the snapshot tail so the next
|
|
242
413
|
// reconnect never immediately re-expires (AC-003). The refs below
|
|
243
414
|
// are reset so any follow-up tail replay re-applies cleanly.
|
|
@@ -246,7 +417,7 @@ export function useChatThread(params) {
|
|
|
246
417
|
refs.turnAssistantIdsRef.current = new Map();
|
|
247
418
|
refs.sealedAssistantIdsRef.current = new Set();
|
|
248
419
|
const hydrated = hydrateSessionThread(body.session.messages ?? []);
|
|
249
|
-
setMessages(hydrated.messages);
|
|
420
|
+
setMessages(mergeCompactionMessages(hydrated.messages, body.compactions));
|
|
250
421
|
setToolCalls(hydrated.toolCalls);
|
|
251
422
|
setStreamingAssistantId(null);
|
|
252
423
|
return true;
|
|
@@ -264,6 +435,8 @@ export function useChatThread(params) {
|
|
|
264
435
|
setStreamingAssistantId,
|
|
265
436
|
setTurnPhase,
|
|
266
437
|
setDeferredTurn,
|
|
438
|
+
setPendingQuestion,
|
|
439
|
+
setTodos,
|
|
267
440
|
]);
|
|
268
441
|
return {
|
|
269
442
|
messages,
|
|
@@ -281,12 +454,18 @@ export function useChatThread(params) {
|
|
|
281
454
|
taskContext,
|
|
282
455
|
contextLoading,
|
|
283
456
|
usage,
|
|
457
|
+
sessionStats,
|
|
284
458
|
streaming,
|
|
285
459
|
setStreaming,
|
|
286
460
|
turnPhase,
|
|
287
461
|
setTurnPhase,
|
|
288
462
|
deferredTurn,
|
|
289
463
|
setDeferredTurn,
|
|
464
|
+
deferredTurns,
|
|
465
|
+
setDeferredTurns,
|
|
466
|
+
pendingQuestion,
|
|
467
|
+
setPendingQuestion,
|
|
468
|
+
todos,
|
|
290
469
|
streamingAssistantId,
|
|
291
470
|
setStreamingAssistantId,
|
|
292
471
|
visibleMessages,
|
|
@@ -295,6 +474,7 @@ export function useChatThread(params) {
|
|
|
295
474
|
applyChatSseEvent,
|
|
296
475
|
applySseBlock,
|
|
297
476
|
loadTaskContext,
|
|
477
|
+
applyOperationLinkMutation,
|
|
298
478
|
resetForNewSession,
|
|
299
479
|
applyActivatedSession,
|
|
300
480
|
recoverSession,
|
|
@@ -1,21 +1,32 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
2
|
import { ACTIVE_CHAT_SESSION_STORAGE_KEY } from "../../chat/workspace-landing.js";
|
|
3
|
-
/**
|
|
3
|
+
/** Details-rail repo tree + file preview tabs state. */
|
|
4
4
|
export function useRepoBrowser(params) {
|
|
5
|
-
const { origin, refs, sessionId, setError } = params;
|
|
5
|
+
const { origin, refs, sessionId, workspaceId, blocked, setError } = params;
|
|
6
6
|
const [tree, setTree] = useState({});
|
|
7
7
|
const [expandedDirs, setExpandedDirs] = useState(new Set());
|
|
8
8
|
const [treeError, setTreeError] = useState(null);
|
|
9
9
|
const [previews, setPreviews] = useState([]);
|
|
10
10
|
const [activePreviewPath, setActivePreviewPath] = useState(null);
|
|
11
11
|
const [previewLoading, setPreviewLoading] = useState(false);
|
|
12
|
+
const workspaceScopeRef = useRef(workspaceId ?? null);
|
|
13
|
+
workspaceScopeRef.current = workspaceId ?? null;
|
|
14
|
+
const autoPreviewedWorkspaceRef = useRef(new Set());
|
|
12
15
|
const expandedDirsRef = useRef(new Set());
|
|
13
16
|
expandedDirsRef.current = expandedDirs;
|
|
17
|
+
const workspaceQuery = workspaceId
|
|
18
|
+
? `&workspaceId=${encodeURIComponent(workspaceId)}`
|
|
19
|
+
: "";
|
|
14
20
|
const loadTree = useCallback(async (directory = "") => {
|
|
21
|
+
if (blocked)
|
|
22
|
+
return; // AC-R5-002: fail-closed, zero repo requests
|
|
23
|
+
const requestWorkspaceId = workspaceId ?? null;
|
|
15
24
|
try {
|
|
16
25
|
setTreeError(null);
|
|
17
|
-
const res = await fetch(`${origin}/api/operator/v1/chat/repo/tree?path=${encodeURIComponent(directory)}`, { credentials: "include" });
|
|
26
|
+
const res = await fetch(`${origin}/api/operator/v1/chat/repo/tree?path=${encodeURIComponent(directory)}${workspaceQuery}`, { credentials: "include" });
|
|
18
27
|
const body = (await res.json());
|
|
28
|
+
if (workspaceScopeRef.current !== requestWorkspaceId)
|
|
29
|
+
return;
|
|
19
30
|
if (!res.ok)
|
|
20
31
|
throw new Error(body.error?.message ?? "无法读取仓库目录");
|
|
21
32
|
setTree((current) => ({
|
|
@@ -24,9 +35,11 @@ export function useRepoBrowser(params) {
|
|
|
24
35
|
}));
|
|
25
36
|
}
|
|
26
37
|
catch (error) {
|
|
38
|
+
if (workspaceScopeRef.current !== requestWorkspaceId)
|
|
39
|
+
return;
|
|
27
40
|
setTreeError(error instanceof Error ? error.message : String(error));
|
|
28
41
|
}
|
|
29
|
-
}, [origin]);
|
|
42
|
+
}, [origin, workspaceQuery, workspaceId, blocked]);
|
|
30
43
|
useEffect(() => {
|
|
31
44
|
void loadTree();
|
|
32
45
|
}, [loadTree]);
|
|
@@ -53,10 +66,15 @@ export function useRepoBrowser(params) {
|
|
|
53
66
|
return () => document.removeEventListener("visibilitychange", onVisibility);
|
|
54
67
|
}, [refreshTree]);
|
|
55
68
|
const openPreview = useCallback(async (file) => {
|
|
69
|
+
if (blocked)
|
|
70
|
+
return; // AC-R5-002: fail-closed
|
|
71
|
+
const requestWorkspaceId = workspaceId ?? null;
|
|
56
72
|
setPreviewLoading(true);
|
|
57
73
|
try {
|
|
58
|
-
const res = await fetch(`${origin}/api/operator/v1/chat/repo/file?path=${encodeURIComponent(file)}`, { credentials: "include" });
|
|
74
|
+
const res = await fetch(`${origin}/api/operator/v1/chat/repo/file?path=${encodeURIComponent(file)}${workspaceQuery}`, { credentials: "include" });
|
|
59
75
|
const body = (await res.json());
|
|
76
|
+
if (workspaceScopeRef.current !== requestWorkspaceId)
|
|
77
|
+
return;
|
|
60
78
|
if (!res.ok || !body.data)
|
|
61
79
|
throw new Error(body.error?.message ?? "无法预览文件");
|
|
62
80
|
setPreviews((current) => {
|
|
@@ -72,12 +90,48 @@ export function useRepoBrowser(params) {
|
|
|
72
90
|
setActivePreviewPath(body.data.path);
|
|
73
91
|
}
|
|
74
92
|
catch (error) {
|
|
93
|
+
if (workspaceScopeRef.current !== requestWorkspaceId)
|
|
94
|
+
return;
|
|
75
95
|
setError(error instanceof Error ? error.message : String(error));
|
|
76
96
|
}
|
|
77
97
|
finally {
|
|
78
|
-
|
|
98
|
+
if (workspaceScopeRef.current === requestWorkspaceId)
|
|
99
|
+
setPreviewLoading(false);
|
|
79
100
|
}
|
|
80
|
-
}, [origin, refs, setError]);
|
|
101
|
+
}, [origin, workspaceQuery, workspaceId, refs, setError, blocked]);
|
|
102
|
+
// A workspace switch must immediately remove the previous workspace's tree
|
|
103
|
+
// and file contents while the newly scoped requests are in flight.
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
setTree({});
|
|
106
|
+
setExpandedDirs(new Set());
|
|
107
|
+
setTreeError(null);
|
|
108
|
+
setPreviews([]);
|
|
109
|
+
setActivePreviewPath(null);
|
|
110
|
+
setPreviewLoading(false);
|
|
111
|
+
}, [workspaceId]);
|
|
112
|
+
// DSH parity: Details opens as a real tree + preview split. Select a real
|
|
113
|
+
// root file once per workspace (README first), never placeholder content.
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
if (blocked || activePreviewPath || previews.length > 0)
|
|
116
|
+
return;
|
|
117
|
+
const workspaceKey = workspaceId ?? "__default__";
|
|
118
|
+
if (autoPreviewedWorkspaceRef.current.has(workspaceKey))
|
|
119
|
+
return;
|
|
120
|
+
const rootFiles = (tree[""] ?? []).filter((entry) => entry.kind === "file");
|
|
121
|
+
if (rootFiles.length === 0)
|
|
122
|
+
return;
|
|
123
|
+
const initial = rootFiles.find((entry) => /^readme(?:\.[^.]+)?$/i.test(entry.name)) ??
|
|
124
|
+
rootFiles[0];
|
|
125
|
+
autoPreviewedWorkspaceRef.current.add(workspaceKey);
|
|
126
|
+
void openPreview(initial.path);
|
|
127
|
+
}, [
|
|
128
|
+
activePreviewPath,
|
|
129
|
+
blocked,
|
|
130
|
+
openPreview,
|
|
131
|
+
previews.length,
|
|
132
|
+
tree,
|
|
133
|
+
workspaceId,
|
|
134
|
+
]);
|
|
81
135
|
useEffect(() => {
|
|
82
136
|
if (!sessionId)
|
|
83
137
|
return;
|
|
@@ -95,7 +149,7 @@ export function useRepoBrowser(params) {
|
|
|
95
149
|
if (!paths.length)
|
|
96
150
|
return;
|
|
97
151
|
void Promise.all(paths.map(async (file) => {
|
|
98
|
-
const res = await fetch(`${origin}/api/operator/v1/chat/repo/file?path=${encodeURIComponent(file)}`, { credentials: "include" });
|
|
152
|
+
const res = await fetch(`${origin}/api/operator/v1/chat/repo/file?path=${encodeURIComponent(file)}${workspaceQuery}`, { credentials: "include" });
|
|
99
153
|
if (!res.ok)
|
|
100
154
|
return undefined;
|
|
101
155
|
return (await res.json()).data;
|
|
@@ -106,7 +160,7 @@ export function useRepoBrowser(params) {
|
|
|
106
160
|
setPreviews(available);
|
|
107
161
|
setActivePreviewPath(available.at(-1)?.path ?? null);
|
|
108
162
|
});
|
|
109
|
-
}, [origin, sessionId, refs]);
|
|
163
|
+
}, [origin, workspaceQuery, sessionId, refs]);
|
|
110
164
|
const toggleDir = useCallback((dir) => {
|
|
111
165
|
setExpandedDirs((current) => {
|
|
112
166
|
const next = new Set(current);
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/** Workspace layout modes resolved from measured Chat container width. */
|
|
2
2
|
/** Width thresholds in CSS px against `.oc-workspace` (not window/UA). */
|
|
3
3
|
export const WORKSPACE_LAYOUT_THRESHOLDS = {
|
|
4
|
-
comfortableMin:
|
|
5
|
-
compactMin:
|
|
4
|
+
comfortableMin: 1200,
|
|
5
|
+
compactMin: 720,
|
|
6
6
|
};
|
|
7
7
|
/**
|
|
8
8
|
* Resolve the three-band workspace mode from a measured container width.
|
|
9
|
-
* Boundaries: comfortable >=
|
|
9
|
+
* Boundaries: comfortable >=1200, compact 720-1199, narrow <720.
|
|
10
10
|
*/
|
|
11
11
|
export function resolveWorkspaceLayoutMode(widthPx) {
|
|
12
12
|
if (!Number.isFinite(widthPx) || widthPx < 0)
|