@kingsoftcloud/agentkit-harness-ui 0.0.0-stage → 0.0.1

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.
Files changed (56) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +116 -0
  3. package/README.md +292 -2
  4. package/markdown/build/build-markdown.cjs +66 -0
  5. package/markdown/dist/THIRD_PARTY_LICENSES.txt +2993 -0
  6. package/markdown/dist/markdown.css +1 -0
  7. package/markdown/dist/markdown.mjs +325 -0
  8. package/markdown/source/markdown-factory.mjs +613 -0
  9. package/markdown/source/markdown.css +288 -0
  10. package/markdown/source/table-factory.mjs +1588 -0
  11. package/markdown/source/table.css +487 -0
  12. package/package.json +96 -4
  13. package/src/activity-groups.mjs +74 -0
  14. package/src/browser-response-groups.mjs +57 -0
  15. package/src/command-output.mjs +57 -0
  16. package/src/components.mjs +9 -0
  17. package/src/context-markers.mjs +19 -0
  18. package/src/context-status.mjs +34 -0
  19. package/src/diff.mjs +240 -0
  20. package/src/explore-tool-call.mjs +139 -0
  21. package/src/file-preview.mjs +50 -0
  22. package/src/host/native-a2a.mjs +90 -0
  23. package/src/host/native-child-file-pane.mjs +49 -0
  24. package/src/host/native-child-pane.mjs +94 -0
  25. package/src/host/native-command-pane.mjs +64 -0
  26. package/src/host/native-run-panel.mjs +202 -0
  27. package/src/host/session-task-events.mjs +247 -0
  28. package/src/host/state/native-a2a.mjs +122 -0
  29. package/src/host/state/native-child-observation.mjs +35 -0
  30. package/src/host/state/native-children.mjs +93 -0
  31. package/src/host/state/native-outcome.mjs +35 -0
  32. package/src/host/state/native-recovery.mjs +187 -0
  33. package/src/host/state/native-runtime-route.mjs +26 -0
  34. package/src/host/state/native-session-stop.mjs +40 -0
  35. package/src/host/state/native-status-panel.mjs +23 -0
  36. package/src/host/state/native-task-read-model.mjs +89 -0
  37. package/src/host/state/task-links.mjs +40 -0
  38. package/src/host/task-timeline.mjs +185 -0
  39. package/src/index.mjs +52 -0
  40. package/src/input-preview.mjs +44 -0
  41. package/src/inspector.mjs +40 -0
  42. package/src/project.mjs +132 -0
  43. package/src/protocol.mjs +362 -0
  44. package/src/react-binding.mjs +486 -0
  45. package/src/reducer.mjs +355 -0
  46. package/src/renderers.mjs +644 -0
  47. package/src/status-panel.mjs +1156 -0
  48. package/src/styles.mjs +278 -0
  49. package/src/summary-playback.mjs +39 -0
  50. package/src/summary.mjs +160 -0
  51. package/src/terminal.mjs +157 -0
  52. package/src/tool-detail.mjs +208 -0
  53. package/src/tool-error.mjs +33 -0
  54. package/src/tool-identity.mjs +40 -0
  55. package/src/view.mjs +364 -0
  56. package/src/work-segments.mjs +41 -0
@@ -0,0 +1,187 @@
1
+ const check = (value, message) => { if (!value) throw Error(message); };
2
+ const counters = ['inputTokens', 'outputTokens', 'modelCalls', 'toolCalls', 'cost'];
3
+ const optionalCounters = ['cachedInputTokens', 'reasoningTokens'];
4
+ const validUsage = usage => usage && typeof usage === 'object' && !Array.isArray(usage)
5
+ && Object.keys(usage).every(key => [...counters, ...optionalCounters].includes(key))
6
+ && [...counters, ...optionalCounters.filter(key => key in usage)].every(key => typeof usage[key] === 'number'
7
+ && Number.isFinite(usage[key]) && usage[key] >= 0 && (key === 'cost' || Number.isSafeInteger(usage[key])));
8
+
9
+ export function validateNativeRecoveryStatus(value, { agentId, sessionId, runId } = {}) {
10
+ check(value?.contract === 'agentkit.studio-native-recovery/v1' && value.agentId === agentId && value.sessionId === sessionId
11
+ && ['available', 'recovering', 'reconciliation_required', 'failed', 'closed'].includes(value.state)
12
+ && typeof value.canResume === 'boolean' && (!value.canResume || typeof value.runId === 'string' && value.runId.trim())
13
+ && (!runId || value.runId === runId), '恢复记录与原会话或运行不一致,请刷新核对。');
14
+ if (value.error != null) check(typeof value.error?.code === 'string' && /^[A-Za-z0-9_.:/-]{1,256}$/.test(value.error.code)
15
+ && typeof value.error.message === 'string' && value.error.message.length > 0 && value.error.message.length <= 512,
16
+ '运行错误记录无法识别,请刷新核对。');
17
+ if (value.outcome != null) check(['verified', 'unverified', 'partial', 'blocked', 'failed', 'budget_exhausted'].includes(value.outcome), '任务结果状态无法识别,请刷新核对。');
18
+ if (value.verificationRequired !== undefined) check(typeof value.verificationRequired === 'boolean', '任务验证要求无法识别,请刷新核对。');
19
+ if (value.canRetireResources !== undefined) check(typeof value.canRetireResources === 'boolean'
20
+ && (!value.canRetireResources || !value.managed && value.phase === 'interrupted'
21
+ && Number.isSafeInteger(value.resourceRetirementEventSeq) && value.resourceRetirementEventSeq >= 0),
22
+ '资源退役权限无法识别,请刷新核对。');
23
+ if(value.resourceRetirementReview!==undefined)check(value.resourceRetirementReview?.contract==='agentkit.resource-retirement-review/v1'
24
+ && typeof value.resourceRetirementReview.commandId==='string' && value.resourceRetirementReview.commandId.trim()
25
+ && value.resourceRetirementReview.commandId.length<=256 && value.resourceRetirementReview.resourceKind==='ephemeral-browser'
26
+ && value.resourceRetirementReview.eventSeq===value.resourceRetirementEventSeq,'资源退役记录与原运行不一致,请刷新核对。');
27
+ if (value.canReviewBackground !== undefined) check(typeof value.canReviewBackground === 'boolean'
28
+ && (!value.canReviewBackground || !value.managed && value.phase === 'interrupted'
29
+ && value.reason === 'effect_outcome_unknown' && Number.isSafeInteger(value.backgroundReviewEventSeq)
30
+ && value.backgroundReviewEventSeq >= 0), '后台操作核对权限无法识别,请刷新核对。');
31
+ if (value.backgroundInterruptionReview !== undefined) {
32
+ const review = value.backgroundInterruptionReview;
33
+ check(review?.contract === 'agentkit.background-interruption-review/v1' && review.confirmedStopped === true
34
+ && typeof review.commandId === 'string' && !!review.commandId.trim() && review.commandId.length <= 256
35
+ && review.eventSeq === value.backgroundReviewEventSeq && value.phase === 'interrupted'
36
+ && value.reason === 'effect_outcome_unknown', '后台操作处置记录无法识别,请刷新核对。');
37
+ }
38
+ if (value.capabilities != null) check(Array.isArray(value.capabilities.toolNames)
39
+ && value.capabilities.toolNames.every(name => typeof name === 'string' && name.length > 0 && name.length <= 256), '能力清单无法识别,请刷新核对。');
40
+ const receiptWait = value.receiptWait;
41
+ if (value.managed !== undefined) check(value.managed?.contract === 'agentkit.studio-managed-native-recovery/v1'
42
+ && value.managed.controlAuthority === 'server' && ['open', 'closed', 'pending'].includes(value.managed.owner)
43
+ && ['accepted', 'settled', 'discarded'].includes(value.managed.commandState)
44
+ && ['queued', 'running', 'idle'].includes(value.managed.executionState)
45
+ && typeof value.managed.commandId === 'string' && value.managed.commandId.trim() && value.managed.commandId.length <= 256
46
+ && typeof value.managed.retryAcceptedCommand === 'boolean'
47
+ && (!value.canResume || value.managed.retryAcceptedCommand) && value.canReconcile === false,
48
+ '托管运行的控制权限与原记录不一致,请刷新核对。');
49
+ const grant = value.managed?.executionGrant;
50
+ if (grant !== undefined) check(grant?.contract === 'agentkit.managed-native-execution-grants/v1'
51
+ && typeof grant.grantId === 'string' && grant.grantId === grant.grantId.trim() && grant.grantId.length > 0 && grant.grantId.length <= 512
52
+ && ['active', 'suspended', 'revoked', 'missing'].includes(grant.state)
53
+ && [grant.revision, grant.admissionRevision].every(revision => grant.state === 'missing' ? revision === null : Number.isSafeInteger(revision) && revision >= 1)
54
+ && (grant.attemptEpoch === null || Number.isSafeInteger(grant.attemptEpoch) && grant.attemptEpoch >= 1)
55
+ && (grant.expiresAt === null || typeof grant.expiresAt === 'string' && Number.isFinite(Date.parse(grant.expiresAt)))
56
+ && typeof grant.admissionAllowed === 'boolean' && typeof grant.qualified === 'boolean'
57
+ && (grant.blockedReason === null || ['not_found', 'scope_mismatch', 'revoked', 'attempt_epoch_mismatch', 'expired',
58
+ 'clock_unverified', 'suspended', 'admission_paused'].some(reason => grant.blockedReason === `execution_grant_${reason}`))
59
+ && (!grant.blockedReason || !value.canResume && !value.managed.retryAcceptedCommand),
60
+ '执行授权与原托管运行不一致,请刷新核对。');
61
+ if (value.managed?.commandState === 'discarded') check(value.state === 'closed' && value.phase === 'discarded'
62
+ && !value.canResume && !value.managed.retryAcceptedCommand && value.managed.owner === 'closed'
63
+ && !grant?.qualified, '已丢弃的托管命令不能重新执行。');
64
+ if (receiptWait !== undefined) check(receiptWait?.contract === 'agentkit.native-a2a-receipt-wait/v1'
65
+ && value.phase === 'awaiting_reconciliation'
66
+ && ['operationKey', 'callId'].every(key => typeof receiptWait[key] === 'string' && receiptWait[key].trim()
67
+ && receiptWait[key] === value.reconciliation?.[key]), '回执等待与原运行不一致,请刷新核对。');
68
+ if (value.canReconcile !== undefined) check(typeof value.canReconcile === 'boolean'
69
+ && (!value.canReconcile || (value.state === 'reconciliation_required' && (!value.canResume || receiptWait)
70
+ || value.state === 'recovering' && !value.canResume)
71
+ && value.phase === 'awaiting_reconciliation' && typeof value.runId === 'string' && value.runId.trim()
72
+ && value.reconciliation?.contract === 'agentkit.tool-receipt-observation/v1'
73
+ && ['operationKey', 'callId'].every(key => typeof value.reconciliation[key] === 'string'
74
+ && value.reconciliation[key].trim() && value.reconciliation[key].length <= 256)),
75
+ '核对记录与原运行不一致,请刷新核对。');
76
+ if (value.canRepairWorkspace !== undefined) check(typeof value.canRepairWorkspace === 'boolean'
77
+ && (!value.canRepairWorkspace || !value.managed && value.canReconcile === true
78
+ && value.phase === 'awaiting_reconciliation'), '文件修复权限与原运行不一致,请刷新核对。');
79
+ if (value.a2aTasks !== undefined) check(Array.isArray(value.a2aTasks) && value.a2aTasks.length <= 10000
80
+ && value.a2aTasks.every(task => task?.runId === value.runId && ['runId', 'operationKey', 'callId']
81
+ .every(key => typeof task[key] === 'string' && task[key].trim() && task[key].length <= 256)),
82
+ '远端任务清单与原运行不一致,请刷新核对。');
83
+ const memory = value.cloudMemory;
84
+ if (memory != null) {
85
+ const id = text => typeof text === 'string' && text.trim().length > 0 && text.length <= 256;
86
+ const base = ['contract', 'sessionId', 'runId', 'state'];
87
+ const fields = [...base, 'resourceId', 'operationId', 'requestId', 'messageIds', 'phase', 'receiptRequestId', 'extraction', 'createdAt', 'completedAt'];
88
+ check(memory.contract === 'agentkit.studio-native-cloud-memory-status/v1' && memory.runId === value.runId
89
+ && memory.sessionId === sessionId && ['accepted', 'unconfirmed', 'outcome_unknown', 'unavailable'].includes(memory.state)
90
+ && Object.keys(memory).every(key => (memory.state === 'unavailable' ? base : fields).includes(key))
91
+ && (memory.state === 'unavailable' || id(memory.resourceId) && /^[0-9a-f]{64}$/.test(memory.operationId)
92
+ && id(memory.requestId) && Array.isArray(memory.messageIds) && memory.messageIds.length > 0 && memory.messageIds.length <= 20
93
+ && memory.messageIds.every(id) && memory.extraction === 'not_confirmed'
94
+ && memory.phase === (memory.state === 'unconfirmed' ? 'prepared' : 'finished')
95
+ && (memory.state === 'accepted' ? id(memory.receiptRequestId) : memory.receiptRequestId === null)
96
+ && Number.isSafeInteger(memory.createdAt) && memory.createdAt >= 0
97
+ && (memory.phase === 'prepared' ? memory.completedAt === null : Number.isSafeInteger(memory.completedAt) && memory.completedAt >= 0)),
98
+ '云记忆提交记录与原运行不一致,请刷新核对。');
99
+ }
100
+ const extraction = value.memoryExtraction;
101
+ if (extraction !== undefined) {
102
+ const id = text => typeof text === 'string' && text.trim().length > 0 && text.length <= 256;
103
+ check(extraction?.contract === 'agentkit.studio-native-memory-extraction/v1'
104
+ && extraction.agentId === agentId && extraction.sessionId === sessionId
105
+ && (extraction.cursor === null || id(extraction.cursor))
106
+ && Array.isArray(extraction.jobs) && extraction.jobs.length <= 100
107
+ && extraction.jobs.every(job => id(job.jobId) && id(job.parentRunId)
108
+ && ['queued', 'running', 'success', 'no-op', 'error', 'aborted', 'superseded'].includes(job.status)
109
+ && (job.usage === null || validUsage(job.usage))
110
+ && (job.errorCode === null || id(job.errorCode))
111
+ && Number.isSafeInteger(job.createdAt) && job.createdAt >= 0
112
+ && Number.isSafeInteger(job.updatedAt) && job.updatedAt >= job.createdAt),
113
+ '自动记忆提炼记录与原会话不一致,请刷新核对。');
114
+ }
115
+ const accounting = value.accounting;
116
+ if (accounting != null) {
117
+ check(accounting.contract === 'agentkit.studio-native-usage/v1' && accounting.runId === value.runId
118
+ && Number.isSafeInteger(accounting.snapshotRevision) && accounting.snapshotRevision >= 0
119
+ && ['final', 'incomplete', 'complete'].every(key => typeof accounting[key] === 'boolean')
120
+ && accounting.complete === (accounting.final && !accounting.incomplete)
121
+ && [accounting.own, accounting.descendant, accounting.total].every(validUsage)
122
+ && [...counters, ...optionalCounters].every(key => (accounting.total[key] ?? 0)
123
+ === (accounting.own[key] ?? 0) + (accounting.descendant[key] ?? 0)), '运行用量与原记录不一致,请刷新核对。');
124
+ }
125
+ return value;
126
+ }
127
+
128
+ /** Reads never resume work. A lost response retains the original Run identity. */
129
+ export function createNativeRecoveryState({ hostRemote, agentId, sessionId, newCommandId = () => globalThis.crypto.randomUUID() }) {
130
+ const identity = { agentId, sessionId }, lifetime = new AbortController(), listeners = new Set();
131
+ let disposed = false, state = { status: null, loading: false, sending: false, pending: null, error: '' };
132
+ const set = change => { if (!disposed) { state = { ...state, ...change }; for (const listener of listeners) listener(state); } };
133
+ const validate = (value, runId) => validateNativeRecoveryStatus(value, { agentId, sessionId, runId });
134
+
135
+ async function refresh() {
136
+ if (disposed || state.loading) return;
137
+ set({ loading: true });
138
+ try {
139
+ const status = validate(await hostRemote.nativeRecoveryStatus(identity, lifetime.signal));
140
+ set({ status, error: '' });
141
+ } catch { set({ error: '恢复状态读取失败,请刷新。' }); }
142
+ finally { set({ loading: false }); }
143
+ }
144
+ async function send() {
145
+ if (disposed || state.sending || !state.pending) return;
146
+ const pending = state.pending;
147
+ set({ sending: true, error: '' });
148
+ try {
149
+ const { repairWorkspace, reviewBackground, retireResources, ...request } = pending;
150
+ const operation = retireResources ? hostRemote.retireNativeResources : reviewBackground ? hostRemote.reviewNativeBackground : repairWorkspace ? hostRemote.repairNativeWorkspace
151
+ : pending.operationKey ? hostRemote.reconcileNativeTool : hostRemote.resumeNativeRun;
152
+ const status = validate(await operation(request, lifetime.signal), pending.runId);
153
+ set({ status, pending: null });
154
+ await refresh();
155
+ } catch { set({ error: '恢复请求尚未确认。可确认原运行的请求,请勿创建替代任务。' }); }
156
+ finally { set({ sending: false }); }
157
+ }
158
+ return { getSnapshot: () => state, subscribe(listener) { listeners.add(listener); return () => listeners.delete(listener); },
159
+ refresh, retry: send,
160
+ resume() {
161
+ if (disposed || state.pending || !state.status?.canResume) return;
162
+ set({ pending: { ...identity, runId: state.status.runId } }); return send();
163
+ },
164
+ reconcile() {
165
+ if (disposed || state.pending || !state.status?.canReconcile) return;
166
+ set({ pending: { ...identity, runId: state.status.runId, operationKey: state.status.reconciliation.operationKey,
167
+ commandId: newCommandId() } }); return send();
168
+ },
169
+ repairWorkspace() {
170
+ if (disposed || state.pending || !state.status?.canRepairWorkspace) return;
171
+ set({ pending: { ...identity, runId: state.status.runId, operationKey: state.status.reconciliation.operationKey,
172
+ commandId: newCommandId(), repairWorkspace: true } }); return send();
173
+ },
174
+ reviewBackground() {
175
+ if (disposed || state.pending || !state.status?.canReviewBackground) return;
176
+ set({ pending: { ...identity, runId: state.status.runId,
177
+ commandId: state.status.backgroundInterruptionReview?.commandId ?? newCommandId(),
178
+ eventSeq: state.status.backgroundReviewEventSeq, confirmedStopped: true, reviewBackground: true } }); return send();
179
+ },
180
+ retireResources() {
181
+ if (disposed || state.pending || !state.status?.canRetireResources) return;
182
+ set({pending:{...identity,runId:state.status.runId,commandId:state.status.resourceRetirementReview?.commandId??newCommandId(),
183
+ eventSeq:state.status.resourceRetirementEventSeq,confirmedRetired:true,retireResources:true}});return send();
184
+ },
185
+ dispose() { disposed = true; lifetime.abort(); listeners.clear(); },
186
+ };
187
+ }
@@ -0,0 +1,26 @@
1
+ /** Runtime routing is resolved before choosing a transcript renderer. A failed
2
+ * lookup is not evidence that the conversation belongs to another runtime. */
3
+ export function nativeRuntimeRouteInput(_session, openState, projection, boundPresetId) {
4
+ // Identity comes from the current Session's keyed projection subscription,
5
+ // not the manager's asynchronously batched catalog/list snapshot. Read state
6
+ // only establishes readiness; it cannot overwrite the live preset identity.
7
+ return { presetId: boundPresetId, ready: openState === 'open' && projection?.state === 'ready' };
8
+ }
9
+
10
+ export async function resolveNativeRuntimeRoute({ hostRemote, presetId, resolveAgentFromPreset, signal }) {
11
+ const first = await hostRemote.agents({ source: 'local', pageSize: 100 });
12
+ signal.throwIfAborted();
13
+ if (!Array.isArray(first?.items)) throw Error('智能体目录响应无效');
14
+ const items = [...first.items];
15
+ for (let page = 2; items.length < Number(first.total || 0); page++) {
16
+ const next = await hostRemote.agents({ source: 'local', pageSize: 100, page });
17
+ signal.throwIfAborted();
18
+ if (!next.items?.length) throw Error('智能体目录尚未完整读取');
19
+ items.push(...next.items);
20
+ }
21
+ const agentId = resolveAgentFromPreset(presetId, items);
22
+ if (!agentId) return { kind: 'other', agentId: null };
23
+ const agent = items.find(item => (item.metadata?.id ?? item.id) === agentId);
24
+ if (!agent) throw Error('当前会话的智能体配置暂不可用');
25
+ return agent.spec?.runtime?.type === 'harness' ? { kind: 'native', agentId } : { kind: 'other', agentId };
26
+ }
@@ -0,0 +1,40 @@
1
+ /** Session cancellation is a Host control, independent of tool interaction UI.
2
+ * An acknowledgement means requested, never that an effect has stopped. */
3
+ export function createNativeSessionStop({ sessionId, requestStop }) {
4
+ let live = true, observation, running = false, generation = 0, pending = false;
5
+ let phase = 'idle', error = null;
6
+ const listeners = new Set();
7
+ const owned = () => Boolean(observation?.sessionId === sessionId && observation.runId
8
+ && !observation.managed && observation.state === 'recovering' && running);
9
+ const eligible = () => owned() && observation.phase === 'awaiting_interaction';
10
+ const getSnapshot = () => ({ phase, error, visible: eligible() || owned() && phase !== 'idle',
11
+ canStop: typeof requestStop === 'function' && eligible() && !pending && phase !== 'requested' });
12
+ const emit = () => { const value = getSnapshot(); for (const listener of listeners) listener(value); };
13
+ return {
14
+ getSnapshot,
15
+ subscribe(listener) { listeners.add(listener); return () => listeners.delete(listener); },
16
+ observe(status, transportRunning) {
17
+ if (!live) return;
18
+ if (observation?.runId !== status?.runId || !transportRunning || status?.state !== 'recovering') {
19
+ generation++; pending = false; phase = 'idle'; error = null;
20
+ }
21
+ observation = status; running = transportRunning === true; emit();
22
+ },
23
+ async request() {
24
+ if (!live || !getSnapshot().canStop || typeof requestStop !== 'function') return false;
25
+ const current = generation; pending = true; phase = 'requesting'; error = null; emit();
26
+ try {
27
+ // The adapter invokes the existing Session owner, not a tool decision,
28
+ // ledger mutation, resume operation, or a guessed Native Run command.
29
+ const result = await requestStop(sessionId);
30
+ if (!live || current !== generation) return false;
31
+ if (result?.ok !== true || result.value?.accepted !== true) throw Error('stop_unconfirmed');
32
+ phase = 'requested'; return true;
33
+ } catch {
34
+ if (live && current === generation) { phase = 'error'; error = '停止请求未确认,请重试。'; }
35
+ return false;
36
+ } finally { if (live && current === generation) { pending = false; emit(); } }
37
+ },
38
+ dispose() { live = false; generation++; listeners.clear(); },
39
+ };
40
+ }
@@ -0,0 +1,23 @@
1
+ /** Host facts adapted to the independent status presentation. No execution capability. */
2
+ export function nativeStatusPanelModel(status, children = [], commandObservations = []) {
3
+ const activity = status?.activity;
4
+ const admitted = activity?.contract === 'agentkit.native-activity-status/v1' && activity.runId === status?.runId;
5
+ const sourceGit = status?.workspaceStatus?.state === 'available' ? status.workspaceStatus.git : null;
6
+ const git = sourceGit && ['added', 'removed', 'dirtyFileCount'].every(key => Number.isSafeInteger(sourceGit[key]) && sourceGit[key] >= 0)
7
+ && (sourceGit.branchName === null || typeof sourceGit.branchName === 'string') && sourceGit.added + sourceGit.removed > 0
8
+ ? { ...sourceGit } : null;
9
+ const plan = admitted && activity.plan && Array.isArray(activity.plan.items) ? activity.plan : null;
10
+ const facts = commandObservations.filter(fact => fact?.contract === 'agentkit.command-output-status/v1'
11
+ && fact.agentId === status?.agentId && fact.sessionId === status?.sessionId && fact.runId === status?.runId
12
+ && ['starting', 'running', 'completed', 'failed', 'canceled', 'timed_out', 'output_limit', 'unknown', 'unavailable'].includes(fact.status));
13
+ const terminals = admitted && Array.isArray(activity.terminals) ? activity.terminals.map(terminal => {
14
+ const observed = facts.find(fact => fact.taskId === terminal.id);
15
+ return observed ? { ...terminal, state: observed.status,
16
+ ...(typeof observed.command === 'string' && observed.command ? { title: observed.command.slice(0, 512) } : {}) } : terminal;
17
+ }) : [];
18
+ const agents = children.filter(task => task && ['childRunId', 'sessionId', 'parentRunId', 'role', 'phase'].every(key => typeof task[key] === 'string' && task[key])).map(task => ({ runId: task.childRunId, sessionId: task.sessionId, parentRunId: task.parentRunId,
19
+ role: task.role, title: task.goalPreview, phase: task.phase,
20
+ isRunning: task.canCancel === true, canCancel: false }));
21
+ return { git, plan, terminals, agents, isRunning: admitted && (activity.isRunning === true
22
+ || terminals.some(terminal => ['starting', 'running'].includes(terminal.state))) };
23
+ }
@@ -0,0 +1,89 @@
1
+ import { validateNativeRecoveryStatus } from './native-recovery.mjs';
2
+
3
+ const id = value => typeof value === 'string' && value.trim() && value.length <= 256;
4
+ const check = (value, message) => { if (!value) throw Error(message); };
5
+ const loading = () => ({ phase: 'loading', observation: null, refreshing: false, error: null });
6
+
7
+ /** One coherent presentation snapshot per Agent/Session. It never resumes work.
8
+ * SDK content supplies text deltas; only a complete authenticated read snapshot
9
+ * supplies Native outcomes, historical receipt states and child bindings.
10
+ */
11
+ export function createNativeTaskReadModel({ hostRemote, agentId, sessionId }) {
12
+ check(id(agentId) && id(sessionId), '任务读取身份无效');
13
+ const identity = { agentId, sessionId }, listeners = new Set(), lifetime = new AbortController();
14
+ let state = loading(), active = null, timer = null, disposed = false, polling = false;
15
+ const publish = change => {
16
+ if (disposed) return;
17
+ state = { ...state, ...change };for (const listener of listeners) listener(state);
18
+ };
19
+ const readStatus = async () => validateNativeRecoveryStatus(
20
+ await hostRemote.nativeRecoveryStatus(identity, lifetime.signal), identity);
21
+ const readChildren = async () => {
22
+ const first = await hostRemote.nativeChildHistory({ ...identity, limit: 64 }, lifetime.signal);
23
+ check(first?.contract === 'agentkit.studio-native-child-history/v1' && first.sessionId === sessionId
24
+ && (first.runId === null || id(first.runId)) && /^[a-f0-9]{64}$/.test(first.inventoryDigest ?? '')
25
+ && Number.isSafeInteger(first.totalTasks) && first.totalTasks >= 0 && first.totalTasks <= 10000,
26
+ '子任务目录与原会话不一致');
27
+ const tasks = [], seen = new Set();let offset = 0, page = first;
28
+ while (true) {
29
+ check(page.contract === first.contract && page.sessionId === sessionId && page.runId === first.runId
30
+ && page.inventoryDigest === first.inventoryDigest && page.totalTasks === first.totalTasks
31
+ && page.offset === offset && Array.isArray(page.tasks) && page.tasks.length <= 64
32
+ && page.nextOffset === offset + page.tasks.length && page.nextOffset <= first.totalTasks
33
+ && page.complete === (page.nextOffset === first.totalTasks), '子任务目录读取期间发生变化');
34
+ for (const task of page.tasks) {
35
+ check(id(task?.childRunId) && id(task.parentRunId) && id(task.sessionId) && id(task.role)
36
+ && Number.isSafeInteger(task.snapshotRevision) && task.snapshotRevision >= 0 && !seen.has(task.childRunId),
37
+ '子任务目录包含无效或重复身份');
38
+ seen.add(task.childRunId);tasks.push(task);
39
+ }
40
+ if (page.complete) break;
41
+ check(page.nextOffset > offset, '子任务目录未取得进展');
42
+ offset = page.nextOffset;
43
+ page = await hostRemote.nativeChildHistory({ ...identity, limit: 64, offset,
44
+ inventoryDigest: first.inventoryDigest }, lifetime.signal);
45
+ lifetime.signal.throwIfAborted();
46
+ }
47
+ return { runId: first.runId, inventoryDigest: first.inventoryDigest, tasks };
48
+ };
49
+ const refresh = ({ fresh = false } = {}) => {
50
+ if (disposed) return Promise.resolve();
51
+ if (active) return fresh ? active.then(() => refresh()) : active;
52
+ publish({ refreshing: true, ...(state.observation ? {} : { phase: 'loading' }), error: null });
53
+ active = (async () => {
54
+ try {
55
+ for (let attempt = 0; attempt < 3; attempt++) {
56
+ const before = await readStatus();lifetime.signal.throwIfAborted();
57
+ const inventory = await readChildren();lifetime.signal.throwIfAborted();
58
+ const status = await readStatus();lifetime.signal.throwIfAborted();
59
+ // A new turn can be admitted between reads. Never combine its status
60
+ // with the prior turn's directory; read the current owner again.
61
+ if (status.runId !== before.runId || inventory.runId !== status.runId) continue;
62
+ publish({ phase: 'ready', observation: { status, children: inventory.tasks,
63
+ inventoryDigest: inventory.inventoryDigest }, error: null });
64
+ return;
65
+ }
66
+ throw Error('任务运行已变化,正在重新同步');
67
+ } catch {
68
+ if (!disposed) publish({ phase: state.observation ? 'ready' : 'error', error: '任务过程同步失败,请重试。' });
69
+ } finally { active = null;publish({ refreshing: false }); }
70
+ })();
71
+ return active;
72
+ };
73
+ return {
74
+ getSnapshot: () => state,
75
+ subscribe(listener) { listeners.add(listener);return () => listeners.delete(listener); },
76
+ refresh,
77
+ startPolling(intervalMs = () => 1500) {
78
+ check(!polling, '任务读取已在轮询');polling = true;
79
+ let stopped = false;
80
+ const poll = async () => {
81
+ await refresh();
82
+ if (!stopped && !disposed) timer = setTimeout(poll, Math.max(100, Number(intervalMs()) || 1500));
83
+ };
84
+ void poll();
85
+ return () => { stopped = true;polling = false;clearTimeout(timer);timer = null; };
86
+ },
87
+ dispose() { disposed = true;lifetime.abort();clearTimeout(timer);listeners.clear(); },
88
+ };
89
+ }
@@ -0,0 +1,40 @@
1
+ /** A Native execution root comes from the authenticated Host, not the model or
2
+ * the conversation shell's independently selected working directory. */
3
+ export function nativeTaskWorkspaceRoot(status, { agentId, sessionId, fallback } = {}) {
4
+ const workspace = status?.workspace;
5
+ return status?.contract === 'agentkit.studio-native-recovery/v1' && !status.managed && status.agentId === agentId
6
+ && status.sessionId === sessionId && typeof status.runId === 'string' && status.runId
7
+ && workspace?.contract === 'agentkit.native-execution-workspace/v1'
8
+ && typeof workspace.workspaceId === 'string' && workspace.workspaceId
9
+ && typeof workspace.root === 'string' && workspace.root.length <= 32768
10
+ && !/[\u0000-\u001f]/.test(workspace.root) && /^(?:\/|[a-z]:[\\/])/i.test(workspace.root)
11
+ ? workspace.root : fallback;
12
+ }
13
+
14
+ /** Validate the original Markdown href before URL normalization can erase traversal. */
15
+ export function taskWorkspaceFileTarget(href, workspaceRoot) {
16
+ if (typeof href !== 'string' || typeof workspaceRoot !== 'string' || !workspaceRoot) return null;
17
+ let value;
18
+ try { value = decodeURIComponent(href); } catch { return null; }
19
+ if (!value || /[\u0000-\u001f?#]/.test(value)) return null;
20
+ const position = value.match(/:(\d+)(?::(\d+))?$/);
21
+ if (position) {
22
+ if (![position[1], position[2]].filter(Boolean).every(v => Number.isSafeInteger(Number(v)) && Number(v) > 0)) return null;
23
+ value = value.slice(0, position.index);
24
+ }
25
+ value = value.replaceAll('\\', '/');
26
+ const base = workspaceRoot.replaceAll('\\', '/').replace(/\/+$/, '');
27
+ if (value.split('/').includes('..') || /^(?![a-z]:\/)[a-z][a-z\d+.-]*:/i.test(value) || value.startsWith('//')) return null;
28
+ const absolute = value.startsWith('/') || /^[a-z]:\//i.test(value);
29
+ if (absolute) {
30
+ const insensitive = /^[a-z]:\//i.test(base);
31
+ const input = insensitive ? value.toLowerCase() : value;
32
+ const prefix = (insensitive ? base.toLowerCase() : base) + '/';
33
+ if (!input.startsWith(prefix)) return null;
34
+ } else {
35
+ value = value.replace(/^(?:\.\/)+/, '');
36
+ if (!value || value.includes(':')) return null;
37
+ value = base + '/' + value;
38
+ }
39
+ return { path: value, ...(position ? { line: Number(position[1]), ...(position[2] ? { column: Number(position[2]) } : {}) } : {}) };
40
+ }
@@ -0,0 +1,185 @@
1
+ import { installNativeChildPane } from './native-child-pane.mjs';
2
+ import { installNativeCommandPane } from './native-command-pane.mjs';
3
+ import { createHarnessComponents } from '@kingsoftcloud/agentkit-harness-ui/react-binding';
4
+ import { createMarkdownComponents, markdownStyles } from '@kingsoftcloud/agentkit-harness-ui/markdown';
5
+ import { createStatusPanelComponents, statusPanelStyles } from '@kingsoftcloud/agentkit-harness-ui/status-panel';
6
+ import { nativeStatusPanelModel } from './state/native-status-panel.mjs';
7
+ import { nativeTaskWorkspaceRoot, taskWorkspaceFileTarget } from './state/task-links.mjs';
8
+ import { resolveNativeRuntimeRoute, nativeRuntimeRouteInput } from './state/native-runtime-route.mjs';
9
+ import { createNativeTaskReadModel } from './state/native-task-read-model.mjs';
10
+ import { createSessionTaskProjector, nativeTaskState, nativeStatusForTurn, bindNativeSubagents, nativeHistoricalToolState } from './session-task-events.mjs';
11
+
12
+ const labels = { result: '执行结果', thought: '思考', running: '执行中', thinking: '思考中…', ended: '本轮已结束', contextRunning: '正在压缩上下文', contextCompleted: '上下文已自动压缩',
13
+ contextFailed: '上下文压缩失败', contextInterrupted: '上下文压缩已中断', contextNoop: '上下文已是最新,无需压缩', interrupted: '任务已中断', recovered: '已恢复',
14
+ failed: '任务失败', completed: '已完成', cancelled: '已停止', blocked: '任务受阻', unverified: '已结束,结果未验证',
15
+ waiting: '等待确认或回执', paused: '已暂停', toolScheduled: '排队中', toolRunning: '执行中', toolCompleted: '已返回',
16
+ toolFailed: '执行失败', toolUncertain: '执行结果未知', toolNotDispatched: '未执行', jumpLatest: '回到最新',
17
+ readKind: '读取', readReading: '读取中', listKind: '列出文件', listSearching: '列出文件中', searchKind: '搜索', searchSearching: '搜索中',
18
+ searchListIn: '目录 {cwd}', searchFindQuery: '查找 {query}', searchList: '列出文件', searchFind: '查找',
19
+ copy: '复制', openFile: '打开文件', openTerminal: '查看终端输出', noOutput: '没有输出', inspect: '查看详情',
20
+ expandCommand: '展开命令', collapseCommand: '收起命令',
21
+ toolDetails: '工具详情', close: '关闭', state: '状态', arguments: '调用参数', noSettledResult: '尚无已确认的结果', appliedChanges: '实际文件变更',
22
+ diffTruncated: '差异预览已截断', diffUnavailable: '无可用差异预览', diffFilesOmitted: '个文件预览已省略', plan: '计划',
23
+ writePending: '请求写入', editPending: '请求修改',
24
+ subagent: '子智能体', openSubagent: '在右侧打开', childSyncing: '同步中', childOmitted: '当前显示过程窗口,较早内容或较长字段已省略。',
25
+ toolPreparing: '正在生成工具参数', previewStopped: '工具参数生成已中断' };
26
+
27
+ /** Only the transport adapter and runtime controls belong to Studio. */
28
+ export function installTaskTimeline(ctx, { ui, hostRemote, resolveAgentFromPreset }) {
29
+ if (typeof resolveAgentFromPreset !== 'function') throw TypeError('The host preset-to-Agent resolver is required');
30
+ const { React, ReactDOM } = ui;
31
+ const { createElement: h, useMemo, useEffect, useLayoutEffect, useState, useRef } = React;
32
+ const { HarnessTimeline } = createHarnessComponents(React);
33
+ const { ConversationStatusPanel } = createStatusPanelComponents(React, ReactDOM);
34
+ const { MarkdownResponse } = createMarkdownComponents(React, ReactDOM);
35
+ const commandPane = installNativeCommandPane(ctx, { React, hostRemote });
36
+ const childPane = installNativeChildPane(ctx, { React, hostRemote, labels, theme: 'system',
37
+ renderText: (text, { streaming, cwd, onOpenFile }) => h(MarkdownResponse, { streaming, theme: ctx.theme.getTheme().active.colorScheme,
38
+ onOpenExternalUrl: url => { if (['http:', 'https:'].includes(new URL(url).protocol)) window.open(url, '_blank', 'noopener,noreferrer'); },
39
+ onOpenFileLink: onOpenFile && (({ href }) => { const target = taskWorkspaceFileTarget(href, cwd); if (target) onOpenFile(target); }) }, text) });
40
+ const markdownLabels = { copyCode: '复制代码', copied: '已复制', wrapLines: '自动换行', languageLabel: '代码' };
41
+ function TaskView({ sessionId, useHarnessEvents, useSession, useSessions, useProjection, cwd, openFile, fallback, statusPanel }) {
42
+ const [variant, setVariant] = useState(() => {
43
+ try { const saved = localStorage.getItem('native.status-panel.variant'); return ['auto', 'mini', 'panel'].includes(saved) ? saved : 'auto'; }
44
+ catch { return 'auto'; }
45
+ });
46
+ const [theme, setTheme] = useState(() => ctx.theme.getTheme());
47
+ useEffect(() => ctx.on('theme/change', setTheme), []);
48
+ const eventWindow = useHarnessEvents(value => value);
49
+ const running = useSession(value => value.running);
50
+ const pending = useSession(value => value.pendingSubmissions);
51
+ const openState = useSession(value => value.openState);
52
+ const session = useSessions(value => value.byId[sessionId]);
53
+ const projection = useSessions(value => value.projectionsBySession?.[sessionId]);
54
+ const boundPresetId = useProjection('agentPreset');
55
+ const { presetId, ready: routeReady } = nativeRuntimeRouteInput(session, openState, projection, boundPresetId);
56
+ const routeKey = JSON.stringify([sessionId, presetId ?? null]);
57
+ const [route, setRoute] = useState({ key: routeKey, kind: 'loading', agentId: null });
58
+ const routeGeneration = useRef(0);
59
+ const [routeAttempt, setRouteAttempt] = useState(0);
60
+ const [projectionError, setProjectionError] = useState(null);
61
+ const [commandObservations, setCommandObservations] = useState([]);
62
+ const nativeAgent = route.key === routeKey && route.kind === 'native' ? route.agentId : null;
63
+ const observationKey = JSON.stringify([routeKey, nativeAgent, route.generation ?? 0]);
64
+ const readerRef = useRef(null), runningRef = useRef(running);runningRef.current = running;
65
+ const [readFrame, setReadFrame] = useState(null), [localError, setError] = useState(null);
66
+ const readSnapshot = readFrame?.key === observationKey ? readFrame.snapshot : null;
67
+ const observation = readSnapshot?.observation;
68
+ const status = observation?.status ?? null, childTasks = observation?.children ?? [];
69
+ const error = localError ?? readSnapshot?.error;
70
+ const projector = useMemo(() => createSessionTaskProjector({ sessionId }), [sessionId]);
71
+ const tasks = useMemo(() => projector.project(eventWindow), [projector, eventWindow]);
72
+ useEffect(() => {
73
+ let active = true;
74
+ setProjectionError(null);
75
+ if (typeof ctx.sessions.refreshProjections !== 'function') setProjectionError('宿主尚未提供会话运行时确认接口');
76
+ else void ctx.sessions.refreshProjections(sessionId).catch(failure => {
77
+ if (active) setProjectionError(failure.message || '无法读取会话运行时');
78
+ });
79
+ return () => { active = false; };
80
+ }, [sessionId, routeAttempt]);
81
+ useEffect(() => {
82
+ const controller = new AbortController();
83
+ const generation = ++routeGeneration.current;
84
+ setRoute({ key: routeKey, kind: 'loading', agentId: null, generation }); setError(null);
85
+ if (routeReady) void resolveNativeRuntimeRoute({ hostRemote, presetId, resolveAgentFromPreset, signal: controller.signal })
86
+ .then(next => { if (!controller.signal.aborted) setRoute({ ...next, key: routeKey, generation }); })
87
+ .catch(failure => { if (!controller.signal.aborted) setRoute({ key: routeKey, kind: 'error', agentId: null, generation,
88
+ error: failure.message || '无法读取当前会话的运行时' }); });
89
+ return () => controller.abort();
90
+ }, [routeKey, routeReady, routeAttempt]);
91
+ useEffect(() => {
92
+ if (!nativeAgent) return;
93
+ const reader = createNativeTaskReadModel({ hostRemote, agentId: nativeAgent, sessionId });
94
+ readerRef.current = reader;
95
+ const publish = snapshot => setReadFrame({ key: observationKey, snapshot });
96
+ publish(reader.getSnapshot());
97
+ const unsubscribe = reader.subscribe(publish);
98
+ const stop = reader.startPolling(() => runningRef.current ? 1500 : 3000);
99
+ return () => { unsubscribe();stop();reader.dispose();if (readerRef.current === reader) readerRef.current = null; };
100
+ }, [nativeAgent, sessionId, observationKey]);
101
+ useEffect(() => {
102
+ if (!nativeAgent || !observation) return;
103
+ childPane.bindHost({ agentId: nativeAgent, parentSessionId: sessionId }, { cwd, openFile, children: childTasks });
104
+ for (const task of childTasks) childPane.register({ agentId: nativeAgent, parentSessionId: sessionId,
105
+ parentRunId: task.parentRunId, runId: task.childRunId, sessionId: task.sessionId, role: task.role, title: task.goalPreview });
106
+ }, [childTasks, nativeAgent, sessionId, cwd, openFile, observation]);
107
+ useEffect(() => {
108
+ setCommandObservations([]);
109
+ if (nativeAgent) return commandPane.subscribe({ agentId: nativeAgent, sessionId }, setCommandObservations);
110
+ }, [nativeAgent, sessionId]);
111
+ const panelModel = useMemo(() => nativeStatusPanelModel(status, childTasks, commandObservations), [status, childTasks, commandObservations]);
112
+ useEffect(() => {
113
+ if (nativeAgent) for (const terminal of panelModel.terminals)
114
+ commandPane.register({ ...terminal, agentId: nativeAgent, sessionId });
115
+ }, [nativeAgent, sessionId, panelModel.terminals]);
116
+ const executionRoot = nativeTaskWorkspaceRoot(status, { agentId: nativeAgent, sessionId, fallback: cwd });
117
+ const openWorkspaceFile = path => {
118
+ const target = taskWorkspaceFileTarget(path, executionRoot);
119
+ if (!target) { setError('文件链接不在执行工作区或位置无效'); return; }
120
+ void Promise.resolve(openFile(target.path)).catch(failure => setError(failure.message || '无法打开文件'));
121
+ };
122
+ const panelVisible = nativeAgent && (panelModel.git || panelModel.plan || panelModel.agents.length || panelModel.terminals.length || panelModel.isRunning);
123
+ useLayoutEffect(() => {
124
+ // The presentation owns the summary seat while routing/loading too.
125
+ // Lack of polled data is not permission to paint the resident Todo dock.
126
+ const resident = route.key === routeKey && route.kind === 'other';
127
+ statusPanel?.setLayout(resident ? null : panelVisible ? variant : 'mini');
128
+ return () => statusPanel?.setLayout(null);
129
+ }, [statusPanel, panelVisible, variant, routeKey, route.key, route.kind]);
130
+ const openSubagent = target => {
131
+ try { childPane.open({ ...target, agentId: nativeAgent, parentSessionId: sessionId }); }
132
+ catch (failure) { setError(failure.message || '无法打开子会话'); }
133
+ };
134
+ const presented = tasks.map(task => ({ ...task, state: bindNativeSubagents(nativeHistoricalToolState(nativeStatusForTurn(status, task.turn)
135
+ ? nativeTaskState(task.state, status, running) : task.state, status, task.turn, sessionId), childTasks) }));
136
+ const resume = async () => {
137
+ const reader = readerRef.current;
138
+ try { setError(null); await hostRemote.resumeNativeRun({ agentId: nativeAgent, sessionId, runId: status.runId });await reader?.refresh({ fresh: true }); }
139
+ catch (failure) { setError(failure.message || '原任务尚未恢复'); }
140
+ };
141
+ if (projectionError || ['error', 'migration-required'].includes(projection?.state)) return h('div', { role: 'alert', 'data-testid': 'task-runtime-error' },
142
+ h('p', null, projectionError || (projection.state === 'migration-required' ? '会话元数据需要迁移,暂时无法确认运行时' : '无法读取会话运行时')),
143
+ h('button', { type: 'button', onClick: () => setRouteAttempt(value => value + 1) }, '重试加载'));
144
+ if (!routeReady || route.key !== routeKey || route.kind === 'loading') return h('div', { role: 'status', 'data-testid': 'task-runtime-loading' }, '正在加载任务过程…');
145
+ if (route.kind === 'error') return h('div', { role: 'alert', 'data-testid': 'task-runtime-error' },
146
+ h('p', null, route.error), h('button', { type: 'button', onClick: () => setRouteAttempt(value => value + 1) }, '重试加载'));
147
+ if (route.kind === 'other') return fallback;
148
+ if (!observation) return readSnapshot?.phase === 'error'
149
+ ? h('div', { role: 'alert', 'data-testid': 'task-observation-error' }, h('p', null, readSnapshot.error),
150
+ h('button', { type: 'button', onClick: () => void readerRef.current?.refresh() }, '重试加载任务过程'))
151
+ : h('div', { role: 'status', 'data-testid': 'task-observation-loading' }, '正在同步任务过程…');
152
+ return h('div', { 'data-testid': 'studio-task-timeline', style: { width: '100%', boxSizing: 'border-box', '--ui-font-size': 'var(--dsh-content-font-size, 14px)' } },
153
+ h('style', { 'data-harness-markdown': true }, markdownStyles),
154
+ statusPanel?.target ? ReactDOM.createPortal(h(React.Fragment, null,
155
+ h('style', { 'data-harness-status-panel': true }, statusPanelStyles),
156
+ h(ConversationStatusPanel, { model: panelModel, variant, theme: theme.active.colorScheme,
157
+ onVariantChange: mode => { setVariant(mode); try { localStorage.setItem('native.status-panel.variant', mode); } catch {} },
158
+ onOpenSubagent: openSubagent,
159
+ onOpenTerminal: terminal => commandPane.open({ ...terminal, agentId: nativeAgent, sessionId }),
160
+ onOpenSubagentDirectory: () => childPane.openDirectory({ agentId: nativeAgent, parentSessionId: sessionId }) })), statusPanel.target) : null,
161
+ ...presented.map(({ turn, state }) => h(HarnessTimeline, { key: turn,
162
+ state, labels, locale: 'zh-CN', showPlanDock: false, theme: theme.active.colorScheme,
163
+ workspaceRoot: executionRoot,
164
+ onOpenFile: ({ path }) => openWorkspaceFile(path),
165
+ onOpenSubagent: openSubagent,
166
+ onCopy: text => { void Promise.resolve().then(() => navigator.clipboard.writeText(text)).catch(() => setError('无法复制,请检查剪贴板权限')); },
167
+ renderText: (text, { streaming }) => h(MarkdownResponse, { streaming, theme: theme.active.colorScheme, scopeKey: sessionId + ':' + executionRoot,
168
+ labels: markdownLabels,
169
+ onCopyCode: text => navigator.clipboard.writeText(text),
170
+ onOpenExternalUrl: url => { if (['http:', 'https:'].includes(new URL(url).protocol)) window.open(url, '_blank', 'noopener,noreferrer'); },
171
+ onOpenFileLink: ({ href }) => openWorkspaceFile(href) }, text) })),
172
+ ...(pending ?? []).filter(item => item.placement === 'transcript').map(item => h('p', { key: item.requestId, 'aria-label': '待发送输入' }, item.text)),
173
+ error ? h('p', { role: 'alert' }, error) : null,
174
+ status?.canResume ? h('button', { type: 'button', onClick: () => void resume() }, '恢复原任务') : null);
175
+
176
+ }
177
+ ctx.slots.inject('conversation.chat.presentation', () => ctx.slots.register({
178
+ name: 'conversation.chat.presentation',
179
+ inject: sessionId => {
180
+ const binding = ctx.sessions.binding(sessionId);
181
+ if (!binding) throw Error('Task timeline session is unavailable');
182
+ return { hooks: { harnessEvents: binding.eventSource } };
183
+ },
184
+ }, TaskView));
185
+ }