@kingsoftcloud/agentkit-harness-ui 0.0.0-stage → 0.0.2
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 -0
- package/NOTICE +116 -0
- package/README.md +292 -2
- package/markdown/build/build-markdown.cjs +66 -0
- package/markdown/dist/THIRD_PARTY_LICENSES.txt +2993 -0
- package/markdown/dist/markdown.css +1 -0
- package/markdown/dist/markdown.mjs +325 -0
- package/markdown/source/markdown-factory.mjs +613 -0
- package/markdown/source/markdown.css +288 -0
- package/markdown/source/table-factory.mjs +1588 -0
- package/markdown/source/table.css +487 -0
- package/package.json +96 -4
- package/src/activity-groups.mjs +74 -0
- package/src/browser-response-groups.mjs +57 -0
- package/src/command-output.mjs +57 -0
- package/src/components.mjs +9 -0
- package/src/context-markers.mjs +19 -0
- package/src/context-status.mjs +34 -0
- package/src/diff.mjs +240 -0
- package/src/explore-tool-call.mjs +139 -0
- package/src/file-preview.mjs +50 -0
- package/src/host/native-a2a.mjs +90 -0
- package/src/host/native-child-file-pane.mjs +49 -0
- package/src/host/native-child-pane.mjs +94 -0
- package/src/host/native-command-pane.mjs +64 -0
- package/src/host/native-run-panel.mjs +202 -0
- package/src/host/session-task-events.mjs +247 -0
- package/src/host/state/native-a2a.mjs +122 -0
- package/src/host/state/native-child-observation.mjs +35 -0
- package/src/host/state/native-children.mjs +93 -0
- package/src/host/state/native-outcome.mjs +35 -0
- package/src/host/state/native-recovery.mjs +187 -0
- package/src/host/state/native-runtime-route.mjs +26 -0
- package/src/host/state/native-session-stop.mjs +40 -0
- package/src/host/state/native-status-panel.mjs +23 -0
- package/src/host/state/native-task-read-model.mjs +89 -0
- package/src/host/state/task-links.mjs +40 -0
- package/src/host/task-timeline.mjs +185 -0
- package/src/index.mjs +52 -0
- package/src/input-preview.mjs +44 -0
- package/src/inspector.mjs +40 -0
- package/src/project.mjs +132 -0
- package/src/protocol.mjs +362 -0
- package/src/react-binding.mjs +486 -0
- package/src/reducer.mjs +355 -0
- package/src/renderers.mjs +644 -0
- package/src/status-panel.mjs +1156 -0
- package/src/styles.mjs +278 -0
- package/src/summary-playback.mjs +39 -0
- package/src/summary.mjs +160 -0
- package/src/terminal.mjs +157 -0
- package/src/tool-detail.mjs +208 -0
- package/src/tool-error.mjs +33 -0
- package/src/tool-identity.mjs +40 -0
- package/src/view.mjs +364 -0
- package/src/work-segments.mjs +41 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { createNativeA2aTaskState } from './state/native-a2a.mjs';
|
|
2
|
+
const terminal = new Set(['completed', 'failed', 'cancelled', 'rejected']);
|
|
3
|
+
const states = { submitted: '已提交', working: '处理中', completed: '已完成', failed: '失败', cancelled: '已取消',
|
|
4
|
+
rejected: '已拒绝', input_required: '等待补充输入', auth_required: '等待认证或授权输入' };
|
|
5
|
+
const outcomes = { submitted: '补充输入已提交。', remote_cancelled: '已核实远端取消。', already_terminal: '远端任务已结束。',
|
|
6
|
+
local_cancelled_before_send: '已在发送前取消,请求未发送到远端。',
|
|
7
|
+
remote_task_not_bound: '原调用尚未确认远端任务 ID。可请求取消,待原任务绑定后投递。',
|
|
8
|
+
unknown: '操作结果未知,请核对原任务。', cancel_requested_but_remote_unknown: '取消尚未得到远端确认。',
|
|
9
|
+
cancel_requested_but_remote_unsupported: '远端不支持取消该任务。' };
|
|
10
|
+
|
|
11
|
+
export function createNativeA2aPanel({ React: { createElement: h, useEffect, useState }, hostRemote, buttonStyle }) {
|
|
12
|
+
return function A2aTask({ task }) {
|
|
13
|
+
const [controller, setController] = useState(null), [state, setState] = useState({ task: null });
|
|
14
|
+
const [text, setText] = useState(''), [json, setJson] = useState(false), [inputError, setInputError] = useState('');
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const next = createNativeA2aTaskState({ hostRemote, task }); setController(next); setState(next.getSnapshot());
|
|
17
|
+
const stop = next.subscribe(setState); return () => { stop(); next.dispose(); };
|
|
18
|
+
}, [task.agentId, task.sessionId, task.runId, task.childRunId, task.childAgentId, task.childSessionId,
|
|
19
|
+
task.operationKey, task.callId, task.ownerKind, task.agentRevision, task.buildId]);
|
|
20
|
+
const send = () => {
|
|
21
|
+
try {
|
|
22
|
+
const structured = state.task?.input?.contract === 'agentkit.a2a-remote-input-control/v1';
|
|
23
|
+
const asJson = json || structured && state.task.input.request?.schema?.type !== 'string';
|
|
24
|
+
if (asJson) JSON.parse(text); // Syntax only; the original bytes go to the Host's exact JSON parser.
|
|
25
|
+
setInputError('');
|
|
26
|
+
if (asJson) void controller.continueJson(text);
|
|
27
|
+
else if (structured) void controller.continueJson(JSON.stringify(text));
|
|
28
|
+
else void controller.continue([{ text }]);
|
|
29
|
+
} catch { setInputError('请输入有效的 JSON 值。'); }
|
|
30
|
+
};
|
|
31
|
+
const value = state.task, busy = state.sending || state.loading;
|
|
32
|
+
const forcedJson = Boolean(value?.input && value.input.request?.schema?.type !== 'string');
|
|
33
|
+
const outcomeText = outcomes[value?.outcome === 'remote_task_not_bound' ? value.outcome : state.outcome];
|
|
34
|
+
const waiting = value && ['input_required', 'auth_required'].includes(value.state);
|
|
35
|
+
const recorded = value?.question?.contract === 'agentkit.a2a-question-ledger/v1' && value.question.status !== 'pending';
|
|
36
|
+
const decided = recorded || value?.input?.decision || value?.approval?.decision;
|
|
37
|
+
return h('article', { 'aria-label': 'A2A 远端任务', style: { borderTop: '1px solid var(--border, #ddd)', padding: '14px 0' } },
|
|
38
|
+
h('strong', null, 'A2A 远端任务'), h('p', null, value ? states[value.state] : '尚未核对远端状态'),
|
|
39
|
+
h('button', { type: 'button', style: buttonStyle(), disabled: !controller || busy, onClick: () => void controller.refresh() }, '核对远端任务'),
|
|
40
|
+
value?.subscriptionContract === 'agentkit.studio-a2a-task-subscription/v1' && !terminal.has(value.state)
|
|
41
|
+
? h('button', { type: 'button', style: buttonStyle(), disabled: busy && !state.watching,
|
|
42
|
+
onClick: () => state.watching ? controller.stopWatching() : void controller.watch() }, state.watching ? '停止观察' : '观察远端进展') : null,
|
|
43
|
+
value?.subscription ? h('p', { role: 'status' }, terminal.has(value.state) ? '已核对远端终态。' : '本次观察已结束,远端任务仍可核对。') : null,
|
|
44
|
+
value && !terminal.has(value.state) ? h('button', { type: 'button', style: buttonStyle(), disabled: busy || Boolean(state.pending),
|
|
45
|
+
onClick: () => void controller.cancel() }, '请求远端取消') : null,
|
|
46
|
+
outcomeText ? h('p', { role: 'status' }, outcomeText) : null,
|
|
47
|
+
state.pending ? h('button', { type: 'button', style: buttonStyle(), disabled: busy, onClick: () => void controller.retry() }, '确认原操作回执') : null,
|
|
48
|
+
state.error ? h('p', { role: 'alert' }, state.error) : null,
|
|
49
|
+
value?.eventDelivery ? h('section', { 'aria-label': '任务事件同步' },
|
|
50
|
+
h('p', { role: value.eventDelivery.state === 'failed' ? 'alert' : 'status' }, ({
|
|
51
|
+
delivered: '任务事件已获平台确认。', pending: '任务事件等待投递到平台。',
|
|
52
|
+
retrying: '任务事件暂未送达,宿主将重试原事件。', failed: '部分任务事件投递失败,原记录已保留。',
|
|
53
|
+
})[value.eventDelivery.state]),
|
|
54
|
+
h('p', null, `待投递 ${value.eventDelivery.pendingBatches} 批 · 已确认 ${value.eventDelivery.acknowledgedBatches} 批 · 失败 ${value.eventDelivery.failedBatches} 批`),
|
|
55
|
+
value.eventDelivery.errorCode ? h('p', null, `诊断:${value.eventDelivery.errorCode}`) : null,
|
|
56
|
+
value.eventDelivery.state !== 'delivered' ? h('p', null, '事件投递状态不改变远端任务结果。请携带原调用标识核对平台记录,不要重新执行任务。') : null) : null,
|
|
57
|
+
recorded ? h('p', { role: 'status' }, ({ awaiting_permission: '回答已保存,正在等待本次远端操作的权限确认。',
|
|
58
|
+
operation_denied: '回答已保存,本次远端操作未获授权;原工具调用已在本地结束,远端状态保持可核对。',
|
|
59
|
+
expired: '等待已超时;原工具调用已在本地结束,远端状态保持可核对。',
|
|
60
|
+
rejected: '已拒绝补充输入;原工具调用已在本地结束,未请求远端取消。',
|
|
61
|
+
operation_admitted: '回答已保存,操作已登记;可核对原远端任务的结果。',
|
|
62
|
+
})[value.question.stage] || '回答已保存。') : null,
|
|
63
|
+
waiting && value.approval ? h('section', { 'aria-label': '远端审批' },
|
|
64
|
+
h('p', null, value.approval.request?.detail?.name || '远端任务请求审批'),
|
|
65
|
+
h('p', null, decided ? '此审批已有决定,可核对原任务或请求取消。' : '批准后将请求本地主机确认,再继续原远端任务。'),
|
|
66
|
+
!decided ? h('button', { type: 'button', style: buttonStyle(), disabled: busy || Boolean(state.pending),
|
|
67
|
+
onClick: () => void controller.continueJson('"approve"') }, '批准并继续原任务') : null,
|
|
68
|
+
!decided ? h('button', { type: 'button', style: buttonStyle(), disabled: busy || Boolean(state.pending),
|
|
69
|
+
onClick: () => void controller.reject() }, '拒绝并请求远端取消') : null) : null,
|
|
70
|
+
waiting && !value.approval ? h('section', { 'aria-label': '远端补充输入' },
|
|
71
|
+
...(value.pause?.parts ?? []).filter(part => typeof part.text === 'string').map((part, index) => h('p', { key: index, style: { whiteSpace: 'pre-wrap' } }, part.text)),
|
|
72
|
+
value.pause ? h('details', null, h('summary', null, '远端输入要求'), h('pre', { style: { whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' } }, JSON.stringify(value.pause, null, 2))) : null,
|
|
73
|
+
decided ? h('p', { role: 'status' }, '此输入已有决定,可核对原任务或请求取消。') : null,
|
|
74
|
+
!decided ? h('label', null, h('input', { type: 'checkbox', checked: json || forcedJson, disabled: forcedJson,
|
|
75
|
+
onChange: event => setJson(event.target.checked) }), '按远端要求提供 JSON 值') : null,
|
|
76
|
+
!decided ? h('textarea', { 'aria-label': '补充输入内容', value: text, onChange: event => setText(event.target.value), disabled: busy || Boolean(state.pending) }) : null,
|
|
77
|
+
!decided ? h('button', { type: 'button', style: buttonStyle(), disabled: busy || Boolean(state.pending)
|
|
78
|
+
|| (!text.trim() && !(value.input?.request?.schema?.type === 'string' && !json)), onClick: send }, '提交补充输入') : null,
|
|
79
|
+
inputError ? h('p', { role: 'alert' }, inputError) : null) : null,
|
|
80
|
+
h('p', { style: { fontSize: '12px' } }, task.ownerKind === 'dsh'
|
|
81
|
+
? '此处操作原远端任务,不会重新发送模型提示,也不会改写已封存的工具回执。'
|
|
82
|
+
: '此处操作原远端任务。当前会话等待期间,任务结束后会继续原运行;重启后可在原运行恢复区核对结果并继续。'),
|
|
83
|
+
value?.output_refs?.length ? h('details', null, h('summary', null, '远端任务输出'),
|
|
84
|
+
...value.output_refs.map(ref => h('pre', { key: `${ref.scope_id}:${ref.item_id}`, style: { whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' } },
|
|
85
|
+
Object.values(value.snapshots?.[`${ref.scope_id}:${ref.item_id}`] ?? {}).join('\n')))) : null,
|
|
86
|
+
h('details', null, h('summary', null, '原调用标识'),
|
|
87
|
+
task.ownerKind === 'dsh' ? h('p', null, `Agent 版本:${task.agentRevision}${task.buildId ? ` · Build:${task.buildId}` : ''}`) : h('p', null, task.runId),
|
|
88
|
+
h('p', null, task.callId)));
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { createFilePreviewComponents } from '@kingsoftcloud/agentkit-harness-ui/file-preview';
|
|
2
|
+
|
|
3
|
+
/** The reusable package renders a read-only page; the Host authenticates the
|
|
4
|
+
* exact parent/child and owns byte paging and right-pane docking.
|
|
5
|
+
*/
|
|
6
|
+
export function installNativeChildFilePane(ctx, { React, hostRemote }) {
|
|
7
|
+
const { createElement: h, useEffect, useState } = React;
|
|
8
|
+
const { FilePreviewPane } = createFilePreviewComponents(React);
|
|
9
|
+
const releases = new Map();
|
|
10
|
+
function Body({ useTabInfo, target }) {
|
|
11
|
+
const info = useTabInfo(), [page, setPage] = useState(null), [loading, setLoading] = useState(false);
|
|
12
|
+
const [error, setError] = useState(''), [offset, setOffset] = useState(0), [previous, setPrevious] = useState([]);
|
|
13
|
+
const [theme, setTheme] = useState(() => ctx.theme.getTheme().active.colorScheme);
|
|
14
|
+
useEffect(() => ctx.on('theme/change', next => setTheme(next.active.colorScheme)), []);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (!info.tab.visible) return;
|
|
17
|
+
const controller = new AbortController(); setLoading(true); setError(''); setPage(null);
|
|
18
|
+
void hostRemote.nativeChildFile({ agentId: target.agentId, sessionId: target.parentSessionId,
|
|
19
|
+
runId: target.parentRunId, childRunId: target.runId, path: target.path, offset, limit: 32768 }, controller.signal)
|
|
20
|
+
.then(value => { if (!controller.signal.aborted) setPage(value); })
|
|
21
|
+
.catch(() => { if (!controller.signal.aborted) setError('无法读取该子工作区文件;不会打开父工作区的同名文件。'); })
|
|
22
|
+
.finally(() => { if (!controller.signal.aborted) setLoading(false); });
|
|
23
|
+
return () => controller.abort();
|
|
24
|
+
}, [info.tab.visible, target.agentId, target.parentSessionId, target.parentRunId, target.runId, target.path, offset]);
|
|
25
|
+
return h(FilePreviewPane, { page, loading, error, theme,
|
|
26
|
+
labels: { loading: '正在读取文件', unavailable: '文件预览不可用', readOnly: '子工作区 · 只读',
|
|
27
|
+
observation: '当前文件观察,不代表任务验证结果', copy: '复制此页', next: '下一页', previous: '上一页', bytes: '字节' },
|
|
28
|
+
onCopy: text => { void navigator.clipboard.writeText(text).catch(() => setError('无法复制此页')); },
|
|
29
|
+
onNext: page?.nextOffset !== null && Number.isSafeInteger(page?.nextOffset) ? () => {
|
|
30
|
+
setPrevious([...previous, offset]); setOffset(page.nextOffset);
|
|
31
|
+
} : undefined,
|
|
32
|
+
onPrevious: previous.length ? () => { setOffset(previous.at(-1)); setPrevious(previous.slice(0, -1)); } : undefined });
|
|
33
|
+
}
|
|
34
|
+
ctx.effect(() => () => { for (const release of releases.values()) release(); releases.clear(); });
|
|
35
|
+
return { open(target) {
|
|
36
|
+
const kind = `agentkit-native-child-file-${target.runId}-${encodeURIComponent(target.path)}`;
|
|
37
|
+
if (!releases.has(kind)) {
|
|
38
|
+
const title = target.path.split('/').at(-1), disposers = [
|
|
39
|
+
ctx.sidebarRightTabs.register({ id: kind, kind, title: () => title }),
|
|
40
|
+
ctx.slots.inject('sidebar.right.pane.tab', () => ctx.slots.register({ name: 'sidebar.right.pane.tab', key: kind },
|
|
41
|
+
props => h(Body, { ...props, target }))),
|
|
42
|
+
ctx.slots.inject('sidebar.right.pane.tab.title', () => ctx.slots.register({ name: 'sidebar.right.pane.tab.title', key: kind },
|
|
43
|
+
() => h('span', null, title))),
|
|
44
|
+
];
|
|
45
|
+
releases.set(kind, () => disposers.reverse().forEach(release => release()));
|
|
46
|
+
}
|
|
47
|
+
ctx.sidebarRight.openTab(kind, { params: target });
|
|
48
|
+
} };
|
|
49
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { createStatusPanelComponents } from '@kingsoftcloud/agentkit-harness-ui/status-panel';
|
|
2
|
+
import { nativeStatusPanelModel } from './state/native-status-panel.mjs';
|
|
3
|
+
import { createHarnessComponents } from '@kingsoftcloud/agentkit-harness-ui/react-binding';
|
|
4
|
+
import { nativeChildObservationState } from './state/native-child-observation.mjs';
|
|
5
|
+
import { installNativeChildFilePane } from './native-child-file-pane.mjs';
|
|
6
|
+
|
|
7
|
+
/** The Host owns docking only. Child content uses the same independent timeline. */
|
|
8
|
+
export function installNativeChildPane(ctx, { React, hostRemote, labels, renderText, theme }) {
|
|
9
|
+
const { createElement: h, useEffect, useState } = React;
|
|
10
|
+
const { SubagentPane } = createHarnessComponents(React);
|
|
11
|
+
const { SubagentDirectory } = createStatusPanelComponents(React);
|
|
12
|
+
const filePane = installNativeChildFilePane(ctx, { React, hostRemote });
|
|
13
|
+
const releases = new Map(), hostBindings = new Map(), observers = new Map(), kindPrefix = 'agentkit-native-child-';
|
|
14
|
+
const hostKey = target => JSON.stringify([target.agentId, target.parentSessionId]);
|
|
15
|
+
function Body({ useTabInfo, target }) {
|
|
16
|
+
const info = useTabInfo();
|
|
17
|
+
const [view, setView] = useState(null), [error, setError] = useState('');
|
|
18
|
+
const [activeTheme, setActiveTheme] = useState(() => ctx.theme.getTheme().active.colorScheme);
|
|
19
|
+
useEffect(() => ctx.on('theme/change', next => setActiveTheme(next.active.colorScheme)), []);
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (!info.tab.visible || !target) return;
|
|
22
|
+
const controller = new AbortController(); let timer, terminal = false;
|
|
23
|
+
const read = async () => {
|
|
24
|
+
try {
|
|
25
|
+
const detail = await hostRemote.nativeChildResult({ agentId: target.agentId, sessionId: target.parentSessionId,
|
|
26
|
+
runId: target.parentRunId, childRunId: target.runId }, controller.signal);
|
|
27
|
+
if (!controller.signal.aborted) { setView(nativeChildObservationState(detail)); setError(''); terminal = ['completed', 'failed', 'canceled'].includes(detail.observation.phase); }
|
|
28
|
+
} catch { if (!controller.signal.aborted) setError('子会话同步失败,请重新打开'); }
|
|
29
|
+
finally { if (!controller.signal.aborted) timer = setTimeout(read, terminal ? 3000 : 500); }
|
|
30
|
+
};
|
|
31
|
+
setView(null); void read();
|
|
32
|
+
return () => { controller.abort(); clearTimeout(timer); };
|
|
33
|
+
}, [info.tab.visible, target?.agentId, target?.parentSessionId, target?.parentRunId, target?.runId]);
|
|
34
|
+
const host = target && hostBindings.get(hostKey(target));
|
|
35
|
+
const onOpenFile = view?.workspaceView === 'isolated' ? ({ path }) => filePane.open({ ...target, path })
|
|
36
|
+
: view?.workspaceView === 'parent' && host ? ({ path }) => {
|
|
37
|
+
void Promise.resolve().then(() => host.openFile(path)).catch(() => setError('无法打开子会话文件'));
|
|
38
|
+
} : undefined;
|
|
39
|
+
return h(SubagentPane, { state: view?.state, labels, locale: 'zh-CN', showPlanDock: false, loading: !view, error, onOpenFile,
|
|
40
|
+
onCopy: text => { void navigator.clipboard.writeText(text).catch(() => setError('无法复制')); },
|
|
41
|
+
omittedRows: view?.omittedRows, omittedChars: view?.omittedChars,
|
|
42
|
+
renderText: (text, meta) => renderText(text, { ...meta, target,
|
|
43
|
+
cwd: view?.workspaceView === 'parent' ? host?.cwd : undefined, onOpenFile }), theme: activeTheme, autoFollow: true });
|
|
44
|
+
}
|
|
45
|
+
function Directory({ target }) {
|
|
46
|
+
const key = hostKey(target), [tasks, setTasks] = useState(() => hostBindings.get(key)?.children ?? []);
|
|
47
|
+
const [activeTheme, setActiveTheme] = useState(() => ctx.theme.getTheme().active.colorScheme);
|
|
48
|
+
useEffect(() => ctx.on('theme/change', next => setActiveTheme(next.active.colorScheme)), []);
|
|
49
|
+
useEffect(() => { const listeners = observers.get(key) ?? new Set(); observers.set(key, listeners); listeners.add(setTasks);
|
|
50
|
+
setTasks(hostBindings.get(key)?.children ?? []);
|
|
51
|
+
return () => { listeners.delete(setTasks); if (!listeners.size) observers.delete(key); };
|
|
52
|
+
}, [key]);
|
|
53
|
+
return h(SubagentDirectory, { theme: activeTheme, agents: nativeStatusPanelModel(null, tasks).agents,
|
|
54
|
+
onOpenSubagent: child => {
|
|
55
|
+
const kind = ensure({ ...child, ...target }); ctx.sidebarRight.openTab(kind, { params: { ...child, ...target } });
|
|
56
|
+
} });
|
|
57
|
+
}
|
|
58
|
+
const ensureDirectory = target => {
|
|
59
|
+
const kind = kindPrefix + 'directory-' + target.parentSessionId;
|
|
60
|
+
if (!releases.has(kind)) {
|
|
61
|
+
const disposers = [ctx.sidebarRightTabs.register({ id: kind, kind, title: () => '已结束的智能体' }),
|
|
62
|
+
ctx.slots.inject('sidebar.right.pane.tab', () => ctx.slots.register({ name: 'sidebar.right.pane.tab', key: kind }, () => h(Directory, { target }))),
|
|
63
|
+
ctx.slots.inject('sidebar.right.pane.tab.title', () => ctx.slots.register({ name: 'sidebar.right.pane.tab.title', key: kind }, () => h('span', null, '已结束的智能体')))];
|
|
64
|
+
releases.set(kind, () => disposers.reverse().forEach(release => release()));
|
|
65
|
+
}
|
|
66
|
+
return kind;
|
|
67
|
+
};
|
|
68
|
+
function Title({ target }) { return h('span', null, target.title || target.role || '子智能体'); }
|
|
69
|
+
const ensure = target => {
|
|
70
|
+
const kind = kindPrefix + target.runId;
|
|
71
|
+
if (!releases.has(kind)) {
|
|
72
|
+
const disposers = [ctx.sidebarRightTabs.register({ id: kind, kind, title: () => target.title || target.role || '子智能体' }),
|
|
73
|
+
ctx.slots.inject('sidebar.right.pane.tab', () => ctx.slots.register({ name: 'sidebar.right.pane.tab', key: kind }, props => h(Body, { ...props, target }))),
|
|
74
|
+
ctx.slots.inject('sidebar.right.pane.tab.title', () => ctx.slots.register({ name: 'sidebar.right.pane.tab.title', key: kind }, () => h(Title, { target })))];
|
|
75
|
+
releases.set(kind, () => disposers.reverse().forEach(release => release()));
|
|
76
|
+
}
|
|
77
|
+
return kind;
|
|
78
|
+
};
|
|
79
|
+
ctx.effect(() => () => { for (const release of releases.values()) release(); releases.clear(); hostBindings.clear(); observers.clear(); });
|
|
80
|
+
return {
|
|
81
|
+
bindHost(target, binding) {
|
|
82
|
+
const key = hostKey(target); hostBindings.delete(key); hostBindings.set(key, binding);
|
|
83
|
+
ensureDirectory(target);
|
|
84
|
+
for (const listener of observers.get(key) ?? []) listener(binding.children ?? []);
|
|
85
|
+
while (hostBindings.size > 64) hostBindings.delete(hostBindings.keys().next().value);
|
|
86
|
+
},
|
|
87
|
+
register: ensure,
|
|
88
|
+
openDirectory(target) { const kind = ensureDirectory(target); ctx.sidebarRight.openTab(kind, { params: target }); },
|
|
89
|
+
open(target) {
|
|
90
|
+
if (!ctx.sidebarRight || !ctx.sidebarRightTabs) throw Error('子会话侧栏尚未就绪');
|
|
91
|
+
const kind = ensure(target); ctx.sidebarRight.openTab(kind, { params: target });
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { appendCommandOutput, createCommandOutputComponents } from '@kingsoftcloud/agentkit-harness-ui/command-output';
|
|
2
|
+
|
|
3
|
+
/** Studio owns right-pane docking and authenticated reads; the package owns
|
|
4
|
+
* the output view. Opening a pane never submits a task or executes a tool.
|
|
5
|
+
*/
|
|
6
|
+
export function installNativeCommandPane(ctx, { React, hostRemote }) {
|
|
7
|
+
const { createElement: h, useEffect, useState } = React;
|
|
8
|
+
const { CommandOutputPane } = createCommandOutputComponents(React), releases = new Map();
|
|
9
|
+
const observations = new Map(), listeners = new Map();
|
|
10
|
+
const sessionKey = target => JSON.stringify([target.agentId, target.sessionId]);
|
|
11
|
+
const publish = (target, output) => {
|
|
12
|
+
const key = sessionKey(target), facts = observations.get(key) ?? new Map();
|
|
13
|
+
observations.delete(key); observations.set(key, facts);
|
|
14
|
+
facts.set(output.taskId, { contract: 'agentkit.command-output-status/v1', agentId: target.agentId,
|
|
15
|
+
sessionId: target.sessionId, runId: output.runId, taskId: output.taskId, status: output.status, command: output.command ?? '' });
|
|
16
|
+
while (facts.size > 128) facts.delete(facts.keys().next().value);
|
|
17
|
+
while (observations.size > 64) observations.delete(observations.keys().next().value);
|
|
18
|
+
for (const listener of listeners.get(key) ?? []) listener([...facts.values()]);
|
|
19
|
+
};
|
|
20
|
+
function Body({ useTabInfo, target }) {
|
|
21
|
+
const info = useTabInfo(), [view, setView] = useState(null), [error, setError] = useState('');
|
|
22
|
+
const [theme, setTheme] = useState(() => ctx.theme.getTheme().active.colorScheme);
|
|
23
|
+
useEffect(() => ctx.on('theme/change', next => setTheme(next.active.colorScheme)), []);
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (!info.tab.visible) return;
|
|
26
|
+
const controller = new AbortController(); let timer, current = null;
|
|
27
|
+
const poll = async () => {
|
|
28
|
+
let delay = 750;
|
|
29
|
+
try {
|
|
30
|
+
const request = { agentId: target.agentId, sessionId: target.sessionId, taskId: target.id,
|
|
31
|
+
stdoutCursor: current?.stdoutCursor ?? 0, stderrCursor: current?.stderrCursor ?? 0, maxBytes: 65536 };
|
|
32
|
+
const page = await hostRemote.nativeCommandOutput(request, controller.signal);
|
|
33
|
+
if (controller.signal.aborted) return;
|
|
34
|
+
const advanced = page.stdoutCursor > request.stdoutCursor || page.stderrCursor > request.stderrCursor;
|
|
35
|
+
current = appendCommandOutput(current, page, request); setView(current); setError(''); publish(target, current);
|
|
36
|
+
delay = advanced ? 50 : ['starting', 'running'].includes(page.status) ? 750 : 3000;
|
|
37
|
+
} catch { if (!controller.signal.aborted) setError('命令输出同步失败,正在重新读取'); }
|
|
38
|
+
finally { if (!controller.signal.aborted) timer = setTimeout(poll, delay); }
|
|
39
|
+
};
|
|
40
|
+
setView(null); void poll();
|
|
41
|
+
return () => { controller.abort(); clearTimeout(timer); };
|
|
42
|
+
}, [info.tab.visible, target.agentId, target.sessionId, target.id]);
|
|
43
|
+
return h(CommandOutputPane, { output: view, title: target.title || '终端输出', error, theme,
|
|
44
|
+
onCopy: text => { void navigator.clipboard.writeText(text).catch(() => setError('无法复制')); } });
|
|
45
|
+
}
|
|
46
|
+
const ensure = target => {
|
|
47
|
+
const kind = 'agentkit-native-command-' + target.sessionId + '-' + target.id;
|
|
48
|
+
if (!releases.has(kind)) {
|
|
49
|
+
const disposers = [ctx.sidebarRightTabs.register({ id: kind, kind, title: () => '终端输出' }),
|
|
50
|
+
ctx.slots.inject('sidebar.right.pane.tab', () => ctx.slots.register({ name: 'sidebar.right.pane.tab', key: kind }, props => h(Body, { ...props, target }))),
|
|
51
|
+
ctx.slots.inject('sidebar.right.pane.tab.title', () => ctx.slots.register({ name: 'sidebar.right.pane.tab.title', key: kind }, () => h('span', null, '终端输出')))];
|
|
52
|
+
releases.set(kind, () => disposers.reverse().forEach(release => release()));
|
|
53
|
+
}
|
|
54
|
+
return kind;
|
|
55
|
+
};
|
|
56
|
+
ctx.effect(() => () => { for (const release of releases.values()) release(); releases.clear(); observations.clear(); listeners.clear(); });
|
|
57
|
+
return { register: ensure,
|
|
58
|
+
subscribe(target, listener) {
|
|
59
|
+
const key = sessionKey(target), active = listeners.get(key) ?? new Set(); listeners.set(key, active); active.add(listener);
|
|
60
|
+
listener([...(observations.get(key)?.values() ?? [])]);
|
|
61
|
+
return () => { active.delete(listener); if (!active.size) listeners.delete(key); };
|
|
62
|
+
},
|
|
63
|
+
open(target) { const kind = ensure(target); ctx.sidebarRight.openTab(kind, { params: target }); } };
|
|
64
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { createContextStatus, contextStatusStyles } from '@kingsoftcloud/agentkit-harness-ui/context-status';
|
|
2
|
+
import { createNativeChildrenState } from './state/native-children.mjs';
|
|
3
|
+
import { createNativeRecoveryState } from './state/native-recovery.mjs';
|
|
4
|
+
import { createNativeA2aPanel } from './native-a2a.mjs';
|
|
5
|
+
import { nativeRunPresentation } from './state/native-outcome.mjs';
|
|
6
|
+
import { createNativeSessionStop } from './state/native-session-stop.mjs';
|
|
7
|
+
|
|
8
|
+
const phases = { created: '已准备', preparing: '准备中', requesting_model: '推理中', requesting_context: '整理上下文',
|
|
9
|
+
requesting_input: '读取输入', executing_tools: '执行工具', awaiting_interaction: '等待审批', paused: '已暂停',
|
|
10
|
+
awaiting_reconciliation: '等待核对结果', reconciling_tool: '正在核对结果',
|
|
11
|
+
completed: '已完成', failed: '已失败', interrupted: '需要恢复核对', canceled: '已取消', finalizing: '收尾中' };
|
|
12
|
+
const memoryError = code => ({ native_memory_extraction_budget_exhausted: '已达到提炼预算上限',
|
|
13
|
+
studio_native_memory_extraction_not_current_head: '会话已有新进展,本次提炼已停止',
|
|
14
|
+
studio_native_memory_extraction_policy_changed: '记忆配置已变化,本次提炼已停止',
|
|
15
|
+
studio_native_memory_model_binding_changed: '原模型配置已变化,请检查配置' })[code] ?? '请检查模型与记忆配置';
|
|
16
|
+
|
|
17
|
+
export function createNativeChildPanel({ ui: { React: { createElement: h, useEffect, useState, Fragment }, ResourceDialog, buttonStyle },
|
|
18
|
+
hostRemote, resolveAgentFromPreset, createForeignRuntimePanel = () => () => null,
|
|
19
|
+
setInterval = globalThis.setInterval, clearInterval = globalThis.clearInterval }) {
|
|
20
|
+
if (typeof resolveAgentFromPreset !== 'function') throw TypeError('The host preset-to-Agent resolver is required');
|
|
21
|
+
const ContextStatus = createContextStatus({ createElement: h });
|
|
22
|
+
const A2aTask = createNativeA2aPanel({ React: { createElement: h, useEffect, useState }, hostRemote, buttonStyle });
|
|
23
|
+
const DshA2aPanel = createForeignRuntimePanel({ ui: { React: { createElement: h, useEffect, useState, Fragment }, ResourceDialog, buttonStyle }, hostRemote });
|
|
24
|
+
function TaskPanel({ agentId, sessionId, transportRunning, requestSessionStop }) {
|
|
25
|
+
const [controller, setController] = useState(null), [state, setState] = useState({ page: null, detail: null }), [open, setOpen] = useState(false);
|
|
26
|
+
const [drafts, setDrafts] = useState({});
|
|
27
|
+
const [backgroundStopped, setBackgroundStopped] = useState(false);
|
|
28
|
+
const [recovery, setRecovery] = useState(null), [recoveryState, setRecoveryState] = useState({ status: null });
|
|
29
|
+
const [stop, setStop] = useState(null), [stopState, setStopState] = useState({ visible: false });
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (typeof requestSessionStop !== 'function') { setStop(null); setStopState({ visible: false }); return; }
|
|
32
|
+
const next = createNativeSessionStop({ sessionId, requestStop: requestSessionStop });
|
|
33
|
+
setStop(next); setStopState(next.getSnapshot()); const unsubscribe = next.subscribe(setStopState);
|
|
34
|
+
return () => { unsubscribe(); next.dispose(); };
|
|
35
|
+
}, [sessionId, requestSessionStop]);
|
|
36
|
+
useEffect(() => { stop?.observe(recoveryState.status, transportRunning); }, [stop, recoveryState.status, transportRunning]);
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
const next = createNativeRecoveryState({ hostRemote, agentId, sessionId });
|
|
39
|
+
setRecovery(next); setRecoveryState(next.getSnapshot()); const stop = next.subscribe(setRecoveryState);
|
|
40
|
+
return () => { stop(); next.dispose(); };
|
|
41
|
+
}, [agentId, sessionId]);
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
const next = createNativeChildrenState({ hostRemote, agentId, sessionId });
|
|
44
|
+
setController(next); setState(next.getSnapshot()); const stop = next.subscribe(setState);
|
|
45
|
+
return () => { stop(); next.dispose(); };
|
|
46
|
+
}, [agentId, sessionId]);
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (!controller) return;
|
|
49
|
+
if (open) void controller.refresh();
|
|
50
|
+
void recovery?.refresh();
|
|
51
|
+
const timer = setInterval(() => { if (open && (controller.getSnapshot().page?.offset ?? 0) === 0) void controller.refresh(); void recovery?.refresh(); }, 2500);
|
|
52
|
+
return () => clearInterval(timer);
|
|
53
|
+
}, [open, controller, recovery]);
|
|
54
|
+
const page = state.page, detail = state.detail;
|
|
55
|
+
useEffect(() => setBackgroundStopped(false), [recoveryState.status?.runId]);
|
|
56
|
+
const accounting = recoveryState.status?.accounting;
|
|
57
|
+
const cloudMemory = recoveryState.status?.cloudMemory;
|
|
58
|
+
const memoryJobs = recoveryState.status?.memoryExtraction?.jobs ?? [];
|
|
59
|
+
const executionGrant = recoveryState.status?.managed?.executionGrant;
|
|
60
|
+
const presentation = nativeRunPresentation(recoveryState.status);
|
|
61
|
+
const ordinaryEnd = recoveryState.status?.phase === 'completed'
|
|
62
|
+
&& recoveryState.status?.outcome === 'unverified' && recoveryState.status?.verificationRequired === false;
|
|
63
|
+
return h(Fragment, null,
|
|
64
|
+
presentation && !ordinaryEnd ? h('span', { role: 'status', title: presentation.detail, 'aria-label': 'Native 任务状态', style: { fontSize: '12px', marginRight: '8px' } }, presentation.label) : null,
|
|
65
|
+
stopState.visible ? h('button', { type: 'button', style: buttonStyle(), disabled: !stopState.canStop,
|
|
66
|
+
onClick: () => void stop?.request() }, ['requesting', 'requested'].includes(stopState.phase) ? '正在停止…' : '停止当前任务') : null,
|
|
67
|
+
stopState.visible && stopState.error ? h('span', { role: 'alert' }, stopState.error) : null,
|
|
68
|
+
h('button', { type: 'button', style: buttonStyle(), onClick: () => setOpen(true) }, '运行与子任务'),
|
|
69
|
+
open ? h(ResourceDialog, { title: '本会话的运行与子任务', detail: '查看运行用量、恢复状态及各轮子任务。子任务结果仍需父任务验收。', wide: true, onClose: () => setOpen(false) },
|
|
70
|
+
h('button', { type: 'button', style: buttonStyle(), disabled: !controller || state.loading, onClick: () => { void controller?.refresh(); void recovery?.refresh(); } }, state.loading ? '读取中…' : '刷新'),
|
|
71
|
+
recoveryState.error ? h('p', { role: 'alert' }, recoveryState.error) : null,
|
|
72
|
+
recoveryState.status?.error ? h('p', { role: 'alert' }, recoveryState.status.error.message) : null,
|
|
73
|
+
presentation ? h('section', { 'aria-label': '任务结果状态' }, h('strong', null, presentation.label), h('p', null, presentation.detail)) : null,
|
|
74
|
+
recoveryState.status?.capabilities ? h('details', null, h('summary', null, '本轮实际可用工具'),
|
|
75
|
+
h('p', { style: { overflowWrap: 'anywhere' } }, recoveryState.status.capabilities.toolNames.join('、') || '未绑定工具'),
|
|
76
|
+
h('p', null, '工具仅可在获准范围内执行。需要其他能力时,请编辑 Agent 的「绑定能力」并新建会话。')) : null,
|
|
77
|
+
h('style', null, contextStatusStyles),
|
|
78
|
+
h(ContextStatus, { value: recoveryState.status?.contextStatus, runId: recoveryState.status?.runId, labels: {
|
|
79
|
+
context: '上下文', automatic: '自动压缩', disabled: '已关闭', trigger: '触发阈值', reserved: '预留输出',
|
|
80
|
+
lastRequest: '最近一次请求的上下文估算', unknown: '尚未计量', compacting: '正在压缩上下文',
|
|
81
|
+
interrupted: '上下文压缩已中断', blocked: '上下文预算不足', completed: '已完成压缩',
|
|
82
|
+
} }),
|
|
83
|
+
accounting ? h('section', { 'aria-label': 'Native 运行用量' },
|
|
84
|
+
h('p', null, `运行用量${accounting.incomplete ? '(统计不完整,显示已知用量)' : accounting.final ? '' : '(截至目前)'}:模型 ${accounting.total.modelCalls} 次 · 工具 ${accounting.total.toolCalls} 次 · 输入 ${accounting.total.inputTokens} / 输出 ${accounting.total.outputTokens} tokens。`),
|
|
85
|
+
h('p', { style: { fontSize: '12px' } }, '包含本运行、工具内模型调用及已结算的子任务用量。')) : null,
|
|
86
|
+
memoryJobs.length ? h('section', { 'aria-label': '自动记忆提炼' },
|
|
87
|
+
h('p', null, '自动记忆提炼的额外用量单独列出。成功表示提炼流程结束,是否写入以记忆工具回执为准。'),
|
|
88
|
+
...memoryJobs.slice(-5).reverse().map(job => h('p', { key: job.jobId,
|
|
89
|
+
role: job.status === 'error' ? 'alert' : 'status' },
|
|
90
|
+
`记忆提炼:${({ queued: '排队中', running: '运行中', success: '已完成', 'no-op': '无需提炼',
|
|
91
|
+
error: '未完成', aborted: '已停止', superseded: '已合并到新窗口' })[job.status]}`,
|
|
92
|
+
job.usage ? ` · 模型 ${job.usage.modelCalls} 次 · 输入 ${job.usage.inputTokens} / 输出 ${job.usage.outputTokens} tokens` : '',
|
|
93
|
+
job.errorCode ? ` · ${memoryError(job.errorCode)}` : ''))) : null,
|
|
94
|
+
cloudMemory ? h('section', { 'aria-label': '云记忆提交' },
|
|
95
|
+
h('p', { role: cloudMemory.state === 'accepted' ? 'status' : 'alert' },
|
|
96
|
+
cloudMemory.state === 'accepted' ? '云记忆服务已受理;记忆抽取和索引尚未确认。'
|
|
97
|
+
: cloudMemory.state === 'unavailable' ? '云记忆提交记录暂时无法读取,请刷新核对。'
|
|
98
|
+
: cloudMemory.state === 'unconfirmed' ? '云记忆原提交尚无确认回执,可能仍在处理或已中断。'
|
|
99
|
+
: '云记忆原提交结果未知。'),
|
|
100
|
+
cloudMemory.state !== 'unavailable' ? h(Fragment, null,
|
|
101
|
+
cloudMemory.state !== 'accepted' ? h('p', null, '请携带原提交标识到云记忆服务核对。刷新只读取本地记录,不会重新提交。') : null,
|
|
102
|
+
h('details', { style: { overflowWrap: 'anywhere' } }, h('summary', null, '云记忆原提交标识'),
|
|
103
|
+
h('p', null, `记忆库:${cloudMemory.resourceId}`), h('p', null, `原请求:${cloudMemory.requestId}`),
|
|
104
|
+
cloudMemory.receiptRequestId ? h('p', null, `服务回执:${cloudMemory.receiptRequestId}`) : null,
|
|
105
|
+
...cloudMemory.messageIds.map(id => h('p', { key: id }, `原消息:${id}`)),
|
|
106
|
+
h('p', null, `运行:${cloudMemory.runId}`), h('p', null, `本地提交记录:${cloudMemory.operationId}`))) : null) : null,
|
|
107
|
+
executionGrant && (executionGrant.blockedReason || recoveryState.status.managed.commandState === 'discarded')
|
|
108
|
+
? h('section', { 'aria-label': '任务执行授权' }, h('p', null,
|
|
109
|
+
recoveryState.status.managed.commandState === 'discarded' ? '排队任务已因执行授权撤销而关闭。'
|
|
110
|
+
: ['execution_grant_suspended', 'execution_grant_admission_paused'].includes(executionGrant.blockedReason)
|
|
111
|
+
? '任务正在排队,等待执行授权恢复。' : '任务正在排队,执行授权需要重新确认。')) : null,
|
|
112
|
+
recoveryState.status && recoveryState.status.state !== 'closed' && !executionGrant?.blockedReason ? h('section', { 'aria-label': '原运行恢复' },
|
|
113
|
+
h('p', null, recoveryState.status.managed && recoveryState.status.phase === 'paused'
|
|
114
|
+
? '原托管运行已暂停,请通过原控制端恢复。'
|
|
115
|
+
: recoveryState.status.managed && recoveryState.status.phase === 'awaiting_interaction'
|
|
116
|
+
? '原托管运行正在等待审批,请通过原控制端答复。'
|
|
117
|
+
: recoveryState.status.managed && recoveryState.status.state === 'reconciliation_required'
|
|
118
|
+
? '原托管运行有待核对的执行结果,请通过原控制端处理;不会自动重跑。'
|
|
119
|
+
: recoveryState.status.managed?.executionState === 'queued'
|
|
120
|
+
? '原托管命令已接收,正在等待接回会话;尚未确认开始执行。'
|
|
121
|
+
: recoveryState.status.state === 'recovering' ? '原运行已接管,正在继续。'
|
|
122
|
+
: recoveryState.status.receiptWait ? '原远端调用仍在等待。可接回原运行继续等待,或主动核对远端结果;不会重发原调用。'
|
|
123
|
+
: recoveryState.status.state === 'reconciliation_required' ? '有执行结果未知的操作,需要核对;不会自动重跑。'
|
|
124
|
+
: recoveryState.status.state === 'failed' ? '恢复未完成,请核对原配置后重试。' : '检测到尚未结束的原运行,可继续原任务。'),
|
|
125
|
+
recoveryState.status.canResume && !recoveryState.pending ? h('button', { type: 'button', style: buttonStyle(), disabled: recoveryState.sending,
|
|
126
|
+
onClick: () => void recovery.resume() }, recoveryState.status.receiptWait ? '接回原运行并继续等待' : '恢复原运行') : null,
|
|
127
|
+
recoveryState.status.canReconcile && !recoveryState.pending ? h('button', { type: 'button', style: buttonStyle(), disabled: recoveryState.sending,
|
|
128
|
+
onClick: () => void recovery.reconcile() }, '核对结果并继续原运行') : null,
|
|
129
|
+
recoveryState.status.canReviewBackground && !recoveryState.pending ? h('section', { 'aria-label': '结束后台结果等待' },
|
|
130
|
+
h('p', null, '确认原进程已停止后,可以关闭本轮并继续新任务。原命令结果仍为未知,不会重跑,也不会撤销已经发生的修改。'),
|
|
131
|
+
!recoveryState.status.backgroundInterruptionReview ? h('label', null,
|
|
132
|
+
h('input', { type: 'checkbox', checked: backgroundStopped,
|
|
133
|
+
onChange: event => setBackgroundStopped(event.target.checked) }), '我已核对原进程停止') : null,
|
|
134
|
+
h('button', { type: 'button', style: buttonStyle(),
|
|
135
|
+
disabled: recoveryState.sending || !backgroundStopped && !recoveryState.status.backgroundInterruptionReview,
|
|
136
|
+
onClick: () => void recovery.reviewBackground() }, '结束等待并保留未知结果')) : null,
|
|
137
|
+
recoveryState.status.canRetireResources && !recoveryState.pending ? h('section', { 'aria-label': '隔离旧浏览器资源' },
|
|
138
|
+
h('p', null, '可隔离旧浏览器并结束本轮,之后获取新的页面观察。原动作结果仍为未知,不会重放。'),
|
|
139
|
+
h('button', {type:'button',style:buttonStyle(),disabled:recoveryState.sending,
|
|
140
|
+
onClick:()=>void recovery.retireResources()}, '隔离旧浏览器并继续新一轮')) : null,
|
|
141
|
+
recoveryState.status.canRepairWorkspace && !recoveryState.pending ? h('details', null,
|
|
142
|
+
h('summary', null, '无法取得文件操作结果?'),
|
|
143
|
+
h('p', null, '可撤销这次未确认操作的文件修改,再继续原运行。已被外部修改的文件会阻止撤销;请优先核对原结果。'),
|
|
144
|
+
h('button', { type: 'button', style: buttonStyle(), disabled: recoveryState.sending,
|
|
145
|
+
onClick: () => void recovery.repairWorkspace() }, '撤销未确认文件修改并继续')) : null) : null,
|
|
146
|
+
recoveryState.pending ? h('button', { type: 'button', style: buttonStyle(), disabled: recoveryState.sending,
|
|
147
|
+
onClick: () => void recovery.retry() }, recoveryState.sending ? '正在确认…' : '确认原运行恢复请求') : null,
|
|
148
|
+
...(recoveryState.status?.a2aTasks ?? []).map(task => h(A2aTask, { key: `${task.runId}:${task.operationKey}:${task.callId}`,
|
|
149
|
+
task: { ...task, agentId, sessionId } })),
|
|
150
|
+
state.error ? h('p', { role: 'alert' }, state.error) : null,
|
|
151
|
+
state.pending ? h('button', { type: 'button', style: buttonStyle(), disabled: state.sending, onClick: () => void controller.retryControl() }, state.sending ? '正在确认…' : '重试原子任务请求') : null,
|
|
152
|
+
page && !page.tasks.length ? h('p', null, '本会话还没有子任务。') : null,
|
|
153
|
+
...(page?.tasks ?? []).map(task => h('article', { key: task.childRunId, style: { borderTop: '1px solid var(--border, #ddd)', padding: '14px 0', overflowWrap: 'anywhere' }, 'aria-label': `子任务:${task.role}` },
|
|
154
|
+
h('strong', null, task.role), ' · ', phases[task.phase] || '运行中', ' · ', task.delivery === 'foreground' ? '前台等待' : '后台',
|
|
155
|
+
task.parentRunId !== page.runId ? h('span', null, ' · 历史轮次') : null,
|
|
156
|
+
task.continuationOf ? h('span', null, ' · 续接任务') : null,
|
|
157
|
+
task.lastControl ? h('p', { role: task.lastControl.status === 'rejected' ? 'alert' : 'status' },
|
|
158
|
+
task.lastControl.status === 'pending' ? '操作已接收,等待投递。' : task.lastControl.status === 'delivered' ? '操作已投递至原子任务。'
|
|
159
|
+
: `操作未投递:${task.lastControl.reason === 'stale_child_goal_revision' ? '子任务目标已变化,请刷新后核对指导。' : task.lastControl.reason}`) : null,
|
|
160
|
+
h('p', { style: { whiteSpace: 'pre-wrap' } }, task.goalPreview, task.goalComplete ? '' : '…'),
|
|
161
|
+
h('p', { style: { fontSize: '12px' } }, `子任务用量${task.usageComplete ? '' : '(可能未完整)'}:模型 ${task.usage.modelCalls} 次 · 工具 ${task.usage.toolCalls} 次 · 输入 ${task.usage.inputTokens} / 输出 ${task.usage.outputTokens} tokens;${task.usageIncludedInParent ? '已计入父任务' : '结算后计入父任务'}。`),
|
|
162
|
+
...(task.a2aTasks ?? []).map(remote => h(A2aTask, { key: `${remote.childRunId}:${remote.operationKey}:${remote.callId}`,
|
|
163
|
+
task: { ...remote, agentId, sessionId } })),
|
|
164
|
+
h('button', { type: 'button', disabled: state.reading, style: buttonStyle(), onClick: () => void controller.read(task) }, task.resultAvailable ? '查看结果' : '查看状态'),
|
|
165
|
+
task.canPromote ? h('button', { type: 'button', disabled: Boolean(state.pending), style: buttonStyle(), onClick: () => void controller.promote(task) }, '转为后台') : null,
|
|
166
|
+
task.canCancel ? h('button', { type: 'button', disabled: Boolean(state.pending), style: buttonStyle(), onClick: () => void controller.cancel(task) }, '停止子任务') : null,
|
|
167
|
+
task.canSendInput ? h('div', null,
|
|
168
|
+
h('textarea', { 'aria-label': `给 ${task.role} 补充指导`, maxLength: 32768, rows: 2, value: drafts[task.childRunId] ?? '',
|
|
169
|
+
disabled: Boolean(state.pending), onChange: event => setDrafts({ ...drafts, [task.childRunId]: event.target.value }) }),
|
|
170
|
+
h('button', { type: 'button', disabled: Boolean(state.pending) || !drafts[task.childRunId]?.trim(), style: buttonStyle(),
|
|
171
|
+
onClick: () => void controller.sendInput(task, drafts[task.childRunId]) }, '发送指导')) : null,
|
|
172
|
+
h('details', null, h('summary', null, '任务标识'), h('p', null, task.childRunId), h('p', null, `会话:${task.sessionId}`), h('p', null, `父任务:${task.parentRunId}`)))),
|
|
173
|
+
page && !page.complete ? h('button', { type: 'button', disabled: state.loading, style: buttonStyle(), onClick: () => void controller.next() }, '加载更多子任务') : null,
|
|
174
|
+
detail ? h('section', { 'aria-label': '子任务结果', style: { marginTop: '16px' } }, h('h4', null, detail.task.role),
|
|
175
|
+
detail.task.resultAvailable ? h('pre', { style: { whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' } }, detail.text)
|
|
176
|
+
: h('p', null, detail.task.phase === 'interrupted' ? '结果未知,需要恢复核对。不会自动重跑副作用。' : '尚无安全封存且已结算的结果。'),
|
|
177
|
+
!detail.complete ? h('button', { type: 'button', disabled: state.reading, style: buttonStyle(), onClick: () => void controller.read(detail.task, true) }, '继续读取结果') : null) : null) : null);
|
|
178
|
+
}
|
|
179
|
+
return function NativeChildPanel({ sessionId, useSessions, requestSessionStop }) {
|
|
180
|
+
const session = useSessions(state => state?.byId?.[sessionId]), presetId = session?.projectionValues?.agentPreset;
|
|
181
|
+
const [agent, setAgent] = useState(null);
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
let active = true; setAgent(null);
|
|
184
|
+
(async () => {
|
|
185
|
+
const first = await hostRemote.agents({ source: 'local', pageSize: 100 }), items = [...(first.items ?? [])];
|
|
186
|
+
for (let page = 2; active && items.length < Number(first.total || 0); page++) {
|
|
187
|
+
const next = await hostRemote.agents({ source: 'local', pageSize: 100, page });
|
|
188
|
+
if (!next.items?.length) break; items.push(...next.items);
|
|
189
|
+
}
|
|
190
|
+
const id = resolveAgentFromPreset(presetId, items), found = items.find(item => item.metadata?.id === id);
|
|
191
|
+
if (active && found?.metadata?.local === true && ['dsh-native', 'harness', 'codex'].includes(found.spec?.runtime?.type))
|
|
192
|
+
setAgent({ id: found.metadata.id, runtime: found.spec.runtime.type, sessionId, presetId });
|
|
193
|
+
})().catch(() => {});
|
|
194
|
+
return () => { active = false; };
|
|
195
|
+
}, [sessionId, presetId]);
|
|
196
|
+
return agent?.sessionId === sessionId && agent?.presetId === presetId
|
|
197
|
+
? h(Fragment, null, agent.runtime === 'harness'
|
|
198
|
+
? h(TaskPanel, { key: `${agent.id}:${sessionId}`, agentId: agent.id, sessionId,
|
|
199
|
+
transportRunning: session?.running === true, requestSessionStop }) : null,
|
|
200
|
+
h(DshA2aPanel, { key: `a2a:${agent.id}:${sessionId}`, agentId: agent.id, sessionId })) : null;
|
|
201
|
+
};
|
|
202
|
+
}
|