@sanqianx/claude-ai-workbench 0.13.2 → 0.13.4
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/LICENSE +201 -201
- package/README.md +156 -156
- package/apps/agent-terminal/app.css +652 -613
- package/apps/agent-terminal/app.js +143 -67
- package/apps/agent-terminal/conversation-composer.js +44 -0
- package/apps/agent-terminal/index.html +172 -170
- package/apps/agent-terminal/panel-host-proof.html +152 -152
- package/apps/agent-terminal/panel-proof-content.html +41 -41
- package/apps/agent-terminal/right-workspace-dock.css +189 -159
- package/apps/agent-terminal/right-workspace-dock.js +652 -521
- package/apps/agent-terminal/user-question-card.js +68 -0
- package/apps/ai-profile-settings-host/index.html +25 -25
- package/apps/ai-profile-settings-host/page.css +1 -1
- package/apps/ai-profile-settings-host/page.js +113 -113
- package/apps/minimal-test-host/README.md +15 -15
- package/apps/minimal-test-host/host.css +2 -2
- package/apps/minimal-test-host/host.js +35 -35
- package/apps/minimal-test-host/index.html +58 -58
- package/apps/minimal-test-host/optional-mock/fault-injection.js +15 -15
- package/docs/sdk/FILE_LAYOUT.md +31 -31
- package/docs/sdk/INTEGRATION.md +126 -126
- package/docs/sdk/README.md +21 -21
- package/docs/sdk/SESSION_EVENTS.md +15 -13
- package/docs/sdk/openapi.yaml +16 -1
- package/package.json +1 -1
- package/packages/client/README.md +29 -29
- package/packages/client/index.d.ts +122 -122
- package/packages/client/index.js +260 -260
- package/packages/client/package.json +2 -2
- package/packages/contracts/README.md +9 -9
- package/packages/contracts/index.d.ts +519 -517
- package/packages/contracts/index.js +312 -312
- package/packages/contracts/package.json +1 -1
- package/packages/core/README.md +8 -8
- package/packages/core/index.d.ts +76 -76
- package/packages/core/index.js +222 -222
- package/packages/core/package.json +2 -2
- package/packages/server/README.md +17 -17
- package/packages/server/bin/agent-terminal-server.js +42 -42
- package/packages/server/bin/claude-workbench.js +225 -225
- package/packages/server/index.d.ts +171 -171
- package/packages/server/index.js +230 -230
- package/packages/server/lib/agent-discovery.js +133 -133
- package/packages/server/lib/agent-registry.js +143 -143
- package/packages/server/lib/agent-spawn-env.js +17 -17
- package/packages/server/lib/attachment-store.js +78 -78
- package/packages/server/lib/bridges/app-server-bridge.js +373 -373
- package/packages/server/lib/bridges/opencode-server-bridge.js +506 -506
- package/packages/server/lib/capture-settings-store.js +35 -35
- package/packages/server/lib/claude-history-recovery.js +87 -87
- package/packages/server/lib/cli-session-recovery.js +28 -28
- package/packages/server/lib/context-resolver.js +55 -55
- package/packages/server/lib/conversation-capture.js +51 -51
- package/packages/server/lib/delegation-phase-store.js +357 -357
- package/packages/server/lib/delegation-store.js +474 -468
- package/packages/server/lib/diagnostics.js +32 -32
- package/packages/server/lib/drivers/cli-json-runner.js +974 -974
- package/packages/server/lib/drivers/codex-driver.js +1461 -1442
- package/packages/server/lib/drivers/opencode-driver.js +1554 -1554
- package/packages/server/lib/drivers/zcode-driver.js +1250 -1250
- package/packages/server/lib/drivers/zcode-profile-runtime.js +71 -71
- package/packages/server/lib/event-stream.js +138 -138
- package/packages/server/lib/folder-picker.js +623 -623
- package/packages/server/lib/http-handler.js +339 -339
- package/packages/server/lib/json-file.js +26 -26
- package/packages/server/lib/knowledge-context.js +33 -33
- package/packages/server/lib/model-detection.js +295 -295
- package/packages/server/lib/parent-context.js +44 -0
- package/packages/server/lib/profile-store.js +283 -283
- package/packages/server/lib/project-store.js +102 -102
- package/packages/server/lib/runtime-loader.js +120 -120
- package/packages/server/lib/session-archive-store.js +53 -53
- package/packages/server/lib/session-history.js +612 -612
- package/packages/server/lib/system-doctor.js +125 -125
- package/packages/server/lib/ui-preferences-store.js +48 -48
- package/packages/server/lib/user-questions.js +67 -0
- package/packages/server/lib/workbench-service.js +1696 -1675
- package/packages/server/package.json +2 -2
- package/packages/ui/README.md +30 -30
- package/packages/ui/ai-profile-settings.d.ts +45 -45
- package/packages/ui/ai-profile-settings.js +283 -283
- package/packages/ui/claude-terminal.d.ts +31 -31
- package/packages/ui/claude-terminal.js +343 -343
- package/packages/ui/index.d.ts +18 -18
- package/packages/ui/index.js +51 -51
- package/packages/ui/package.json +2 -2
- package/packages/ui/theme.css +13 -13
- package/src/assets/claude-prompts.json +21 -21
- package/src/backend/http/ai-profile-routes.js +61 -61
- package/src/backend/http/claude-routes.js +170 -170
- package/src/backend/http/node-helpers.js +45 -45
- package/src/backend/index.js +21 -21
- package/src/backend/lib/ai-adapter.js +144 -144
- package/src/backend/lib/ai-profile-store.js +259 -259
- package/src/backend/lib/ai-vendor-presets.js +83 -83
- package/src/backend/lib/ai-workspace.js +140 -140
- package/src/backend/lib/automation-config.js +253 -253
- package/src/backend/lib/claude-cli-runner.js +2408 -2392
- package/src/backend/lib/data-dir.js +169 -169
- package/src/backend/lib/llm-client.js +176 -176
- package/src/backend/lib/model-context-windows.js +82 -82
- package/src/backend/lib/native-task-control.js +217 -217
- package/src/backend/lib/native-task-history-index.js +475 -475
- package/src/backend/lib/native-task-history-reader.js +526 -526
- package/src/backend/lib/native-task-messages.js +457 -457
- package/src/backend/lib/native-task-registry.js +325 -325
- package/src/backend/lib/native-task-runtime.js +242 -242
- package/src/backend/lib/native-task-trace.js +440 -440
- package/src/backend/lib/native-task-transcript.js +393 -393
- package/src/backend/lib/prompt-registry.js +166 -166
- package/src/backend/lib/runtime-config.js +121 -121
- package/src/backend/lib/sdk-session-stream.js +143 -143
- package/src/backend/lib/sdk-turn-stream.js +16 -16
- package/src/backend/lib/store-scope.js +131 -131
- package/src/backend/lib/task-identity.js +104 -104
- package/src/backend/lib/task-state-projection.js +58 -58
- package/src/index.js +7 -7
- package/src/sdk-wave-a.ts +1 -1
- package/src/sdk-wave-b.ts +1 -1
- package/src/sdk-wave-c.ts +1 -1
- package/src/sdk-wave-d.ts +1 -1
- package/src/sdk-wave-e.ts +1 -1
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
(function (global) {
|
|
8
8
|
'use strict';
|
|
9
9
|
|
|
10
|
-
const DOCK_MIN_WIDTH =
|
|
10
|
+
const DOCK_MIN_WIDTH = 340;
|
|
11
11
|
const DOCK_MAX_WIDTH = 560;
|
|
12
|
-
const DOCK_DEFAULT_WIDTH =
|
|
12
|
+
const DOCK_DEFAULT_WIDTH = 420;
|
|
13
13
|
const DOCK_WIDTH_STEP = 24;
|
|
14
14
|
const DOCK_PAGE_LIMIT = 50;
|
|
15
15
|
const DOCK_STORAGE_PREFIX = 'kb.workbench.dock:';
|
|
@@ -18,21 +18,11 @@
|
|
|
18
18
|
const TASK_STATUSES = ['pending', 'running', 'completed', 'failed', 'stopped'];
|
|
19
19
|
const TERMINAL_STATUSES = ['completed', 'failed', 'stopped'];
|
|
20
20
|
const STATUS_LABELS = { pending: '待执行', running: '运行中', completed: '已完成', failed: '失败', stopped: '已停止' };
|
|
21
|
-
const KIND_LABELS = { agent: '子代理', local_bash: '本地命令' };
|
|
22
21
|
const RESYNC_CODES = ['FILE_ERA_CHANGED', 'ANCHOR_MISMATCH', 'HISTORY_MUTATED', 'HISTORY_TRUNCATED'];
|
|
23
22
|
const LIVE_EVENT_TYPES = new Set([
|
|
24
23
|
'claude/native-task-state', 'claude/native-task-transcript', 'claude/native-trace', 'claude/task-interrupted',
|
|
25
24
|
'claude/task-started', 'claude/task-progress', 'claude/task-updated', 'claude/task-notification',
|
|
26
25
|
]);
|
|
27
|
-
// Event types buildRow turns into timeline DOM. The reading anchor is
|
|
28
|
-
// restored by scanning rendered rows, so anchor math may only use these.
|
|
29
|
-
const RENDERABLE_ROW_TYPES = new Set([
|
|
30
|
-
'claude/native-task-state', 'claude/task-progress', 'claude/native-trace',
|
|
31
|
-
'claude/task-interrupted', 'claude/native-task-transcript',
|
|
32
|
-
]);
|
|
33
|
-
const STOP_NOTE = '单任务精确停止 API 未交付(本切片明确禁用);不会以主会话 abort 替代。';
|
|
34
|
-
const PICKER_NOTE = '仅列出原生后台任务(SDK 公共读取)。知识 / 浏览器 / 代码评审 provider 未在本切片交付,不显示为可用。';
|
|
35
|
-
const PICKER_REGISTRY_NOTE = '原生后台任务来自 SDK 公共读取;可信宿主面板来自宿主注册(同源校验)。未注册 provider 的面板类型不显示为可用。';
|
|
36
26
|
const NATIVE_PROVIDER_ID = 'workbench.native-subagent';
|
|
37
27
|
|
|
38
28
|
function isId(value) { return typeof value === 'string' && value.trim() !== ''; }
|
|
@@ -70,11 +60,6 @@
|
|
|
70
60
|
return Math.min(DOCK_MAX_WIDTH, Math.max(DOCK_MIN_WIDTH, n));
|
|
71
61
|
}
|
|
72
62
|
|
|
73
|
-
function shortId(value) {
|
|
74
|
-
const s = String(value == null ? '' : value);
|
|
75
|
-
return s.length > 14 ? s.slice(0, 12) + '…' : s;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
63
|
// Same transition rules as the server index: terminal observations never
|
|
79
64
|
// revive, and a second conflicting terminal outcome keeps the first.
|
|
80
65
|
function nextStatus(current, incoming) {
|
|
@@ -97,6 +82,11 @@
|
|
|
97
82
|
const apiBase = String((deps && deps.apiBase) || '');
|
|
98
83
|
const storage = deps && deps.storage && typeof deps.storage.getItem === 'function' && typeof deps.storage.setItem === 'function' ? deps.storage : null;
|
|
99
84
|
const renderMd = deps && typeof deps.renderMarkdown === 'function' ? deps.renderMarkdown : null;
|
|
85
|
+
const renderToolStep = deps && typeof deps.renderToolStep === 'function' ? deps.renderToolStep : null;
|
|
86
|
+
const renderToolResult = deps && typeof deps.renderToolResult === 'function' ? deps.renderToolResult : null;
|
|
87
|
+
const renderMessage = deps && typeof deps.renderMessage === 'function' ? deps.renderMessage : null;
|
|
88
|
+
const renderProcess = deps && typeof deps.renderProcess === 'function' ? deps.renderProcess : null;
|
|
89
|
+
const renderChanges = deps && typeof deps.renderChanges === 'function' ? deps.renderChanges : null;
|
|
100
90
|
const toast = deps && typeof deps.toast === 'function' ? deps.toast : function () {};
|
|
101
91
|
const storageKey = DOCK_STORAGE_PREFIX + apiBase;
|
|
102
92
|
const registry = deps && deps.registry && typeof deps.registry.register === 'function' ? deps.registry : null;
|
|
@@ -134,6 +124,7 @@
|
|
|
134
124
|
let persistTimer = null;
|
|
135
125
|
const taskIndex = new Map();
|
|
136
126
|
const delegationIndex = new Map();
|
|
127
|
+
const delegationTitles = new Map();
|
|
137
128
|
let delegationState = { loading: false, error: null, code: null };
|
|
138
129
|
let delegationsBusy = false;
|
|
139
130
|
let delegationsQueued = false;
|
|
@@ -145,23 +136,25 @@
|
|
|
145
136
|
|
|
146
137
|
function persistedShape() {
|
|
147
138
|
return {
|
|
148
|
-
v: 1, open, width, activeTabId,
|
|
139
|
+
v: 1, open, width, activeTabId, titles: Object.fromEntries(delegationTitles),
|
|
149
140
|
// Host panel views are transient embed-session state; only native task
|
|
150
141
|
// views persist per scope.
|
|
151
|
-
tabs: tabs.
|
|
142
|
+
tabs: tabs.map(tab => ['draft', 'launcher', 'host'].includes(tab.kind) ? ({ id: tab.id, kind: tab.kind, title: tab.title, draft: tab.draft || '', requestId: tab.requestId, command: tab.command, uncertain: tab.uncertain || tab.sending || false, providerId: tab.providerId }) : tab.kind === 'delegated' ? ({
|
|
152
143
|
id: tab.id,
|
|
153
144
|
kind: 'delegated',
|
|
154
|
-
delegationId: tab.delegationId,
|
|
145
|
+
delegationId: tab.delegationId, draft: tab.draft || '', title: tab.title,
|
|
155
146
|
anchor: {
|
|
156
|
-
follow:
|
|
157
|
-
sequence: tab.pane
|
|
147
|
+
follow: tab.pane ? tab.pane.follow !== false : !tab.restoredAnchor || tab.restoredAnchor.follow !== false,
|
|
148
|
+
sequence: tab.pane ? tab.pane.anchorSeq : tab.restoredAnchor && tab.restoredAnchor.sequence,
|
|
149
|
+
top: tab.pane ? tab.pane.scroll.scrollTop : tab.restoredAnchor && tab.restoredAnchor.top,
|
|
158
150
|
},
|
|
159
151
|
}) : ({
|
|
160
152
|
id: tab.id,
|
|
161
153
|
taskRef: { ...tab.taskRef },
|
|
162
154
|
anchor: {
|
|
163
|
-
follow:
|
|
164
|
-
sequence: tab.pane
|
|
155
|
+
follow: tab.pane ? tab.pane.follow !== false : !tab.restoredAnchor || tab.restoredAnchor.follow !== false,
|
|
156
|
+
sequence: tab.pane ? tab.pane.anchorSeq : tab.restoredAnchor && tab.restoredAnchor.sequence,
|
|
157
|
+
top: tab.pane ? tab.pane.scroll.scrollTop : tab.restoredAnchor && tab.restoredAnchor.top,
|
|
165
158
|
},
|
|
166
159
|
})),
|
|
167
160
|
};
|
|
@@ -176,7 +169,7 @@
|
|
|
176
169
|
const key = scopeStorageKey(scope);
|
|
177
170
|
// A null storeScope bucket can never be re-bound (identity re-binding
|
|
178
171
|
// restores the real bucket), so it must not linger as a residual entry.
|
|
179
|
-
if (scope.storeScope && (tabs.length || open)) scopes[key] = { ...persistedShape(), updatedAt: Date.now() };
|
|
172
|
+
if (scope.storeScope && (tabs.length || open || delegationTitles.size)) scopes[key] = { ...persistedShape(), updatedAt: Date.now() };
|
|
180
173
|
else delete scopes[key];
|
|
181
174
|
const kept = Object.entries(scopes).sort((a, b) => (b[1].updatedAt || 0) - (a[1].updatedAt || 0)).slice(0, DOCK_SCOPES_KEPT);
|
|
182
175
|
storage.setItem(storageKey, JSON.stringify({ v: 1, scopes: Object.fromEntries(kept) }));
|
|
@@ -190,6 +183,7 @@
|
|
|
190
183
|
}
|
|
191
184
|
|
|
192
185
|
function restoreFor(scopeObj, previousScope = scope) {
|
|
186
|
+
const carriedOpen = open;
|
|
193
187
|
// A host can register a project-scoped knowledge panel after kb:ready
|
|
194
188
|
// but before the terminal has opened/restored a session. Attaching that
|
|
195
189
|
// first native scope must not destroy the already-open host view.
|
|
@@ -199,29 +193,40 @@
|
|
|
199
193
|
if (!carryHostTabs.length) return;
|
|
200
194
|
const known = new Set(tabs.map(tab => tab.id));
|
|
201
195
|
for (const tab of carryHostTabs) if (!known.has(tab.id)) tabs.push(tab);
|
|
202
|
-
open =
|
|
196
|
+
open = carriedOpen;
|
|
203
197
|
if (!activeTabId && tabs.length) activeTabId = tabs[0].id;
|
|
204
198
|
};
|
|
205
|
-
|
|
199
|
+
for (const tab of tabs) releaseSubscription(tab);
|
|
200
|
+
tabs = []; activeTabId = null; open = false; width = DOCK_DEFAULT_WIDTH; delegationTitles.clear();
|
|
206
201
|
if (!scopeObj || !storage) { mergeCarriedHostTabs(); return; }
|
|
207
202
|
try {
|
|
208
203
|
const raw = storage.getItem(storageKey);
|
|
209
204
|
const parsed = raw ? JSON.parse(raw) : null;
|
|
210
205
|
const saved = parsed && parsed.scopes ? parsed.scopes[scopeStorageKey(scopeObj)] : null;
|
|
211
206
|
if (!saved || typeof saved !== 'object') { mergeCarriedHostTabs(); return; }
|
|
207
|
+
for (const [id, title] of Object.entries(saved.titles || {})) if (typeof title === 'string') delegationTitles.set(id, title);
|
|
212
208
|
open = saved.open === true;
|
|
213
209
|
width = clampWidth(saved.width);
|
|
214
210
|
const restored = [];
|
|
215
211
|
for (const item of Array.isArray(saved.tabs) ? saved.tabs : []) {
|
|
212
|
+
if (item && ['draft', 'launcher', 'host'].includes(item.kind)) {
|
|
213
|
+
if (item.kind === 'host' && !isId(item.providerId)) continue;
|
|
214
|
+
restored.push({ id: isId(item.id) ? item.id : nextTabId(), kind: item.kind, providerId: item.providerId,
|
|
215
|
+
descriptor: item.kind === 'host' && registry ? registry.get(item.providerId) : null,
|
|
216
|
+
title: item.title, draft: typeof item.draft === 'string' ? item.draft : '', requestId: item.requestId || newRequestId(),
|
|
217
|
+
command: item.command && item.command.scope && item.command.scope.storeScope === scopeObj.storeScope && item.command.scope.contextId === scopeObj.contextId ? item.command : null,
|
|
218
|
+
uncertain: item.uncertain === true, pane: null, panel: null });
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
216
221
|
const ref = sanitizeTaskRef(item && item.taskRef);
|
|
217
222
|
if (item && item.kind === 'delegated') {
|
|
218
223
|
if (!isId(item.delegationId)) continue;
|
|
219
224
|
const dAnchor = item.anchor && typeof item.anchor === 'object' ? item.anchor : {};
|
|
220
225
|
restored.push({
|
|
221
226
|
id: isId(item.id) ? item.id : 'tab-' + (++tabSeq),
|
|
222
|
-
kind: 'delegated', delegationId: String(item.delegationId).trim(), taskRef: null,
|
|
227
|
+
kind: 'delegated', delegationId: String(item.delegationId).trim(), taskRef: null, draft: typeof item.draft === 'string' ? item.draft : '', title: item.title,
|
|
223
228
|
invalid: null, panel: null, pane: null, invalidRendered: null,
|
|
224
|
-
restoredAnchor: { follow: dAnchor.follow !== false, sequence: Number.isFinite(Number(dAnchor.sequence)) ? Number(dAnchor.sequence) : null },
|
|
229
|
+
restoredAnchor: { follow: dAnchor.follow !== false, sequence: Number.isFinite(Number(dAnchor.sequence)) ? Number(dAnchor.sequence) : null, top: Number.isFinite(dAnchor.top) ? dAnchor.top : null },
|
|
225
230
|
});
|
|
226
231
|
continue;
|
|
227
232
|
}
|
|
@@ -230,10 +235,11 @@
|
|
|
230
235
|
restored.push({
|
|
231
236
|
id: isId(item.id) ? item.id : 'tab-' + (++tabSeq),
|
|
232
237
|
taskRef: ref, invalid: null, panel: null, pane: null, invalidRendered: null,
|
|
233
|
-
restoredAnchor: { follow: anchor.follow !== false, sequence: Number.isFinite(Number(anchor.sequence)) ? Number(anchor.sequence) : null },
|
|
238
|
+
restoredAnchor: { follow: anchor.follow !== false, sequence: Number.isFinite(Number(anchor.sequence)) ? Number(anchor.sequence) : null, top: Number.isFinite(anchor.top) ? anchor.top : null },
|
|
234
239
|
});
|
|
235
240
|
}
|
|
236
241
|
tabs = restored;
|
|
242
|
+
if (!tabs.length) open = false;
|
|
237
243
|
if (saved.activeTabId && tabs.some(tab => tab.id === saved.activeTabId)) activeTabId = saved.activeTabId;
|
|
238
244
|
else if (tabs.length) activeTabId = tabs[0].id;
|
|
239
245
|
} catch { /* storage is optional */ }
|
|
@@ -245,6 +251,8 @@
|
|
|
245
251
|
return {
|
|
246
252
|
key: ref ? taskRefKey(ref) : 'bare|' + (row.sdkSessionId || '') + '|' + (row.taskId || ''),
|
|
247
253
|
taskRef: ref,
|
|
254
|
+
description: isId(row.description) ? row.description : isId(row.summary) ? row.summary : null,
|
|
255
|
+
launcherToolUseId: row.owner && row.owner.launcherKind === 'root' && !row.owner.parentNativeAgentId && isId(row.owner.launcherToolUseId) ? row.owner.launcherToolUseId : null,
|
|
248
256
|
sdkSessionId: isId(row.sdkSessionId) ? row.sdkSessionId : null,
|
|
249
257
|
taskId: isId(row.taskId) ? row.taskId : null,
|
|
250
258
|
taskKind: isId(row.taskKind) ? row.taskKind : null,
|
|
@@ -269,12 +277,16 @@
|
|
|
269
277
|
|
|
270
278
|
function upsertIndexEntry(ref, patch) {
|
|
271
279
|
const key = taskRefKey(ref);
|
|
280
|
+
// A trusted binding replaces the earlier unassigned status echo. Keeping
|
|
281
|
+
// both would leave a phantom running task after the bound task completes.
|
|
282
|
+
for (const [oldKey, old] of taskIndex) if (!old.taskRef && old.taskId === ref.taskId) taskIndex.delete(oldKey);
|
|
272
283
|
const entry = taskIndex.get(key) || { key, taskRef: ref, sdkSessionId: null, taskId: null, taskKind: null, taskType: null, typeConflict: false, status: null, statusSource: null, association: null, rowCount: null };
|
|
273
284
|
taskIndex.set(key, { ...entry, ...patch });
|
|
274
285
|
}
|
|
275
286
|
|
|
276
287
|
function upsertFromStateEvent(ref, event) {
|
|
277
288
|
const patch = {};
|
|
289
|
+
if (event.owner && event.owner.launcherKind === 'root' && !event.owner.parentNativeAgentId && isId(event.owner.launcherToolUseId)) patch.launcherToolUseId = event.owner.launcherToolUseId;
|
|
278
290
|
if (TASK_STATUSES.includes(event.status)) { patch.status = nextStatus((taskIndex.get(taskRefKey(ref)) || {}).status, event.status); patch.statusSource = isId(event.statusSource) ? event.statusSource : event.type; }
|
|
279
291
|
if (isId(event.taskKind)) patch.taskKind = event.taskKind;
|
|
280
292
|
if (isId(event.taskType)) patch.taskType = event.taskType;
|
|
@@ -433,25 +445,6 @@
|
|
|
433
445
|
return items;
|
|
434
446
|
}
|
|
435
447
|
|
|
436
|
-
const DELEGATION_PHASE_LABELS = {
|
|
437
|
-
'reserved': '已预留', 'child-created': '子会话已创建', 'child-create-failed': '子会话创建失败',
|
|
438
|
-
'dispatching': '派发中', 'dispatched': '已派发', 'dispatch-unknown': '派发结果未知',
|
|
439
|
-
};
|
|
440
|
-
const DELEGATION_OUTCOME_LABELS = {
|
|
441
|
-
'not-started': '未启动', 'not-dispatched': '未派发', 'pending': '待定', 'dispatched': '已送达', 'unknown': '未知',
|
|
442
|
-
};
|
|
443
|
-
const DELEGATION_DISPATCH_LABELS = {
|
|
444
|
-
'not-dispatched': '未派发', 'awaiting-dispatch': '等待派发', 'dispatching': '派发中',
|
|
445
|
-
'dispatched': '已派发', 'dispatch-unknown': '派发未知',
|
|
446
|
-
};
|
|
447
|
-
const CHILD_STATE_LABELS = {
|
|
448
|
-
'created': '已创建', 'spawning': '启动中', 'running': '运行中', 'thinking': '思考中',
|
|
449
|
-
'tool-running': '执行工具', 'pending-permission': '等待授权', 'idle': '空闲',
|
|
450
|
-
'aborted': '已中止', 'failed': '失败', 'missing': '缺失',
|
|
451
|
-
};
|
|
452
|
-
const DELEGATION_PICKER_NOTE = '委派会话为只读视图:关闭视图不会中止子会话,也不会重新派发输入;停止控件在本切片保持禁用。';
|
|
453
|
-
const DELEGATION_STOP_NOTE = '委派子会话的精确 stop 未交付:关闭视图只销毁面板,绝不中止子会话,也不能用父会话 abort 替代。';
|
|
454
|
-
|
|
455
448
|
function sanitizeDelegationView(view) {
|
|
456
449
|
if (!view || typeof view !== 'object') return null;
|
|
457
450
|
const d = view.delegation;
|
|
@@ -471,6 +464,7 @@
|
|
|
471
464
|
issuedAt: isId(d.issuedAt) ? String(d.issuedAt) : null,
|
|
472
465
|
phase: isId(view.phase) ? String(view.phase) : 'unknown-phase',
|
|
473
466
|
outcome: isId(view.outcome) ? String(view.outcome) : null,
|
|
467
|
+
context: view.context && view.context.mode === 'parent-snapshot' ? { mode: 'parent-snapshot', truncated: view.context.truncated === true } : null,
|
|
474
468
|
dispatch: view.dispatch && typeof view.dispatch === 'object' ? {
|
|
475
469
|
state: isId(view.dispatch.state) ? String(view.dispatch.state) : null,
|
|
476
470
|
attemptId: isId(view.dispatch.attemptId) ? String(view.dispatch.attemptId) : null,
|
|
@@ -529,10 +523,8 @@
|
|
|
529
523
|
return false;
|
|
530
524
|
}
|
|
531
525
|
|
|
532
|
-
//
|
|
533
|
-
//
|
|
534
|
-
// A changed summary for the parent or any known delegated child re-reads
|
|
535
|
-
// the trusted delegation list; summaries for anything else are ignored.
|
|
526
|
+
// Parent/child summary changes refresh trusted relations. Only the active
|
|
527
|
+
// child conversation owns its own message stream; there is no timer poll.
|
|
536
528
|
function notifySessionChanged(summary) {
|
|
537
529
|
if (!scope || !summary || typeof summary !== 'object') return;
|
|
538
530
|
const id = isId(summary.sessionId) ? String(summary.sessionId) : null;
|
|
@@ -587,6 +579,7 @@
|
|
|
587
579
|
}
|
|
588
580
|
delegationState = { loading: false, error: null, code: null };
|
|
589
581
|
for (const tab of tabs) {
|
|
582
|
+
if (tab.kind === 'draft' && tab.command) { const recovered = [...delegationIndex.values()].find(view => view.requestId === tab.command.requestId); if (recovered && hasDispatch(recovered)) { acceptCreatedView(tab, { delegation: { ...recovered }, child: recovered.child, phase: recovered.phase, outcome: recovered.outcome, dispatch: recovered.dispatch, context: recovered.context }); } }
|
|
590
583
|
if (tab.kind !== 'delegated') continue;
|
|
591
584
|
const fresh = delegationIndex.get(tab.delegationId);
|
|
592
585
|
if (fresh) {
|
|
@@ -612,10 +605,228 @@
|
|
|
612
605
|
if (queued) void validateScopeDelegations();
|
|
613
606
|
}
|
|
614
607
|
|
|
608
|
+
function newRequestId() { return 'side-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2); }
|
|
609
|
+
function hasDispatch(view) { return ['dispatched', 'completed', 'dispatching', 'dispatch-unknown'].includes(view.phase); }
|
|
610
|
+
|
|
611
|
+
function releaseSubscription(tab) {
|
|
612
|
+
tab.subscriptionToken = null;
|
|
613
|
+
if (tab.unsubscribe) { tab.unsubscribe(); tab.unsubscribe = null; }
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function openNewAgent(text = '') {
|
|
617
|
+
if (!scope || !scope.storeScope) { toast('请先打开一个会话。', true); return null; }
|
|
618
|
+
const launcher = tabs.find(tab => tab.id === activeTabId && tab.kind === 'launcher');
|
|
619
|
+
const tab = launcher || { id: nextTabId() };
|
|
620
|
+
if (tab.pane && tab.pane.node) tab.pane.node.remove();
|
|
621
|
+
Object.assign(tab, { kind: 'draft', title: '新子代理', draft: text, requestId: newRequestId(), pane: null, panel: null });
|
|
622
|
+
if (!launcher) tabs.push(tab);
|
|
623
|
+
activeTabId = tab.id; open = true; pickerOpen = false;
|
|
624
|
+
render(); flushPersist();
|
|
625
|
+
return tab.id;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function acceptCreatedView(tab, raw) {
|
|
629
|
+
const view = sanitizeDelegationView(raw);
|
|
630
|
+
if (!view || view.requestId !== tab.command.requestId) throw new Error('untrusted child relation');
|
|
631
|
+
delegationIndex.set(view.delegationId, view);
|
|
632
|
+
delegationTitles.set(view.delegationId, tab.command.text.replace(/\s+/g, ' ').slice(0, 60));
|
|
633
|
+
if (tab.pane && tab.pane.node) tab.pane.node.remove();
|
|
634
|
+
Object.assign(tab, { kind: 'delegated', delegationId: view.delegationId, pane: null, panel: null,
|
|
635
|
+
title: tab.command.text.replace(/\s+/g, ' ').slice(0, 60), draft: '', sending: false, uncertain: false });
|
|
636
|
+
if (raw.context && raw.context.truncated) toast('主对话较长,已保留最近的完整消息作为上下文。');
|
|
637
|
+
render(); flushPersist(); notifyBackgroundActivity();
|
|
638
|
+
void loadDelegationFirstPage(tab);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
async function submitDraft(tab) {
|
|
642
|
+
if (!scope || tab.sending || !String(tab.draft || '').trim()) return;
|
|
643
|
+
const epoch = scopeEpoch, parent = scope.parentSessionId;
|
|
644
|
+
if (!tab.command) tab.command = { ...(typeof deps.getSelection === 'function' ? deps.getSelection() : {}),
|
|
645
|
+
scope: { storeScope: scope.storeScope, contextId: scope.contextId }, requestId: tab.requestId,
|
|
646
|
+
text: tab.draft.trim(), permissionMode: 'default', contextMode: 'parent-snapshot' };
|
|
647
|
+
tab.sending = true; tab.error = null; flushPersist(); render();
|
|
648
|
+
try {
|
|
649
|
+
if (tab.uncertain) {
|
|
650
|
+
const page = await client.listDelegations(parent, tab.command.scope);
|
|
651
|
+
if (epoch !== scopeEpoch) return;
|
|
652
|
+
const existing = page.delegations.find(view => view.delegation && view.delegation.requestId === tab.command.requestId);
|
|
653
|
+
if (existing && hasDispatch(existing)) { acceptCreatedView(tab, existing); return; }
|
|
654
|
+
}
|
|
655
|
+
const result = await client.createDelegation(parent, tab.command);
|
|
656
|
+
if (epoch !== scopeEpoch) return;
|
|
657
|
+
if (!tabs.includes(tab)) { void validateScopeDelegations(); return; }
|
|
658
|
+
acceptCreatedView(tab, result);
|
|
659
|
+
} catch (error) {
|
|
660
|
+
if (epoch !== scopeEpoch || !tabs.includes(tab)) return;
|
|
661
|
+
tab.uncertain = !error.status || error.status >= 500 || ['DELEGATION_CONCURRENCY_LIMIT', 'DELEGATION_CHILD_BUSY'].includes(error.code);
|
|
662
|
+
// A known rejection before reservation can be edited; an uncertain command remains frozen.
|
|
663
|
+
if (!tab.uncertain && error.code !== 'DELEGATION_PAYLOAD_CONFLICT') tab.command = null;
|
|
664
|
+
tab.error = tab.uncertain ? '暂未确认任务是否创建。点击发送会先检查结果,避免重复创建。' : error.code === 'DELEGATION_AGENT_UNSUPPORTED' ? '当前 Agent 暂不支持新建子代理。' : '任务未能创建,内容已保留,请稍后重试。';
|
|
665
|
+
tab.sending = false; render(); flushPersist();
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function buildComposer(tab) {
|
|
670
|
+
const options = { value: tab.draft || '', placeholder: tab.kind === 'draft' ? '输入一项任务…' : '继续与子代理对话…',
|
|
671
|
+
onInput: value => { tab.draft = value; saveTabDraft(tab); schedulePersist(); },
|
|
672
|
+
onSend: () => { if (tab.kind === 'draft') void submitDraft(tab); else void sendChild(tab); },
|
|
673
|
+
onStop: () => { void stopChild(tab); }, isBusy: () => childBusy(tab), disabled: () => Boolean(tab.sending || tab.uncertain || tab.invalid) };
|
|
674
|
+
if (typeof deps.createComposer === 'function') return deps.createComposer(options);
|
|
675
|
+
const node = el('div', 'composer-wrap rd-composer-wrap'), box = el('div', 'composer'), input = el('textarea'), send = el('button', 'send', '↑');
|
|
676
|
+
input.value = options.value; input.setAttribute('aria-label', options.placeholder);
|
|
677
|
+
input.addEventListener('input', () => options.onInput(input.value));
|
|
678
|
+
send.addEventListener('click', () => options.isBusy() ? options.onStop() : options.onSend());
|
|
679
|
+
box.append(input, send); node.append(box);
|
|
680
|
+
return { node, input, send, setBusy: () => {} };
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function childBusy(tab) {
|
|
684
|
+
const view = delegationIndex.get(tab.delegationId);
|
|
685
|
+
return Boolean(tab.sending || view && view.child && view.child.active);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function saveTabDraft(tab) {
|
|
689
|
+
if (!scope || !storage || !tab.delegationId) return;
|
|
690
|
+
try { storage.setItem(storageKey + ':draft:' + scopeStorageKey(scope) + ':' + tab.delegationId, tab.draft || ''); } catch { /* optional */ }
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
async function sendChild(tab) {
|
|
694
|
+
const view = delegationIndex.get(tab.delegationId), text = String(tab.draft || '').trim();
|
|
695
|
+
if (!view || tab.invalid || childBusy(tab) || !text) return;
|
|
696
|
+
if ((!['dispatched', 'completed'].includes(view.phase)) && !(view.child && view.child.turns > 0)) {
|
|
697
|
+
tab.error = '上一次任务状态仍待确认,请稍后查看结果。'; render(); return;
|
|
698
|
+
}
|
|
699
|
+
const epoch = scopeEpoch;
|
|
700
|
+
tab.sending = true; tab.error = null; render();
|
|
701
|
+
try {
|
|
702
|
+
await client.send(view.childSessionId, text, { permissionMode: 'default' });
|
|
703
|
+
if (epoch !== scopeEpoch || !tabs.includes(tab)) return;
|
|
704
|
+
tab.draft = ''; saveTabDraft(tab); tab.pane.composer.input.value = '';
|
|
705
|
+
void loadDelegationFirstPage(tab); void validateScopeDelegations();
|
|
706
|
+
} catch { if (epoch === scopeEpoch && tabs.includes(tab)) tab.error = '消息暂未发送成功,草稿已保留。请查看对话结果后再决定是否重试。'; }
|
|
707
|
+
finally { if (epoch === scopeEpoch && tabs.includes(tab)) { tab.sending = false; render(); flushPersist(); } }
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
async function stopChild(tab) {
|
|
711
|
+
const view = delegationIndex.get(tab.delegationId);
|
|
712
|
+
if (!view || tab.invalid || tab.sending || !view.child.active) return;
|
|
713
|
+
try { await client.abort(view.childSessionId); } catch { toast('暂时无法停止此子代理,请稍后重试。', true); }
|
|
714
|
+
if (tabs.includes(tab)) void validateScopeDelegations();
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function subscribeChild(tab) {
|
|
718
|
+
if (!open || tab.id !== activeTabId || typeof client.subscribe !== 'function' || tab.subscriptionToken || tab.invalid || !tab.panel || !tab.panel.loaded) return;
|
|
719
|
+
const view = delegationIndex.get(tab.delegationId);
|
|
720
|
+
if (!view || !view.child.present) return;
|
|
721
|
+
const epoch = scopeEpoch;
|
|
722
|
+
const token = {}; tab.subscriptionToken = token;
|
|
723
|
+
const current = () => epoch === scopeEpoch && tabs.includes(tab) && tab.subscriptionToken === token;
|
|
724
|
+
let opened = false;
|
|
725
|
+
tab.unsubscribe = client.subscribe(view.childSessionId, event => {
|
|
726
|
+
if (!current()) return;
|
|
727
|
+
const currentView = delegationIndex.get(tab.delegationId);
|
|
728
|
+
if (!currentView || currentView.childSessionId !== view.childSessionId) return;
|
|
729
|
+
tab.connectionError = null;
|
|
730
|
+
const panel = ensureDelegationPanelState(tab);
|
|
731
|
+
if (event.type === 'workbench/resync') {
|
|
732
|
+
panel.events = []; panel.olderEvents = []; panel.loadedEarlier = false;
|
|
733
|
+
void loadDelegationFirstPage(tab); return;
|
|
734
|
+
}
|
|
735
|
+
panel.events = mergeDelegationEvents(panel.events, [event]); panel.rendered = false;
|
|
736
|
+
if (event.type === 'claude/permission-request') { panel.currentPermission = event; panel.permissionRevision = (panel.permissionRevision || 0) + 1; }
|
|
737
|
+
if (event.type === 'claude/permission-resolved') { if (panel.currentPermission?.requestId === event.requestId) panel.currentPermission = null; panel.permissionRevision = (panel.permissionRevision || 0) + 1; }
|
|
738
|
+
panel.highWater = Math.max(panel.highWater || 0, Number(event.sequence) || 0);
|
|
739
|
+
if (event.type === 'claude/state') { currentView.child.state = event.state; currentView.child.active = ['created', 'spawning', 'running', 'thinking', 'tool-running', 'pending-permission'].includes(event.state); }
|
|
740
|
+
if (event.type === 'claude/result') { currentView.child.active = false; currentView.child.turns = (currentView.child.turns || 0) + 1; }
|
|
741
|
+
render(); notifyBackgroundActivity();
|
|
742
|
+
}, () => { if (current()) { tab.connectionError = '连接暂时中断,正在恢复对话。'; render(); } }, {
|
|
743
|
+
afterSequence: tab.panel.highWater || 0,
|
|
744
|
+
onOpen: () => {
|
|
745
|
+
if (!current()) return;
|
|
746
|
+
tab.connectionError = null;
|
|
747
|
+
if (opened) { void loadDelegationFirstPage(tab); void validateScopeDelegations(); }
|
|
748
|
+
else render();
|
|
749
|
+
opened = true;
|
|
750
|
+
},
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function buildDraftPane(tab) {
|
|
755
|
+
const node = el('section', 'rd-pane rd-draft-pane');
|
|
756
|
+
const empty = el('div', 'rd-draft-empty');
|
|
757
|
+
empty.append(el('strong', null, '想让子代理处理什么?'), el('p', null, '沿用当前项目,独立开展对话。'));
|
|
758
|
+
const errorBox = el('div', 'rd-error');
|
|
759
|
+
const composer = buildComposer(tab);
|
|
760
|
+
node.append(empty, errorBox, composer.node);
|
|
761
|
+
tab.pane = { node, errorBox, composer };
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function buildLauncherPane(tab) {
|
|
765
|
+
const node = el('section', 'rd-pane rd-launcher');
|
|
766
|
+
node.append(el('h3', 'rd-picker-heading', '工具'));
|
|
767
|
+
const create = el('button', 'rd-picker-row rd-create-agent', '新建子代理');
|
|
768
|
+
create.disabled = !scope || !scope.storeScope || typeof client.createDelegation !== 'function';
|
|
769
|
+
create.addEventListener('click', () => { openNewAgent(); }); node.append(create);
|
|
770
|
+
const providers = registry ? registry.listAvailable({ projectId: scope ? scope.contextId : null }) : [];
|
|
771
|
+
for (const descriptor of providers) {
|
|
772
|
+
const row = el('button', 'rd-picker-row rd-picker-host-row');
|
|
773
|
+
if (typeof deps.renderPageIcon === 'function') row.append(deps.renderPageIcon('file'));
|
|
774
|
+
row.append(el('span', null, descriptor.title));
|
|
775
|
+
row.addEventListener('click', () => { const id = tab.id; openPanel(descriptor.providerId); closeTab(id); });
|
|
776
|
+
node.append(row);
|
|
777
|
+
}
|
|
778
|
+
tab.pane = { node };
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function renderPermissions(tab) {
|
|
782
|
+
const pane = tab.pane, view = delegationIndex.get(tab.delegationId);
|
|
783
|
+
if (!pane.permissions || !view) return;
|
|
784
|
+
pane.permissions.textContent = '';
|
|
785
|
+
if (!view.child.active) return;
|
|
786
|
+
const pending = new Map();
|
|
787
|
+
for (const event of tab.panel.events) {
|
|
788
|
+
if (event.type === 'claude/permission-request') pending.set(event.requestId, event);
|
|
789
|
+
if (event.type === 'claude/permission-resolved') pending.delete(event.requestId);
|
|
790
|
+
}
|
|
791
|
+
if (tab.panel.currentPermission !== undefined) {
|
|
792
|
+
for (const [id, event] of pending) if (event.kind === 'question' && id !== tab.panel.currentPermission?.requestId) pending.delete(id);
|
|
793
|
+
if (tab.panel.currentPermission?.kind === 'question') pending.set(tab.panel.currentPermission.requestId, tab.panel.currentPermission);
|
|
794
|
+
}
|
|
795
|
+
for (const [id, card] of pane.questionCards || []) if (!pending.has(id)) { card.settle({ cancelled: true }); pane.questionCards.delete(id); }
|
|
796
|
+
for (const [requestId, event] of pending) {
|
|
797
|
+
if (event.kind === 'question' && typeof global.createUserQuestionCard === 'function') {
|
|
798
|
+
pane.questionCards ||= new Map();
|
|
799
|
+
let card = pane.questionCards.get(requestId);
|
|
800
|
+
if (!card) {
|
|
801
|
+
const childId = view.childSessionId;
|
|
802
|
+
card = global.createUserQuestionCard({ document: doc, event,
|
|
803
|
+
storageKey: `kb.workbench.questions:${deps.apiBase || ''}:${childId}:${requestId}`,
|
|
804
|
+
onSubmit: async answers => {
|
|
805
|
+
if (tab.invalid || !tabs.includes(tab)) throw new Error('任务已关闭');
|
|
806
|
+
await client.resolvePermission({ sessionId: childId, requestId, allow: true, answers });
|
|
807
|
+
},
|
|
808
|
+
}); pane.questionCards.set(requestId, card);
|
|
809
|
+
}
|
|
810
|
+
pane.permissions.append(card); continue;
|
|
811
|
+
}
|
|
812
|
+
const card = el('div', 'perm'); card.append(el('span', 'p-title', (event.toolName || '子代理') + ' 请求执行操作'));
|
|
813
|
+
for (const allow of [true, false]) {
|
|
814
|
+
const button = el('button', allow ? 'btn-allow' : 'btn-deny', allow ? '允许' : '拒绝');
|
|
815
|
+
button.addEventListener('click', async () => {
|
|
816
|
+
if (tab.invalid || !tabs.includes(tab)) return;
|
|
817
|
+
button.disabled = true;
|
|
818
|
+
try { await client.resolvePermission(view.childSessionId, requestId, allow); void loadDelegationFirstPage(tab); }
|
|
819
|
+
catch { button.disabled = false; toast('授权操作未完成,请重试。', true); }
|
|
820
|
+
}); card.append(button);
|
|
821
|
+
}
|
|
822
|
+
pane.permissions.append(card);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
615
826
|
function openDelegation(delegationId) {
|
|
616
|
-
if (!scope) { toast('
|
|
827
|
+
if (!scope) { toast('请先打开此任务所属的会话。', true); return null; }
|
|
617
828
|
const id = typeof delegationId === 'string' ? delegationId.trim() : '';
|
|
618
|
-
if (!isId(id)) { toast('
|
|
829
|
+
if (!isId(id)) { toast('暂时无法打开此任务。', true); return null; }
|
|
619
830
|
const existing = tabs.find(tab => tab.kind === 'delegated' && tab.delegationId === id);
|
|
620
831
|
if (existing) {
|
|
621
832
|
activeTabId = existing.id;
|
|
@@ -625,9 +836,9 @@
|
|
|
625
836
|
return existing.id;
|
|
626
837
|
}
|
|
627
838
|
const view = delegationIndex.get(id);
|
|
628
|
-
if (!view) { toast('
|
|
839
|
+
if (!view) { toast('此任务不属于当前会话,请返回所属会话查看。', true); return null; }
|
|
629
840
|
const tab = {
|
|
630
|
-
id: nextTabId(), kind: 'delegated', delegationId: id,
|
|
841
|
+
id: nextTabId(), kind: 'delegated', delegationId: id, draft: (() => { try { return storage && storage.getItem(storageKey + ':draft:' + scopeStorageKey(scope) + ':' + id) || ''; } catch { return ''; } })(),
|
|
631
842
|
taskRef: null, providerId: null, descriptor: null,
|
|
632
843
|
invalid: null, panel: null, pane: null, invalidRendered: null,
|
|
633
844
|
};
|
|
@@ -674,9 +885,14 @@
|
|
|
674
885
|
panel.loading = true; panel.error = null; panel.loaded = false; panel.rendered = false;
|
|
675
886
|
render();
|
|
676
887
|
let page = null, error = null;
|
|
677
|
-
|
|
678
|
-
|
|
888
|
+
const permissionRevision = panel.permissionRevision || 0;
|
|
889
|
+
const [historyResult, snapshotResult] = await Promise.allSettled([
|
|
890
|
+
client.loadHistory(view.childSessionId, { limit: DOCK_PAGE_LIMIT }),
|
|
891
|
+
typeof client.loadSession === 'function' ? client.loadSession(view.childSessionId) : Promise.resolve(null),
|
|
892
|
+
]);
|
|
893
|
+
if (historyResult.status === 'fulfilled') page = historyResult.value; else error = historyResult.reason;
|
|
679
894
|
if (epoch !== scopeEpoch || !tabs.includes(tab) || panel.revision !== revision) return;
|
|
895
|
+
if (snapshotResult.status === 'fulfilled' && snapshotResult.value && permissionRevision === (panel.permissionRevision || 0)) panel.currentPermission = snapshotResult.value.permission || null;
|
|
680
896
|
panel.loading = false;
|
|
681
897
|
if (error) {
|
|
682
898
|
const code = (error && error.code) || String((error && error.status) || '');
|
|
@@ -686,10 +902,14 @@
|
|
|
686
902
|
return;
|
|
687
903
|
}
|
|
688
904
|
const events = page && Array.isArray(page.events) ? page.events : [];
|
|
689
|
-
panel.events = panel.
|
|
905
|
+
panel.events = mergeDelegationEvents(events, panel.events.filter(event => Number(event.sequence) > Number(page && page.highWater || Infinity)));
|
|
906
|
+
if (panel.loadedEarlier) panel.events = mergeDelegationEvents(panel.olderEvents || [], panel.events);
|
|
907
|
+
panel.highWater = Number(page && page.highWater) || Math.max(0, ...events.map(event => Number(event.sequence) || 0));
|
|
908
|
+
if (!delegationTitles.has(tab.delegationId)) { const prompt = events.find(event => event.type === 'claude/user-prompt' && event.text); if (prompt) delegationTitles.set(tab.delegationId, String(prompt.text).replace(/\s+/g, ' ').slice(0, 60)); }
|
|
690
909
|
panel.nextCursor = page && page.nextCursor || null;
|
|
691
910
|
panel.hasMore = isId(panel.nextCursor);
|
|
692
911
|
panel.loaded = true;
|
|
912
|
+
tab.connectionError = null;
|
|
693
913
|
panel.rendered = false;
|
|
694
914
|
panel.lastTurns = view.child && Number.isFinite(Number(view.child.turns)) ? Number(view.child.turns) : null;
|
|
695
915
|
render();
|
|
@@ -721,68 +941,163 @@
|
|
|
721
941
|
panel.events = [...older.filter(row => !known.has(delegationRowKey(row))), ...panel.events];
|
|
722
942
|
panel.nextCursor = page && page.nextCursor || null;
|
|
723
943
|
panel.hasMore = isId(panel.nextCursor);
|
|
724
|
-
panel.loadedEarlier = true;
|
|
944
|
+
panel.loadedEarlier = true; panel.olderEvents = panel.events.slice();
|
|
725
945
|
panel.rendered = false;
|
|
726
946
|
render();
|
|
727
947
|
}
|
|
728
948
|
|
|
729
|
-
function
|
|
730
|
-
if (
|
|
731
|
-
const
|
|
732
|
-
|
|
733
|
-
const
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
node.className = 'rd-item rd-dmsg user';
|
|
737
|
-
node.append(el('span', 'rd-role', '用户'));
|
|
738
|
-
const body = el('div', 'rd-text');
|
|
739
|
-
body.textContent = isId(event.text) ? String(event.text).slice(0, 4000) : '(空输入)';
|
|
740
|
-
node.append(body);
|
|
741
|
-
} else if (type === 'claude/text-delta') {
|
|
742
|
-
node.className = 'rd-item rd-dmsg assistant';
|
|
743
|
-
node.append(el('span', 'rd-role', '助手'));
|
|
744
|
-
const body = el('div', 'rd-text');
|
|
745
|
-
renderMdInto(body, isId(event.text) ? String(event.text).slice(0, 8000) : '');
|
|
746
|
-
node.append(body);
|
|
747
|
-
} else if (type === 'claude/thinking-delta') {
|
|
748
|
-
node.className = 'rd-item rd-dthink';
|
|
749
|
-
node.append(el('span', 'rd-role', '思考'));
|
|
750
|
-
const body = el('div', 'rd-text');
|
|
751
|
-
body.textContent = isId(event.text) ? String(event.text).slice(0, 2000) : '';
|
|
752
|
-
node.append(body);
|
|
753
|
-
} else if (type === 'claude/result') {
|
|
754
|
-
node.className = 'rd-item rd-dresult';
|
|
755
|
-
node.append(el('span', 'rd-state-text', `回复完成${isId(event.model) ? '(' + shortId(event.model) + ')' : ''}`));
|
|
756
|
-
if (isId(event.result)) node.append(el('pre', 'rd-pre', String(event.result).slice(0, 2000)));
|
|
757
|
-
} else if (type === 'claude/aborted') {
|
|
758
|
-
node.className = 'rd-item rd-interrupted';
|
|
759
|
-
node.textContent = '子会话回复被中止(aborted)';
|
|
760
|
-
} else if (type === 'claude/error') {
|
|
761
|
-
node.className = 'rd-item rd-derror';
|
|
762
|
-
node.textContent = '子会话错误:' + (isId(event.message) ? String(event.message).slice(0, 300) : isId(event.error) ? String(event.error).slice(0, 300) : '未知错误');
|
|
763
|
-
} else if (type === 'claude/session-ready') {
|
|
764
|
-
node.className = 'rd-item rd-dstate';
|
|
765
|
-
node.textContent = `子会话就绪${isId(event.model) ? '(模型 ' + shortId(event.model) + ')' : ''}`;
|
|
766
|
-
} else if (type === 'claude/state') {
|
|
767
|
-
node.className = 'rd-item rd-dstate';
|
|
768
|
-
node.textContent = '状态 ' + (isId(event.state) ? String(event.state) : '未知');
|
|
769
|
-
} else {
|
|
770
|
-
return null;
|
|
771
|
-
}
|
|
949
|
+
function buildReadOnlyTool(event) {
|
|
950
|
+
if (renderToolStep) return renderToolStep(event, event.input || {});
|
|
951
|
+
const node = el('details', 'step rd-toolstep');
|
|
952
|
+
node.append(el('summary', null, event.name || event.toolName || '工具'));
|
|
953
|
+
const body = el('div', 'step-body');
|
|
954
|
+
body.append(el('pre', 'rd-pre', JSON.stringify(event.input || {}, null, 2)));
|
|
955
|
+
node.append(body);
|
|
772
956
|
return node;
|
|
773
957
|
}
|
|
774
958
|
|
|
959
|
+
function finishReadOnlyTool(step, event) {
|
|
960
|
+
if (renderToolResult) { renderToolResult(step, event); return; }
|
|
961
|
+
const output = event.output == null ? '' : typeof event.output === 'string' ? event.output : JSON.stringify(event.output, null, 2);
|
|
962
|
+
step.append(el('pre', 'rd-pre', output));
|
|
963
|
+
if (event.isError) step.classList.add('failed');
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// Normalize transport events into conversation turns. Internal identities,
|
|
967
|
+
// dispatch state and diagnostics never enter customer DOM.
|
|
968
|
+
function createConversation(scroll) {
|
|
969
|
+
let turn = null;
|
|
970
|
+
const tools = new Map();
|
|
971
|
+
function current() {
|
|
972
|
+
if (!turn) turn = { messages: [], changes: [], process: null, live: null, thinking: null };
|
|
973
|
+
return turn;
|
|
974
|
+
}
|
|
975
|
+
function mark(node, sequence) {
|
|
976
|
+
if (Number.isFinite(Number(sequence))) node.dataset.seq = String(sequence);
|
|
977
|
+
return node;
|
|
978
|
+
}
|
|
979
|
+
function message(role, text, sequence) {
|
|
980
|
+
const node = renderMessage ? renderMessage(role, text) : el('div', role === 'user' ? 'msg user' : 'msg ai');
|
|
981
|
+
let body = node.querySelector && node.querySelector(role === 'user' ? '.bubble' : '.md-body');
|
|
982
|
+
if (!body) {
|
|
983
|
+
body = el('div', role === 'user' ? 'bubble' : 'md-body');
|
|
984
|
+
if (role === 'user') body.textContent = text;
|
|
985
|
+
else renderMdInto(body, text);
|
|
986
|
+
node.append(body);
|
|
987
|
+
}
|
|
988
|
+
mark(node, sequence);
|
|
989
|
+
scroll.append(node);
|
|
990
|
+
return { node, body, text };
|
|
991
|
+
}
|
|
992
|
+
function process(sequence) {
|
|
993
|
+
const t = current();
|
|
994
|
+
if (!t.process) {
|
|
995
|
+
if (renderProcess) t.process = renderProcess();
|
|
996
|
+
else {
|
|
997
|
+
const node = el('details', 'proc');
|
|
998
|
+
const meta = el('summary', 'p-meta');
|
|
999
|
+
const steps = el('div', 'proc-steps');
|
|
1000
|
+
node.append(meta, steps);
|
|
1001
|
+
t.process = { node, meta, steps };
|
|
1002
|
+
}
|
|
1003
|
+
t.process.node.open = false;
|
|
1004
|
+
scroll.append(t.process.node);
|
|
1005
|
+
}
|
|
1006
|
+
for (const msg of t.messages.splice(0)) {
|
|
1007
|
+
if (typeof msg.node.remove === 'function') msg.node.remove();
|
|
1008
|
+
t.process.steps.append(msg.node);
|
|
1009
|
+
}
|
|
1010
|
+
t.live = null;
|
|
1011
|
+
mark(t.process.node, sequence);
|
|
1012
|
+
return t.process;
|
|
1013
|
+
}
|
|
1014
|
+
function updateProcess(p) { p.meta.textContent = p.steps.children.length + ' 个步骤'; }
|
|
1015
|
+
function finish(sequence) {
|
|
1016
|
+
if (!turn) return;
|
|
1017
|
+
if (renderChanges && turn.changes.length) {
|
|
1018
|
+
const changes = renderChanges(turn.changes);
|
|
1019
|
+
if (changes) scroll.append(mark(changes, sequence));
|
|
1020
|
+
}
|
|
1021
|
+
turn = null;
|
|
1022
|
+
}
|
|
1023
|
+
function accept(event) {
|
|
1024
|
+
if (!event) return;
|
|
1025
|
+
const seq = event.sequence, type = event.type;
|
|
1026
|
+
if (type === 'claude/user-prompt') {
|
|
1027
|
+
finish(seq);
|
|
1028
|
+
if (event.text) message('user', String(event.text), seq);
|
|
1029
|
+
current();
|
|
1030
|
+
} else if (type === 'claude/text-start') {
|
|
1031
|
+
current().live = null;
|
|
1032
|
+
} else if (type === 'claude/text-delta' || type === 'conversation/assistant') {
|
|
1033
|
+
const text = String(event.text || '');
|
|
1034
|
+
if (!text) return;
|
|
1035
|
+
const t = current();
|
|
1036
|
+
if (type === 'conversation/assistant' || !t.live || t.live.id !== event.id) {
|
|
1037
|
+
t.live = message('assistant', text, seq);
|
|
1038
|
+
t.live.id = event.id;
|
|
1039
|
+
t.messages.push(t.live);
|
|
1040
|
+
} else {
|
|
1041
|
+
t.live.text += text;
|
|
1042
|
+
renderMdInto(t.live.body, t.live.text);
|
|
1043
|
+
mark(t.live.node, seq);
|
|
1044
|
+
}
|
|
1045
|
+
t.thinking = null;
|
|
1046
|
+
} else if (type === 'claude/tool-use') {
|
|
1047
|
+
const p = process(seq);
|
|
1048
|
+
const step = buildReadOnlyTool(event);
|
|
1049
|
+
p.steps.append(step);
|
|
1050
|
+
tools.set(event.id, step);
|
|
1051
|
+
updateProcess(p);
|
|
1052
|
+
current().thinking = null;
|
|
1053
|
+
} else if (type === 'claude/tool-result') {
|
|
1054
|
+
const step = tools.get(event.id);
|
|
1055
|
+
const p = current().process || process(seq);
|
|
1056
|
+
const target = step || buildReadOnlyTool({ id: event.id, name: event.toolName || '工具', input: {} });
|
|
1057
|
+
if (!step) p.steps.append(target);
|
|
1058
|
+
finishReadOnlyTool(target, event);
|
|
1059
|
+
current().changes.push({ ...event, input: target._toolInput || {} });
|
|
1060
|
+
mark(p.node, seq);
|
|
1061
|
+
updateProcess(p);
|
|
1062
|
+
} else if (type === 'claude/thinking-delta') {
|
|
1063
|
+
const t = current();
|
|
1064
|
+
const p = t.process || process(seq);
|
|
1065
|
+
if (!t.thinking) {
|
|
1066
|
+
const node = el('div', 'think-seg');
|
|
1067
|
+
node.append(el('div', 'think-label', '思考'));
|
|
1068
|
+
const body = el('div', 'think-body');
|
|
1069
|
+
node.append(body);
|
|
1070
|
+
p.steps.append(node);
|
|
1071
|
+
t.thinking = body;
|
|
1072
|
+
}
|
|
1073
|
+
t.thinking.textContent += String(event.text || '');
|
|
1074
|
+
mark(p.node, seq);
|
|
1075
|
+
updateProcess(p);
|
|
1076
|
+
} else if (type === 'claude/result') {
|
|
1077
|
+
const t = current();
|
|
1078
|
+
if (event.isError) scroll.append(mark(el('div', 'turn-note', '回复失败,请稍后重试。'), seq));
|
|
1079
|
+
else if (!t.messages.length && event.result) message('assistant', String(event.result), seq);
|
|
1080
|
+
finish(seq);
|
|
1081
|
+
} else if (type === 'conversation/unavailable') {
|
|
1082
|
+
scroll.append(mark(el('div', 'turn-note', '任务记录暂不可用,请刷新重试。'), seq));
|
|
1083
|
+
} else if (type === 'claude/aborted' || type === 'claude/task-interrupted' || type === 'claude/error') {
|
|
1084
|
+
scroll.append(mark(el('div', 'turn-note', type === 'claude/error' ? '回复失败,请稍后重试。' : '回复已中断'), seq));
|
|
1085
|
+
finish(seq);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
return { accept, finish };
|
|
1089
|
+
}
|
|
1090
|
+
|
|
775
1091
|
function renderDelegationTimeline(tab) {
|
|
776
1092
|
const pane = tab.pane;
|
|
777
1093
|
const panel = ensureDelegationPanelState(tab);
|
|
778
1094
|
if (!pane) return;
|
|
779
1095
|
const keepScroll = pane.follow === false ? pane.scroll.scrollTop : null;
|
|
780
1096
|
pane.scroll.textContent = '';
|
|
1097
|
+
const conversation = createConversation(pane.scroll);
|
|
781
1098
|
const ordered = [...panel.events].sort((a, b) => (Number(a && a.sequence) || 0) - (Number(b && b.sequence) || 0));
|
|
782
|
-
for (const event of ordered)
|
|
783
|
-
|
|
784
|
-
if (node) pane.scroll.append(node);
|
|
785
|
-
}
|
|
1099
|
+
for (const event of ordered) conversation.accept(event);
|
|
1100
|
+
pane.conversation = conversation;
|
|
786
1101
|
panel.rendered = true;
|
|
787
1102
|
if (keepScroll !== null) pane.scroll.scrollTop = keepScroll;
|
|
788
1103
|
else if (pane.follow !== false) pane.scroll.scrollTop = pane.scroll.scrollHeight;
|
|
@@ -798,104 +1113,62 @@
|
|
|
798
1113
|
return null;
|
|
799
1114
|
}
|
|
800
1115
|
|
|
1116
|
+
function taskTitle(entry) {
|
|
1117
|
+
const title = entry && entry.launcherToolUseId && deps && typeof deps.taskTitle === 'function' ? deps.taskTitle(entry.launcherToolUseId) : null;
|
|
1118
|
+
return String(title || entry && entry.description || '子代理任务').replace(/\s+/g, ' ').slice(0, 60);
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
function delegationTitle(id) {
|
|
1122
|
+
if (delegationTitles.has(id)) return delegationTitles.get(id);
|
|
1123
|
+
const tab = tabs.find(item => item.kind === 'delegated' && item.delegationId === id && item.panel);
|
|
1124
|
+
if (tab && tab.title) return tab.title;
|
|
1125
|
+
const prompt = tab && tab.panel.events.find(event => event.type === 'claude/user-prompt' && event.text);
|
|
1126
|
+
return prompt ? String(prompt.text).replace(/\s+/g, ' ').slice(0, 60) : '子代理任务';
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
function delegationStatus(view) {
|
|
1130
|
+
if (!view) return { status: 'pending', label: '状态待确认' };
|
|
1131
|
+
if (view.phase === 'dispatch-unknown' || view.outcome === 'unknown') return { status: 'pending', label: '状态待确认' };
|
|
1132
|
+
if (view.phase === 'child-create-failed') return { status: 'failed', label: '任务未能启动' };
|
|
1133
|
+
const child = view.child;
|
|
1134
|
+
if (!child.present) return { status: 'failed', label: '任务暂不可用' };
|
|
1135
|
+
if (child.state === 'failed') return { status: 'failed', label: '失败' };
|
|
1136
|
+
if (child.state === 'aborted') return { status: 'stopped', label: '已停止' };
|
|
1137
|
+
if (child.state === 'pending-permission') return { status: 'pending', label: '等待你确认' };
|
|
1138
|
+
if (child.active) return { status: 'running', label: '运行中' };
|
|
1139
|
+
if (['dispatched', 'completed'].includes(view.phase) && child.state === 'idle' && child.turns > 0) return { status: 'completed', label: '已完成' };
|
|
1140
|
+
return { status: 'pending', label: '待执行' };
|
|
1141
|
+
}
|
|
1142
|
+
|
|
801
1143
|
function buildDelegationPane(tab) {
|
|
802
|
-
|
|
803
|
-
node.
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
head.append(title, chips);
|
|
808
|
-
const meta = el('div', 'rd-meta');
|
|
809
|
-
const stopRow = el('div', 'rd-stoprow');
|
|
810
|
-
const stop = el('button', 'rd-stop');
|
|
811
|
-
stop.disabled = true;
|
|
812
|
-
stop.textContent = '停止子会话';
|
|
813
|
-
stop.title = DELEGATION_STOP_NOTE;
|
|
814
|
-
const stopNote = el('span', 'rd-stopnote', 'stop 未交付,禁用;关闭视图不中止子会话。');
|
|
815
|
-
stopRow.append(stop, stopNote);
|
|
816
|
-
const errorBox = el('div', 'rd-error');
|
|
817
|
-
errorBox.hidden = true;
|
|
818
|
-
const scroll = el('div', 'rd-scroll');
|
|
819
|
-
scroll.addEventListener('scroll', () => {
|
|
820
|
-
const pane = tab.pane;
|
|
821
|
-
if (!pane) return;
|
|
822
|
-
const distance = (scroll.scrollHeight || 0) - (scroll.scrollTop || 0) - (scroll.clientHeight || 0);
|
|
823
|
-
const wasFollowing = pane.follow !== false;
|
|
824
|
-
pane.follow = distance <= 2;
|
|
825
|
-
if (wasFollowing && !pane.follow) pane.anchorSeq = lastDelegationSequenceOf(tab);
|
|
826
|
-
schedulePersist();
|
|
827
|
-
});
|
|
828
|
-
const more = el('button', 'rd-more', '加载更早记录');
|
|
829
|
-
more.addEventListener('click', () => { void loadEarlierDelegation(tab); });
|
|
830
|
-
const note = el('div', 'rd-note', DELEGATION_PICKER_NOTE);
|
|
831
|
-
node.append(head, meta, stopRow, errorBox, scroll, more, note);
|
|
832
|
-
tab.pane = { node, head, title, chips, meta, stopRow, errorBox, scroll, more, note,
|
|
833
|
-
follow: true, anchorSeq: null, anchorNote: null, pendingNew: false, finalNode: null };
|
|
1144
|
+
buildPane(tab);
|
|
1145
|
+
tab.pane.node.classList.add('rd-delegation-pane');
|
|
1146
|
+
tab.pane.composer = buildComposer(tab);
|
|
1147
|
+
tab.pane.permissions = el('div', 'rd-permissions');
|
|
1148
|
+
tab.pane.node.append(tab.pane.permissions, tab.pane.composer.node);
|
|
834
1149
|
}
|
|
835
1150
|
|
|
836
|
-
// Chips and meta mirror the trusted delegation view, never a guess: a
|
|
837
|
-
// dispatch-unknown verdict shows as an explicit conflict and is never
|
|
838
|
-
// rendered as a success, and a missing child stays a dangling fact.
|
|
839
1151
|
function refreshDelegationPane(tab) {
|
|
840
1152
|
const pane = tab.pane;
|
|
841
1153
|
if (!pane) return;
|
|
842
1154
|
const panel = ensureDelegationPanelState(tab);
|
|
843
1155
|
const view = delegationIndex.get(tab.delegationId) || null;
|
|
1156
|
+
pane.title.textContent = delegationTitle(tab.delegationId);
|
|
1157
|
+
pane.status.textContent = delegationStatus(view).label;
|
|
1158
|
+
pane.errorBox.hidden = !tab.invalid && !panel.error;
|
|
1159
|
+
if (tab.invalid || panel.error) pane.errorBox.textContent = '暂时无法查看此任务,请刷新后重试。';
|
|
1160
|
+
pane.banner.hidden = true;
|
|
844
1161
|
if (tab.invalid) {
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
pane.meta.textContent = '';
|
|
848
|
-
pane.scroll.textContent = '';
|
|
849
|
-
pane.more.hidden = true;
|
|
850
|
-
pane.errorBox.hidden = false;
|
|
851
|
-
pane.errorBox.textContent = `委派不可用(${tab.invalid.code || '未知'}):${tab.invalid.message || ''}`;
|
|
852
|
-
tab.invalidRendered = true;
|
|
853
|
-
}
|
|
1162
|
+
pane.scroll.textContent = '';
|
|
1163
|
+
pane.more.hidden = true;
|
|
854
1164
|
return;
|
|
855
1165
|
}
|
|
856
|
-
tab.invalidRendered = null;
|
|
857
|
-
pane.errorBox.hidden = !panel.error;
|
|
858
|
-
if (panel.error) pane.errorBox.textContent = panel.error;
|
|
859
|
-
pane.chips.textContent = '';
|
|
860
|
-
const phase = view ? view.phase : null;
|
|
861
|
-
const phaseClass = phase === 'dispatch-unknown' || phase === 'child-create-failed' ? 'conflict' : (phase || 'unknown');
|
|
862
|
-
pane.chips.append(el('span', 'rd-chip ' + phaseClass, view ? (DELEGATION_PHASE_LABELS[phase] || phase) : '关系未索引'));
|
|
863
|
-
if (view && view.outcome) pane.chips.append(el('span', 'rd-chip kind', DELEGATION_OUTCOME_LABELS[view.outcome] || view.outcome));
|
|
864
|
-
if (view) {
|
|
865
|
-
const child = view.child;
|
|
866
|
-
const childLabel = child.present ? (CHILD_STATE_LABELS[child.state] || child.state || '状态未知') : '子会话缺失(悬空)';
|
|
867
|
-
const childClass = child.present ? (child.active ? 'running' : (child.state || 'unknown')) : 'missing';
|
|
868
|
-
pane.chips.append(el('span', 'rd-chip ' + childClass, childLabel));
|
|
869
|
-
}
|
|
870
|
-
// Meta mirrors the latest trusted view instead of a load-time
|
|
871
|
-
// snapshot, so dispatch state/attempt/turns stay current on refresh.
|
|
872
|
-
pane.meta.textContent = '';
|
|
873
|
-
const d = view || { parentSessionId: scope ? scope.parentSessionId : '' };
|
|
874
|
-
pane.meta.append(
|
|
875
|
-
metaRow('delegationId', tab.delegationId.length > 44 ? shortId(tab.delegationId) : tab.delegationId),
|
|
876
|
-
metaRow('childSessionId', d.childSessionId ? shortId(d.childSessionId) : '—'),
|
|
877
|
-
metaRow('parentSessionId', d.parentSessionId ? shortId(d.parentSessionId) : '—'),
|
|
878
|
-
metaRow('workspaceRef', d.workspaceRef ? shortId(d.workspaceRef) : '—'),
|
|
879
|
-
metaRow('requestId', d.requestId ? shortId(d.requestId) : '—'),
|
|
880
|
-
metaRow('phase', d.phase ? (DELEGATION_PHASE_LABELS[d.phase] || d.phase) : '—'),
|
|
881
|
-
metaRow('outcome', d.outcome ? (DELEGATION_OUTCOME_LABELS[d.outcome] || d.outcome) : '—'),
|
|
882
|
-
);
|
|
883
|
-
if (view && view.dispatch) {
|
|
884
|
-
pane.meta.append(metaRow('dispatch', view.dispatch.state ? (DELEGATION_DISPATCH_LABELS[view.dispatch.state] || view.dispatch.state) : '—'));
|
|
885
|
-
if (view.dispatch.attemptId) pane.meta.append(metaRow('attemptId', shortId(view.dispatch.attemptId)));
|
|
886
|
-
if (view.dispatch.at) pane.meta.append(metaRow('派发时间', shortId(view.dispatch.at)));
|
|
887
|
-
if (view.dispatch.reason) pane.meta.append(metaRow('派发原因', view.dispatch.reason));
|
|
888
|
-
if (view.dispatch.code) pane.meta.append(metaRow('派发错误码', view.dispatch.code));
|
|
889
|
-
if (view.dispatch.errorCode) pane.meta.append(metaRow('errorCode', view.dispatch.errorCode));
|
|
890
|
-
}
|
|
891
|
-
if (view && view.child) {
|
|
892
|
-
if (view.child.agentId) pane.meta.append(metaRow('agentId', shortId(view.child.agentId)));
|
|
893
|
-
if (view.child.model) pane.meta.append(metaRow('model', view.child.model));
|
|
894
|
-
if (view.child.turns != null) pane.meta.append(metaRow('turns', view.child.turns));
|
|
895
|
-
if (view.child.archived) pane.meta.append(metaRow('归档', '是'));
|
|
896
|
-
}
|
|
897
|
-
if (view && view.issuedAt) pane.meta.append(metaRow('issuedAt', shortId(view.issuedAt)));
|
|
898
1166
|
if (!panel.rendered) renderDelegationTimeline(tab);
|
|
1167
|
+
if (tab.error || tab.connectionError) { pane.errorBox.hidden = false; pane.errorBox.textContent = tab.error || tab.connectionError; }
|
|
1168
|
+
if (pane.composer) { pane.composer.setBusy(childBusy(tab)); pane.composer.send.disabled = Boolean(tab.invalid || tab.sending || !view || !view.child.present); }
|
|
1169
|
+
if (pane.composer && view) pane.composer.setLabel?.((view.child.model || '子代理') + ' · 默认防护');
|
|
1170
|
+
renderPermissions(tab);
|
|
1171
|
+
subscribeChild(tab);
|
|
899
1172
|
pane.more.hidden = !(panel.hasMore && panel.nextCursor);
|
|
900
1173
|
pane.more.disabled = panel.loading === true;
|
|
901
1174
|
}
|
|
@@ -926,6 +1199,7 @@
|
|
|
926
1199
|
}
|
|
927
1200
|
renderTabBar();
|
|
928
1201
|
renderToggleState();
|
|
1202
|
+
for (const tab of tabs) if (!open || tab.id !== activeTabId) releaseSubscription(tab);
|
|
929
1203
|
// Live native state events move task status immediately and with it
|
|
930
1204
|
// backgroundActivity; notify the host listener on the same beat.
|
|
931
1205
|
notifyBackgroundActivity();
|
|
@@ -963,15 +1237,11 @@
|
|
|
963
1237
|
panel.usageSummary = event.usage.summary;
|
|
964
1238
|
}
|
|
965
1239
|
if (!panel.loaded || !tab.pane) return true;
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
if (built.textNode && tab.pane.finalNode && tab.pane.finalNode.classList) tab.pane.finalNode.classList.remove('rd-final');
|
|
969
|
-
tab.pane.scroll.append(built.node);
|
|
970
|
-
if (built.textNode) { built.textNode.classList.add('rd-final'); tab.pane.finalNode = built.textNode; }
|
|
971
|
-
}
|
|
1240
|
+
if (!tab.pane.conversation) renderTimeline(tab);
|
|
1241
|
+
else appendNativeConversation(tab.pane.conversation, event);
|
|
972
1242
|
if (tab.pane.follow !== false) tab.pane.scroll.scrollTop = tab.pane.scroll.scrollHeight;
|
|
973
1243
|
else tab.pane.pendingNew = true;
|
|
974
|
-
|
|
1244
|
+
|
|
975
1245
|
return true;
|
|
976
1246
|
}
|
|
977
1247
|
|
|
@@ -1037,6 +1307,7 @@
|
|
|
1037
1307
|
flushPersist();
|
|
1038
1308
|
scopeEpoch += 1;
|
|
1039
1309
|
scope = null;
|
|
1310
|
+
for (const tab of tabs) releaseSubscription(tab);
|
|
1040
1311
|
tabs = [];
|
|
1041
1312
|
activeTabId = null;
|
|
1042
1313
|
pickerOpen = false;
|
|
@@ -1047,10 +1318,19 @@
|
|
|
1047
1318
|
notifyPanelsChanged('scope');
|
|
1048
1319
|
}
|
|
1049
1320
|
|
|
1050
|
-
function openTask(refInput) {
|
|
1321
|
+
function openTask(refInput, options = {}) {
|
|
1051
1322
|
const ref = sanitizeTaskRef(refInput);
|
|
1052
1323
|
if (!ref) { toast('无效的任务引用,未打开面板。', true); return null; }
|
|
1053
1324
|
if (!scope || !scopeRefMatches(scope, ref)) { toast('该任务不属于当前会话范围;请在所属会话中打开。', true); return null; }
|
|
1325
|
+
const existing = options.reuse && tabs.find(tab => tab.taskRef && taskRefKey(tab.taskRef) === taskRefKey(ref));
|
|
1326
|
+
if (existing) {
|
|
1327
|
+
activeTabId = existing.id;
|
|
1328
|
+
open = true;
|
|
1329
|
+
pickerOpen = false;
|
|
1330
|
+
render();
|
|
1331
|
+
schedulePersist();
|
|
1332
|
+
return existing.id;
|
|
1333
|
+
}
|
|
1054
1334
|
const tab = {
|
|
1055
1335
|
id: 'tab-' + Date.now().toString(36) + '-' + (++tabSeq),
|
|
1056
1336
|
taskRef: ref, invalid: null, panel: null, pane: null, invalidRendered: null,
|
|
@@ -1077,10 +1357,10 @@
|
|
|
1077
1357
|
}
|
|
1078
1358
|
|
|
1079
1359
|
function openPanel(providerIdInput) {
|
|
1080
|
-
if (!registry) { toast('
|
|
1360
|
+
if (!registry) { toast('暂时无法打开此内容。', true); return null; }
|
|
1081
1361
|
const providerId = isId(providerIdInput) ? String(providerIdInput).trim() : '';
|
|
1082
1362
|
const descriptor = registry.get(providerId);
|
|
1083
|
-
if (!descriptor) { toast('
|
|
1363
|
+
if (!descriptor) { toast('此内容当前不可用。', true); return null; }
|
|
1084
1364
|
if (!providerAppliesToScope(descriptor)) { toast('该面板不适用于当前项目范围。', true); return null; }
|
|
1085
1365
|
if (descriptor.multiInstance !== 'multiple') {
|
|
1086
1366
|
const existing = tabs.find(tab => tab.kind === 'host' && tab.providerId === providerId);
|
|
@@ -1137,8 +1417,10 @@
|
|
|
1137
1417
|
const index = tabs.findIndex(tab => tab.id === tabId);
|
|
1138
1418
|
if (index === -1) return;
|
|
1139
1419
|
const [tab] = tabs.splice(index, 1);
|
|
1420
|
+
saveTabDraft(tab); releaseSubscription(tab);
|
|
1140
1421
|
if (tab.pane && tab.pane.node && typeof tab.pane.node.remove === 'function') tab.pane.node.remove();
|
|
1141
1422
|
if (activeTabId === tabId) activeTabId = (tabs[index] || tabs[index - 1] || {}).id || null;
|
|
1423
|
+
if (!tabs.length) { open = false; pickerOpen = false; }
|
|
1142
1424
|
render();
|
|
1143
1425
|
schedulePersist();
|
|
1144
1426
|
if (tab.kind === 'host') {
|
|
@@ -1150,6 +1432,7 @@
|
|
|
1150
1432
|
|
|
1151
1433
|
function toggle() {
|
|
1152
1434
|
open = !open;
|
|
1435
|
+
if (open && !tabs.length) { openPicker(); return open; }
|
|
1153
1436
|
if (open) pickerOpen = false;
|
|
1154
1437
|
render();
|
|
1155
1438
|
schedulePersist();
|
|
@@ -1223,13 +1506,23 @@
|
|
|
1223
1506
|
root.append(resizer, body);
|
|
1224
1507
|
}
|
|
1225
1508
|
if (toggleBtn) toggleBtn.addEventListener('click', () => { toggle(); });
|
|
1509
|
+
if (typeof global.ResizeObserver === 'function' && root && root.parentNode) { const observer = new global.ResizeObserver(applyDockVisibility); observer.observe(root.parentNode); }
|
|
1226
1510
|
domBound = true;
|
|
1227
1511
|
}
|
|
1228
1512
|
|
|
1229
1513
|
function applyDockVisibility() {
|
|
1230
1514
|
if (!root) return;
|
|
1231
1515
|
root.hidden = !open;
|
|
1232
|
-
|
|
1516
|
+
const parent = root.parentNode;
|
|
1517
|
+
const rect = parent && typeof parent.getBoundingClientRect === 'function' ? parent.getBoundingClientRect() : null;
|
|
1518
|
+
const sidebar = parent && parent.querySelector ? parent.querySelector('.sidebar') : null;
|
|
1519
|
+
const sideWidth = sidebar && sidebar.getBoundingClientRect ? sidebar.getBoundingClientRect().width : 0;
|
|
1520
|
+
const available = rect ? rect.width - sideWidth : 0;
|
|
1521
|
+
const drawer = available > 0 && available < 821;
|
|
1522
|
+
if (root.classList) { if (drawer) root.classList.add('rd-drawer'); else root.classList.remove('rd-drawer'); }
|
|
1523
|
+
const effective = available ? drawer ? Math.min(width, available - 24) : Math.min(width, available - 481) : width;
|
|
1524
|
+
root.style.width = open ? Math.max(0, effective) + 'px' : '';
|
|
1525
|
+
if (root.parentNode && root.parentNode.classList) root.parentNode.classList.toggle('dock-open', open);
|
|
1233
1526
|
}
|
|
1234
1527
|
|
|
1235
1528
|
function runningCount() {
|
|
@@ -1243,7 +1536,7 @@
|
|
|
1243
1536
|
if (!toggleBtn) return;
|
|
1244
1537
|
toggleBtn.hidden = false;
|
|
1245
1538
|
toggleBtn.setAttribute('aria-expanded', String(open));
|
|
1246
|
-
toggleBtn.title = open ? '
|
|
1539
|
+
toggleBtn.title = open ? '收起右侧面板' : '展开右侧面板';
|
|
1247
1540
|
const badge = toggleBtn.querySelector && toggleBtn.querySelector('.rd-toggle-count');
|
|
1248
1541
|
if (badge) {
|
|
1249
1542
|
const running = runningCount();
|
|
@@ -1257,68 +1550,87 @@
|
|
|
1257
1550
|
function render() {
|
|
1258
1551
|
ensureDom();
|
|
1259
1552
|
applyDockVisibility();
|
|
1553
|
+
for (const tab of tabs) if (!open || tab.id !== activeTabId) releaseSubscription(tab);
|
|
1260
1554
|
renderTabBar();
|
|
1261
1555
|
renderActivePane();
|
|
1262
1556
|
renderPicker();
|
|
1263
1557
|
renderToggleState();
|
|
1264
1558
|
}
|
|
1265
1559
|
|
|
1560
|
+
function collapseButton() {
|
|
1561
|
+
const button = el('button', 'rd-collapse', '收起');
|
|
1562
|
+
button.type = 'button';
|
|
1563
|
+
button.setAttribute('aria-label', '收起任务面板');
|
|
1564
|
+
button.addEventListener('click', () => { toggle(); });
|
|
1565
|
+
return button;
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1266
1568
|
function renderTabBar() {
|
|
1267
1569
|
if (!tabBar) return;
|
|
1268
1570
|
tabBar.textContent = '';
|
|
1269
|
-
|
|
1571
|
+
|
|
1270
1572
|
tabBar.hidden = false;
|
|
1573
|
+
const tabList = el('div', 'rd-tab-list');
|
|
1574
|
+
tabBar.append(tabList);
|
|
1271
1575
|
for (const tab of tabs) {
|
|
1272
1576
|
const entry = tab.taskRef ? taskIndex.get(taskRefKey(tab.taskRef)) : null;
|
|
1273
|
-
const status = entry ? entry.status : null;
|
|
1274
|
-
const item = el('
|
|
1275
|
-
const
|
|
1276
|
-
|
|
1277
|
-
|
|
1577
|
+
const status = entry && !entry.typeConflict ? entry.status : null;
|
|
1578
|
+
const item = el('div', 'rd-tab' + (tab.id === activeTabId ? ' active' : ''));
|
|
1579
|
+
const tabOpen = el('button', 'rd-tab-open'); tabOpen.setAttribute('role', 'tab'); tabOpen.setAttribute('aria-selected', String(tab.id === activeTabId));
|
|
1580
|
+
const close = el('button', 'rd-tab-close'); close.type = 'button';
|
|
1581
|
+
if (tab.kind === 'draft' || tab.kind === 'launcher') {
|
|
1582
|
+
item.title = tab.kind === 'launcher' ? '新标签页' : '新子代理';
|
|
1583
|
+
close.textContent = '×'; close.setAttribute('aria-label', '关闭' + item.title);
|
|
1584
|
+
close.addEventListener('click', ev => { if (ev && ev.stopPropagation) ev.stopPropagation(); closeTab(tab.id); });
|
|
1585
|
+
tabOpen.append(el('span', 'rd-tab-label', item.title)); item.append(tabOpen, close);
|
|
1586
|
+
} else if (tab.kind === 'host') {
|
|
1587
|
+
const descriptor = tab.descriptor || (registry ? registry.get(tab.providerId) : null) || { title: '参考资料', panelType: 'knowledge' };
|
|
1278
1588
|
item.title = descriptor.title;
|
|
1279
1589
|
const dot = el('span', 'rd-dot host');
|
|
1280
1590
|
const label = el('span', 'rd-tab-label', descriptor.title);
|
|
1281
|
-
close.title = '
|
|
1591
|
+
close.title = '关闭';
|
|
1282
1592
|
close.setAttribute('aria-label', close.title);
|
|
1283
1593
|
close.textContent = '×';
|
|
1284
1594
|
close.addEventListener('click', ev => { if (ev && ev.stopPropagation) ev.stopPropagation(); closeTab(tab.id); });
|
|
1285
|
-
|
|
1595
|
+
tabOpen.append(dot, label); item.append(tabOpen, close);
|
|
1286
1596
|
} else if (tab.kind === 'delegated') {
|
|
1287
|
-
item.title = tab.delegationId;
|
|
1597
|
+
item.title = delegationTitle(tab.delegationId);
|
|
1288
1598
|
const entry = delegationIndex.get(tab.delegationId);
|
|
1289
|
-
const
|
|
1290
|
-
const dotClass = entry && entry.child && entry.child.active ? 'running' : childState || 'unknown';
|
|
1599
|
+
const dotClass = delegationStatus(entry).status;
|
|
1291
1600
|
const dot = el('span', 'rd-dot ' + dotClass);
|
|
1292
|
-
const label = el('span', 'rd-tab-label',
|
|
1293
|
-
close.title = '
|
|
1601
|
+
const label = el('span', 'rd-tab-label', delegationTitle(tab.delegationId));
|
|
1602
|
+
close.title = '关闭';
|
|
1294
1603
|
close.setAttribute('aria-label', close.title);
|
|
1295
1604
|
close.textContent = '×';
|
|
1296
1605
|
close.addEventListener('click', ev => { if (ev && ev.stopPropagation) ev.stopPropagation(); closeTab(tab.id); });
|
|
1297
|
-
|
|
1606
|
+
tabOpen.append(dot, label); item.append(tabOpen, close);
|
|
1298
1607
|
} else {
|
|
1299
|
-
item.title =
|
|
1608
|
+
item.title = taskTitle(entry);
|
|
1300
1609
|
const dot = el('span', 'rd-dot ' + (status || 'unknown'));
|
|
1301
|
-
const label = el('span', 'rd-tab-label',
|
|
1302
|
-
close.title = '
|
|
1610
|
+
const label = el('span', 'rd-tab-label', taskTitle(entry));
|
|
1611
|
+
close.title = '关闭';
|
|
1303
1612
|
close.setAttribute('aria-label', close.title);
|
|
1304
1613
|
close.textContent = '×';
|
|
1305
1614
|
close.addEventListener('click', ev => { if (ev && ev.stopPropagation) ev.stopPropagation(); closeTab(tab.id); });
|
|
1306
|
-
|
|
1615
|
+
tabOpen.append(dot, label); item.append(tabOpen, close);
|
|
1307
1616
|
}
|
|
1617
|
+
close.setAttribute('aria-label', '关闭' + item.title);
|
|
1308
1618
|
item.addEventListener('click', () => {
|
|
1309
1619
|
activeTabId = tab.id;
|
|
1310
1620
|
render();
|
|
1311
1621
|
schedulePersist();
|
|
1312
1622
|
if (tab.kind === 'host') notifyPanelsChanged('active');
|
|
1313
1623
|
});
|
|
1314
|
-
|
|
1624
|
+
tabList.append(item);
|
|
1315
1625
|
}
|
|
1316
1626
|
const plus = el('button', 'rd-plus');
|
|
1317
|
-
plus.title = '
|
|
1627
|
+
plus.title = '新建标签页';
|
|
1318
1628
|
plus.setAttribute('aria-label', plus.title);
|
|
1319
1629
|
plus.textContent = '+';
|
|
1320
|
-
plus.addEventListener('click', () => {
|
|
1321
|
-
tabBar.append(plus);
|
|
1630
|
+
plus.addEventListener('click', () => { openPicker(); });
|
|
1631
|
+
tabBar.append(plus, collapseButton());
|
|
1632
|
+
const selected = tabList.querySelector && tabList.querySelector('[aria-selected="true"]');
|
|
1633
|
+
if (selected && selected.scrollIntoView) selected.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
1322
1634
|
}
|
|
1323
1635
|
|
|
1324
1636
|
function renderActivePane() {
|
|
@@ -1336,24 +1648,33 @@
|
|
|
1336
1648
|
tab.pane.node.hidden = tab.id !== activeTabId;
|
|
1337
1649
|
}
|
|
1338
1650
|
const activeBeforeScope = tabs.find(tab => tab.id === activeTabId) || tabs[0];
|
|
1651
|
+
if (activeBeforeScope && ['draft', 'launcher'].includes(activeBeforeScope.kind)) {
|
|
1652
|
+
const tab = activeBeforeScope;
|
|
1653
|
+
if (!tab.pane) { if (tab.kind === 'draft') buildDraftPane(tab); else buildLauncherPane(tab); paneHost.append(tab.pane.node); }
|
|
1654
|
+
hideEmptyPane(); tab.pane.node.hidden = false;
|
|
1655
|
+
if (tab.kind === 'draft') { tab.pane.errorBox.hidden = !tab.error; tab.pane.errorBox.textContent = tab.error || ''; tab.pane.composer.setBusy(tab.sending); tab.pane.composer.send.disabled = Boolean(tab.sending); }
|
|
1656
|
+
return;
|
|
1657
|
+
}
|
|
1339
1658
|
if (!scope && (!activeBeforeScope || activeBeforeScope.kind !== 'host')) {
|
|
1340
|
-
renderEmptyPane('
|
|
1659
|
+
renderEmptyPane('打开会话后,可在这里查看子代理任务。');
|
|
1341
1660
|
return;
|
|
1342
1661
|
}
|
|
1343
1662
|
if (!tabs.length) {
|
|
1344
|
-
renderEmptyPane(
|
|
1663
|
+
renderEmptyPane('点击 + 添加页面。');
|
|
1345
1664
|
return;
|
|
1346
1665
|
}
|
|
1347
1666
|
hideEmptyPane();
|
|
1348
1667
|
const active = tabs.find(tab => tab.id === activeTabId) || tabs[0];
|
|
1349
1668
|
if (active && active.id !== activeTabId) activeTabId = active.id;
|
|
1350
1669
|
if (!active) return;
|
|
1670
|
+
if (active.kind === 'host' && !active.descriptor) { active.descriptor = registry && registry.get(active.providerId); if (!active.descriptor) { renderEmptyPane('正在准备知识引用…'); return; } }
|
|
1351
1671
|
if (!active.pane) {
|
|
1352
1672
|
if (active.kind === 'host') buildHostPane(active);
|
|
1353
1673
|
else if (active.kind === 'delegated') buildDelegationPane(active);
|
|
1354
1674
|
else buildPane(active);
|
|
1355
1675
|
paneHost.append(active.pane.node);
|
|
1356
1676
|
}
|
|
1677
|
+
active.pane.node.hidden = false;
|
|
1357
1678
|
if (active.kind === 'host') refreshHostPane(active);
|
|
1358
1679
|
else if (active.kind === 'delegated') refreshDelegationPane(active);
|
|
1359
1680
|
else refreshPane(active);
|
|
@@ -1374,21 +1695,13 @@
|
|
|
1374
1695
|
const node = el('section', 'rd-pane');
|
|
1375
1696
|
node.hidden = true;
|
|
1376
1697
|
const head = el('div', 'rd-head');
|
|
1377
|
-
const title = el('strong', 'rd-title', '
|
|
1378
|
-
const
|
|
1379
|
-
head.append(
|
|
1380
|
-
const meta = el('div', 'rd-meta');
|
|
1381
|
-
const stopRow = el('div', 'rd-stoprow');
|
|
1382
|
-
const stop = el('button', 'rd-stop');
|
|
1383
|
-
stop.disabled = true;
|
|
1384
|
-
stop.textContent = '停止此任务';
|
|
1385
|
-
stop.title = STOP_NOTE;
|
|
1386
|
-
const stopNote = el('span', 'rd-stopnote', '精确 stop API 未交付,本切片禁用。');
|
|
1387
|
-
stopRow.append(stop, stopNote);
|
|
1698
|
+
const title = el('strong', 'rd-title', '子代理任务');
|
|
1699
|
+
const status = el('span', 'rd-task-status');
|
|
1700
|
+
head.append(el('span', 'rd-thread-note', '关联主对话'), status); title.hidden = true;
|
|
1388
1701
|
const banner = el('div', 'rd-banner');
|
|
1389
1702
|
banner.hidden = true;
|
|
1390
1703
|
const bannerText = el('span', 'rd-banner-text');
|
|
1391
|
-
const resyncBtn = el('button', 'rd-resync', '
|
|
1704
|
+
const resyncBtn = el('button', 'rd-resync', '刷新');
|
|
1392
1705
|
resyncBtn.addEventListener('click', () => { void validateScopeTasks(); });
|
|
1393
1706
|
banner.append(bannerText, resyncBtn);
|
|
1394
1707
|
const errorBox = el('div', 'rd-error');
|
|
@@ -1400,15 +1713,27 @@
|
|
|
1400
1713
|
const distance = (scroll.scrollHeight || 0) - (scroll.scrollTop || 0) - (scroll.clientHeight || 0);
|
|
1401
1714
|
const wasFollowing = pane.follow !== false;
|
|
1402
1715
|
pane.follow = distance <= 2;
|
|
1403
|
-
if (wasFollowing && !pane.follow) pane.anchorSeq = lastSequenceOf(tab);
|
|
1716
|
+
if (wasFollowing && !pane.follow) pane.anchorSeq = tab.kind === 'delegated' ? lastDelegationSequenceOf(tab) : lastSequenceOf(tab);
|
|
1404
1717
|
schedulePersist();
|
|
1405
1718
|
});
|
|
1406
1719
|
const more = el('button', 'rd-more', '加载更早记录');
|
|
1407
|
-
more.addEventListener('click', () => { void loadEarlier(tab); });
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1720
|
+
more.addEventListener('click', () => { void (tab.kind === 'delegated' ? loadEarlierDelegation(tab) : loadEarlier(tab)); });
|
|
1721
|
+
node.append(head, banner, errorBox, scroll, more);
|
|
1722
|
+
if (tab.kind !== 'delegated') {
|
|
1723
|
+
const continuation = el('button', 'rd-continue', '基于此结果继续');
|
|
1724
|
+
continuation.addEventListener('click', () => {
|
|
1725
|
+
const texts = [];
|
|
1726
|
+
for (const event of ensurePanelState(tab).events) for (const entry of event.entries || []) {
|
|
1727
|
+
if (entry.role !== 'assistant') continue;
|
|
1728
|
+
const text = (entry.blocks || []).filter(block => block.kind === 'text' && typeof block.text === 'string').map(block => block.text).join('\n');
|
|
1729
|
+
if (text.trim()) texts.push(text);
|
|
1730
|
+
}
|
|
1731
|
+
openNewAgent('请基于“' + taskTitle(taskIndex.get(taskRefKey(tab.taskRef))) + '”的结果继续:\n\n' + (texts.pop() || ''));
|
|
1732
|
+
});
|
|
1733
|
+
node.append(continuation);
|
|
1734
|
+
}
|
|
1735
|
+
tab.pane = { node, head, title, status, banner, bannerText, resyncBtn, errorBox, scroll, more,
|
|
1736
|
+
follow: true, anchorSeq: null, anchorNote: null, pendingNew: false };
|
|
1412
1737
|
}
|
|
1413
1738
|
|
|
1414
1739
|
// Host panels are validated iframe descriptors from the registry. The
|
|
@@ -1419,17 +1744,13 @@
|
|
|
1419
1744
|
const descriptor = tab.descriptor || {};
|
|
1420
1745
|
const node = el('section', 'rd-pane rd-host-pane');
|
|
1421
1746
|
node.hidden = true;
|
|
1422
|
-
const
|
|
1423
|
-
const title = el('strong', 'rd-title', descriptor.title || tab.providerId);
|
|
1424
|
-
const chips = el('span', 'rd-chips');
|
|
1425
|
-
head.append(title, chips);
|
|
1426
|
-
const status = el('div', 'rd-host-status', '正在加载宿主面板…');
|
|
1747
|
+
const status = el('div', 'rd-host-status', '正在加载…');
|
|
1427
1748
|
const errorBox = el('div', 'rd-error');
|
|
1428
1749
|
errorBox.hidden = true;
|
|
1429
1750
|
const frameHolder = el('div', 'rd-host-frame-holder');
|
|
1430
1751
|
const frame = el('iframe', 'rd-host-frame');
|
|
1431
1752
|
frame.setAttribute('src', descriptor.url || '');
|
|
1432
|
-
frame.setAttribute('title', descriptor.title || '
|
|
1753
|
+
frame.setAttribute('title', descriptor.title || '知识引用');
|
|
1433
1754
|
frame.setAttribute('sandbox', 'allow-scripts');
|
|
1434
1755
|
frame.setAttribute('referrerpolicy', 'no-referrer');
|
|
1435
1756
|
frame.setAttribute('loading', 'eager');
|
|
@@ -1449,12 +1770,12 @@
|
|
|
1449
1770
|
tab.pane.error = 'LOAD_FAILED';
|
|
1450
1771
|
tab.pane.status.hidden = true;
|
|
1451
1772
|
tab.pane.errorBox.hidden = false;
|
|
1452
|
-
tab.pane.errorBox.textContent = '
|
|
1773
|
+
tab.pane.errorBox.textContent = '内容加载失败,请稍后重试。';
|
|
1453
1774
|
notifyPanelsChanged('error');
|
|
1454
1775
|
});
|
|
1455
1776
|
frameHolder.append(frame);
|
|
1456
|
-
node.append(
|
|
1457
|
-
tab.pane = { node,
|
|
1777
|
+
node.append(status, errorBox, frameHolder);
|
|
1778
|
+
tab.pane = { node, status, errorBox, frameHolder, frame, follow: true, anchorSeq: null, anchorNote: null, pendingNew: false, finalNode: null, error: null };
|
|
1458
1779
|
}
|
|
1459
1780
|
|
|
1460
1781
|
function refreshHostPane(tab) {
|
|
@@ -1462,12 +1783,9 @@
|
|
|
1462
1783
|
if (!pane) return;
|
|
1463
1784
|
const descriptor = tab.descriptor || (registry ? registry.get(tab.providerId) : null) || {};
|
|
1464
1785
|
if (!tab.descriptor && descriptor) tab.descriptor = descriptor;
|
|
1465
|
-
const typeLabel = registry && registry.panelTypeLabel ? registry.panelTypeLabel(descriptor.panelType) : descriptor.panelType;
|
|
1466
|
-
pane.chips.textContent = '';
|
|
1467
|
-
pane.chips.append(el('span', 'rd-chip host', typeLabel || '宿主面板'));
|
|
1468
|
-
pane.chips.append(el('span', 'rd-chip kind', '宿主注册'));
|
|
1469
1786
|
pane.errorBox.hidden = !pane.error;
|
|
1470
|
-
if (pane.error) pane.errorBox.textContent =
|
|
1787
|
+
if (pane.error) pane.errorBox.textContent = '内容加载失败,请稍后重试。';
|
|
1788
|
+
|
|
1471
1789
|
pane.status.hidden = pane.error != null || pane.loaded === true;
|
|
1472
1790
|
}
|
|
1473
1791
|
|
|
@@ -1511,6 +1829,7 @@
|
|
|
1511
1829
|
if (typeof data.version === 'number' && Number.isFinite(data.version)) version = data.version;
|
|
1512
1830
|
else if (typeof data.version === 'string' && data.version.trim() && data.version.length <= 200) version = data.version.trim();
|
|
1513
1831
|
if (version === null) return null;
|
|
1832
|
+
if (tab.pane.contentVersion == null || String(version) !== String(tab.pane.contentVersion)) return null;
|
|
1514
1833
|
const ok = data.ok === true;
|
|
1515
1834
|
const error = typeof data.error === 'string' && data.error.trim() ? data.error.slice(0, 300) : null;
|
|
1516
1835
|
tab.pane.contentAck = { version, ok, error, at: Date.now() };
|
|
@@ -1532,19 +1851,7 @@
|
|
|
1532
1851
|
const value = Number(children[i] && children[i].dataset ? children[i].dataset.seq : NaN);
|
|
1533
1852
|
if (Number.isFinite(value)) return value;
|
|
1534
1853
|
}
|
|
1535
|
-
|
|
1536
|
-
for (const row of (tab.panel && tab.panel.events) || []) {
|
|
1537
|
-
if (!row || typeof row !== 'object' || !RENDERABLE_ROW_TYPES.has(String(row.type || ''))) continue;
|
|
1538
|
-
const seq = Number(row.sequence);
|
|
1539
|
-
if (Number.isFinite(seq) && (last === null || seq > last)) last = seq;
|
|
1540
|
-
}
|
|
1541
|
-
return last;
|
|
1542
|
-
}
|
|
1543
|
-
|
|
1544
|
-
function metaRow(label, value) {
|
|
1545
|
-
const row = el('div', 'rd-meta-row');
|
|
1546
|
-
row.append(el('span', 'rd-meta-label', label), el('code', 'rd-meta-value', value == null ? '—' : String(value)));
|
|
1547
|
-
return row;
|
|
1854
|
+
return null;
|
|
1548
1855
|
}
|
|
1549
1856
|
|
|
1550
1857
|
function refreshPane(tab) {
|
|
@@ -1552,52 +1859,21 @@
|
|
|
1552
1859
|
if (!pane) return;
|
|
1553
1860
|
const panel = ensurePanelState(tab);
|
|
1554
1861
|
const entry = taskIndex.get(taskRefKey(tab.taskRef)) || null;
|
|
1862
|
+
pane.title.textContent = taskTitle(entry);
|
|
1863
|
+
pane.status.textContent = entry && !entry.typeConflict ? (STATUS_LABELS[entry.status] || '状态待确认') : '状态待确认';
|
|
1864
|
+
pane.errorBox.hidden = !tab.invalid && !panel.error;
|
|
1865
|
+
if (tab.invalid || panel.error) pane.errorBox.textContent = '暂时无法查看此任务,请刷新后重试。';
|
|
1866
|
+
pane.banner.hidden = !panel.resync;
|
|
1867
|
+
if (panel.resync) pane.bannerText.textContent = '记录已更新,请刷新查看。';
|
|
1555
1868
|
if (tab.invalid) {
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
pane.scroll.textContent = '';
|
|
1560
|
-
pane.foot.textContent = '';
|
|
1561
|
-
pane.more.hidden = true;
|
|
1562
|
-
pane.banner.hidden = true;
|
|
1563
|
-
pane.errorBox.hidden = false;
|
|
1564
|
-
pane.errorBox.textContent = `引用不可用(${tab.invalid.code || '未知'}):${tab.invalid.message || ''}`;
|
|
1565
|
-
tab.invalidRendered = true;
|
|
1566
|
-
}
|
|
1869
|
+
pane.scroll.textContent = '';
|
|
1870
|
+
pane.more.hidden = true;
|
|
1871
|
+
pane.banner.hidden = true;
|
|
1567
1872
|
return;
|
|
1568
1873
|
}
|
|
1569
|
-
tab.invalidRendered = null;
|
|
1570
|
-
pane.errorBox.hidden = !panel.error;
|
|
1571
|
-
if (panel.error) pane.errorBox.textContent = panel.error;
|
|
1572
|
-
if (panel.resync) {
|
|
1573
|
-
pane.banner.hidden = false;
|
|
1574
|
-
pane.bannerText.textContent = panel.resync === 'incomplete' ? '任务列表标记为不完整,建议重新同步。' : '检测到任务索引/历史已变更,需要重新同步。';
|
|
1575
|
-
} else pane.banner.hidden = true;
|
|
1576
|
-
const status = entry ? entry.status : null;
|
|
1577
|
-
pane.chips.textContent = '';
|
|
1578
|
-
pane.chips.append(el('span', 'rd-chip ' + (status || 'unknown'), status ? STATUS_LABELS[status] : '状态未知'));
|
|
1579
|
-
const kindRaw = (entry && (entry.taskKind || entry.taskType)) || null;
|
|
1580
|
-
pane.chips.append(el('span', 'rd-chip kind', kindRaw ? (KIND_LABELS[entry.taskKind] || kindRaw) : '种类未知'));
|
|
1581
|
-
if (entry && entry.typeConflict) pane.chips.append(el('span', 'rd-chip conflict', '类型冲突'));
|
|
1582
|
-
if (entry && entry.association) pane.chips.append(el('span', 'rd-chip assoc', entry.association === 'bound' ? '已绑定' : '未绑定'));
|
|
1583
|
-
if (!pane.meta.dataset.filled) {
|
|
1584
|
-
const ref = tab.taskRef;
|
|
1585
|
-
pane.meta.append(
|
|
1586
|
-
metaRow('storeScope', ref.storeScope ? shortId(ref.storeScope) : '—'),
|
|
1587
|
-
metaRow('contextId', shortId(ref.contextId)),
|
|
1588
|
-
metaRow('parentSessionId', shortId(ref.parentSessionId)),
|
|
1589
|
-
metaRow('ownerRunId', shortId(ref.ownerRunId)),
|
|
1590
|
-
metaRow('taskId', ref.taskId.length > 44 ? shortId(ref.taskId) : ref.taskId),
|
|
1591
|
-
);
|
|
1592
|
-
if (entry && entry.sdkSessionId) pane.meta.append(metaRow('sdkSessionId', shortId(entry.sdkSessionId)));
|
|
1593
|
-
if (entry && entry.taskType) pane.meta.append(metaRow('taskType', entry.taskType));
|
|
1594
|
-
if (entry && Number.isFinite(entry.rowCount)) pane.meta.append(metaRow('索引行数', entry.rowCount));
|
|
1595
|
-
pane.meta.dataset.filled = '1';
|
|
1596
|
-
}
|
|
1597
1874
|
if (!panel.rendered) renderTimeline(tab);
|
|
1598
1875
|
pane.more.hidden = !(panel.hasMore && panel.nextCursor);
|
|
1599
1876
|
pane.more.disabled = panel.loading === true;
|
|
1600
|
-
refreshFoot(tab);
|
|
1601
1877
|
}
|
|
1602
1878
|
|
|
1603
1879
|
function renderTimeline(tab) {
|
|
@@ -1606,61 +1882,36 @@
|
|
|
1606
1882
|
if (!pane) return;
|
|
1607
1883
|
const keepScroll = pane.follow === false ? pane.scroll.scrollTop : null;
|
|
1608
1884
|
pane.scroll.textContent = '';
|
|
1609
|
-
|
|
1610
|
-
let lastTextNode = null;
|
|
1885
|
+
const conversation = createConversation(pane.scroll);
|
|
1611
1886
|
const ordered = [...panel.events].sort((a, b) => (Number(a && a.sequence) || 0) - (Number(b && b.sequence) || 0));
|
|
1612
|
-
for (const event of ordered)
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
if (built.textNode) lastTextNode = built.textNode;
|
|
1617
|
-
}
|
|
1618
|
-
if (lastTextNode && lastTextNode.classList) { lastTextNode.classList.add('rd-final'); pane.finalNode = lastTextNode; }
|
|
1887
|
+
for (const event of ordered) appendNativeConversation(conversation, event);
|
|
1888
|
+
const entry = taskIndex.get(taskRefKey(tab.taskRef));
|
|
1889
|
+
if (entry && TERMINAL_STATUSES.includes(entry.status)) conversation.finish(ordered.length ? ordered[ordered.length - 1].sequence : null);
|
|
1890
|
+
pane.conversation = conversation;
|
|
1619
1891
|
panel.rendered = true;
|
|
1620
1892
|
if (keepScroll !== null) pane.scroll.scrollTop = keepScroll;
|
|
1621
1893
|
else if (pane.follow !== false) pane.scroll.scrollTop = pane.scroll.scrollHeight;
|
|
1622
1894
|
}
|
|
1623
1895
|
|
|
1624
|
-
function
|
|
1625
|
-
if (!event || typeof event !== 'object') return
|
|
1626
|
-
|
|
1627
|
-
if (
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
if (
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
node.append(el('span', 'rd-state-text', isId(event.summary) ? String(event.summary).slice(0, 400) : '(进度更新,无摘要)'));
|
|
1642
|
-
} else if (type === 'claude/native-trace') {
|
|
1643
|
-
node = el('div', 'rd-item rd-trace');
|
|
1644
|
-
const head = el('span', 'rd-trace-head', `工具 ${isId(event.toolName) ? event.toolName : '未知'}`);
|
|
1645
|
-
node.append(head);
|
|
1646
|
-
if (isId(event.nativeAgentId)) node.append(el('code', 'rd-dim', 'agent ' + shortId(event.nativeAgentId)));
|
|
1647
|
-
if (isId(event.traceId)) node.append(el('code', 'rd-dim', 'trace ' + shortId(event.traceId)));
|
|
1648
|
-
const response = event.toolResponse;
|
|
1649
|
-
if (response != null) {
|
|
1650
|
-
let preview = '';
|
|
1651
|
-
try { preview = typeof response === 'string' ? response : JSON.stringify(response); } catch { preview = String(response); }
|
|
1652
|
-
node.append(el('pre', 'rd-pre', preview.slice(0, 2000)));
|
|
1896
|
+
function appendNativeConversation(conversation, event) {
|
|
1897
|
+
if (!event || typeof event !== 'object') return;
|
|
1898
|
+
if (event.type === 'claude/task-interrupted') { conversation.accept(event); return; }
|
|
1899
|
+
if (event.type !== 'claude/native-task-transcript') return;
|
|
1900
|
+
if (!event.read || event.read.status !== 'ok') {
|
|
1901
|
+
conversation.accept({ type: 'conversation/unavailable', sequence: event.sequence });
|
|
1902
|
+
return;
|
|
1903
|
+
}
|
|
1904
|
+
for (const entry of Array.isArray(event.entries) ? event.entries : []) {
|
|
1905
|
+
if (!entry || typeof entry !== 'object') continue;
|
|
1906
|
+
const blocks = Array.isArray(entry.blocks) ? entry.blocks : [];
|
|
1907
|
+
const text = blocks.filter(block => block && block.kind === 'text' && typeof block.text === 'string').map(block => block.text).join('\n');
|
|
1908
|
+
if (text) conversation.accept({ type: entry.role === 'assistant' ? 'conversation/assistant' : 'claude/user-prompt', text, sequence: event.sequence });
|
|
1909
|
+
for (const block of blocks) {
|
|
1910
|
+
if (!block || typeof block !== 'object') continue;
|
|
1911
|
+
if (block.kind === 'tool_use') conversation.accept({ type: 'claude/tool-use', id: block.toolUseId || block.id, name: block.name, input: block.input || {}, sequence: event.sequence });
|
|
1912
|
+
else if (block.kind === 'tool_result') conversation.accept({ type: 'claude/tool-result', id: block.toolUseId || block.tool_use_id, output: (Array.isArray(block.texts) ? block.texts : []).filter(value => typeof value === 'string').join('\n'), isError: block.isError === true, sequence: event.sequence });
|
|
1653
1913
|
}
|
|
1654
|
-
} else if (type === 'claude/task-interrupted') {
|
|
1655
|
-
node = el('div', 'rd-item rd-interrupted', '任务被中断(interrupted)');
|
|
1656
|
-
} else if (type === 'claude/native-task-transcript') {
|
|
1657
|
-
const built = buildTranscriptRow(event);
|
|
1658
|
-
node = built.node;
|
|
1659
|
-
textNode = built.textNode;
|
|
1660
1914
|
}
|
|
1661
|
-
const seq = Number(event.sequence);
|
|
1662
|
-
if (node && Number.isFinite(seq) && node.dataset) node.dataset.seq = String(seq);
|
|
1663
|
-
return { node, textNode };
|
|
1664
1915
|
}
|
|
1665
1916
|
|
|
1666
1917
|
function renderMdInto(node, text) {
|
|
@@ -1671,99 +1922,10 @@
|
|
|
1671
1922
|
node.textContent = value;
|
|
1672
1923
|
}
|
|
1673
1924
|
|
|
1674
|
-
function buildTranscriptRow(event) {
|
|
1675
|
-
const node = el('div', 'rd-item rd-transcript');
|
|
1676
|
-
let textNode = null;
|
|
1677
|
-
const read = event.read;
|
|
1678
|
-
if (!read || read.status !== 'ok') {
|
|
1679
|
-
node.append(el('div', 'rd-readfail', `任务记录读取${read && read.reason ? '失败:' + read.reason : '不可用'};未渲染任何内容`));
|
|
1680
|
-
return { node, textNode: null };
|
|
1681
|
-
}
|
|
1682
|
-
const entries = Array.isArray(event.entries) ? event.entries : [];
|
|
1683
|
-
for (const entry of entries) {
|
|
1684
|
-
if (!entry || typeof entry !== 'object') continue;
|
|
1685
|
-
const wrap = el('div', 'rd-msg ' + (entry.role === 'assistant' ? 'assistant' : 'user'));
|
|
1686
|
-
wrap.append(el('span', 'rd-role', entry.role === 'assistant' ? '助手' : '用户'));
|
|
1687
|
-
const blocks = Array.isArray(entry.blocks) ? entry.blocks : [];
|
|
1688
|
-
for (const block of blocks) {
|
|
1689
|
-
if (!block || typeof block !== 'object') continue;
|
|
1690
|
-
if (block.kind === 'text' && typeof block.text === 'string') {
|
|
1691
|
-
const p = el('div', 'rd-text');
|
|
1692
|
-
renderMdInto(p, block.text);
|
|
1693
|
-
wrap.append(p);
|
|
1694
|
-
if (entry.role === 'assistant') textNode = p;
|
|
1695
|
-
} else if (block.kind === 'tool_use') {
|
|
1696
|
-
const step = el('div', 'rd-toolstep');
|
|
1697
|
-
step.append(el('span', 'rd-toolname', `工具调用 ${isId(block.name) ? block.name : '未知'}`));
|
|
1698
|
-
let dump = '';
|
|
1699
|
-
try { dump = JSON.stringify(block.input == null ? {} : block.input, null, 2); } catch { dump = String(block.input); }
|
|
1700
|
-
step.append(el('pre', 'rd-pre', dump.slice(0, 4000)));
|
|
1701
|
-
wrap.append(step);
|
|
1702
|
-
} else if (block.kind === 'tool_result') {
|
|
1703
|
-
const res = el('div', 'rd-toolresult' + (block.isError === true ? ' failed' : ''));
|
|
1704
|
-
res.append(el('span', 'rd-toolname', block.isError === true ? '工具结果(失败)' : '工具结果'));
|
|
1705
|
-
const texts = Array.isArray(block.texts) ? block.texts.filter(t => typeof t === 'string') : [];
|
|
1706
|
-
res.append(el('pre', 'rd-pre', texts.join('\n').slice(0, 4000)));
|
|
1707
|
-
wrap.append(res);
|
|
1708
|
-
}
|
|
1709
|
-
}
|
|
1710
|
-
node.append(wrap);
|
|
1711
|
-
}
|
|
1712
|
-
const steps = Array.isArray(event.toolSteps) ? event.toolSteps.length : 0;
|
|
1713
|
-
if (steps) node.append(el('div', 'rd-dim', `工具步骤 ${steps}`));
|
|
1714
|
-
const chunk = event.chunk;
|
|
1715
|
-
if (chunk && (Number(chunk.count) > 1 || chunk.complete === false)) {
|
|
1716
|
-
node.append(el('div', 'rd-dim', `分片 ${Number(chunk.index) + 1}/${Number(chunk.count)}${chunk.complete === false ? '(未完)' : ''}`));
|
|
1717
|
-
}
|
|
1718
|
-
return { node, textNode };
|
|
1719
|
-
}
|
|
1720
1925
|
|
|
1721
|
-
function usageRow(key, value) {
|
|
1722
|
-
const line = el('div', 'rd-usage-row');
|
|
1723
|
-
line.append(el('span', 'rd-usage-key', String(key)), el('span', 'rd-usage-val', value == null ? '未知' : String(value)));
|
|
1724
|
-
return line;
|
|
1725
|
-
}
|
|
1726
|
-
|
|
1727
|
-
function refreshFoot(tab) {
|
|
1728
|
-
const pane = tab.pane;
|
|
1729
|
-
if (!pane) return;
|
|
1730
|
-
const panel = ensurePanelState(tab);
|
|
1731
|
-
pane.foot.textContent = '';
|
|
1732
|
-
const usage = panel.usageSummary;
|
|
1733
|
-
if (usage && typeof usage === 'object') {
|
|
1734
|
-
const box = el('div', 'rd-usage');
|
|
1735
|
-
box.append(el('span', 'rd-usage-title', '用量'));
|
|
1736
|
-
for (const [key, value] of Object.entries(usage)) {
|
|
1737
|
-
if (key === 'totals' || key === 'knownTotals') continue;
|
|
1738
|
-
box.append(usageRow(key, value));
|
|
1739
|
-
}
|
|
1740
|
-
const totals = usage.totals;
|
|
1741
|
-
if (totals && typeof totals === 'object') {
|
|
1742
|
-
for (const [key, value] of Object.entries(totals)) box.append(usageRow(key, value));
|
|
1743
|
-
} else {
|
|
1744
|
-
const known = usage.knownTotals;
|
|
1745
|
-
if (known && typeof known === 'object' && Object.keys(known).length) {
|
|
1746
|
-
const line = el('div', 'rd-usage-row partial');
|
|
1747
|
-
const parts = Object.entries(known).map(([k, v]) => `${k}=${v}`);
|
|
1748
|
-
line.append(el('span', 'rd-usage-key', '已知部分'), el('span', 'rd-usage-val', parts.join(' / ') + '(总计未知)'));
|
|
1749
|
-
box.append(line);
|
|
1750
|
-
} else box.append(usageRow('总计', null));
|
|
1751
|
-
}
|
|
1752
|
-
pane.foot.append(box);
|
|
1753
|
-
}
|
|
1754
|
-
if (panel.diagnostics.length) {
|
|
1755
|
-
const box = el('div', 'rd-diags');
|
|
1756
|
-
box.append(el('span', 'rd-usage-title', '诊断'));
|
|
1757
|
-
for (const item of panel.diagnostics.slice(0, 8)) {
|
|
1758
|
-
box.append(el('code', 'rd-diag', item.code + (item.source ? ' @' + item.source : '')));
|
|
1759
|
-
}
|
|
1760
|
-
pane.foot.append(box);
|
|
1761
|
-
}
|
|
1762
|
-
}
|
|
1763
1926
|
|
|
1764
1927
|
function renderAnchorNote(pane, text) {
|
|
1765
1928
|
pane.anchorNote = text;
|
|
1766
|
-
if (pane.note) { pane.note.textContent = text; pane.note.hidden = !text; }
|
|
1767
1929
|
}
|
|
1768
1930
|
|
|
1769
1931
|
function applyAnchor(tab) {
|
|
@@ -1779,6 +1941,7 @@
|
|
|
1779
1941
|
renderAnchorNote(pane, '已定位到最新记录');
|
|
1780
1942
|
return;
|
|
1781
1943
|
}
|
|
1944
|
+
if (anchor && Number.isFinite(anchor.top)) { pane.scroll.scrollTop = anchor.top; renderAnchorNote(pane, '已恢复阅读位置'); return; }
|
|
1782
1945
|
if (Number.isFinite(seq)) {
|
|
1783
1946
|
let matched = null;
|
|
1784
1947
|
for (const node of Array.from(pane.scroll.children || [])) {
|
|
@@ -1796,75 +1959,14 @@
|
|
|
1796
1959
|
} else renderAnchorNote(pane, '已恢复读取位置(跟随关闭)');
|
|
1797
1960
|
}
|
|
1798
1961
|
|
|
1799
|
-
function openPicker() {
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
if (!pickerHost) return;
|
|
1805
|
-
pickerHost.hidden = !pickerOpen;
|
|
1806
|
-
if (!pickerOpen) return;
|
|
1807
|
-
pickerHost.textContent = '';
|
|
1808
|
-
const head = el('div', 'rd-picker-head');
|
|
1809
|
-
head.append(el('strong', null, '选择原生后台任务'));
|
|
1810
|
-
const refresh = el('button', 'rd-picker-refresh', '刷新');
|
|
1811
|
-
refresh.addEventListener('click', () => { void validateScopeTasks(); void validateScopeDelegations(); });
|
|
1812
|
-
const close = el('button', 'rd-picker-close', '关闭');
|
|
1813
|
-
close.addEventListener('click', () => { closePicker(); });
|
|
1814
|
-
head.append(refresh, close);
|
|
1815
|
-
pickerHost.append(head);
|
|
1816
|
-
if (listState.loading) pickerHost.append(el('div', 'rd-picker-empty', '正在读取原生任务列表…'));
|
|
1817
|
-
else if (listState.error) pickerHost.append(el('div', 'rd-picker-empty', listState.error));
|
|
1818
|
-
else if (!taskIndex.size) pickerHost.append(el('div', 'rd-picker-empty', '当前会话没有原生后台任务记录。'));
|
|
1819
|
-
else {
|
|
1820
|
-
const rows = Array.from(taskIndex.values())
|
|
1821
|
-
.sort((a, b) => String(a.taskId || '').localeCompare(String(b.taskId || '')));
|
|
1822
|
-
for (const entry of rows) {
|
|
1823
|
-
const row = el('button', 'rd-picker-row');
|
|
1824
|
-
row.disabled = !entry.taskRef;
|
|
1825
|
-
const status = entry.status || 'unknown';
|
|
1826
|
-
let label = `#${shortId(entry.taskId || entry.sdkSessionId)} · ${STATUS_LABELS[status] || '状态未知'}`;
|
|
1827
|
-
if (entry.taskKind) label += ` · ${KIND_LABELS[entry.taskKind] || entry.taskKind}`;
|
|
1828
|
-
if (!entry.taskRef) label += '(未绑定 taskRef,不可打开)';
|
|
1829
|
-
row.textContent = label;
|
|
1830
|
-
row.title = entry.taskRef ? entry.taskRef.taskId : label;
|
|
1831
|
-
if (entry.taskRef) row.addEventListener('click', () => { openTask(entry.taskRef); });
|
|
1832
|
-
pickerHost.append(row);
|
|
1833
|
-
}
|
|
1834
|
-
}
|
|
1835
|
-
if (delegationSupported) {
|
|
1836
|
-
pickerHost.append(el('div', 'rd-picker-subhead', '委派会话(可信 delegation 关系)'));
|
|
1837
|
-
if (delegationState.loading) pickerHost.append(el('div', 'rd-picker-empty', '正在读取委派会话…'));
|
|
1838
|
-
else if (delegationState.error) pickerHost.append(el('div', 'rd-picker-empty', delegationState.error));
|
|
1839
|
-
else if (!delegationIndex.size) pickerHost.append(el('div', 'rd-picker-empty', '当前会话没有委派记录。'));
|
|
1840
|
-
else {
|
|
1841
|
-
const dRows = Array.from(delegationIndex.values())
|
|
1842
|
-
.sort((a, b) => String(a.issuedAt || '').localeCompare(String(b.issuedAt || '')));
|
|
1843
|
-
for (const entry of dRows) {
|
|
1844
|
-
const row = el('button', 'rd-picker-row rd-picker-delegation-row');
|
|
1845
|
-
const childLabel = entry.child.present ? (CHILD_STATE_LABELS[entry.child.state] || entry.child.state || '状态未知') : '子会话缺失(悬空)';
|
|
1846
|
-
row.textContent = `委派 ${shortId(entry.delegationId)} · ${DELEGATION_PHASE_LABELS[entry.phase] || entry.phase || '阶段未知'} · ${childLabel}`;
|
|
1847
|
-
row.title = entry.delegationId;
|
|
1848
|
-
row.addEventListener('click', () => { openDelegation(entry.delegationId); });
|
|
1849
|
-
pickerHost.append(row);
|
|
1850
|
-
}
|
|
1851
|
-
}
|
|
1852
|
-
pickerHost.append(el('div', 'rd-picker-note', DELEGATION_PICKER_NOTE));
|
|
1853
|
-
}
|
|
1854
|
-
const hostProviders = registry ? registry.listAvailable({ projectId: scope ? scope.contextId : null }) : [];
|
|
1855
|
-
if (hostProviders.length) {
|
|
1856
|
-
pickerHost.append(el('div', 'rd-picker-subhead', '可信宿主面板(已注册)'));
|
|
1857
|
-
for (const descriptor of hostProviders) {
|
|
1858
|
-
const row = el('button', 'rd-picker-row rd-picker-host-row');
|
|
1859
|
-
const typeLabel = registry.panelTypeLabel ? registry.panelTypeLabel(descriptor.panelType) : descriptor.panelType;
|
|
1860
|
-
row.textContent = `${descriptor.title} · ${typeLabel || descriptor.panelType}${descriptor.multiInstance === 'multiple' ? ' · 可多实例' : ''}`;
|
|
1861
|
-
row.title = descriptor.providerId;
|
|
1862
|
-
row.addEventListener('click', () => { openPanel(descriptor.providerId); });
|
|
1863
|
-
pickerHost.append(row);
|
|
1864
|
-
}
|
|
1865
|
-
}
|
|
1866
|
-
pickerHost.append(el('div', 'rd-picker-note', registry ? PICKER_REGISTRY_NOTE : PICKER_NOTE));
|
|
1962
|
+
function openPicker() {
|
|
1963
|
+
let tab = tabs.find(item => item.kind === 'launcher');
|
|
1964
|
+
if (!tab) { tab = { id: nextTabId(), kind: 'launcher', pane: null }; tabs.push(tab); }
|
|
1965
|
+
else if (tab.pane) { tab.pane.node.remove(); tab.pane = null; }
|
|
1966
|
+
activeTabId = tab.id; open = true; pickerOpen = true; render(); schedulePersist();
|
|
1867
1967
|
}
|
|
1968
|
+
function closePicker() { pickerOpen = false; }
|
|
1969
|
+
function renderPicker() { if (pickerHost) pickerHost.hidden = true; }
|
|
1868
1970
|
|
|
1869
1971
|
function snapshot() {
|
|
1870
1972
|
return {
|
|
@@ -1885,6 +1987,7 @@
|
|
|
1885
1987
|
providerId: tab.kind === 'host' ? tab.providerId : null,
|
|
1886
1988
|
panelType: tab.kind === 'host' && tab.descriptor ? tab.descriptor.panelType : null,
|
|
1887
1989
|
hostError: tab.pane && tab.pane.error ? tab.pane.error : null,
|
|
1990
|
+
hostReady: Boolean(tab.kind === 'host' && tab.pane && tab.pane.loaded),
|
|
1888
1991
|
delegation: tab.kind === 'delegated' ? delegationSnapshotOf(tab) : null,
|
|
1889
1992
|
content: tab.kind === 'host' && tab.pane ? {
|
|
1890
1993
|
version: tab.pane.contentVersion !== undefined ? tab.pane.contentVersion : null,
|
|
@@ -1909,12 +2012,40 @@
|
|
|
1909
2012
|
return Array.from(tab.pane.scroll.children || []).map(node => String(node.textContent || '')).join('\n');
|
|
1910
2013
|
}
|
|
1911
2014
|
|
|
1912
|
-
function pickerText() { return
|
|
2015
|
+
function pickerText() { const tab = tabs.find(item => item.kind === 'launcher'); return tab && tab.pane ? String(tab.pane.node.textContent || '') : ''; }
|
|
2016
|
+
|
|
2017
|
+
function taskViews(parentSessionId) {
|
|
2018
|
+
if (parentSessionId !== undefined && (!scope || scope.parentSessionId !== parentSessionId)) return [];
|
|
2019
|
+
const native = [...taskIndex.values()].filter(entry => entry.taskRef && scopeRefMatches(scope, entry.taskRef)).map(entry => ({
|
|
2020
|
+
taskRef: { ...entry.taskRef }, taskId: entry.taskId, status: entry.status,
|
|
2021
|
+
description: taskTitle(entry),
|
|
2022
|
+
}));
|
|
2023
|
+
const delegated = [...delegationIndex.values()].map(view => ({
|
|
2024
|
+
delegationId: view.delegationId, taskId: view.childSessionId,
|
|
2025
|
+
description: delegationTitle(view.delegationId),
|
|
2026
|
+
status: delegationStatus(view).status, statusLabel: delegationStatus(view).label,
|
|
2027
|
+
}));
|
|
2028
|
+
return [...native, ...delegated];
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
async function openTaskForToolUseId(toolUseId) {
|
|
2032
|
+
if (!isId(toolUseId) || !scope) return null;
|
|
2033
|
+
const epoch = scopeEpoch;
|
|
2034
|
+
await validateScopeTasks();
|
|
2035
|
+
if (epoch !== scopeEpoch) return null;
|
|
2036
|
+
const matches = [...taskIndex.values()].filter(entry => entry.launcherToolUseId === toolUseId && entry.taskRef && scopeRefMatches(scope, entry.taskRef));
|
|
2037
|
+
if (matches.length !== 1) {
|
|
2038
|
+
toast('此子代理暂未提供可读取的独立任务记录。', true);
|
|
2039
|
+
return null;
|
|
2040
|
+
}
|
|
2041
|
+
return openTask(matches[0].taskRef, { reuse: true });
|
|
2042
|
+
}
|
|
1913
2043
|
|
|
1914
2044
|
const api = {
|
|
1915
2045
|
attachSession, detachSession, handleSessionEvent, handleResync,
|
|
1916
2046
|
refreshIdentity,
|
|
1917
|
-
|
|
2047
|
+
refreshProviders() { for (const tab of tabs) if (tab.kind === 'launcher' && tab.pane) { tab.pane.node.remove(); tab.pane = null; } render(); },
|
|
2048
|
+
openTask, openTaskForToolUseId, taskViews, openNewAgent, openDelegation, notifySessionChanged, backgroundActivity,
|
|
1918
2049
|
closeTab, toggle, setWidth, adjustWidth,
|
|
1919
2050
|
openPanel, closeHostPanels, activateHostPanel,
|
|
1920
2051
|
relayHostPanelContent, ackHostPanelContent,
|