@sanqianx/claude-ai-workbench 0.13.2 → 0.13.3
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 +636 -613
- package/apps/agent-terminal/app.js +125 -65
- package/apps/agent-terminal/conversation-composer.js +44 -0
- package/apps/agent-terminal/index.html +171 -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 +187 -159
- package/apps/agent-terminal/right-workspace-dock.js +623 -519
- 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 +13 -13
- package/docs/sdk/openapi.yaml +8 -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 +1442 -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/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 +2392 -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,206 @@
|
|
|
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
|
+
panel.highWater = Math.max(panel.highWater || 0, Number(event.sequence) || 0);
|
|
737
|
+
if (event.type === 'claude/state') { currentView.child.state = event.state; currentView.child.active = ['created', 'spawning', 'running', 'thinking', 'tool-running', 'pending-permission'].includes(event.state); }
|
|
738
|
+
if (event.type === 'claude/result') { currentView.child.active = false; currentView.child.turns = (currentView.child.turns || 0) + 1; }
|
|
739
|
+
render(); notifyBackgroundActivity();
|
|
740
|
+
}, () => { if (current()) { tab.connectionError = '连接暂时中断,正在恢复对话。'; render(); } }, {
|
|
741
|
+
afterSequence: tab.panel.highWater || 0,
|
|
742
|
+
onOpen: () => {
|
|
743
|
+
if (!current()) return;
|
|
744
|
+
tab.connectionError = null;
|
|
745
|
+
if (opened) { void loadDelegationFirstPage(tab); void validateScopeDelegations(); }
|
|
746
|
+
else render();
|
|
747
|
+
opened = true;
|
|
748
|
+
},
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function buildDraftPane(tab) {
|
|
753
|
+
const node = el('section', 'rd-pane rd-draft-pane');
|
|
754
|
+
const empty = el('div', 'rd-draft-empty');
|
|
755
|
+
empty.append(el('strong', null, '想让子代理处理什么?'), el('p', null, '沿用当前项目,独立开展对话。'));
|
|
756
|
+
const errorBox = el('div', 'rd-error');
|
|
757
|
+
const composer = buildComposer(tab);
|
|
758
|
+
node.append(empty, errorBox, composer.node);
|
|
759
|
+
tab.pane = { node, errorBox, composer };
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function buildLauncherPane(tab) {
|
|
763
|
+
const node = el('section', 'rd-pane rd-launcher');
|
|
764
|
+
node.append(el('h3', 'rd-picker-heading', '工具'));
|
|
765
|
+
const create = el('button', 'rd-picker-row rd-create-agent', '新建子代理');
|
|
766
|
+
create.disabled = !scope || !scope.storeScope || typeof client.createDelegation !== 'function';
|
|
767
|
+
create.addEventListener('click', () => { openNewAgent(); }); node.append(create);
|
|
768
|
+
const providers = registry ? registry.listAvailable({ projectId: scope ? scope.contextId : null }) : [];
|
|
769
|
+
for (const descriptor of providers) {
|
|
770
|
+
const row = el('button', 'rd-picker-row rd-picker-host-row');
|
|
771
|
+
if (typeof deps.renderPageIcon === 'function') row.append(deps.renderPageIcon('file'));
|
|
772
|
+
row.append(el('span', null, descriptor.title));
|
|
773
|
+
row.addEventListener('click', () => { const id = tab.id; openPanel(descriptor.providerId); closeTab(id); });
|
|
774
|
+
node.append(row);
|
|
775
|
+
}
|
|
776
|
+
tab.pane = { node };
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function renderPermissions(tab) {
|
|
780
|
+
const pane = tab.pane, view = delegationIndex.get(tab.delegationId);
|
|
781
|
+
if (!pane.permissions || !view) return;
|
|
782
|
+
pane.permissions.textContent = '';
|
|
783
|
+
if (!view.child.active) return;
|
|
784
|
+
const pending = new Map();
|
|
785
|
+
for (const event of tab.panel.events) {
|
|
786
|
+
if (event.type === 'claude/permission-request') pending.set(event.requestId, event);
|
|
787
|
+
if (event.type === 'claude/permission-resolved') pending.delete(event.requestId);
|
|
788
|
+
}
|
|
789
|
+
for (const [requestId, event] of pending) {
|
|
790
|
+
const card = el('div', 'perm'); card.append(el('span', 'p-title', (event.toolName || '子代理') + ' 请求执行操作'));
|
|
791
|
+
for (const allow of [true, false]) {
|
|
792
|
+
const button = el('button', allow ? 'btn-allow' : 'btn-deny', allow ? '允许' : '拒绝');
|
|
793
|
+
button.addEventListener('click', async () => {
|
|
794
|
+
if (tab.invalid || !tabs.includes(tab)) return;
|
|
795
|
+
button.disabled = true;
|
|
796
|
+
try { await client.resolvePermission(view.childSessionId, requestId, allow); void loadDelegationFirstPage(tab); }
|
|
797
|
+
catch { button.disabled = false; toast('授权操作未完成,请重试。', true); }
|
|
798
|
+
}); card.append(button);
|
|
799
|
+
}
|
|
800
|
+
pane.permissions.append(card);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
615
804
|
function openDelegation(delegationId) {
|
|
616
|
-
if (!scope) { toast('
|
|
805
|
+
if (!scope) { toast('请先打开此任务所属的会话。', true); return null; }
|
|
617
806
|
const id = typeof delegationId === 'string' ? delegationId.trim() : '';
|
|
618
|
-
if (!isId(id)) { toast('
|
|
807
|
+
if (!isId(id)) { toast('暂时无法打开此任务。', true); return null; }
|
|
619
808
|
const existing = tabs.find(tab => tab.kind === 'delegated' && tab.delegationId === id);
|
|
620
809
|
if (existing) {
|
|
621
810
|
activeTabId = existing.id;
|
|
@@ -625,9 +814,9 @@
|
|
|
625
814
|
return existing.id;
|
|
626
815
|
}
|
|
627
816
|
const view = delegationIndex.get(id);
|
|
628
|
-
if (!view) { toast('
|
|
817
|
+
if (!view) { toast('此任务不属于当前会话,请返回所属会话查看。', true); return null; }
|
|
629
818
|
const tab = {
|
|
630
|
-
id: nextTabId(), kind: 'delegated', delegationId: id,
|
|
819
|
+
id: nextTabId(), kind: 'delegated', delegationId: id, draft: (() => { try { return storage && storage.getItem(storageKey + ':draft:' + scopeStorageKey(scope) + ':' + id) || ''; } catch { return ''; } })(),
|
|
631
820
|
taskRef: null, providerId: null, descriptor: null,
|
|
632
821
|
invalid: null, panel: null, pane: null, invalidRendered: null,
|
|
633
822
|
};
|
|
@@ -686,10 +875,14 @@
|
|
|
686
875
|
return;
|
|
687
876
|
}
|
|
688
877
|
const events = page && Array.isArray(page.events) ? page.events : [];
|
|
689
|
-
panel.events = panel.
|
|
878
|
+
panel.events = mergeDelegationEvents(events, panel.events.filter(event => Number(event.sequence) > Number(page && page.highWater || Infinity)));
|
|
879
|
+
if (panel.loadedEarlier) panel.events = mergeDelegationEvents(panel.olderEvents || [], panel.events);
|
|
880
|
+
panel.highWater = Number(page && page.highWater) || Math.max(0, ...events.map(event => Number(event.sequence) || 0));
|
|
881
|
+
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
882
|
panel.nextCursor = page && page.nextCursor || null;
|
|
691
883
|
panel.hasMore = isId(panel.nextCursor);
|
|
692
884
|
panel.loaded = true;
|
|
885
|
+
tab.connectionError = null;
|
|
693
886
|
panel.rendered = false;
|
|
694
887
|
panel.lastTurns = view.child && Number.isFinite(Number(view.child.turns)) ? Number(view.child.turns) : null;
|
|
695
888
|
render();
|
|
@@ -721,68 +914,163 @@
|
|
|
721
914
|
panel.events = [...older.filter(row => !known.has(delegationRowKey(row))), ...panel.events];
|
|
722
915
|
panel.nextCursor = page && page.nextCursor || null;
|
|
723
916
|
panel.hasMore = isId(panel.nextCursor);
|
|
724
|
-
panel.loadedEarlier = true;
|
|
917
|
+
panel.loadedEarlier = true; panel.olderEvents = panel.events.slice();
|
|
725
918
|
panel.rendered = false;
|
|
726
919
|
render();
|
|
727
920
|
}
|
|
728
921
|
|
|
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
|
-
}
|
|
922
|
+
function buildReadOnlyTool(event) {
|
|
923
|
+
if (renderToolStep) return renderToolStep(event, event.input || {});
|
|
924
|
+
const node = el('details', 'step rd-toolstep');
|
|
925
|
+
node.append(el('summary', null, event.name || event.toolName || '工具'));
|
|
926
|
+
const body = el('div', 'step-body');
|
|
927
|
+
body.append(el('pre', 'rd-pre', JSON.stringify(event.input || {}, null, 2)));
|
|
928
|
+
node.append(body);
|
|
772
929
|
return node;
|
|
773
930
|
}
|
|
774
931
|
|
|
932
|
+
function finishReadOnlyTool(step, event) {
|
|
933
|
+
if (renderToolResult) { renderToolResult(step, event); return; }
|
|
934
|
+
const output = event.output == null ? '' : typeof event.output === 'string' ? event.output : JSON.stringify(event.output, null, 2);
|
|
935
|
+
step.append(el('pre', 'rd-pre', output));
|
|
936
|
+
if (event.isError) step.classList.add('failed');
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// Normalize transport events into conversation turns. Internal identities,
|
|
940
|
+
// dispatch state and diagnostics never enter customer DOM.
|
|
941
|
+
function createConversation(scroll) {
|
|
942
|
+
let turn = null;
|
|
943
|
+
const tools = new Map();
|
|
944
|
+
function current() {
|
|
945
|
+
if (!turn) turn = { messages: [], changes: [], process: null, live: null, thinking: null };
|
|
946
|
+
return turn;
|
|
947
|
+
}
|
|
948
|
+
function mark(node, sequence) {
|
|
949
|
+
if (Number.isFinite(Number(sequence))) node.dataset.seq = String(sequence);
|
|
950
|
+
return node;
|
|
951
|
+
}
|
|
952
|
+
function message(role, text, sequence) {
|
|
953
|
+
const node = renderMessage ? renderMessage(role, text) : el('div', role === 'user' ? 'msg user' : 'msg ai');
|
|
954
|
+
let body = node.querySelector && node.querySelector(role === 'user' ? '.bubble' : '.md-body');
|
|
955
|
+
if (!body) {
|
|
956
|
+
body = el('div', role === 'user' ? 'bubble' : 'md-body');
|
|
957
|
+
if (role === 'user') body.textContent = text;
|
|
958
|
+
else renderMdInto(body, text);
|
|
959
|
+
node.append(body);
|
|
960
|
+
}
|
|
961
|
+
mark(node, sequence);
|
|
962
|
+
scroll.append(node);
|
|
963
|
+
return { node, body, text };
|
|
964
|
+
}
|
|
965
|
+
function process(sequence) {
|
|
966
|
+
const t = current();
|
|
967
|
+
if (!t.process) {
|
|
968
|
+
if (renderProcess) t.process = renderProcess();
|
|
969
|
+
else {
|
|
970
|
+
const node = el('details', 'proc');
|
|
971
|
+
const meta = el('summary', 'p-meta');
|
|
972
|
+
const steps = el('div', 'proc-steps');
|
|
973
|
+
node.append(meta, steps);
|
|
974
|
+
t.process = { node, meta, steps };
|
|
975
|
+
}
|
|
976
|
+
t.process.node.open = false;
|
|
977
|
+
scroll.append(t.process.node);
|
|
978
|
+
}
|
|
979
|
+
for (const msg of t.messages.splice(0)) {
|
|
980
|
+
if (typeof msg.node.remove === 'function') msg.node.remove();
|
|
981
|
+
t.process.steps.append(msg.node);
|
|
982
|
+
}
|
|
983
|
+
t.live = null;
|
|
984
|
+
mark(t.process.node, sequence);
|
|
985
|
+
return t.process;
|
|
986
|
+
}
|
|
987
|
+
function updateProcess(p) { p.meta.textContent = p.steps.children.length + ' 个步骤'; }
|
|
988
|
+
function finish(sequence) {
|
|
989
|
+
if (!turn) return;
|
|
990
|
+
if (renderChanges && turn.changes.length) {
|
|
991
|
+
const changes = renderChanges(turn.changes);
|
|
992
|
+
if (changes) scroll.append(mark(changes, sequence));
|
|
993
|
+
}
|
|
994
|
+
turn = null;
|
|
995
|
+
}
|
|
996
|
+
function accept(event) {
|
|
997
|
+
if (!event) return;
|
|
998
|
+
const seq = event.sequence, type = event.type;
|
|
999
|
+
if (type === 'claude/user-prompt') {
|
|
1000
|
+
finish(seq);
|
|
1001
|
+
if (event.text) message('user', String(event.text), seq);
|
|
1002
|
+
current();
|
|
1003
|
+
} else if (type === 'claude/text-start') {
|
|
1004
|
+
current().live = null;
|
|
1005
|
+
} else if (type === 'claude/text-delta' || type === 'conversation/assistant') {
|
|
1006
|
+
const text = String(event.text || '');
|
|
1007
|
+
if (!text) return;
|
|
1008
|
+
const t = current();
|
|
1009
|
+
if (type === 'conversation/assistant' || !t.live || t.live.id !== event.id) {
|
|
1010
|
+
t.live = message('assistant', text, seq);
|
|
1011
|
+
t.live.id = event.id;
|
|
1012
|
+
t.messages.push(t.live);
|
|
1013
|
+
} else {
|
|
1014
|
+
t.live.text += text;
|
|
1015
|
+
renderMdInto(t.live.body, t.live.text);
|
|
1016
|
+
mark(t.live.node, seq);
|
|
1017
|
+
}
|
|
1018
|
+
t.thinking = null;
|
|
1019
|
+
} else if (type === 'claude/tool-use') {
|
|
1020
|
+
const p = process(seq);
|
|
1021
|
+
const step = buildReadOnlyTool(event);
|
|
1022
|
+
p.steps.append(step);
|
|
1023
|
+
tools.set(event.id, step);
|
|
1024
|
+
updateProcess(p);
|
|
1025
|
+
current().thinking = null;
|
|
1026
|
+
} else if (type === 'claude/tool-result') {
|
|
1027
|
+
const step = tools.get(event.id);
|
|
1028
|
+
const p = current().process || process(seq);
|
|
1029
|
+
const target = step || buildReadOnlyTool({ id: event.id, name: event.toolName || '工具', input: {} });
|
|
1030
|
+
if (!step) p.steps.append(target);
|
|
1031
|
+
finishReadOnlyTool(target, event);
|
|
1032
|
+
current().changes.push({ ...event, input: target._toolInput || {} });
|
|
1033
|
+
mark(p.node, seq);
|
|
1034
|
+
updateProcess(p);
|
|
1035
|
+
} else if (type === 'claude/thinking-delta') {
|
|
1036
|
+
const t = current();
|
|
1037
|
+
const p = t.process || process(seq);
|
|
1038
|
+
if (!t.thinking) {
|
|
1039
|
+
const node = el('div', 'think-seg');
|
|
1040
|
+
node.append(el('div', 'think-label', '思考'));
|
|
1041
|
+
const body = el('div', 'think-body');
|
|
1042
|
+
node.append(body);
|
|
1043
|
+
p.steps.append(node);
|
|
1044
|
+
t.thinking = body;
|
|
1045
|
+
}
|
|
1046
|
+
t.thinking.textContent += String(event.text || '');
|
|
1047
|
+
mark(p.node, seq);
|
|
1048
|
+
updateProcess(p);
|
|
1049
|
+
} else if (type === 'claude/result') {
|
|
1050
|
+
const t = current();
|
|
1051
|
+
if (event.isError) scroll.append(mark(el('div', 'turn-note', '回复失败,请稍后重试。'), seq));
|
|
1052
|
+
else if (!t.messages.length && event.result) message('assistant', String(event.result), seq);
|
|
1053
|
+
finish(seq);
|
|
1054
|
+
} else if (type === 'conversation/unavailable') {
|
|
1055
|
+
scroll.append(mark(el('div', 'turn-note', '任务记录暂不可用,请刷新重试。'), seq));
|
|
1056
|
+
} else if (type === 'claude/aborted' || type === 'claude/task-interrupted' || type === 'claude/error') {
|
|
1057
|
+
scroll.append(mark(el('div', 'turn-note', type === 'claude/error' ? '回复失败,请稍后重试。' : '回复已中断'), seq));
|
|
1058
|
+
finish(seq);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
return { accept, finish };
|
|
1062
|
+
}
|
|
1063
|
+
|
|
775
1064
|
function renderDelegationTimeline(tab) {
|
|
776
1065
|
const pane = tab.pane;
|
|
777
1066
|
const panel = ensureDelegationPanelState(tab);
|
|
778
1067
|
if (!pane) return;
|
|
779
1068
|
const keepScroll = pane.follow === false ? pane.scroll.scrollTop : null;
|
|
780
1069
|
pane.scroll.textContent = '';
|
|
1070
|
+
const conversation = createConversation(pane.scroll);
|
|
781
1071
|
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
|
-
}
|
|
1072
|
+
for (const event of ordered) conversation.accept(event);
|
|
1073
|
+
pane.conversation = conversation;
|
|
786
1074
|
panel.rendered = true;
|
|
787
1075
|
if (keepScroll !== null) pane.scroll.scrollTop = keepScroll;
|
|
788
1076
|
else if (pane.follow !== false) pane.scroll.scrollTop = pane.scroll.scrollHeight;
|
|
@@ -798,104 +1086,62 @@
|
|
|
798
1086
|
return null;
|
|
799
1087
|
}
|
|
800
1088
|
|
|
1089
|
+
function taskTitle(entry) {
|
|
1090
|
+
const title = entry && entry.launcherToolUseId && deps && typeof deps.taskTitle === 'function' ? deps.taskTitle(entry.launcherToolUseId) : null;
|
|
1091
|
+
return String(title || entry && entry.description || '子代理任务').replace(/\s+/g, ' ').slice(0, 60);
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
function delegationTitle(id) {
|
|
1095
|
+
if (delegationTitles.has(id)) return delegationTitles.get(id);
|
|
1096
|
+
const tab = tabs.find(item => item.kind === 'delegated' && item.delegationId === id && item.panel);
|
|
1097
|
+
if (tab && tab.title) return tab.title;
|
|
1098
|
+
const prompt = tab && tab.panel.events.find(event => event.type === 'claude/user-prompt' && event.text);
|
|
1099
|
+
return prompt ? String(prompt.text).replace(/\s+/g, ' ').slice(0, 60) : '子代理任务';
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
function delegationStatus(view) {
|
|
1103
|
+
if (!view) return { status: 'pending', label: '状态待确认' };
|
|
1104
|
+
if (view.phase === 'dispatch-unknown' || view.outcome === 'unknown') return { status: 'pending', label: '状态待确认' };
|
|
1105
|
+
if (view.phase === 'child-create-failed') return { status: 'failed', label: '任务未能启动' };
|
|
1106
|
+
const child = view.child;
|
|
1107
|
+
if (!child.present) return { status: 'failed', label: '任务暂不可用' };
|
|
1108
|
+
if (child.state === 'failed') return { status: 'failed', label: '失败' };
|
|
1109
|
+
if (child.state === 'aborted') return { status: 'stopped', label: '已停止' };
|
|
1110
|
+
if (child.state === 'pending-permission') return { status: 'pending', label: '等待授权' };
|
|
1111
|
+
if (child.active) return { status: 'running', label: '运行中' };
|
|
1112
|
+
if (['dispatched', 'completed'].includes(view.phase) && child.state === 'idle' && child.turns > 0) return { status: 'completed', label: '已完成' };
|
|
1113
|
+
return { status: 'pending', label: '待执行' };
|
|
1114
|
+
}
|
|
1115
|
+
|
|
801
1116
|
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 };
|
|
1117
|
+
buildPane(tab);
|
|
1118
|
+
tab.pane.node.classList.add('rd-delegation-pane');
|
|
1119
|
+
tab.pane.composer = buildComposer(tab);
|
|
1120
|
+
tab.pane.permissions = el('div', 'rd-permissions');
|
|
1121
|
+
tab.pane.node.append(tab.pane.permissions, tab.pane.composer.node);
|
|
834
1122
|
}
|
|
835
1123
|
|
|
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
1124
|
function refreshDelegationPane(tab) {
|
|
840
1125
|
const pane = tab.pane;
|
|
841
1126
|
if (!pane) return;
|
|
842
1127
|
const panel = ensureDelegationPanelState(tab);
|
|
843
1128
|
const view = delegationIndex.get(tab.delegationId) || null;
|
|
1129
|
+
pane.title.textContent = delegationTitle(tab.delegationId);
|
|
1130
|
+
pane.status.textContent = delegationStatus(view).label;
|
|
1131
|
+
pane.errorBox.hidden = !tab.invalid && !panel.error;
|
|
1132
|
+
if (tab.invalid || panel.error) pane.errorBox.textContent = '暂时无法查看此任务,请刷新后重试。';
|
|
1133
|
+
pane.banner.hidden = true;
|
|
844
1134
|
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
|
-
}
|
|
1135
|
+
pane.scroll.textContent = '';
|
|
1136
|
+
pane.more.hidden = true;
|
|
854
1137
|
return;
|
|
855
1138
|
}
|
|
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
1139
|
if (!panel.rendered) renderDelegationTimeline(tab);
|
|
1140
|
+
if (tab.error || tab.connectionError) { pane.errorBox.hidden = false; pane.errorBox.textContent = tab.error || tab.connectionError; }
|
|
1141
|
+
if (pane.composer) { pane.composer.setBusy(childBusy(tab)); pane.composer.send.disabled = Boolean(tab.invalid || tab.sending || !view || !view.child.present); }
|
|
1142
|
+
if (pane.composer && view) pane.composer.setLabel?.((view.child.model || '子代理') + ' · 默认防护');
|
|
1143
|
+
renderPermissions(tab);
|
|
1144
|
+
subscribeChild(tab);
|
|
899
1145
|
pane.more.hidden = !(panel.hasMore && panel.nextCursor);
|
|
900
1146
|
pane.more.disabled = panel.loading === true;
|
|
901
1147
|
}
|
|
@@ -926,6 +1172,7 @@
|
|
|
926
1172
|
}
|
|
927
1173
|
renderTabBar();
|
|
928
1174
|
renderToggleState();
|
|
1175
|
+
for (const tab of tabs) if (!open || tab.id !== activeTabId) releaseSubscription(tab);
|
|
929
1176
|
// Live native state events move task status immediately and with it
|
|
930
1177
|
// backgroundActivity; notify the host listener on the same beat.
|
|
931
1178
|
notifyBackgroundActivity();
|
|
@@ -963,15 +1210,11 @@
|
|
|
963
1210
|
panel.usageSummary = event.usage.summary;
|
|
964
1211
|
}
|
|
965
1212
|
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
|
-
}
|
|
1213
|
+
if (!tab.pane.conversation) renderTimeline(tab);
|
|
1214
|
+
else appendNativeConversation(tab.pane.conversation, event);
|
|
972
1215
|
if (tab.pane.follow !== false) tab.pane.scroll.scrollTop = tab.pane.scroll.scrollHeight;
|
|
973
1216
|
else tab.pane.pendingNew = true;
|
|
974
|
-
|
|
1217
|
+
|
|
975
1218
|
return true;
|
|
976
1219
|
}
|
|
977
1220
|
|
|
@@ -1037,6 +1280,7 @@
|
|
|
1037
1280
|
flushPersist();
|
|
1038
1281
|
scopeEpoch += 1;
|
|
1039
1282
|
scope = null;
|
|
1283
|
+
for (const tab of tabs) releaseSubscription(tab);
|
|
1040
1284
|
tabs = [];
|
|
1041
1285
|
activeTabId = null;
|
|
1042
1286
|
pickerOpen = false;
|
|
@@ -1047,10 +1291,19 @@
|
|
|
1047
1291
|
notifyPanelsChanged('scope');
|
|
1048
1292
|
}
|
|
1049
1293
|
|
|
1050
|
-
function openTask(refInput) {
|
|
1294
|
+
function openTask(refInput, options = {}) {
|
|
1051
1295
|
const ref = sanitizeTaskRef(refInput);
|
|
1052
1296
|
if (!ref) { toast('无效的任务引用,未打开面板。', true); return null; }
|
|
1053
1297
|
if (!scope || !scopeRefMatches(scope, ref)) { toast('该任务不属于当前会话范围;请在所属会话中打开。', true); return null; }
|
|
1298
|
+
const existing = options.reuse && tabs.find(tab => tab.taskRef && taskRefKey(tab.taskRef) === taskRefKey(ref));
|
|
1299
|
+
if (existing) {
|
|
1300
|
+
activeTabId = existing.id;
|
|
1301
|
+
open = true;
|
|
1302
|
+
pickerOpen = false;
|
|
1303
|
+
render();
|
|
1304
|
+
schedulePersist();
|
|
1305
|
+
return existing.id;
|
|
1306
|
+
}
|
|
1054
1307
|
const tab = {
|
|
1055
1308
|
id: 'tab-' + Date.now().toString(36) + '-' + (++tabSeq),
|
|
1056
1309
|
taskRef: ref, invalid: null, panel: null, pane: null, invalidRendered: null,
|
|
@@ -1077,10 +1330,10 @@
|
|
|
1077
1330
|
}
|
|
1078
1331
|
|
|
1079
1332
|
function openPanel(providerIdInput) {
|
|
1080
|
-
if (!registry) { toast('
|
|
1333
|
+
if (!registry) { toast('暂时无法打开此内容。', true); return null; }
|
|
1081
1334
|
const providerId = isId(providerIdInput) ? String(providerIdInput).trim() : '';
|
|
1082
1335
|
const descriptor = registry.get(providerId);
|
|
1083
|
-
if (!descriptor) { toast('
|
|
1336
|
+
if (!descriptor) { toast('此内容当前不可用。', true); return null; }
|
|
1084
1337
|
if (!providerAppliesToScope(descriptor)) { toast('该面板不适用于当前项目范围。', true); return null; }
|
|
1085
1338
|
if (descriptor.multiInstance !== 'multiple') {
|
|
1086
1339
|
const existing = tabs.find(tab => tab.kind === 'host' && tab.providerId === providerId);
|
|
@@ -1137,8 +1390,10 @@
|
|
|
1137
1390
|
const index = tabs.findIndex(tab => tab.id === tabId);
|
|
1138
1391
|
if (index === -1) return;
|
|
1139
1392
|
const [tab] = tabs.splice(index, 1);
|
|
1393
|
+
saveTabDraft(tab); releaseSubscription(tab);
|
|
1140
1394
|
if (tab.pane && tab.pane.node && typeof tab.pane.node.remove === 'function') tab.pane.node.remove();
|
|
1141
1395
|
if (activeTabId === tabId) activeTabId = (tabs[index] || tabs[index - 1] || {}).id || null;
|
|
1396
|
+
if (!tabs.length) { open = false; pickerOpen = false; }
|
|
1142
1397
|
render();
|
|
1143
1398
|
schedulePersist();
|
|
1144
1399
|
if (tab.kind === 'host') {
|
|
@@ -1150,6 +1405,7 @@
|
|
|
1150
1405
|
|
|
1151
1406
|
function toggle() {
|
|
1152
1407
|
open = !open;
|
|
1408
|
+
if (open && !tabs.length) { openPicker(); return open; }
|
|
1153
1409
|
if (open) pickerOpen = false;
|
|
1154
1410
|
render();
|
|
1155
1411
|
schedulePersist();
|
|
@@ -1223,13 +1479,23 @@
|
|
|
1223
1479
|
root.append(resizer, body);
|
|
1224
1480
|
}
|
|
1225
1481
|
if (toggleBtn) toggleBtn.addEventListener('click', () => { toggle(); });
|
|
1482
|
+
if (typeof global.ResizeObserver === 'function' && root && root.parentNode) { const observer = new global.ResizeObserver(applyDockVisibility); observer.observe(root.parentNode); }
|
|
1226
1483
|
domBound = true;
|
|
1227
1484
|
}
|
|
1228
1485
|
|
|
1229
1486
|
function applyDockVisibility() {
|
|
1230
1487
|
if (!root) return;
|
|
1231
1488
|
root.hidden = !open;
|
|
1232
|
-
|
|
1489
|
+
const parent = root.parentNode;
|
|
1490
|
+
const rect = parent && typeof parent.getBoundingClientRect === 'function' ? parent.getBoundingClientRect() : null;
|
|
1491
|
+
const sidebar = parent && parent.querySelector ? parent.querySelector('.sidebar') : null;
|
|
1492
|
+
const sideWidth = sidebar && sidebar.getBoundingClientRect ? sidebar.getBoundingClientRect().width : 0;
|
|
1493
|
+
const available = rect ? rect.width - sideWidth : 0;
|
|
1494
|
+
const drawer = available > 0 && available < 821;
|
|
1495
|
+
if (root.classList) { if (drawer) root.classList.add('rd-drawer'); else root.classList.remove('rd-drawer'); }
|
|
1496
|
+
const effective = available ? drawer ? Math.min(width, available - 24) : Math.min(width, available - 481) : width;
|
|
1497
|
+
root.style.width = open ? Math.max(0, effective) + 'px' : '';
|
|
1498
|
+
if (root.parentNode && root.parentNode.classList) root.parentNode.classList.toggle('dock-open', open);
|
|
1233
1499
|
}
|
|
1234
1500
|
|
|
1235
1501
|
function runningCount() {
|
|
@@ -1243,7 +1509,7 @@
|
|
|
1243
1509
|
if (!toggleBtn) return;
|
|
1244
1510
|
toggleBtn.hidden = false;
|
|
1245
1511
|
toggleBtn.setAttribute('aria-expanded', String(open));
|
|
1246
|
-
toggleBtn.title = open ? '
|
|
1512
|
+
toggleBtn.title = open ? '收起右侧面板' : '展开右侧面板';
|
|
1247
1513
|
const badge = toggleBtn.querySelector && toggleBtn.querySelector('.rd-toggle-count');
|
|
1248
1514
|
if (badge) {
|
|
1249
1515
|
const running = runningCount();
|
|
@@ -1257,68 +1523,87 @@
|
|
|
1257
1523
|
function render() {
|
|
1258
1524
|
ensureDom();
|
|
1259
1525
|
applyDockVisibility();
|
|
1526
|
+
for (const tab of tabs) if (!open || tab.id !== activeTabId) releaseSubscription(tab);
|
|
1260
1527
|
renderTabBar();
|
|
1261
1528
|
renderActivePane();
|
|
1262
1529
|
renderPicker();
|
|
1263
1530
|
renderToggleState();
|
|
1264
1531
|
}
|
|
1265
1532
|
|
|
1533
|
+
function collapseButton() {
|
|
1534
|
+
const button = el('button', 'rd-collapse', '收起');
|
|
1535
|
+
button.type = 'button';
|
|
1536
|
+
button.setAttribute('aria-label', '收起任务面板');
|
|
1537
|
+
button.addEventListener('click', () => { toggle(); });
|
|
1538
|
+
return button;
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1266
1541
|
function renderTabBar() {
|
|
1267
1542
|
if (!tabBar) return;
|
|
1268
1543
|
tabBar.textContent = '';
|
|
1269
|
-
|
|
1544
|
+
|
|
1270
1545
|
tabBar.hidden = false;
|
|
1546
|
+
const tabList = el('div', 'rd-tab-list');
|
|
1547
|
+
tabBar.append(tabList);
|
|
1271
1548
|
for (const tab of tabs) {
|
|
1272
1549
|
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
|
-
|
|
1550
|
+
const status = entry && !entry.typeConflict ? entry.status : null;
|
|
1551
|
+
const item = el('div', 'rd-tab' + (tab.id === activeTabId ? ' active' : ''));
|
|
1552
|
+
const tabOpen = el('button', 'rd-tab-open'); tabOpen.setAttribute('role', 'tab'); tabOpen.setAttribute('aria-selected', String(tab.id === activeTabId));
|
|
1553
|
+
const close = el('button', 'rd-tab-close'); close.type = 'button';
|
|
1554
|
+
if (tab.kind === 'draft' || tab.kind === 'launcher') {
|
|
1555
|
+
item.title = tab.kind === 'launcher' ? '新标签页' : '新子代理';
|
|
1556
|
+
close.textContent = '×'; close.setAttribute('aria-label', '关闭' + item.title);
|
|
1557
|
+
close.addEventListener('click', ev => { if (ev && ev.stopPropagation) ev.stopPropagation(); closeTab(tab.id); });
|
|
1558
|
+
tabOpen.append(el('span', 'rd-tab-label', item.title)); item.append(tabOpen, close);
|
|
1559
|
+
} else if (tab.kind === 'host') {
|
|
1560
|
+
const descriptor = tab.descriptor || (registry ? registry.get(tab.providerId) : null) || { title: '参考资料', panelType: 'knowledge' };
|
|
1278
1561
|
item.title = descriptor.title;
|
|
1279
1562
|
const dot = el('span', 'rd-dot host');
|
|
1280
1563
|
const label = el('span', 'rd-tab-label', descriptor.title);
|
|
1281
|
-
close.title = '
|
|
1564
|
+
close.title = '关闭';
|
|
1282
1565
|
close.setAttribute('aria-label', close.title);
|
|
1283
1566
|
close.textContent = '×';
|
|
1284
1567
|
close.addEventListener('click', ev => { if (ev && ev.stopPropagation) ev.stopPropagation(); closeTab(tab.id); });
|
|
1285
|
-
|
|
1568
|
+
tabOpen.append(dot, label); item.append(tabOpen, close);
|
|
1286
1569
|
} else if (tab.kind === 'delegated') {
|
|
1287
|
-
item.title = tab.delegationId;
|
|
1570
|
+
item.title = delegationTitle(tab.delegationId);
|
|
1288
1571
|
const entry = delegationIndex.get(tab.delegationId);
|
|
1289
|
-
const
|
|
1290
|
-
const dotClass = entry && entry.child && entry.child.active ? 'running' : childState || 'unknown';
|
|
1572
|
+
const dotClass = delegationStatus(entry).status;
|
|
1291
1573
|
const dot = el('span', 'rd-dot ' + dotClass);
|
|
1292
|
-
const label = el('span', 'rd-tab-label',
|
|
1293
|
-
close.title = '
|
|
1574
|
+
const label = el('span', 'rd-tab-label', delegationTitle(tab.delegationId));
|
|
1575
|
+
close.title = '关闭';
|
|
1294
1576
|
close.setAttribute('aria-label', close.title);
|
|
1295
1577
|
close.textContent = '×';
|
|
1296
1578
|
close.addEventListener('click', ev => { if (ev && ev.stopPropagation) ev.stopPropagation(); closeTab(tab.id); });
|
|
1297
|
-
|
|
1579
|
+
tabOpen.append(dot, label); item.append(tabOpen, close);
|
|
1298
1580
|
} else {
|
|
1299
|
-
item.title =
|
|
1581
|
+
item.title = taskTitle(entry);
|
|
1300
1582
|
const dot = el('span', 'rd-dot ' + (status || 'unknown'));
|
|
1301
|
-
const label = el('span', 'rd-tab-label',
|
|
1302
|
-
close.title = '
|
|
1583
|
+
const label = el('span', 'rd-tab-label', taskTitle(entry));
|
|
1584
|
+
close.title = '关闭';
|
|
1303
1585
|
close.setAttribute('aria-label', close.title);
|
|
1304
1586
|
close.textContent = '×';
|
|
1305
1587
|
close.addEventListener('click', ev => { if (ev && ev.stopPropagation) ev.stopPropagation(); closeTab(tab.id); });
|
|
1306
|
-
|
|
1588
|
+
tabOpen.append(dot, label); item.append(tabOpen, close);
|
|
1307
1589
|
}
|
|
1590
|
+
close.setAttribute('aria-label', '关闭' + item.title);
|
|
1308
1591
|
item.addEventListener('click', () => {
|
|
1309
1592
|
activeTabId = tab.id;
|
|
1310
1593
|
render();
|
|
1311
1594
|
schedulePersist();
|
|
1312
1595
|
if (tab.kind === 'host') notifyPanelsChanged('active');
|
|
1313
1596
|
});
|
|
1314
|
-
|
|
1597
|
+
tabList.append(item);
|
|
1315
1598
|
}
|
|
1316
1599
|
const plus = el('button', 'rd-plus');
|
|
1317
|
-
plus.title = '
|
|
1600
|
+
plus.title = '新建标签页';
|
|
1318
1601
|
plus.setAttribute('aria-label', plus.title);
|
|
1319
1602
|
plus.textContent = '+';
|
|
1320
|
-
plus.addEventListener('click', () => {
|
|
1321
|
-
tabBar.append(plus);
|
|
1603
|
+
plus.addEventListener('click', () => { openPicker(); });
|
|
1604
|
+
tabBar.append(plus, collapseButton());
|
|
1605
|
+
const selected = tabList.querySelector && tabList.querySelector('[aria-selected="true"]');
|
|
1606
|
+
if (selected && selected.scrollIntoView) selected.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
1322
1607
|
}
|
|
1323
1608
|
|
|
1324
1609
|
function renderActivePane() {
|
|
@@ -1336,24 +1621,33 @@
|
|
|
1336
1621
|
tab.pane.node.hidden = tab.id !== activeTabId;
|
|
1337
1622
|
}
|
|
1338
1623
|
const activeBeforeScope = tabs.find(tab => tab.id === activeTabId) || tabs[0];
|
|
1624
|
+
if (activeBeforeScope && ['draft', 'launcher'].includes(activeBeforeScope.kind)) {
|
|
1625
|
+
const tab = activeBeforeScope;
|
|
1626
|
+
if (!tab.pane) { if (tab.kind === 'draft') buildDraftPane(tab); else buildLauncherPane(tab); paneHost.append(tab.pane.node); }
|
|
1627
|
+
hideEmptyPane(); tab.pane.node.hidden = false;
|
|
1628
|
+
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); }
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1339
1631
|
if (!scope && (!activeBeforeScope || activeBeforeScope.kind !== 'host')) {
|
|
1340
|
-
renderEmptyPane('
|
|
1632
|
+
renderEmptyPane('打开会话后,可在这里查看子代理任务。');
|
|
1341
1633
|
return;
|
|
1342
1634
|
}
|
|
1343
1635
|
if (!tabs.length) {
|
|
1344
|
-
renderEmptyPane(
|
|
1636
|
+
renderEmptyPane('点击 + 添加页面。');
|
|
1345
1637
|
return;
|
|
1346
1638
|
}
|
|
1347
1639
|
hideEmptyPane();
|
|
1348
1640
|
const active = tabs.find(tab => tab.id === activeTabId) || tabs[0];
|
|
1349
1641
|
if (active && active.id !== activeTabId) activeTabId = active.id;
|
|
1350
1642
|
if (!active) return;
|
|
1643
|
+
if (active.kind === 'host' && !active.descriptor) { active.descriptor = registry && registry.get(active.providerId); if (!active.descriptor) { renderEmptyPane('正在准备知识引用…'); return; } }
|
|
1351
1644
|
if (!active.pane) {
|
|
1352
1645
|
if (active.kind === 'host') buildHostPane(active);
|
|
1353
1646
|
else if (active.kind === 'delegated') buildDelegationPane(active);
|
|
1354
1647
|
else buildPane(active);
|
|
1355
1648
|
paneHost.append(active.pane.node);
|
|
1356
1649
|
}
|
|
1650
|
+
active.pane.node.hidden = false;
|
|
1357
1651
|
if (active.kind === 'host') refreshHostPane(active);
|
|
1358
1652
|
else if (active.kind === 'delegated') refreshDelegationPane(active);
|
|
1359
1653
|
else refreshPane(active);
|
|
@@ -1374,21 +1668,13 @@
|
|
|
1374
1668
|
const node = el('section', 'rd-pane');
|
|
1375
1669
|
node.hidden = true;
|
|
1376
1670
|
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);
|
|
1671
|
+
const title = el('strong', 'rd-title', '子代理任务');
|
|
1672
|
+
const status = el('span', 'rd-task-status');
|
|
1673
|
+
head.append(el('span', 'rd-thread-note', '关联主对话'), status); title.hidden = true;
|
|
1388
1674
|
const banner = el('div', 'rd-banner');
|
|
1389
1675
|
banner.hidden = true;
|
|
1390
1676
|
const bannerText = el('span', 'rd-banner-text');
|
|
1391
|
-
const resyncBtn = el('button', 'rd-resync', '
|
|
1677
|
+
const resyncBtn = el('button', 'rd-resync', '刷新');
|
|
1392
1678
|
resyncBtn.addEventListener('click', () => { void validateScopeTasks(); });
|
|
1393
1679
|
banner.append(bannerText, resyncBtn);
|
|
1394
1680
|
const errorBox = el('div', 'rd-error');
|
|
@@ -1400,15 +1686,27 @@
|
|
|
1400
1686
|
const distance = (scroll.scrollHeight || 0) - (scroll.scrollTop || 0) - (scroll.clientHeight || 0);
|
|
1401
1687
|
const wasFollowing = pane.follow !== false;
|
|
1402
1688
|
pane.follow = distance <= 2;
|
|
1403
|
-
if (wasFollowing && !pane.follow) pane.anchorSeq = lastSequenceOf(tab);
|
|
1689
|
+
if (wasFollowing && !pane.follow) pane.anchorSeq = tab.kind === 'delegated' ? lastDelegationSequenceOf(tab) : lastSequenceOf(tab);
|
|
1404
1690
|
schedulePersist();
|
|
1405
1691
|
});
|
|
1406
1692
|
const more = el('button', 'rd-more', '加载更早记录');
|
|
1407
|
-
more.addEventListener('click', () => { void loadEarlier(tab); });
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1693
|
+
more.addEventListener('click', () => { void (tab.kind === 'delegated' ? loadEarlierDelegation(tab) : loadEarlier(tab)); });
|
|
1694
|
+
node.append(head, banner, errorBox, scroll, more);
|
|
1695
|
+
if (tab.kind !== 'delegated') {
|
|
1696
|
+
const continuation = el('button', 'rd-continue', '基于此结果继续');
|
|
1697
|
+
continuation.addEventListener('click', () => {
|
|
1698
|
+
const texts = [];
|
|
1699
|
+
for (const event of ensurePanelState(tab).events) for (const entry of event.entries || []) {
|
|
1700
|
+
if (entry.role !== 'assistant') continue;
|
|
1701
|
+
const text = (entry.blocks || []).filter(block => block.kind === 'text' && typeof block.text === 'string').map(block => block.text).join('\n');
|
|
1702
|
+
if (text.trim()) texts.push(text);
|
|
1703
|
+
}
|
|
1704
|
+
openNewAgent('请基于“' + taskTitle(taskIndex.get(taskRefKey(tab.taskRef))) + '”的结果继续:\n\n' + (texts.pop() || ''));
|
|
1705
|
+
});
|
|
1706
|
+
node.append(continuation);
|
|
1707
|
+
}
|
|
1708
|
+
tab.pane = { node, head, title, status, banner, bannerText, resyncBtn, errorBox, scroll, more,
|
|
1709
|
+
follow: true, anchorSeq: null, anchorNote: null, pendingNew: false };
|
|
1412
1710
|
}
|
|
1413
1711
|
|
|
1414
1712
|
// Host panels are validated iframe descriptors from the registry. The
|
|
@@ -1419,17 +1717,13 @@
|
|
|
1419
1717
|
const descriptor = tab.descriptor || {};
|
|
1420
1718
|
const node = el('section', 'rd-pane rd-host-pane');
|
|
1421
1719
|
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', '正在加载宿主面板…');
|
|
1720
|
+
const status = el('div', 'rd-host-status', '正在加载…');
|
|
1427
1721
|
const errorBox = el('div', 'rd-error');
|
|
1428
1722
|
errorBox.hidden = true;
|
|
1429
1723
|
const frameHolder = el('div', 'rd-host-frame-holder');
|
|
1430
1724
|
const frame = el('iframe', 'rd-host-frame');
|
|
1431
1725
|
frame.setAttribute('src', descriptor.url || '');
|
|
1432
|
-
frame.setAttribute('title', descriptor.title || '
|
|
1726
|
+
frame.setAttribute('title', descriptor.title || '知识引用');
|
|
1433
1727
|
frame.setAttribute('sandbox', 'allow-scripts');
|
|
1434
1728
|
frame.setAttribute('referrerpolicy', 'no-referrer');
|
|
1435
1729
|
frame.setAttribute('loading', 'eager');
|
|
@@ -1449,12 +1743,12 @@
|
|
|
1449
1743
|
tab.pane.error = 'LOAD_FAILED';
|
|
1450
1744
|
tab.pane.status.hidden = true;
|
|
1451
1745
|
tab.pane.errorBox.hidden = false;
|
|
1452
|
-
tab.pane.errorBox.textContent = '
|
|
1746
|
+
tab.pane.errorBox.textContent = '内容加载失败,请稍后重试。';
|
|
1453
1747
|
notifyPanelsChanged('error');
|
|
1454
1748
|
});
|
|
1455
1749
|
frameHolder.append(frame);
|
|
1456
|
-
node.append(
|
|
1457
|
-
tab.pane = { node,
|
|
1750
|
+
node.append(status, errorBox, frameHolder);
|
|
1751
|
+
tab.pane = { node, status, errorBox, frameHolder, frame, follow: true, anchorSeq: null, anchorNote: null, pendingNew: false, finalNode: null, error: null };
|
|
1458
1752
|
}
|
|
1459
1753
|
|
|
1460
1754
|
function refreshHostPane(tab) {
|
|
@@ -1462,12 +1756,9 @@
|
|
|
1462
1756
|
if (!pane) return;
|
|
1463
1757
|
const descriptor = tab.descriptor || (registry ? registry.get(tab.providerId) : null) || {};
|
|
1464
1758
|
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
1759
|
pane.errorBox.hidden = !pane.error;
|
|
1470
|
-
if (pane.error) pane.errorBox.textContent =
|
|
1760
|
+
if (pane.error) pane.errorBox.textContent = '内容加载失败,请稍后重试。';
|
|
1761
|
+
|
|
1471
1762
|
pane.status.hidden = pane.error != null || pane.loaded === true;
|
|
1472
1763
|
}
|
|
1473
1764
|
|
|
@@ -1511,6 +1802,7 @@
|
|
|
1511
1802
|
if (typeof data.version === 'number' && Number.isFinite(data.version)) version = data.version;
|
|
1512
1803
|
else if (typeof data.version === 'string' && data.version.trim() && data.version.length <= 200) version = data.version.trim();
|
|
1513
1804
|
if (version === null) return null;
|
|
1805
|
+
if (tab.pane.contentVersion == null || String(version) !== String(tab.pane.contentVersion)) return null;
|
|
1514
1806
|
const ok = data.ok === true;
|
|
1515
1807
|
const error = typeof data.error === 'string' && data.error.trim() ? data.error.slice(0, 300) : null;
|
|
1516
1808
|
tab.pane.contentAck = { version, ok, error, at: Date.now() };
|
|
@@ -1532,19 +1824,7 @@
|
|
|
1532
1824
|
const value = Number(children[i] && children[i].dataset ? children[i].dataset.seq : NaN);
|
|
1533
1825
|
if (Number.isFinite(value)) return value;
|
|
1534
1826
|
}
|
|
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;
|
|
1827
|
+
return null;
|
|
1548
1828
|
}
|
|
1549
1829
|
|
|
1550
1830
|
function refreshPane(tab) {
|
|
@@ -1552,52 +1832,21 @@
|
|
|
1552
1832
|
if (!pane) return;
|
|
1553
1833
|
const panel = ensurePanelState(tab);
|
|
1554
1834
|
const entry = taskIndex.get(taskRefKey(tab.taskRef)) || null;
|
|
1835
|
+
pane.title.textContent = taskTitle(entry);
|
|
1836
|
+
pane.status.textContent = entry && !entry.typeConflict ? (STATUS_LABELS[entry.status] || '状态待确认') : '状态待确认';
|
|
1837
|
+
pane.errorBox.hidden = !tab.invalid && !panel.error;
|
|
1838
|
+
if (tab.invalid || panel.error) pane.errorBox.textContent = '暂时无法查看此任务,请刷新后重试。';
|
|
1839
|
+
pane.banner.hidden = !panel.resync;
|
|
1840
|
+
if (panel.resync) pane.bannerText.textContent = '记录已更新,请刷新查看。';
|
|
1555
1841
|
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
|
-
}
|
|
1842
|
+
pane.scroll.textContent = '';
|
|
1843
|
+
pane.more.hidden = true;
|
|
1844
|
+
pane.banner.hidden = true;
|
|
1567
1845
|
return;
|
|
1568
1846
|
}
|
|
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
1847
|
if (!panel.rendered) renderTimeline(tab);
|
|
1598
1848
|
pane.more.hidden = !(panel.hasMore && panel.nextCursor);
|
|
1599
1849
|
pane.more.disabled = panel.loading === true;
|
|
1600
|
-
refreshFoot(tab);
|
|
1601
1850
|
}
|
|
1602
1851
|
|
|
1603
1852
|
function renderTimeline(tab) {
|
|
@@ -1606,61 +1855,36 @@
|
|
|
1606
1855
|
if (!pane) return;
|
|
1607
1856
|
const keepScroll = pane.follow === false ? pane.scroll.scrollTop : null;
|
|
1608
1857
|
pane.scroll.textContent = '';
|
|
1609
|
-
|
|
1610
|
-
let lastTextNode = null;
|
|
1858
|
+
const conversation = createConversation(pane.scroll);
|
|
1611
1859
|
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; }
|
|
1860
|
+
for (const event of ordered) appendNativeConversation(conversation, event);
|
|
1861
|
+
const entry = taskIndex.get(taskRefKey(tab.taskRef));
|
|
1862
|
+
if (entry && TERMINAL_STATUSES.includes(entry.status)) conversation.finish(ordered.length ? ordered[ordered.length - 1].sequence : null);
|
|
1863
|
+
pane.conversation = conversation;
|
|
1619
1864
|
panel.rendered = true;
|
|
1620
1865
|
if (keepScroll !== null) pane.scroll.scrollTop = keepScroll;
|
|
1621
1866
|
else if (pane.follow !== false) pane.scroll.scrollTop = pane.scroll.scrollHeight;
|
|
1622
1867
|
}
|
|
1623
1868
|
|
|
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)));
|
|
1869
|
+
function appendNativeConversation(conversation, event) {
|
|
1870
|
+
if (!event || typeof event !== 'object') return;
|
|
1871
|
+
if (event.type === 'claude/task-interrupted') { conversation.accept(event); return; }
|
|
1872
|
+
if (event.type !== 'claude/native-task-transcript') return;
|
|
1873
|
+
if (!event.read || event.read.status !== 'ok') {
|
|
1874
|
+
conversation.accept({ type: 'conversation/unavailable', sequence: event.sequence });
|
|
1875
|
+
return;
|
|
1876
|
+
}
|
|
1877
|
+
for (const entry of Array.isArray(event.entries) ? event.entries : []) {
|
|
1878
|
+
if (!entry || typeof entry !== 'object') continue;
|
|
1879
|
+
const blocks = Array.isArray(entry.blocks) ? entry.blocks : [];
|
|
1880
|
+
const text = blocks.filter(block => block && block.kind === 'text' && typeof block.text === 'string').map(block => block.text).join('\n');
|
|
1881
|
+
if (text) conversation.accept({ type: entry.role === 'assistant' ? 'conversation/assistant' : 'claude/user-prompt', text, sequence: event.sequence });
|
|
1882
|
+
for (const block of blocks) {
|
|
1883
|
+
if (!block || typeof block !== 'object') continue;
|
|
1884
|
+
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 });
|
|
1885
|
+
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
1886
|
}
|
|
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
1887
|
}
|
|
1661
|
-
const seq = Number(event.sequence);
|
|
1662
|
-
if (node && Number.isFinite(seq) && node.dataset) node.dataset.seq = String(seq);
|
|
1663
|
-
return { node, textNode };
|
|
1664
1888
|
}
|
|
1665
1889
|
|
|
1666
1890
|
function renderMdInto(node, text) {
|
|
@@ -1671,99 +1895,10 @@
|
|
|
1671
1895
|
node.textContent = value;
|
|
1672
1896
|
}
|
|
1673
1897
|
|
|
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
1898
|
|
|
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
1899
|
|
|
1764
1900
|
function renderAnchorNote(pane, text) {
|
|
1765
1901
|
pane.anchorNote = text;
|
|
1766
|
-
if (pane.note) { pane.note.textContent = text; pane.note.hidden = !text; }
|
|
1767
1902
|
}
|
|
1768
1903
|
|
|
1769
1904
|
function applyAnchor(tab) {
|
|
@@ -1779,6 +1914,7 @@
|
|
|
1779
1914
|
renderAnchorNote(pane, '已定位到最新记录');
|
|
1780
1915
|
return;
|
|
1781
1916
|
}
|
|
1917
|
+
if (anchor && Number.isFinite(anchor.top)) { pane.scroll.scrollTop = anchor.top; renderAnchorNote(pane, '已恢复阅读位置'); return; }
|
|
1782
1918
|
if (Number.isFinite(seq)) {
|
|
1783
1919
|
let matched = null;
|
|
1784
1920
|
for (const node of Array.from(pane.scroll.children || [])) {
|
|
@@ -1796,75 +1932,14 @@
|
|
|
1796
1932
|
} else renderAnchorNote(pane, '已恢复读取位置(跟随关闭)');
|
|
1797
1933
|
}
|
|
1798
1934
|
|
|
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));
|
|
1935
|
+
function openPicker() {
|
|
1936
|
+
let tab = tabs.find(item => item.kind === 'launcher');
|
|
1937
|
+
if (!tab) { tab = { id: nextTabId(), kind: 'launcher', pane: null }; tabs.push(tab); }
|
|
1938
|
+
else if (tab.pane) { tab.pane.node.remove(); tab.pane = null; }
|
|
1939
|
+
activeTabId = tab.id; open = true; pickerOpen = true; render(); schedulePersist();
|
|
1867
1940
|
}
|
|
1941
|
+
function closePicker() { pickerOpen = false; }
|
|
1942
|
+
function renderPicker() { if (pickerHost) pickerHost.hidden = true; }
|
|
1868
1943
|
|
|
1869
1944
|
function snapshot() {
|
|
1870
1945
|
return {
|
|
@@ -1885,6 +1960,7 @@
|
|
|
1885
1960
|
providerId: tab.kind === 'host' ? tab.providerId : null,
|
|
1886
1961
|
panelType: tab.kind === 'host' && tab.descriptor ? tab.descriptor.panelType : null,
|
|
1887
1962
|
hostError: tab.pane && tab.pane.error ? tab.pane.error : null,
|
|
1963
|
+
hostReady: Boolean(tab.kind === 'host' && tab.pane && tab.pane.loaded),
|
|
1888
1964
|
delegation: tab.kind === 'delegated' ? delegationSnapshotOf(tab) : null,
|
|
1889
1965
|
content: tab.kind === 'host' && tab.pane ? {
|
|
1890
1966
|
version: tab.pane.contentVersion !== undefined ? tab.pane.contentVersion : null,
|
|
@@ -1909,12 +1985,40 @@
|
|
|
1909
1985
|
return Array.from(tab.pane.scroll.children || []).map(node => String(node.textContent || '')).join('\n');
|
|
1910
1986
|
}
|
|
1911
1987
|
|
|
1912
|
-
function pickerText() { return
|
|
1988
|
+
function pickerText() { const tab = tabs.find(item => item.kind === 'launcher'); return tab && tab.pane ? String(tab.pane.node.textContent || '') : ''; }
|
|
1989
|
+
|
|
1990
|
+
function taskViews(parentSessionId) {
|
|
1991
|
+
if (parentSessionId !== undefined && (!scope || scope.parentSessionId !== parentSessionId)) return [];
|
|
1992
|
+
const native = [...taskIndex.values()].filter(entry => entry.taskRef && scopeRefMatches(scope, entry.taskRef)).map(entry => ({
|
|
1993
|
+
taskRef: { ...entry.taskRef }, taskId: entry.taskId, status: entry.status,
|
|
1994
|
+
description: taskTitle(entry),
|
|
1995
|
+
}));
|
|
1996
|
+
const delegated = [...delegationIndex.values()].map(view => ({
|
|
1997
|
+
delegationId: view.delegationId, taskId: view.childSessionId,
|
|
1998
|
+
description: delegationTitle(view.delegationId),
|
|
1999
|
+
status: delegationStatus(view).status, statusLabel: delegationStatus(view).label,
|
|
2000
|
+
}));
|
|
2001
|
+
return [...native, ...delegated];
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
async function openTaskForToolUseId(toolUseId) {
|
|
2005
|
+
if (!isId(toolUseId) || !scope) return null;
|
|
2006
|
+
const epoch = scopeEpoch;
|
|
2007
|
+
await validateScopeTasks();
|
|
2008
|
+
if (epoch !== scopeEpoch) return null;
|
|
2009
|
+
const matches = [...taskIndex.values()].filter(entry => entry.launcherToolUseId === toolUseId && entry.taskRef && scopeRefMatches(scope, entry.taskRef));
|
|
2010
|
+
if (matches.length !== 1) {
|
|
2011
|
+
toast('此子代理暂未提供可读取的独立任务记录。', true);
|
|
2012
|
+
return null;
|
|
2013
|
+
}
|
|
2014
|
+
return openTask(matches[0].taskRef, { reuse: true });
|
|
2015
|
+
}
|
|
1913
2016
|
|
|
1914
2017
|
const api = {
|
|
1915
2018
|
attachSession, detachSession, handleSessionEvent, handleResync,
|
|
1916
2019
|
refreshIdentity,
|
|
1917
|
-
|
|
2020
|
+
refreshProviders() { for (const tab of tabs) if (tab.kind === 'launcher' && tab.pane) { tab.pane.node.remove(); tab.pane = null; } render(); },
|
|
2021
|
+
openTask, openTaskForToolUseId, taskViews, openNewAgent, openDelegation, notifySessionChanged, backgroundActivity,
|
|
1918
2022
|
closeTab, toggle, setWidth, adjustWidth,
|
|
1919
2023
|
openPanel, closeHostPanels, activateHostPanel,
|
|
1920
2024
|
relayHostPanelContent, ackHostPanelContent,
|