@tea-agent/loop-agent 0.36.4-beta.0 → 0.37.1-beta.0
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/AGENTS.md +2 -6
- package/CHANGELOG.md +72 -163
- package/README.md +1 -1
- package/bin/loop-agent.js +1 -37
- package/dist/application/task-lifecycle/advance.js +0 -1
- package/dist/application/task-lifecycle/observe.js +0 -15
- package/dist/application/task-lifecycle/plan-transitions.js +0 -15
- package/dist/cli/command-definitions.js +0 -7
- package/dist/cli/program.js +3 -8
- package/dist/commands/init-upgrade.js +19 -351
- package/dist/commands/init.js +67 -14
- package/dist/commands/run-dag-progress.js +0 -14
- package/dist/commands/task-advance.js +3 -33
- package/dist/executors/dag-pi-executor.js +30 -47
- package/dist/executors/pi-sdk-executor.js +0 -34
- package/dist/executors/shell-executor.js +95 -6
- package/dist/infrastructure/harness/atomic-write.js +1 -2
- package/dist/shared/operator/capabilities.js +13 -200
- package/dist/shared/package-metadata.js +0 -42
- package/dist/task/frontend-project-capability.js +12 -162
- package/dist/task/source-prepare/completeness.js +0 -17
- package/dist/task/source-prepare/parse-intent.js +1 -15
- package/dist/task/source-prepare/semantic-intake.js +23 -144
- package/dist/worker/console/chat/artifact-card.js +1 -8
- package/dist/worker/console/chat/chat-event-store.js +0 -61
- package/dist/worker/console/chat/human-gate-card.js +1 -9
- package/dist/worker/console/chat/operation-card.js +2 -71
- package/dist/worker/console/chat/pi-runtime.js +62 -86
- package/dist/worker/console/chat/routes.js +8 -36
- package/dist/worker/console/chat/session-store.js +0 -3
- package/dist/worker/console/chat/shortcuts.js +7 -9
- package/dist/worker/console/chat/turn-process.js +23 -97
- package/dist/worker/console/chat/workspace-landing.js +1 -2
- package/dist/worker/console/operation-runner.js +6 -155
- package/dist/worker/console/operator-actions.js +13 -373
- package/dist/worker/console/operator-user-error.js +0 -4
- package/dist/worker/console/recovery-cta.js +3 -50
- package/dist/worker/console/static/assets/index-HX1pbOyl.css +1 -0
- package/dist/worker/console/static/assets/index-M0BLEBfh.js +56 -0
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +9 -13
- package/dist/worker/console/static-src/app/useOperatorActions.js +2 -4
- package/dist/worker/console/static-src/app/useRecoveryActions.js +56 -65
- package/dist/worker/console/static-src/app/useRecoveryConsole.js +1 -73
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +30 -89
- package/dist/worker/console/static-src/operator-chat/refs.js +0 -3
- package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +1 -2
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +11 -30
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +6 -12
- package/dist/worker/console/static-src/operator-chat/useComposer.js +8 -45
- package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -7
- package/dist/worker/loop-agent/loop-agent-client.js +3 -17
- package/dist/worker/observability/read-model.js +0 -20
- package/dist/worker/observe/spec-evidence.js +8 -3
- package/dist/worker/observe/static/operator-chrome.d.ts +0 -1
- package/dist/worker/observe/static/operator-chrome.js +1 -6
- package/dist/worker/observe/static/views/dag-inspector.js +71 -6
- package/dist/worker/observe/static/views/dag.js +0 -12
- package/dist/worker/preflight.js +1 -2
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +707 -37
- package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
- package/dist/workflows/dag/backend-test-markdown-workflow.js +109 -7
- package/dist/workflows/dag/backend-test-module-stem.js +5 -0
- package/dist/workflows/dag/backend-test-pytest-collection.js +345 -24
- package/dist/workflows/dag/backend-test-scenario-param.js +890 -164
- package/dist/workflows/dag/backend-test-writer-completeness.js +47 -16
- package/dist/workflows/dag/dynamic-runtime/map.js +24 -8
- package/dist/workflows/dag/dynamic-runtime/shared.js +1 -9
- package/dist/workflows/dag/failure-routing.js +4 -9
- package/dist/workflows/dag/frontend-implementation-contract.js +39 -233
- package/dist/workflows/dag/frontend-prewrite-gate.js +62 -394
- package/dist/workflows/dag/frontend-repair.js +18 -219
- package/dist/workflows/dag/frontend-verification-trace.js +32 -47
- package/dist/workflows/dag/init-hybrid.js +86 -238
- package/dist/workflows/dag/lifecycle.js +0 -4
- package/dist/workflows/dag/node-execution.js +6 -105
- package/dist/workflows/dag/recovery-recommendation.js +0 -58
- package/dist/workflows/dag/report.js +0 -6
- package/dist/workflows/dag/retry-policy.js +0 -11
- package/dist/workflows/dag/runner.js +12 -314
- package/dist/workflows/dag/scheduler.js +3 -257
- package/dist/workflows/dag/types.js +9 -132
- package/dist/workflows/dag/validate.js +2 -7
- package/docs/README.md +3 -3
- package/docs/architecture/evolution.md +67 -102
- package/docs/templates/backend-test-dag.json +50 -32
- package/docs/templates/frontend-design-contract.md +14 -34
- package/docs/templates/frontend-task-constraints.md +13 -33
- package/docs/templates/frontend-task-requirement.md +20 -54
- package/docs/templates/init-managed-agents.md +2 -5
- package/harness.json +2 -2
- package/package.json +3 -4
- package/skills/loop-agent/SKILL.md +0 -1
- package/skills/loop-agent/references/command-reference.md +0 -3
- package/dist/build-stamp.json +0 -6
- package/dist/commands/dag-request-interrupt.js +0 -20
- package/dist/worker/console/chat/assistant-content.js +0 -121
- package/dist/worker/console/chat/semantic-activity.js +0 -471
- package/dist/worker/console/operation-run-facts.js +0 -190
- package/dist/worker/console/operation-wait.js +0 -355
- package/dist/worker/console/recovery-error-copy.js +0 -198
- package/dist/worker/console/static/assets/index-D83DYAFG.css +0 -1
- package/dist/worker/console/static/assets/index-IXm7oYjL.js +0 -59
- package/dist/worker/console/static-src/operator-chat/activity-journey.js +0 -125
- package/dist/worker/console/static-src/operator-chat/activity-rail-presentation.js +0 -73
- package/dist/worker/console/static-src/operator-chat/activity-references.js +0 -20
- package/dist/worker/console/static-src/operator-chat/slash-palette-layout.js +0 -24
- package/dist/worker/console/static-src/operator-chat/slash-palette-nav.js +0 -141
- package/dist/worker/console/static-src/operator-chat/useActivityRailTransition.js +0 -59
- package/dist/worker/observability/interrupt-eligibility.js +0 -264
- package/dist/workflows/dag/contract-output-registry.js +0 -14
- package/dist/workflows/dag/contract-validator-registrations.js +0 -8
- package/dist/workflows/dag/frontend-recovery-plan.js +0 -73
- package/dist/workflows/dag/frontend-recovery-root-manifest.js +0 -123
- package/dist/workflows/dag/frontend-recovery-run.js +0 -539
- package/dist/workflows/dag/frontend-writer-recovery.js +0 -106
- package/dist/workflows/dag/frontend-writer-rollback.js +0 -821
- package/dist/workflows/dag/interrupt-request.js +0 -559
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Mutual-exclusion state for large spatial overlays in Operator Chat.
|
|
3
|
-
* At most one of: sidebar drawer, process, runtime context, tools
|
|
3
|
+
* At most one of: sidebar drawer, process, runtime context, tools.
|
|
4
4
|
* Small composer menus (model/thinking) are out of scope.
|
|
5
5
|
*/
|
|
6
6
|
export function createSpatialOverlayState(active = "none") {
|
|
@@ -33,6 +33,5 @@ export function spatialOverlayFlags(state) {
|
|
|
33
33
|
processOpen: state.active === "process",
|
|
34
34
|
runtimeContextOpen: state.active === "runtime-context",
|
|
35
35
|
toolsOpen: state.active === "tools",
|
|
36
|
-
activityOpen: state.active === "activity",
|
|
37
36
|
};
|
|
38
37
|
}
|
|
@@ -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";
|
|
@@ -90,7 +89,6 @@ export function useChatSessions(params) {
|
|
|
90
89
|
refs.lastEventIdRef.current = null;
|
|
91
90
|
refs.seenEventIdsRef.current = new Set();
|
|
92
91
|
refs.turnAssistantIdsRef.current = new Map();
|
|
93
|
-
refs.sealedAssistantIdsRef.current = new Set();
|
|
94
92
|
refs.settledTurnIdsRef.current = new Set();
|
|
95
93
|
refs.activeTurnIdRef.current = null;
|
|
96
94
|
resetForNewSession();
|
|
@@ -146,6 +144,8 @@ export function useChatSessions(params) {
|
|
|
146
144
|
}
|
|
147
145
|
}, [origin, refs, resetActiveProjection, setSession]);
|
|
148
146
|
const deleteSession = useCallback(async (sessionId) => {
|
|
147
|
+
if (!window.confirm("确定删除该会话?删除后不可恢复。"))
|
|
148
|
+
return;
|
|
149
149
|
try {
|
|
150
150
|
const res = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(sessionId)}`, {
|
|
151
151
|
method: "DELETE",
|
|
@@ -156,7 +156,7 @@ export function useChatSessions(params) {
|
|
|
156
156
|
});
|
|
157
157
|
if (!res.ok) {
|
|
158
158
|
setError("删除会话失败");
|
|
159
|
-
return
|
|
159
|
+
return;
|
|
160
160
|
}
|
|
161
161
|
titleWatcherRef.current?.cancel(sessionId);
|
|
162
162
|
if (refs.sessionRef.current?.sessionId === sessionId) {
|
|
@@ -167,11 +167,9 @@ export function useChatSessions(params) {
|
|
|
167
167
|
window.localStorage.removeItem(ACTIVE_CHAT_SESSION_STORAGE_KEY);
|
|
168
168
|
}
|
|
169
169
|
await refreshSessionList();
|
|
170
|
-
return true;
|
|
171
170
|
}
|
|
172
171
|
catch (error) {
|
|
173
172
|
setError(error instanceof Error ? error.message : String(error));
|
|
174
|
-
return false;
|
|
175
173
|
}
|
|
176
174
|
}, [
|
|
177
175
|
origin,
|
|
@@ -200,7 +198,6 @@ export function useChatSessions(params) {
|
|
|
200
198
|
refs.activeTurnIdRef.current = body.session.activeTurnId ?? null;
|
|
201
199
|
refs.seenEventIdsRef.current = new Set();
|
|
202
200
|
refs.turnAssistantIdsRef.current = new Map();
|
|
203
|
-
refs.sealedAssistantIdsRef.current = new Set();
|
|
204
201
|
refs.settledTurnIdsRef.current = new Set();
|
|
205
202
|
refs.lastEventIdRef.current = null;
|
|
206
203
|
setSession(next);
|
|
@@ -298,23 +295,18 @@ export function useChatSessions(params) {
|
|
|
298
295
|
refs.activeTurnIdRef.current = body.session.activeTurnId ?? null;
|
|
299
296
|
refs.seenEventIdsRef.current = new Set();
|
|
300
297
|
refs.turnAssistantIdsRef.current = new Map();
|
|
301
|
-
refs.sealedAssistantIdsRef.current = new Set();
|
|
302
298
|
refs.settledTurnIdsRef.current = new Set();
|
|
303
299
|
refs.lastEventIdRef.current = null;
|
|
304
300
|
setSession(restored);
|
|
305
301
|
setInput(body.session.composerDraft ?? "");
|
|
306
302
|
setMessages((body.session.messages ?? [])
|
|
307
303
|
.filter((message) => message.role === "user" || message.role === "assistant")
|
|
308
|
-
.map((message) => {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
createdAt: Date.parse(message.at) || Date.now(),
|
|
315
|
-
...(content && content.length > 0 ? { content } : {}),
|
|
316
|
-
};
|
|
317
|
-
}));
|
|
304
|
+
.map((message) => ({
|
|
305
|
+
id: message.id,
|
|
306
|
+
role: message.role,
|
|
307
|
+
text: message.text,
|
|
308
|
+
createdAt: Date.parse(message.at) || Date.now(),
|
|
309
|
+
})));
|
|
318
310
|
}
|
|
319
311
|
catch {
|
|
320
312
|
// Recovery is best-effort; a new session remains available.
|
|
@@ -372,26 +364,15 @@ export function useChatSessions(params) {
|
|
|
372
364
|
createSessionAndPrimeTitleWatcherForHook,
|
|
373
365
|
]);
|
|
374
366
|
// Phase-1: fork / in-session branch / mainline switch UI deferred; API routes remain.
|
|
375
|
-
|
|
376
|
-
const compactSession = useCallback(async (instructions) => {
|
|
367
|
+
const compactSession = useCallback(async () => {
|
|
377
368
|
const current = refs.sessionRef.current;
|
|
378
369
|
if (!current || refs.streamingRef.current)
|
|
379
370
|
return;
|
|
380
|
-
const trimmed = typeof instructions === "string" ? instructions.trim() : "";
|
|
381
371
|
try {
|
|
382
|
-
const headers = {
|
|
383
|
-
"x-loop-console-confirmation": confirmationToken(),
|
|
384
|
-
};
|
|
385
|
-
if (trimmed) {
|
|
386
|
-
headers["content-type"] = "application/json";
|
|
387
|
-
}
|
|
388
372
|
const response = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(current.sessionId)}/compact`, {
|
|
389
373
|
method: "POST",
|
|
390
374
|
credentials: "include",
|
|
391
|
-
headers,
|
|
392
|
-
...(trimmed
|
|
393
|
-
? { body: JSON.stringify({ instructions: trimmed }) }
|
|
394
|
-
: {}),
|
|
375
|
+
headers: { "x-loop-console-confirmation": confirmationToken() },
|
|
395
376
|
});
|
|
396
377
|
const body = (await response.json().catch(() => ({})));
|
|
397
378
|
if (!response.ok) {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
|
-
import { parseAssistantContent } from "../../chat/assistant-content.js";
|
|
3
2
|
import { buildTurnProcessGroups } from "../../chat/turn-process.js";
|
|
4
3
|
import { createChatSseEventApplier, } from "./chat-sse-events.js";
|
|
5
4
|
import { taskContextUnchanged } from "./format.js";
|
|
@@ -219,19 +218,14 @@ export function useChatThread(params) {
|
|
|
219
218
|
refs.lastEventIdRef.current = body.lastEventId ?? null;
|
|
220
219
|
refs.seenEventIdsRef.current = new Set();
|
|
221
220
|
refs.turnAssistantIdsRef.current = new Map();
|
|
222
|
-
refs.sealedAssistantIdsRef.current = new Set();
|
|
223
221
|
setMessages((body.session.messages ?? [])
|
|
224
222
|
.filter((message) => message.role === "user" || message.role === "assistant")
|
|
225
|
-
.map((message) => {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
createdAt: Date.parse(message.at) || Date.now(),
|
|
232
|
-
...(content && content.length > 0 ? { content } : {}),
|
|
233
|
-
};
|
|
234
|
-
}));
|
|
223
|
+
.map((message) => ({
|
|
224
|
+
id: message.id,
|
|
225
|
+
role: message.role,
|
|
226
|
+
text: message.text,
|
|
227
|
+
createdAt: Date.parse(message.at) || Date.now(),
|
|
228
|
+
})));
|
|
235
229
|
setStreamingAssistantId(null);
|
|
236
230
|
return true;
|
|
237
231
|
}
|
|
@@ -1,52 +1,27 @@
|
|
|
1
1
|
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from "react";
|
|
2
2
|
import { filterSlashCommands, mergeSlashCommands, } from "../../chat/shortcuts.js";
|
|
3
3
|
import { confirmationToken } from "./format.js";
|
|
4
|
-
import { clampShortcutIndex } from "./slash-palette-nav.js";
|
|
5
4
|
import { attachRuntimeSnapshotController } from "./useRuntimeSnapshot.js";
|
|
6
5
|
/** Composer state: input text, @file palette, image attachments, /shortcuts,
|
|
7
6
|
* IME composition guards, auto-grow and server-side draft sync. */
|
|
8
7
|
export function useComposer(params) {
|
|
9
8
|
const { origin, sessionId, refs, setError } = params;
|
|
10
|
-
const [input,
|
|
9
|
+
const [input, setInput] = useState("");
|
|
11
10
|
const [fileOptions, setFileOptions] = useState([]);
|
|
12
11
|
const [pendingImages, setPendingImages] = useState([]);
|
|
13
12
|
const [shortcutOpen, setShortcutOpen] = useState(false);
|
|
14
|
-
/** Any composer text that starts with `/` should reveal the palette,
|
|
15
|
-
* including draft restore. Escape can still close it without clearing. */
|
|
16
|
-
const setInput = useCallback((value) => {
|
|
17
|
-
if (typeof value === "function") {
|
|
18
|
-
setInputState((prev) => {
|
|
19
|
-
const next = value(prev);
|
|
20
|
-
setShortcutOpen(next.startsWith("/"));
|
|
21
|
-
return next;
|
|
22
|
-
});
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
setInputState(value);
|
|
26
|
-
setShortcutOpen(value.startsWith("/"));
|
|
27
|
-
}, []);
|
|
28
13
|
const [shortcutIndex, setShortcutIndex] = useState(0);
|
|
29
14
|
const [dynamicCommands, setDynamicCommands] = useState([]);
|
|
30
15
|
const [dynamicCommandsLoading, setDynamicCommandsLoading] = useState(false);
|
|
31
16
|
const [dynamicCommandsError, setDynamicCommandsError] = useState(null);
|
|
32
17
|
const [commandsEpoch, setCommandsEpoch] = useState(0);
|
|
33
18
|
const textareaRef = useRef(null);
|
|
34
|
-
|
|
35
|
-
const commandsSessionRef = useRef(sessionId);
|
|
36
|
-
const commandsGenerationRef = useRef(0);
|
|
19
|
+
const commandsSessionRef = useRef(undefined);
|
|
37
20
|
const handleInputChange = useCallback((value) => {
|
|
38
21
|
setInput(value);
|
|
39
22
|
setShortcutOpen(value.startsWith("/"));
|
|
40
23
|
setShortcutIndex(0);
|
|
41
24
|
}, []);
|
|
42
|
-
// Immediate invalidation on A→B even when B palette never opens (AC-2).
|
|
43
|
-
useLayoutEffect(() => {
|
|
44
|
-
commandsSessionRef.current = sessionId;
|
|
45
|
-
commandsGenerationRef.current += 1;
|
|
46
|
-
setDynamicCommands([]);
|
|
47
|
-
setDynamicCommandsError(null);
|
|
48
|
-
setDynamicCommandsLoading(false);
|
|
49
|
-
}, [sessionId]);
|
|
50
25
|
const loadDynamicCommands = useCallback(async () => {
|
|
51
26
|
if (!sessionId) {
|
|
52
27
|
setDynamicCommands([]);
|
|
@@ -55,15 +30,12 @@ export function useComposer(params) {
|
|
|
55
30
|
return;
|
|
56
31
|
}
|
|
57
32
|
const target = sessionId;
|
|
58
|
-
const generation = commandsGenerationRef.current;
|
|
59
33
|
commandsSessionRef.current = target;
|
|
60
34
|
setDynamicCommandsLoading(true);
|
|
61
35
|
setDynamicCommandsError(null);
|
|
62
|
-
const isCurrent = () => commandsSessionRef.current === target &&
|
|
63
|
-
commandsGenerationRef.current === generation;
|
|
64
36
|
try {
|
|
65
37
|
const res = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(target)}/commands`, { credentials: "include" });
|
|
66
|
-
if (
|
|
38
|
+
if (commandsSessionRef.current !== target)
|
|
67
39
|
return;
|
|
68
40
|
if (!res.ok) {
|
|
69
41
|
setDynamicCommandsError("动态命令加载失败");
|
|
@@ -89,27 +61,26 @@ export function useComposer(params) {
|
|
|
89
61
|
source,
|
|
90
62
|
});
|
|
91
63
|
}
|
|
92
|
-
if (
|
|
64
|
+
if (commandsSessionRef.current !== target)
|
|
93
65
|
return;
|
|
94
66
|
setDynamicCommands(items);
|
|
95
|
-
// Compact server fail-soft / partial-source warning; local Operator/Web still merge.
|
|
96
|
-
const warning = typeof body?.warning === "string" ? body.warning.trim() : "";
|
|
97
|
-
setDynamicCommandsError(warning ? warning.slice(0, 160) : null);
|
|
98
67
|
}
|
|
99
68
|
catch {
|
|
100
|
-
if (
|
|
69
|
+
if (commandsSessionRef.current !== target)
|
|
101
70
|
return;
|
|
102
71
|
setDynamicCommandsError("动态命令加载失败");
|
|
103
72
|
setDynamicCommands([]);
|
|
104
73
|
}
|
|
105
74
|
finally {
|
|
106
|
-
if (
|
|
75
|
+
if (commandsSessionRef.current === target) {
|
|
107
76
|
setDynamicCommandsLoading(false);
|
|
108
77
|
}
|
|
109
78
|
}
|
|
110
79
|
}, [origin, sessionId]);
|
|
111
80
|
// Lazy-load dynamic commands when slash palette opens or session changes.
|
|
112
81
|
useEffect(() => {
|
|
82
|
+
setDynamicCommands([]);
|
|
83
|
+
setDynamicCommandsError(null);
|
|
113
84
|
if (!sessionId)
|
|
114
85
|
return;
|
|
115
86
|
if (!shortcutOpen && commandsEpoch === 0)
|
|
@@ -201,14 +172,6 @@ export function useComposer(params) {
|
|
|
201
172
|
const filteredShortcuts = useMemo(() => input.startsWith("/")
|
|
202
173
|
? filterSlashCommands(mergedCommands, input)
|
|
203
174
|
: [], [input, mergedCommands]);
|
|
204
|
-
// Keep the active option index inside the current candidate range (UI-12
|
|
205
|
-
// AC-011): dynamic loads completing/failing empty, retries, session
|
|
206
|
-
// switches, late responses and search shrink all funnel through
|
|
207
|
-
// filteredShortcuts.length. The clamped index always matches a rendered
|
|
208
|
-
// `oc-shortcut-option-${globalIndex}` id (same source of truth).
|
|
209
|
-
useEffect(() => {
|
|
210
|
-
setShortcutIndex((index) => clampShortcutIndex(index, filteredShortcuts.length));
|
|
211
|
-
}, [filteredShortcuts.length]);
|
|
212
175
|
const onCompositionStart = useCallback(() => {
|
|
213
176
|
refs.isComposingRef.current = true;
|
|
214
177
|
}, [refs]);
|
|
@@ -17,13 +17,9 @@ export function resolveWorkspaceLayoutMode(widthPx) {
|
|
|
17
17
|
return "compact";
|
|
18
18
|
return "narrow";
|
|
19
19
|
}
|
|
20
|
-
/**
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
* invalidates Activity Rail's fixed ChatMain anchor.
|
|
24
|
-
*/
|
|
25
|
-
export function processUsesOverlay(_mode) {
|
|
26
|
-
return true;
|
|
20
|
+
/** Whether process should render as a right overlay (no main reflow). */
|
|
21
|
+
export function processUsesOverlay(mode) {
|
|
22
|
+
return mode === "compact" || mode === "narrow";
|
|
27
23
|
}
|
|
28
24
|
/** Whether the permanent full sidebar is allowed. */
|
|
29
25
|
export function fullSidebarAllowed(mode) {
|
|
@@ -2,15 +2,11 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { appendFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { computeBinarySha256, computePackageFingerprint, findPackageRoot, isJavaScriptScript,
|
|
5
|
+
import { computeBinarySha256, computePackageFingerprint, findPackageRoot, isJavaScriptScript, readPackageName, readPackageVersion, resolveBinPaths, resolveCmdShim, } from "../../shared/package-metadata.js";
|
|
6
6
|
import { parseCommandJson } from "./parse-json.js";
|
|
7
7
|
export const DEFAULT_WORKER_COMMAND_TIMEOUT_MS = 120_000;
|
|
8
8
|
export const DEFAULT_HEARTBEAT_INTERVAL_MS = 15_000;
|
|
9
9
|
export const LOOP_AGENT_PACKAGE_NAME = "@tea-agent/loop-agent";
|
|
10
|
-
/** Grace window between the SIGTERM and SIGKILL stages of a hard timeout, so a
|
|
11
|
-
* runner can persist terminal state (see executeDagCheckpoint) before being
|
|
12
|
-
* force-killed. */
|
|
13
|
-
export const HARD_TIMEOUT_KILL_GRACE_MS = 10_000;
|
|
14
10
|
const MAX_BUFFERED_OUTPUT_BYTES = 64 * 1024;
|
|
15
11
|
/**
|
|
16
12
|
* Bind evidence supplied by an orchestration layer to the identity exposed by
|
|
@@ -182,11 +178,10 @@ export class LoopAgentClient {
|
|
|
182
178
|
if (!this._identity) {
|
|
183
179
|
throw new Error("controller identity: cannot record a reported version without a pinned identity");
|
|
184
180
|
}
|
|
185
|
-
|
|
186
|
-
if (normalized !== this._identity.packageVersion) {
|
|
181
|
+
if (reportedVersion !== this._identity.packageVersion) {
|
|
187
182
|
throw new Error(`controller identity: reported version ${reportedVersion} does not match package version ${this._identity.packageVersion}`);
|
|
188
183
|
}
|
|
189
|
-
this._identity = { ...this._identity, reportedVersion
|
|
184
|
+
this._identity = { ...this._identity, reportedVersion };
|
|
190
185
|
return this._identity;
|
|
191
186
|
}
|
|
192
187
|
async run(args, options) {
|
|
@@ -386,17 +381,10 @@ function spawnCommand(input) {
|
|
|
386
381
|
});
|
|
387
382
|
};
|
|
388
383
|
let hardTimeout;
|
|
389
|
-
let killGraceTimeout;
|
|
390
384
|
if (input.hardTimeoutMs !== undefined && input.hardTimeoutMs > 0) {
|
|
391
385
|
hardTimeout = setTimeout(() => {
|
|
392
386
|
timedOut = true;
|
|
393
|
-
// Two-stage termination: SIGTERM first so the runner can persist a
|
|
394
|
-
// terminal state (see executeDagCheckpoint signal handler), then
|
|
395
|
-
// SIGKILL only after the grace window expires.
|
|
396
387
|
child.kill("SIGTERM");
|
|
397
|
-
killGraceTimeout = setTimeout(() => {
|
|
398
|
-
child.kill("SIGKILL");
|
|
399
|
-
}, HARD_TIMEOUT_KILL_GRACE_MS);
|
|
400
388
|
}, input.hardTimeoutMs);
|
|
401
389
|
}
|
|
402
390
|
let heartbeatTimer;
|
|
@@ -406,8 +394,6 @@ function spawnCommand(input) {
|
|
|
406
394
|
const clearTimers = () => {
|
|
407
395
|
if (hardTimeout !== undefined)
|
|
408
396
|
clearTimeout(hardTimeout);
|
|
409
|
-
if (killGraceTimeout !== undefined)
|
|
410
|
-
clearTimeout(killGraceTimeout);
|
|
411
397
|
if (heartbeatTimer !== undefined) {
|
|
412
398
|
clearInterval(heartbeatTimer);
|
|
413
399
|
heartbeatTimer = undefined;
|
|
@@ -1491,7 +1491,6 @@ function mergeDagRun(existing, incoming) {
|
|
|
1491
1491
|
failureCategory: incoming.failureCategory ?? existing.failureCategory,
|
|
1492
1492
|
stateConsistent: incoming.stateConsistent ?? existing.stateConsistent,
|
|
1493
1493
|
recoveryEligibility: incoming.recoveryEligibility ?? existing.recoveryEligibility,
|
|
1494
|
-
frontendRecovery: incoming.frontendRecovery ?? existing.frontendRecovery,
|
|
1495
1494
|
title: incoming.title ?? existing.title,
|
|
1496
1495
|
startedAt,
|
|
1497
1496
|
finishedAt,
|
|
@@ -1791,23 +1790,6 @@ async function loadFrontendLintProjection(runDir) {
|
|
|
1791
1790
|
blockingReasons: parsed.data.blockingReasons,
|
|
1792
1791
|
};
|
|
1793
1792
|
}
|
|
1794
|
-
function projectFrontendRecovery(state) {
|
|
1795
|
-
const s = state.frontendRecoveryState;
|
|
1796
|
-
const r = state.frontendRecoveryResult;
|
|
1797
|
-
if (!s && !r)
|
|
1798
|
-
return undefined;
|
|
1799
|
-
return {
|
|
1800
|
-
...(s?.recoveryRootRunId ? { recoveryRootRunId: s.recoveryRootRunId } : {}),
|
|
1801
|
-
...(s?.parentRunId ? { parentRunId: s.parentRunId } : {}),
|
|
1802
|
-
...(s?.childRunId ? { childRunId: s.childRunId } : {}),
|
|
1803
|
-
...(s?.attemptIndex !== undefined ? { attemptIndex: s.attemptIndex } : {}),
|
|
1804
|
-
...(s?.continuationCount !== undefined
|
|
1805
|
-
? { continuationCount: s.continuationCount }
|
|
1806
|
-
: {}),
|
|
1807
|
-
...(s?.phase ? { phase: s.phase } : {}),
|
|
1808
|
-
...(r?.outcome ? { outcome: r.outcome } : {}),
|
|
1809
|
-
};
|
|
1810
|
-
}
|
|
1811
1793
|
async function parseDagStateFile(statePath, now) {
|
|
1812
1794
|
try {
|
|
1813
1795
|
if (!existsSync(statePath))
|
|
@@ -1878,7 +1860,6 @@ async function parseDagStateFile(statePath, now) {
|
|
|
1878
1860
|
loadBackendTestProjection(runDir, nodes),
|
|
1879
1861
|
loadFrontendLintProjection(runDir),
|
|
1880
1862
|
]);
|
|
1881
|
-
const frontendRecovery = projectFrontendRecovery(state);
|
|
1882
1863
|
const continuationRecord = state.continuation &&
|
|
1883
1864
|
typeof state.continuation.parentRunId === "string" &&
|
|
1884
1865
|
typeof state.continuation.effectiveFromNodeId === "string" &&
|
|
@@ -1937,7 +1918,6 @@ async function parseDagStateFile(statePath, now) {
|
|
|
1937
1918
|
...(continuation ? { continuation } : {}),
|
|
1938
1919
|
...(backendTest ? { backendTest } : {}),
|
|
1939
1920
|
...(frontendLint ? { frontendLint } : {}),
|
|
1940
|
-
...(frontendRecovery ? { frontendRecovery } : {}),
|
|
1941
1921
|
};
|
|
1942
1922
|
}
|
|
1943
1923
|
catch {
|
|
@@ -359,11 +359,16 @@ export async function extractSpecEvidence(repoRoot, dagRunId, nodeId) {
|
|
|
359
359
|
if (status === "no-evidence") {
|
|
360
360
|
summaryLines.push("未观察到任何规范证据:无 skill 注入、无文件读取、无检索操作。");
|
|
361
361
|
}
|
|
362
|
-
//
|
|
363
|
-
//
|
|
364
|
-
// separately from the generic spec reads/searches.
|
|
362
|
+
// Separate openspec spec directory reads and searches for Dashboard display.
|
|
363
|
+
// Knowledge base and openspec are parallel sources.
|
|
365
364
|
const openspecReads = specReads.filter((r) => isOpenspecPath(r.path));
|
|
366
365
|
const openspecSearches = searches.filter((s) => isOpenspecSearch(s.query, s.path));
|
|
366
|
+
if (openspecReads.length > 0) {
|
|
367
|
+
summaryLines.push(`openspec 已读取 ${openspecReads.length} 个文件:${openspecReads.map((r) => r.path).join("、")}`);
|
|
368
|
+
}
|
|
369
|
+
if (openspecSearches.length > 0) {
|
|
370
|
+
summaryLines.push(`openspec 检索 ${openspecSearches.length} 次。`);
|
|
371
|
+
}
|
|
367
372
|
return {
|
|
368
373
|
dagRunId,
|
|
369
374
|
nodeId,
|
|
@@ -49,7 +49,6 @@ export function parseInspectObjectView(
|
|
|
49
49
|
export function activeInspectTabHref(hashValue: string): string | null;
|
|
50
50
|
export function parseInspectContext(hashValue: string): InspectContext | null;
|
|
51
51
|
export function resolveOperateHrefFromInspectHash(hashValue: string): string;
|
|
52
|
-
export function buildOperateInterruptHref(dagRunId: string): string;
|
|
53
52
|
export function buildInspectDagHref(
|
|
54
53
|
dagRunId: string,
|
|
55
54
|
options?: { from?: string },
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Operator Chrome — 统一壳(Operate React 与 Inspect vanilla 共用唯一实现)。
|
|
3
3
|
*
|
|
4
|
-
* 设计真源:docs/design/
|
|
4
|
+
* 设计真源:docs/design/active/2026-08-09-unified-operator-chrome.md
|
|
5
5
|
* - 一条 chrome 两条带:Band1 全局(品牌/仓库身份/模式分段控件/对象上下文/状态格),
|
|
6
6
|
* Band2 工作区(下划线 tab,随模式切换内容)。
|
|
7
7
|
* - 模式语义:操作=做,观测=看同一对象;模式切换在持有 DAG 选中时不丢对象。
|
|
@@ -118,11 +118,6 @@ export function resolveOperateHrefFromInspectHash(hashValue) {
|
|
|
118
118
|
return "/#/";
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
/** Inspect DAG 详情 → Operate Recovery 协作中止深链(只改导航,不发 mutation)。 */
|
|
122
|
-
export function buildOperateInterruptHref(dagRunId) {
|
|
123
|
-
return `/#/recovery?dagRunId=${encodeURIComponent(dagRunId)}&intent=interrupt`;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
121
|
/** 操作侧 → Inspect 的 canonical 深链(可携带 from 以激活上下文 chip)。 */
|
|
127
122
|
export function buildInspectDagHref(dagRunId, options = {}) {
|
|
128
123
|
const base = `/inspect/#/dag/${encodeURIComponent(dagRunId)}`;
|
|
@@ -360,9 +360,15 @@ async function renderSpecEvidence(content, dagRunId, nodeId, cachedEvidence) {
|
|
|
360
360
|
evidence.sourceBinding?.requirementIds,
|
|
361
361
|
(id) => el("li", null, id),
|
|
362
362
|
);
|
|
363
|
+
const openspecReadPaths = new Set(
|
|
364
|
+
(evidence.openspecReads ?? []).map((entry) => entry.path),
|
|
365
|
+
);
|
|
366
|
+
const otherSpecReads = (evidence.specReads ?? []).filter(
|
|
367
|
+
(entry) => !openspecReadPaths.has(entry.path),
|
|
368
|
+
);
|
|
363
369
|
appendListSection(
|
|
364
|
-
|
|
365
|
-
|
|
370
|
+
`其他已读取规范文件(${otherSpecReads.length})`,
|
|
371
|
+
otherSpecReads,
|
|
366
372
|
(read) => {
|
|
367
373
|
const item = el("li", null);
|
|
368
374
|
const button = document.createElement("button");
|
|
@@ -374,7 +380,7 @@ async function renderSpecEvidence(content, dagRunId, nodeId, cachedEvidence) {
|
|
|
374
380
|
button.setAttribute("data-read-at", read.timestamp);
|
|
375
381
|
}
|
|
376
382
|
button.id = `spec-evidence-btn-read-${read.path}`;
|
|
377
|
-
const icon = el("i", "ri-
|
|
383
|
+
const icon = el("i", "ri-file-text-line");
|
|
378
384
|
icon.setAttribute("aria-hidden", "true");
|
|
379
385
|
button.append(icon, el("code", null, read.path));
|
|
380
386
|
if (read.timestamp) {
|
|
@@ -399,10 +405,9 @@ async function renderSpecEvidence(content, dagRunId, nodeId, cachedEvidence) {
|
|
|
399
405
|
item.appendChild(button);
|
|
400
406
|
return item;
|
|
401
407
|
},
|
|
402
|
-
"unavailable:未观察到规范读取。",
|
|
403
408
|
);
|
|
404
409
|
appendListSection(
|
|
405
|
-
|
|
410
|
+
`检索操作(${evidence.specSearches?.length ?? 0})`,
|
|
406
411
|
evidence.specSearches,
|
|
407
412
|
(search) => {
|
|
408
413
|
const item = el("li", null);
|
|
@@ -414,7 +419,6 @@ async function renderSpecEvidence(content, dagRunId, nodeId, cachedEvidence) {
|
|
|
414
419
|
);
|
|
415
420
|
return item;
|
|
416
421
|
},
|
|
417
|
-
"unavailable:未观察到规范检索。",
|
|
418
422
|
);
|
|
419
423
|
appendListSection(
|
|
420
424
|
`知识库查询(${evidence.knowledgeBaseQueries?.length ?? 0})`,
|
|
@@ -429,6 +433,67 @@ async function renderSpecEvidence(content, dagRunId, nodeId, cachedEvidence) {
|
|
|
429
433
|
"unavailable:未观察到知识库查询;connector 可能未配置或未调用。",
|
|
430
434
|
);
|
|
431
435
|
|
|
436
|
+
// openspec reads are displayed independently from skill/docs reads.
|
|
437
|
+
appendListSection(
|
|
438
|
+
`openspec 已读取(${evidence.openspecReads?.length ?? 0})`,
|
|
439
|
+
evidence.openspecReads,
|
|
440
|
+
(read) => {
|
|
441
|
+
const item = el("li", null);
|
|
442
|
+
const button = document.createElement("button");
|
|
443
|
+
button.type = "button";
|
|
444
|
+
button.className = "spec-evidence-file-btn";
|
|
445
|
+
button.setAttribute("data-source", "read");
|
|
446
|
+
button.setAttribute("data-path", read.path);
|
|
447
|
+
if (read.timestamp) {
|
|
448
|
+
button.setAttribute("data-read-at", read.timestamp);
|
|
449
|
+
}
|
|
450
|
+
button.id = `spec-evidence-btn-read-${read.path}`;
|
|
451
|
+
const icon = el("i", "ri-book-open-line");
|
|
452
|
+
icon.setAttribute("aria-hidden", "true");
|
|
453
|
+
button.append(icon, el("code", null, read.path));
|
|
454
|
+
if (read.timestamp) {
|
|
455
|
+
button.appendChild(
|
|
456
|
+
el(
|
|
457
|
+
"span",
|
|
458
|
+
"spec-evidence-time",
|
|
459
|
+
formatSessionEventTime({ timestamp: read.timestamp }),
|
|
460
|
+
),
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
button.addEventListener("click", () =>
|
|
464
|
+
onSpecEvidenceFileClick(
|
|
465
|
+
content,
|
|
466
|
+
dagRunId,
|
|
467
|
+
nodeId,
|
|
468
|
+
"read",
|
|
469
|
+
read.path,
|
|
470
|
+
button,
|
|
471
|
+
),
|
|
472
|
+
);
|
|
473
|
+
item.appendChild(button);
|
|
474
|
+
return item;
|
|
475
|
+
},
|
|
476
|
+
evidence.openspecSearches?.length
|
|
477
|
+
? "已检索但未观察到 openspec 成功读取;若生成期存在候选,预写门禁会阻断 writer。"
|
|
478
|
+
: "unavailable:未观察到 openspec 成功读取。",
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
appendListSection(
|
|
482
|
+
`openspec 检索(${evidence.openspecSearches?.length ?? 0})`,
|
|
483
|
+
evidence.openspecSearches,
|
|
484
|
+
(search) => {
|
|
485
|
+
const item = el("li", null);
|
|
486
|
+
const icon = el("i", "ri-search-line");
|
|
487
|
+
icon.setAttribute("aria-hidden", "true");
|
|
488
|
+
item.append(
|
|
489
|
+
icon,
|
|
490
|
+
document.createTextNode(` ${search.tool}: ${search.query}`),
|
|
491
|
+
);
|
|
492
|
+
return item;
|
|
493
|
+
},
|
|
494
|
+
"unavailable:未观察到 openspec 检索。",
|
|
495
|
+
);
|
|
496
|
+
|
|
432
497
|
if (
|
|
433
498
|
evidence.status === "source-bound" ||
|
|
434
499
|
evidence.status === "spec-injected" ||
|
|
@@ -355,18 +355,6 @@ export async function renderDagDetail(dagRunId, initial = true) {
|
|
|
355
355
|
primaryEntries.push(["状态一致性", "不一致,需要处理"]);
|
|
356
356
|
if (visibility.showResume) primaryEntries.push(["允许恢复", "是"]);
|
|
357
357
|
if (visibility.showReconcile) primaryEntries.push(["允许收口", "是"]);
|
|
358
|
-
const liveStatus = String(dag.effectiveStatus ?? dag.status ?? "").toLowerCase();
|
|
359
|
-
if (liveStatus === "running" || liveStatus === "active") {
|
|
360
|
-
const interruptLink = el(
|
|
361
|
-
"a",
|
|
362
|
-
"page-action-btn",
|
|
363
|
-
"在操作台中止此运行 →",
|
|
364
|
-
);
|
|
365
|
-
interruptLink.href = `/#/recovery?dagRunId=${encodeURIComponent(dag.dagRunId)}&intent=interrupt`;
|
|
366
|
-
interruptLink.setAttribute("data-testid", "inspect-dag-interrupt-link");
|
|
367
|
-
interruptLink.title = "只跳转到操作台 Recovery,不会在观测页执行中止";
|
|
368
|
-
primaryEntries.push(["协作中止", interruptLink]);
|
|
369
|
-
}
|
|
370
358
|
primaryEntries.push(
|
|
371
359
|
["开始时间", formatTs(dag.startedAt)],
|
|
372
360
|
["结束时间", formatTs(dag.finishedAt)],
|
package/dist/worker/preflight.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { access } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { normalizeCliVersionLabel } from "../shared/package-metadata.js";
|
|
4
3
|
import { controllerIdentityExpectationFailure, resolveControllerIdentity, } from "./loop-agent/loop-agent-client.js";
|
|
5
4
|
export const DEFAULT_CHECK_REPO_TIMEOUT_MS = 300_000;
|
|
6
5
|
export async function preflightTargetRepo(input) {
|
|
@@ -48,7 +47,7 @@ export async function preflightTargetRepo(input) {
|
|
|
48
47
|
controllerIdentity: identity,
|
|
49
48
|
};
|
|
50
49
|
}
|
|
51
|
-
const reportedVersion =
|
|
50
|
+
const reportedVersion = version.stdout.trim();
|
|
52
51
|
let controllerIdentity = identity
|
|
53
52
|
? { ...identity, reportedVersion }
|
|
54
53
|
: undefined;
|