zen-gitsync 2.17.10 → 2.17.11
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/package.json +2 -1
- package/src/ui/public/assets/AgentView-DJVwX8Er.js +5 -0
- package/src/ui/public/assets/AgentView-vvVhbIQ5.css +1 -0
- package/src/ui/public/assets/{AppVersionBadge-D5tJzJfa.css → AppVersionBadge-CJrDr7GV.css} +1 -1
- package/src/ui/public/assets/AppVersionBadge-rNwRlkEK.js +2 -0
- package/src/ui/public/assets/BranchSelector-B0eLkAdv.css +1 -0
- package/src/ui/public/assets/BranchSelector-D2JJINuw.js +1 -0
- package/src/ui/public/assets/CommitForm-CQ0K8Yy2.css +1 -0
- package/src/ui/public/assets/CommitForm-CayXdf3-.js +6 -0
- package/src/ui/public/assets/CommonDialog-DF5Vzvi8.js +1 -0
- package/src/ui/public/assets/CommonDialog-jP0E9mco.css +1 -0
- package/src/ui/public/assets/EditorView-Bv-3PWwF.js +1 -0
- package/src/ui/public/assets/EditorView-DZMLvQds.css +1 -0
- package/src/ui/public/assets/FileDiffViewer-B9EkuKdF.css +1 -0
- package/src/ui/public/assets/FileDiffViewer-CUsML3pW.js +15 -0
- package/src/ui/public/assets/FlowExecutionViewer-jNHI5zdm.css +1 -0
- package/src/ui/public/assets/FlowExecutionViewer-n07dZgnE.js +1 -0
- package/src/ui/public/assets/FlowOrchestrationWorkspace-CYGp-PqS.css +1 -0
- package/src/ui/public/assets/FlowOrchestrationWorkspace-CdG8HRux.js +21 -0
- package/src/ui/public/assets/IconButton-GxK-nCF7.js +1 -0
- package/src/ui/public/assets/IconButton-tjkOjzkC.css +1 -0
- package/src/ui/public/assets/LogList-BlJplIi1.css +1 -0
- package/src/ui/public/assets/LogList-CpcpXaIZ.js +7 -0
- package/src/ui/public/assets/MindmapView-BHVJ6OzY.js +2 -0
- package/src/ui/public/assets/MindmapView-BQTqx842.css +1 -0
- package/src/ui/public/assets/MonitorView-DetlFFXq.css +1 -0
- package/src/ui/public/assets/MonitorView-IpfJRJaK.js +1 -0
- package/src/ui/public/assets/ProjectStartupButton-DM2gYw48.js +1 -0
- package/src/ui/public/assets/ProjectStartupButton-QLPltCK5.css +1 -0
- package/src/ui/public/assets/RemoteManagerDialog-BauAeH2w.css +1 -0
- package/src/ui/public/assets/RemoteManagerDialog-DLPnI8Tr.js +4 -0
- package/src/ui/public/assets/RemoteRepoCard-CG-jARTe.css +1 -0
- package/src/ui/public/assets/RemoteRepoCard-D804CRwk.js +7 -0
- package/src/ui/public/assets/SourceMapView-CN4Hkyp4.css +1 -0
- package/src/ui/public/assets/SourceMapView-Ct8J5WA4.js +3 -0
- package/src/ui/public/assets/{SvgIcon-BQ6bR7l0.css → SvgIcon-CDNsk4-Q.css} +1 -1
- package/src/ui/public/assets/SvgIcon-DRYmw37Q.js +1 -0
- package/src/ui/public/assets/TemplateManager-BGEG9EkP.js +1 -0
- package/src/ui/public/assets/TemplateManager-DMuG4i3G.css +1 -0
- package/src/ui/public/assets/UserInputNode-BVgQkB2l.css +1 -0
- package/src/ui/public/assets/UserInputNode-DO4QsmCx.js +1 -0
- package/src/ui/public/assets/WorkbenchView-B1BJhWHi.css +1 -0
- package/src/ui/public/assets/WorkbenchView-D4herpOg.js +51 -0
- package/src/ui/public/assets/_plugin-vue_export-helper-uGHPMZWt.js +19 -0
- package/src/ui/public/assets/configStore-DAZtECRW.js +1 -0
- package/src/ui/public/assets/element-plus-BJ5_9fFh.js +48 -0
- package/src/ui/public/assets/element-plus-C1PloHQu.css +1 -0
- package/src/ui/public/assets/flow-mindmap-9rLOb4W_.js +139 -0
- package/src/ui/public/assets/{gitStore-DMEDMZMk.js → gitStore-BQhrNfOL.js} +4 -4
- package/src/ui/public/assets/{index-CW3xs-af.css → index-B-c23Usy.css} +1 -1
- package/src/ui/public/assets/index-BM9Kz0P0.js +87 -0
- package/src/ui/public/assets/office-docx-BrfnjjUo.js +32 -0
- package/src/ui/public/assets/{office-excel-D-uYLKaA.js → office-excel-C08VoWqf.js} +1 -1
- package/src/ui/public/assets/office-pptx-Cs69NVuQ.js +74 -0
- package/src/ui/public/assets/{previewSandbox-B6Ud9g6a.css → previewSandbox-CgdwPcuW.css} +1 -1
- package/src/ui/public/assets/previewSandbox-D2nklUXY.js +0 -0
- package/src/ui/public/assets/socket-io-BFqQXqAp.js +1 -0
- package/src/ui/public/assets/{toolsStore-CpImRUhI.js → toolsStore-DX4sCU-U.js} +1 -1
- package/src/ui/public/assets/{useThemeObserver-CG8k8U18.js → useThemeObserver-chduV3hr.js} +1 -1
- package/src/ui/public/assets/{vendor-BIgZEtkx.js → vendor-xs0vOPEn.js} +46 -39
- package/src/ui/public/assets/vue-flow-DvidrbDz.js +39 -0
- package/src/ui/public/index.html +25 -25
- package/src/ui/server/routes/remoteRepos.js +73 -19
- package/src/ui/server/routes/remoteRepos.test.js +111 -1
- package/src/ui/server/routes/workbench/agentSessionStore.js +2 -2
- package/src/ui/server/routes/workbench/envContext.test.js +8 -8
- package/src/ui/server/routes/workbench/index.js +32 -736
- package/src/ui/server/routes/workbench/instructionStore.js +3 -113
- package/src/ui/server/routes/workbench/jobStore.js +2 -4
- package/src/ui/server/routes/workbench/jobStore.test.js +1 -1
- package/src/ui/server/routes/workbench/jsonParse.js +6 -143
- package/src/ui/server/routes/workbench/orchestratorStore.js +1 -8
- package/src/ui/server/routes/workbench/projectRegistry.js +6 -29
- package/src/ui/server/routes/workbench/projectRegistry.test.js +33 -56
- package/src/ui/server/routes/workbench/projectTool.js +0 -1
- package/src/ui/server/routes/workbench/projectTool.test.js +2 -3
- package/src/ui/server/routes/workbench/shared.js +2 -7
- package/src/ui/server/routes/workbench/taskRunner.js +18 -180
- package/src/ui/server/routes/workbench.js +4 -5
- package/src/ui/public/assets/AgentView-BV15UtEF.css +0 -1
- package/src/ui/public/assets/AgentView-C-AJo7KS.js +0 -5
- package/src/ui/public/assets/AppVersionBadge-B6R6eN2N.js +0 -2
- package/src/ui/public/assets/BranchSelector-Cj3kMWON.js +0 -1
- package/src/ui/public/assets/BranchSelector-T_rJm-JY.css +0 -1
- package/src/ui/public/assets/CommitForm-DpXuFG-F.js +0 -6
- package/src/ui/public/assets/CommitForm-Oij8R6P-.css +0 -1
- package/src/ui/public/assets/CommonDialog-BLPHDsn0.js +0 -1
- package/src/ui/public/assets/CommonDialog-C_RjF9Mp.css +0 -1
- package/src/ui/public/assets/EditorView-B1OnoXXf.js +0 -1
- package/src/ui/public/assets/EditorView-BgA-_8WV.css +0 -1
- package/src/ui/public/assets/FileDiffViewer-B2_ghtbv.css +0 -1
- package/src/ui/public/assets/FileDiffViewer-BQl0JcWd.js +0 -14
- package/src/ui/public/assets/FlowExecutionViewer-B5kh7VxS.css +0 -1
- package/src/ui/public/assets/FlowExecutionViewer-BgEt5_Hz.js +0 -1
- package/src/ui/public/assets/FlowOrchestrationWorkspace-BAQDU9Jd.css +0 -1
- package/src/ui/public/assets/FlowOrchestrationWorkspace-CbLmsSF8.js +0 -21
- package/src/ui/public/assets/IconButton-CTwyZrv2.css +0 -1
- package/src/ui/public/assets/IconButton-aM3vaSIN.js +0 -1
- package/src/ui/public/assets/LogList-DlWwSZet.css +0 -1
- package/src/ui/public/assets/LogList-UjoVFZ-H.js +0 -7
- package/src/ui/public/assets/MindmapView-CS2t30Ko.css +0 -1
- package/src/ui/public/assets/MindmapView-D_04v0n4.js +0 -1
- package/src/ui/public/assets/MonitorView-BD1_uzq7.js +0 -1
- package/src/ui/public/assets/MonitorView-CphDOlQu.css +0 -1
- package/src/ui/public/assets/ProjectStartupButton-B7zvqRj_.css +0 -1
- package/src/ui/public/assets/ProjectStartupButton-BTYlVvzh.js +0 -1
- package/src/ui/public/assets/RemoteManagerDialog-20mpQMwu.js +0 -1
- package/src/ui/public/assets/RemoteManagerDialog-bpwaVAvX.css +0 -1
- package/src/ui/public/assets/RemoteRepoCard-CDykPlPj.css +0 -1
- package/src/ui/public/assets/RemoteRepoCard-DOrm17Xd.js +0 -1
- package/src/ui/public/assets/SourceMapView-DjYKJZLj.css +0 -1
- package/src/ui/public/assets/SourceMapView-nUU9bYPh.js +0 -3
- package/src/ui/public/assets/SvgIcon-LUynX3Iq.js +0 -1
- package/src/ui/public/assets/TemplateManager-Bo3Q7J9Y.js +0 -1
- package/src/ui/public/assets/TemplateManager-C0rm8xfC.css +0 -1
- package/src/ui/public/assets/UserInputNode-D5tSXiJz.js +0 -1
- package/src/ui/public/assets/UserInputNode-DDtvv9Iq.css +0 -1
- package/src/ui/public/assets/WorkbenchView-C_Gmzvj1.css +0 -1
- package/src/ui/public/assets/WorkbenchView-UGC334JL.js +0 -13
- package/src/ui/public/assets/_plugin-vue_export-helper-D-D9qkRz.js +0 -19
- package/src/ui/public/assets/configStore-BLRcYJdf.js +0 -1
- package/src/ui/public/assets/element-plus-BWbXh9mk.css +0 -1
- package/src/ui/public/assets/element-plus-DcZ-TVp6.js +0 -28
- package/src/ui/public/assets/flow-mindmap-D1VTkZh3.js +0 -139
- package/src/ui/public/assets/index-S0_gRyiO.js +0 -26
- package/src/ui/public/assets/office-docx-C0g5Oepv.js +0 -31
- package/src/ui/public/assets/office-pptx-De3fnc6M.js +0 -60
- package/src/ui/public/assets/previewSandbox-DXAxqDVu.js +0 -0
- package/src/ui/public/assets/socket-io-TBtw0msS.js +0 -1
- package/src/ui/public/assets/vue-flow-CPCFgenl.js +0 -39
- package/src/ui/server/routes/workbench/sessionStore.js +0 -126
|
@@ -33,7 +33,6 @@ import {
|
|
|
33
33
|
} from './projectRegistry.js';
|
|
34
34
|
import { normalizeOrchestrator, buildActivityFeed, buildRunningAgents } from './orchestratorStore.js';
|
|
35
35
|
|
|
36
|
-
const sub = (id, status) => ({ id, title: id, desc: '', status, promptOverride: '' });
|
|
37
36
|
const job = (id, taskId, status, startedAt, endedAt = null) => ({
|
|
38
37
|
id, taskId, subId: `${taskId}__${id}`, title: '', status, startedAt, endedAt,
|
|
39
38
|
});
|
|
@@ -84,28 +83,25 @@ test('resolveTaskRepoPath 优先用任务自己的项目,空则回退当前项
|
|
|
84
83
|
// ── 看板列推导 ──────────────────────────────────────────────────────
|
|
85
84
|
|
|
86
85
|
test('deriveTaskColumn: 从没动过的任务在待处理', () => {
|
|
87
|
-
assert.equal(deriveTaskColumn({
|
|
88
|
-
assert.equal(deriveTaskColumn({ subtasks: [sub('s1', 'todo')] }, []), 'todo');
|
|
86
|
+
assert.equal(deriveTaskColumn({}, []), 'todo');
|
|
89
87
|
});
|
|
90
88
|
|
|
91
|
-
test('deriveTaskColumn: 有 job
|
|
92
|
-
assert.equal(deriveTaskColumn({
|
|
93
|
-
assert.equal(deriveTaskColumn({
|
|
94
|
-
assert.equal(deriveTaskColumn({ subtasks: [sub('s1', 'running')] }, []), 'doing');
|
|
89
|
+
test('deriveTaskColumn: 有 job 在跑就是进行中', () => {
|
|
90
|
+
assert.equal(deriveTaskColumn({}, [job('j1', 't1', 'running', '2026-01-01T00:00:00Z')]), 'doing');
|
|
91
|
+
assert.equal(deriveTaskColumn({}, [job('j1', 't1', 'pending', '2026-01-01T00:00:00Z')]), 'doing');
|
|
95
92
|
});
|
|
96
93
|
|
|
97
|
-
test('deriveTaskColumn:
|
|
98
|
-
assert.equal(deriveTaskColumn({
|
|
99
|
-
//
|
|
100
|
-
assert.equal(deriveTaskColumn({
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
assert.equal(deriveTaskColumn({
|
|
108
|
-
assert.equal(deriveTaskColumn({ subtasks: [], type: 'simple' }, [job('j1', 't1', 'error', '2026-01-01T00:00:00Z')]), 'todo');
|
|
94
|
+
test('deriveTaskColumn: 看执行记录 —— 最近一条 done 才算已完成', () => {
|
|
95
|
+
assert.equal(deriveTaskColumn({}, [job('j1', 't1', 'done', '2026-01-01T00:00:00Z')]), 'done');
|
|
96
|
+
// 错误与取消都退回待处理:列表示任务是否仍待处理,错误是这一轮执行的结果
|
|
97
|
+
assert.equal(deriveTaskColumn({}, [job('j1', 't1', 'cancelled', '2026-01-01T00:00:00Z')]), 'todo');
|
|
98
|
+
assert.equal(deriveTaskColumn({}, [job('j1', 't1', 'error', '2026-01-01T00:00:00Z')]), 'todo');
|
|
99
|
+
// 最新一条失败、前面成功过 → 仍然待处理
|
|
100
|
+
const jobs = [
|
|
101
|
+
job('j1', 't1', 'done', '2026-01-01T00:00:00Z', '2026-01-01T00:01:00Z'),
|
|
102
|
+
job('j2', 't1', 'error', '2026-01-02T00:00:00Z', '2026-01-02T00:01:00Z'),
|
|
103
|
+
];
|
|
104
|
+
assert.equal(deriveTaskColumn({}, jobs), 'todo');
|
|
109
105
|
});
|
|
110
106
|
|
|
111
107
|
test('latestJob 按 startedAt 取最新的一条', () => {
|
|
@@ -152,7 +148,7 @@ test('buildProjectEntries: 目录段大小写不同也是同一个项目(旧
|
|
|
152
148
|
assert.equal(entries[0].name, 'xuze3');
|
|
153
149
|
// 项目行只认这个 key 的任务,key 分叉会连带把任务计数拆到两行上去
|
|
154
150
|
const stats = summarizeProjectTasks(
|
|
155
|
-
[{ id: 't1', projectPath: 'C:\\Users\\xuze3'
|
|
151
|
+
[{ id: 't1', projectPath: 'C:\\Users\\xuze3' }],
|
|
156
152
|
[],
|
|
157
153
|
);
|
|
158
154
|
assert.equal(stats.get('c:\\users\\xuze3').total, 1);
|
|
@@ -172,11 +168,11 @@ test('buildProjectEntries: 目录段大小写不同也是同一个项目(旧
|
|
|
172
168
|
test('summarizeProjectTasks: 按项目汇总列数量、进度、活跃执行与最后活跃时间', () => {
|
|
173
169
|
const tasks = [
|
|
174
170
|
// A 项目:1 个已完成
|
|
175
|
-
{ id: 'a1', projectPath: 'D:\\a',
|
|
171
|
+
{ id: 'a1', projectPath: 'D:\\a', updatedAt: '2026-01-01T00:00:00Z' },
|
|
176
172
|
// A 项目:1 个在跑(job running)
|
|
177
|
-
{ id: 'a2', projectPath: 'D:\\a',
|
|
173
|
+
{ id: 'a2', projectPath: 'D:\\a', updatedAt: '2026-01-02T00:00:00Z' },
|
|
178
174
|
// B 项目:1 个待处理
|
|
179
|
-
{ id: 'b1', projectPath: 'D:\\b',
|
|
175
|
+
{ id: 'b1', projectPath: 'D:\\b', updatedAt: '2026-01-03T00:00:00Z' },
|
|
180
176
|
];
|
|
181
177
|
const jobs = [
|
|
182
178
|
job('j1', 'a2', 'running', '2026-01-04T00:00:00Z'),
|
|
@@ -204,15 +200,10 @@ test('summarizeProjectTasks: 按项目汇总列数量、进度、活跃执行与
|
|
|
204
200
|
test('decorateTaskForBoard: 只回卡片需要的字段', () => {
|
|
205
201
|
const card = decorateTaskForBoard({
|
|
206
202
|
id: 't1', title: '标题', desc: '描述', projectPath: 'D:\\a',
|
|
207
|
-
subtasks: [sub('s1', 'done'), sub('s2', 'error')],
|
|
208
203
|
attachments: [{ id: 'at1' }],
|
|
209
204
|
createdAt: '2026-01-01T00:00:00Z', updatedAt: '2026-01-02T00:00:00Z',
|
|
210
205
|
}, []);
|
|
211
206
|
assert.equal(card.column, 'todo');
|
|
212
|
-
assert.equal(card.type, 'complex');
|
|
213
|
-
assert.equal(card.subtaskCount, 2);
|
|
214
|
-
assert.equal(card.subtaskDoneCount, 1);
|
|
215
|
-
assert.equal(card.subtaskErrorCount, 1);
|
|
216
207
|
assert.equal(card.attachmentCount, 1);
|
|
217
208
|
assert.equal(card.runningJobs, 0);
|
|
218
209
|
// 没跑过就没有完成时间——看板「已完成」列的排序靠它,空值必须显式是 null
|
|
@@ -226,7 +217,7 @@ test('decorateTaskForBoard: lastJobEndedAt 取最近一条 job 的结束时间
|
|
|
226
217
|
job('j2', 't1', 'done', '2026-01-03T00:00:00Z', '2026-01-03T00:20:00Z'),
|
|
227
218
|
job('j3', 't1', 'error', '2026-01-02T00:00:00Z', '2026-01-02T00:05:00Z'),
|
|
228
219
|
];
|
|
229
|
-
const base = { id: 't1', title: '', desc: '',
|
|
220
|
+
const base = { id: 't1', title: '', desc: '', createdAt: '2025-12-31T00:00:00Z' };
|
|
230
221
|
|
|
231
222
|
// 只有 j2 是"最近"的,即使它的结束时间排在 startedAt 序里也是最后
|
|
232
223
|
assert.equal(decorateTaskForBoard(base, jobs).lastJobEndedAt, '2026-01-03T00:20:00Z');
|
|
@@ -261,11 +252,7 @@ test('buildActivityFeed: job 起止合成两条记录,与人类指令一起按
|
|
|
261
252
|
id: 'j1', taskId: 't1', subId: 's1', status: 'done', pid: 123,
|
|
262
253
|
startedAt: '2026-01-01T00:00:00Z', endedAt: '2026-01-01T00:05:00Z', exitCode: 0,
|
|
263
254
|
}];
|
|
264
|
-
const tasks = [{
|
|
265
|
-
id: 't1', projectPath: 'D:\\ws\\proj-a',
|
|
266
|
-
subtasks: [{ id: 's1', title: '子任务一', status: 'done' }],
|
|
267
|
-
title: '任务一',
|
|
268
|
-
}];
|
|
255
|
+
const tasks = [{ id: 't1', projectPath: 'D:\\ws\\proj-a', title: '任务一' }];
|
|
269
256
|
const instructions = [{
|
|
270
257
|
id: 'i1', text: '把这些重构一下', projectPath: 'D:\\ws\\proj-a',
|
|
271
258
|
at: '2026-01-01T00:10:00Z', status: 'accepted',
|
|
@@ -275,7 +262,6 @@ test('buildActivityFeed: job 起止合成两条记录,与人类指令一起按
|
|
|
275
262
|
assert.equal(feed.length, 3);
|
|
276
263
|
assert.deepEqual(feed.map(r => r.kind), ['user', 'done', 'dispatch']);
|
|
277
264
|
assert.equal(feed[1].taskTitle, '任务一');
|
|
278
|
-
assert.equal(feed[1].subTitle, '子任务一');
|
|
279
265
|
assert.equal(feed[1].projectName, 'proj-a');
|
|
280
266
|
assert.equal(feed[2].pid, 123);
|
|
281
267
|
assert.equal(feed[0].instructionStatus, 'accepted');
|
|
@@ -287,7 +273,7 @@ test('buildActivityFeed: 指令带起来的起跑并进指令行,不再重复
|
|
|
287
273
|
startedAt: '2026-01-01T00:00:01Z', endedAt: '2026-01-01T00:05:00Z',
|
|
288
274
|
}];
|
|
289
275
|
// 任务标题就是指令首行,所以派发行和指令行本来长得一模一样
|
|
290
|
-
const tasks = [{ id: 't1', title: '把这些重构一下', projectPath: 'D:\\ws\\proj-a'
|
|
276
|
+
const tasks = [{ id: 't1', title: '把这些重构一下', projectPath: 'D:\\ws\\proj-a' }];
|
|
291
277
|
const instructions = [{
|
|
292
278
|
id: 'i1', text: '把这些重构一下', taskId: 't1', projectPath: 'D:\\ws\\proj-a',
|
|
293
279
|
at: '2026-01-01T00:00:00Z', status: 'accepted',
|
|
@@ -304,7 +290,7 @@ test('buildActivityFeed: 只吸收指令后的首次起跑,手动重跑仍保
|
|
|
304
290
|
{ id: 'j1', taskId: 't1', status: 'done', startedAt: '2026-01-01T00:00:01Z', endedAt: '2026-01-01T00:05:00Z' },
|
|
305
291
|
{ id: 'j2', taskId: 't1', status: 'done', startedAt: '2026-01-01T00:10:00Z', endedAt: '2026-01-01T00:15:00Z' },
|
|
306
292
|
];
|
|
307
|
-
const tasks = [{ id: 't1', title: '跑一下', projectPath: 'D:\\ws\\proj-a'
|
|
293
|
+
const tasks = [{ id: 't1', title: '跑一下', projectPath: 'D:\\ws\\proj-a' }];
|
|
308
294
|
const instructions = [{
|
|
309
295
|
id: 'i1', text: '跑一下', taskId: 't1', projectPath: 'D:\\ws\\proj-a',
|
|
310
296
|
at: '2026-01-01T00:00:00Z', status: 'accepted',
|
|
@@ -318,7 +304,7 @@ test('buildActivityFeed: 只吸收指令后的首次起跑,手动重跑仍保
|
|
|
318
304
|
test('buildActivityFeed: 指令还没跑起来时,起跑行照旧保留给别的 job', () => {
|
|
319
305
|
// 指令建了任务但没执行(调度暂停)——此时该任务的任何起跑都不是它带起来的
|
|
320
306
|
const jobs = [{ id: 'j1', taskId: 't1', status: 'done', startedAt: '2026-01-01T00:00:00Z', endedAt: '2026-01-01T00:05:00Z' }];
|
|
321
|
-
const tasks = [{ id: 't1', title: '手跑的', projectPath: 'D:\\a'
|
|
307
|
+
const tasks = [{ id: 't1', title: '手跑的', projectPath: 'D:\\a' }];
|
|
322
308
|
const instructions = [{
|
|
323
309
|
id: 'i1', text: '手跑的', taskId: 't1', projectPath: 'D:\\a',
|
|
324
310
|
at: '2026-01-01T00:20:00Z', status: 'created',
|
|
@@ -333,7 +319,7 @@ test('buildActivityFeed: 失败与取消分别落成 error / cancelled', () => {
|
|
|
333
319
|
{ id: 'j1', taskId: 't1', subId: 's1', status: 'error', startedAt: '2026-01-01T00:00:00Z', endedAt: '2026-01-01T00:01:00Z', error: 'boom' },
|
|
334
320
|
{ id: 'j2', taskId: 't1', subId: 's2', status: 'cancelled', startedAt: '2026-01-01T00:02:00Z', endedAt: '2026-01-01T00:03:00Z' },
|
|
335
321
|
];
|
|
336
|
-
const feed = buildActivityFeed({ jobs, tasks: [{ id: 't1', projectPath: 'D:\\a'
|
|
322
|
+
const feed = buildActivityFeed({ jobs, tasks: [{ id: 't1', projectPath: 'D:\\a' }] });
|
|
337
323
|
const kinds = feed.map(r => r.kind);
|
|
338
324
|
assert.ok(kinds.includes('error'));
|
|
339
325
|
assert.ok(kinds.includes('cancelled'));
|
|
@@ -348,23 +334,18 @@ test('buildActivityFeed: 任务已被删除时仍能出记录(不留孤儿空
|
|
|
348
334
|
assert.equal(feed[0].projectName, '');
|
|
349
335
|
});
|
|
350
336
|
|
|
351
|
-
test('buildRunningAgents: 只列活跃 job
|
|
352
|
-
const tasks = [{
|
|
353
|
-
id: 't1', projectPath: 'D:\\ws\\proj-a', subtasks: [
|
|
354
|
-
{ id: 's1', title: '子任务一', status: 'running' },
|
|
355
|
-
{ id: 's2', title: '子任务二', status: 'running' },
|
|
356
|
-
],
|
|
357
|
-
}];
|
|
337
|
+
test('buildRunningAgents: 只列活跃 job,按 startedAt 倒序', () => {
|
|
338
|
+
const tasks = [{ id: 't1', title: '任务一', projectPath: 'D:\\ws\\proj-a' }];
|
|
358
339
|
const jobs = [
|
|
359
|
-
{ id: 'j1', taskId: 't1', subId: '
|
|
360
|
-
{ id: 'j2', taskId: 't1', subId: '
|
|
361
|
-
{ id: 'j3', taskId: 't1', subId: '
|
|
340
|
+
{ id: 'j1', taskId: 't1', subId: 't1__simple', status: 'running', pid: 111, startedAt: '2026-01-01T00:02:00Z' },
|
|
341
|
+
{ id: 'j2', taskId: 't1', subId: 't1__simple__r1', status: 'pending', pid: 222, startedAt: '2026-01-01T00:01:00Z' },
|
|
342
|
+
{ id: 'j3', taskId: 't1', subId: 't1__simple__r2', status: 'done', pid: 333, startedAt: '2026-01-01T00:00:00Z' },
|
|
362
343
|
];
|
|
363
344
|
const running = buildRunningAgents({ jobs, tasks });
|
|
364
345
|
assert.equal(running.length, 2);
|
|
365
346
|
// 最新的排在前面
|
|
366
347
|
assert.equal(running[0].jobId, 'j1');
|
|
367
|
-
assert.equal(running[0].
|
|
348
|
+
assert.equal(running[0].taskTitle, '任务一');
|
|
368
349
|
assert.equal(running[0].projectName, 'proj-a');
|
|
369
350
|
assert.equal(running[1].status, 'pending');
|
|
370
351
|
});
|
|
@@ -416,11 +397,7 @@ test('buildTaskDetail: 任务缺失返回 null(交路由转 404)', () => {
|
|
|
416
397
|
});
|
|
417
398
|
|
|
418
399
|
test('buildTaskDetail: lastJob 取最新一条,recentJobs 倒序且被限制条数', () => {
|
|
419
|
-
const task = {
|
|
420
|
-
id: 't1', title: '任务', desc: '', type: 'complex',
|
|
421
|
-
subtasks: [sub('s1', 'done'), sub('s2', 'running')],
|
|
422
|
-
attachments: [{ id: 'a1' }],
|
|
423
|
-
};
|
|
400
|
+
const task = { id: 't1', title: '任务', desc: '', attachments: [{ id: 'a1' }] };
|
|
424
401
|
const jobs = [
|
|
425
402
|
job('j1', 't1', 'done', '2026-01-01T00:00:00Z', '2026-01-01T00:01:00Z'),
|
|
426
403
|
job('j3', 't1', 'running', '2026-01-01T00:02:00Z'),
|
|
@@ -439,7 +416,7 @@ test('buildTaskDetail: lastJob 取最新一条,recentJobs 倒序且被限制
|
|
|
439
416
|
});
|
|
440
417
|
|
|
441
418
|
test('buildTaskDetail: recentJobLimit 生效;无 job 时 lastJob 为 null 而不是抛错', () => {
|
|
442
|
-
const task = { id: 't2', title: '', desc: ''
|
|
419
|
+
const task = { id: 't2', title: '', desc: '' };
|
|
443
420
|
const jobs = [1, 2, 3, 4, 5, 6].map(i => job('j' + i, 't2', 'done', `2026-01-0${i}T00:00:00Z`));
|
|
444
421
|
const d = buildTaskDetail(task, jobs, { recentJobLimit: 2 });
|
|
445
422
|
assert.equal(d.recentJobs.length, 2);
|
|
@@ -79,7 +79,6 @@ function taskLineOf(p) {
|
|
|
79
79
|
if (!s.total) return '任务: 无';
|
|
80
80
|
const parts = [`待处理 ${s.todo || 0}`, `进行中 ${s.doing || 0}`, `已完成 ${s.done || 0}`];
|
|
81
81
|
if (s.runningJobs > 0) parts.push(`正在执行 ${s.runningJobs}`);
|
|
82
|
-
if (s.errorSubtasks > 0) parts.push(`子任务报错 ${s.errorSubtasks}`);
|
|
83
82
|
return `任务: ${parts.join(' / ')}`;
|
|
84
83
|
}
|
|
85
84
|
|
|
@@ -42,7 +42,7 @@ function project(overrides = {}) {
|
|
|
42
42
|
unstaged: 0,
|
|
43
43
|
untracked: 0,
|
|
44
44
|
};
|
|
45
|
-
const baseStats = { total: 0, todo: 0, doing: 0, done: 0, progress: 0, runningJobs: 0
|
|
45
|
+
const baseStats = { total: 0, todo: 0, doing: 0, done: 0, progress: 0, runningJobs: 0 };
|
|
46
46
|
const merged = {
|
|
47
47
|
path: 'D:\\ws\\proj',
|
|
48
48
|
key: 'd:\\ws\\proj',
|
|
@@ -112,10 +112,9 @@ test('非仓库 / 目录不存在 / 探测超时都能区分,不谎报成"干
|
|
|
112
112
|
});
|
|
113
113
|
|
|
114
114
|
test('任务进度按看板三列报出,无任务时说"无"', () => {
|
|
115
|
-
const withTasks = formatProjectList([project({ stats: { total: 9, todo: 1, doing: 2, done: 6, runningJobs: 1
|
|
115
|
+
const withTasks = formatProjectList([project({ stats: { total: 9, todo: 1, doing: 2, done: 6, runningJobs: 1 } })]);
|
|
116
116
|
assert.match(withTasks, /待处理 1 \/ 进行中 2 \/ 已完成 6/);
|
|
117
117
|
assert.match(withTasks, /正在执行 1/);
|
|
118
|
-
assert.match(withTasks, /子任务报错 1/);
|
|
119
118
|
|
|
120
119
|
assert.match(formatProjectList([project()]), /任务: 无/);
|
|
121
120
|
});
|
|
@@ -38,11 +38,6 @@ export const TASKS_FILE = path.join(DATA_DIR, 'tasks.json');
|
|
|
38
38
|
export const CONFIG_FILE = path.join(DATA_DIR, 'config.json');
|
|
39
39
|
export const IMAGES_DIR = path.join(DATA_DIR, 'workbench-images');
|
|
40
40
|
export const INSTRUCTION_FILE = path.join(DATA_DIR, 'ai-instruction.json');
|
|
41
|
-
export const SUBTASK_INSTRUCTION_FILE = path.join(DATA_DIR, 'ai-subtask-instruction.json');
|
|
42
|
-
// AI 对话拆分会话存档:每个 sessionId 一个文件,重启可恢复多轮上下文
|
|
43
|
-
export const SESSIONS_DIR = path.join(DATA_DIR, 'ai-split-sessions');
|
|
44
|
-
export const MAX_SESSIONS = 100; // 软上限,触发清理
|
|
45
|
-
export const SESSIONS_KEEP = 50; // 超过上限时保留最新 N 个
|
|
46
41
|
// 执行日志持久化:jobs.json 是历史档案,jobs-config.json 是保留策略
|
|
47
42
|
export const JOBS_FILE = path.join(DATA_DIR, 'jobs.json');
|
|
48
43
|
export const JOBS_CONFIG_FILE = path.join(DATA_DIR, 'jobs-config.json');
|
|
@@ -82,7 +77,7 @@ export const MANIFEST_FILES = [
|
|
|
82
77
|
// 真正需要小体积的是图片,由前端上传前压缩保证(见 useWorkbenchAttachments.ts),
|
|
83
78
|
// 这里只做最后一道兜底,避免超大 body 把内存吃光。
|
|
84
79
|
export const MAX_IMAGE_BYTES = 20 * 1024 * 1024; // 单个附件最大 20MB
|
|
85
|
-
export const
|
|
80
|
+
export const MAX_ATTACHMENTS_PER_TASK = 9; // 一个任务最多挂 9 个附件
|
|
86
81
|
|
|
87
82
|
// ── 时间 / ID 工具 ─────────────────────────────────────────
|
|
88
83
|
export function nowIso() {
|
|
@@ -109,7 +104,7 @@ export async function readJson(file, fallback) {
|
|
|
109
104
|
}
|
|
110
105
|
}
|
|
111
106
|
|
|
112
|
-
// 原子写:tmp + rename。避免半写状态被读到(与
|
|
107
|
+
// 原子写:tmp + rename。避免半写状态被读到(与 jobStore 一致)
|
|
113
108
|
export async function writeJson(file, data) {
|
|
114
109
|
await ensureDataDir();
|
|
115
110
|
const tmp = `${file}.tmp`;
|
|
@@ -23,12 +23,12 @@
|
|
|
23
23
|
// - launchClaudeInNewWindow(cwd, prompt, resumeSessionId) spawn claude,返回 {pid, child}
|
|
24
24
|
// - launchOpencodeRun(cwd, prompt, resumeSessionId) spawn opencode,返回 {pid, child}
|
|
25
25
|
// - normalizeTaskExecutor(value) 非法值回落 'claude'
|
|
26
|
-
// -
|
|
27
|
-
// - runSingleSubtask(task, sub, repoPath, branch, priorOutputs, opts) 跑单个 sub
|
|
28
|
-
// - syncSubToCancelled(job) cancel 路径专用
|
|
29
|
-
// - persistTaskAfterRun(task) 把 sub.status 落回 tasks.json
|
|
30
|
-
// - collectPriorOutputs(task, targetSub) 收集前序 done sub 的输出
|
|
26
|
+
// - runSingleSubtask(task, sub, repoPath, branch, opts) 跑一次任务(sub 是运行载体,见下)
|
|
31
27
|
// - waitProcessExit(pid) polling 等进程退出
|
|
28
|
+
//
|
|
29
|
+
// 关于 `sub`:任务不再有子任务,但执行器仍以一个「运行载体」对象作为入参 ——
|
|
30
|
+
// 它携带本次要跑的 title / desc / promptOverride / attachments。路由层构造它,
|
|
31
|
+
// 续聊时同一任务会造出新的载体(`{taskId}__simple__r1` 等),job.subId 记的就是它。
|
|
32
32
|
|
|
33
33
|
import fs from 'fs';
|
|
34
34
|
import path from 'path';
|
|
@@ -36,9 +36,7 @@ import { spawn, execFileSync } from 'child_process';
|
|
|
36
36
|
import {
|
|
37
37
|
logger,
|
|
38
38
|
PROMPTS_FILE,
|
|
39
|
-
TASKS_FILE,
|
|
40
39
|
readJson,
|
|
41
|
-
writeJson,
|
|
42
40
|
nowIso,
|
|
43
41
|
genId,
|
|
44
42
|
interpolate,
|
|
@@ -49,8 +47,6 @@ import {
|
|
|
49
47
|
jobs,
|
|
50
48
|
cancelledJobs,
|
|
51
49
|
publish,
|
|
52
|
-
snapshotJobs,
|
|
53
|
-
refreshJobsFromDisk,
|
|
54
50
|
flushJobsSaveNow,
|
|
55
51
|
} from './jobStore.js';
|
|
56
52
|
|
|
@@ -590,55 +586,13 @@ export function createOpencodeEventHandler(job, appendOutput, appendThinking, to
|
|
|
590
586
|
}
|
|
591
587
|
|
|
592
588
|
/**
|
|
593
|
-
*
|
|
594
|
-
*
|
|
595
|
-
* fromIndex>0 时,把 [0, fromIndex) 区间内已 done 的 sub 输出预填到 priorOutputs,
|
|
596
|
-
* 这样后续 sub 仍能拿到前序上下文(跟单 sub 执行的语义保持一致)。
|
|
597
|
-
*
|
|
598
|
-
* 连续模式(默认):任意 sub 终态非 done(cancelled / error)→ 整批停,后续 sub 保持 todo。
|
|
599
|
-
* AI 拆出来的 sub 一般前后强依赖(前一步产出是后一步输入),出错就停下来让用户决策。
|
|
600
|
-
* 关闭后回退旧行为:单个 sub 失败不影响后续 sub 继续跑。
|
|
601
|
-
*/
|
|
602
|
-
export async function runTaskQueue(task, repoPath, branch, opts) {
|
|
603
|
-
// 前序上下文:跑完一个 sub 后把它"完成态"输出存到这里,下一个 sub 启动时
|
|
604
|
-
// 拼到 prompt 头部,让 Claude 知道前面做了什么、产出了什么。
|
|
605
|
-
// 现在 LLM 都是百万 token 上下文窗口,完整透传 raw output,不做截断——
|
|
606
|
-
// 关键产物(生成的代码块、JSON、结论)在中间被砍掉反而会让后续 sub 失去依据。
|
|
607
|
-
const requested = Number(opts && opts.fromIndex);
|
|
608
|
-
const fromIndex = Number.isInteger(requested) && requested >= 0 && requested < task.subtasks.length
|
|
609
|
-
? requested
|
|
610
|
-
: 0;
|
|
611
|
-
// 从 fromIndex 开始时,把前面已 done 的 sub 输出预填进 priorOutputs,
|
|
612
|
-
// 否则"从中间开始"会丢失前序上下文。
|
|
613
|
-
const priorOutputs = fromIndex > 0
|
|
614
|
-
? await collectPriorOutputsUpTo(task, fromIndex)
|
|
615
|
-
: [];
|
|
616
|
-
const sequential = task.sequential !== false;
|
|
617
|
-
for (let i = fromIndex; i < task.subtasks.length; i++) {
|
|
618
|
-
const sub = task.subtasks[i];
|
|
619
|
-
if (sub.status === 'done') continue;
|
|
620
|
-
const outcome = await runSingleSubtask(task, sub, repoPath, branch, priorOutputs, { executor: opts.executor });
|
|
621
|
-
// 逐 sub 落盘:之前只在队列跑完才 persistTaskAfterRun,中途崩溃会丢已完成
|
|
622
|
-
// sub 的状态。runSingleSubtask 已经把 sub.status 改完,这里补一次落盘。
|
|
623
|
-
await persistTaskAfterRun(task);
|
|
624
|
-
if (sequential && outcome !== 'done') {
|
|
625
|
-
// cancelled / error → 后续 sub 全部保持 todo,不再继续
|
|
626
|
-
break;
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
/**
|
|
632
|
-
* 执行单个子任务。被 runTaskQueue(整批)和"单 sub 执行"endpoint 共用。
|
|
589
|
+
* 执行一次任务。任务一次只跑一个进程,本函数是唯一的执行入口
|
|
590
|
+
* (首次执行、手动重跑、续聊都走它)。
|
|
633
591
|
*
|
|
634
|
-
* @param {object} task
|
|
635
|
-
* @param {object} sub
|
|
592
|
+
* @param {object} task 任务对象
|
|
593
|
+
* @param {object} sub 本次运行载体:{ id, title, desc, promptOverride, attachments }
|
|
636
594
|
* @param {string} repoPath 仓库路径
|
|
637
595
|
* @param {string} branch 分支名(可空)
|
|
638
|
-
* @param {Array<{title:string,output:string}>} priorOutputs
|
|
639
|
-
* 前序 done 子任务的输出摘要(in-place 追加)。用于把同一任务下
|
|
640
|
-
* 前面已完成的 sub 产物拼到当前 sub 的 prompt 头部,让 Claude
|
|
641
|
-
* 知道上下文。单独跑一个 sub 时,这个数组里只会有"前面 done 的 sub"。
|
|
642
596
|
* @param {object} [options]
|
|
643
597
|
* @param {string|null} [options.resumeSessionId]
|
|
644
598
|
* 续接历史会话:claude 走 --resume <id>,opencode 走 --session <id>。
|
|
@@ -646,9 +600,9 @@ export async function runTaskQueue(task, repoPath, branch, opts) {
|
|
|
646
600
|
* @param {string} [options.executor]
|
|
647
601
|
* 执行器 'claude' | 'opencode'。缺省回落 'claude';路由层负责把
|
|
648
602
|
* "body 指定 > 配置默认"先解析好再传进来。
|
|
649
|
-
* @returns {Promise<'done'|'cancelled'|'error'>}
|
|
603
|
+
* @returns {Promise<'done'|'cancelled'|'error'>} 本次运行的终态
|
|
650
604
|
*/
|
|
651
|
-
export async function runSingleSubtask(task, sub, repoPath, branch,
|
|
605
|
+
export async function runSingleSubtask(task, sub, repoPath, branch, options) {
|
|
652
606
|
const opts = options || {};
|
|
653
607
|
const resumeSessionId = opts.resumeSessionId || null;
|
|
654
608
|
const executor = normalizeTaskExecutor(opts.executor);
|
|
@@ -662,28 +616,13 @@ export async function runSingleSubtask(task, sub, repoPath, branch, priorOutputs
|
|
|
662
616
|
branch: branch || ''
|
|
663
617
|
};
|
|
664
618
|
const interpolated = interpolate(promptTemplate, ctx);
|
|
665
|
-
//
|
|
619
|
+
// 标题与描述可能逐字相同(派发建的任务就是),去重规则见 promptParts.js
|
|
666
620
|
let prompt = composePromptBody(interpolated, sub.title, sub.desc);
|
|
667
621
|
|
|
668
|
-
// ── 前序上下文:把前几个 done 子任务的输出完整拼到 prompt 头部 ──
|
|
669
|
-
if (priorOutputs && priorOutputs.length > 0) {
|
|
670
|
-
const prevBlock = priorOutputs.map((p, i) => {
|
|
671
|
-
return `### [${i + 1}] ${p.title}\n${p.output || ''}`;
|
|
672
|
-
}).join('\n\n');
|
|
673
|
-
prompt = `以下是同一任务下已经完成的前序子任务输出(仅作上下文参考,请基于这些结论继续当前子任务,无需重复执行它们):
|
|
674
|
-
|
|
675
|
-
${prevBlock}
|
|
676
|
-
|
|
677
|
-
---
|
|
678
|
-
|
|
679
|
-
${prompt}`;
|
|
680
|
-
}
|
|
681
|
-
|
|
682
622
|
// ── 附件:合并 sub.attachments + task.attachments 后拼到 prompt 末尾 ──
|
|
683
623
|
// PDF 附件在服务端预提取全文(Claude CLI v2.1.x 的 pdfParse 有 bug),
|
|
684
624
|
// 图片和其他文件仍列路径让 Claude CLI 直接读取。
|
|
685
|
-
//
|
|
686
|
-
// 注意:run-simple 路径下 virtualSub.attachments 就是 task.attachments 的同一引用,
|
|
625
|
+
// 注意:首次执行时 sub.attachments 就是 task.attachments 的同一引用,
|
|
687
626
|
// 不去重会把同一张图在 prompt 里列两遍。按 absolutePath 去重。
|
|
688
627
|
const taskAtts = Array.isArray(task.attachments) ? task.attachments : [];
|
|
689
628
|
const subAtts = Array.isArray(sub.attachments) ? sub.attachments : [];
|
|
@@ -703,7 +642,7 @@ ${prompt}`;
|
|
|
703
642
|
}
|
|
704
643
|
|
|
705
644
|
// ── 运行环境上下文:项目清单 + 看板概览 + 真相源文件路径 ──
|
|
706
|
-
//
|
|
645
|
+
// 拼在**最前面**、任务正文压尾:
|
|
707
646
|
// 越靠后离模型的注意力中心越近,用户真正要办的那句话必须在最后一屏。
|
|
708
647
|
// task.envContext === false 可以单任务关掉(默认开 —— 老任务不迁移也一并受益)。
|
|
709
648
|
if (task.envContext !== false) {
|
|
@@ -716,7 +655,8 @@ ${prompt}`;
|
|
|
716
655
|
id: jobId,
|
|
717
656
|
taskId: task.id,
|
|
718
657
|
subId: sub.id,
|
|
719
|
-
|
|
658
|
+
// 一条任务 = 一次会话:日志标题就用任务标题(sub 只是运行载体,标题同源)
|
|
659
|
+
title: task.title,
|
|
720
660
|
status: 'pending',
|
|
721
661
|
prompt,
|
|
722
662
|
// 本轮用的执行器。前端日志详情按它显示助手名;简单任务续聊时
|
|
@@ -731,8 +671,6 @@ ${prompt}`;
|
|
|
731
671
|
toolCalls: []
|
|
732
672
|
};
|
|
733
673
|
jobs.set(jobId, job);
|
|
734
|
-
sub.status = 'running';
|
|
735
|
-
publish('sub:update', { taskId: task.id, sub });
|
|
736
674
|
publish('job:update', job);
|
|
737
675
|
|
|
738
676
|
// 声明在 try 之外:启动器就抛错时 finally 也要能安全收口(见 toolTracker?.seal)
|
|
@@ -836,43 +774,28 @@ ${prompt}`;
|
|
|
836
774
|
job.exitCode = 130; // 128 + SIGINT(2),约定俗成的"用户取消"退出码
|
|
837
775
|
job.status = 'cancelled';
|
|
838
776
|
job.error = '用户已停止执行';
|
|
839
|
-
// 同步把闭包里的 sub 也置 cancelled(cancel 接口的 syncSubToCancelled 改了磁盘 task
|
|
840
|
-
// 引用,但 runSingleSubtask 形参里的 sub 是另一份内存引用)。否则 finally publish sub:update
|
|
841
|
-
// 会用 status='running' 覆盖掉前端已渲染的 cancelled 状态,导致 UI 反复跳回 running。
|
|
842
|
-
sub.status = 'cancelled';
|
|
843
|
-
if (!sub.error) sub.error = '用户已停止执行';
|
|
844
777
|
} else if (job.agentError) {
|
|
845
778
|
// 执行器在协议层报了 error(如模型 5xx、配置缺失)。CLI 退出码可能是 0,
|
|
846
|
-
// 但不能标 done ——
|
|
779
|
+
// 但不能标 done —— 那会把失败伪装成完成。
|
|
847
780
|
job.exitCode = 1;
|
|
848
781
|
job.status = 'error';
|
|
849
782
|
job.error = job.agentError;
|
|
850
|
-
sub.status = 'error';
|
|
851
|
-
sub.error = job.agentError;
|
|
852
|
-
sub.errorAt = nowIso();
|
|
853
783
|
appendOutput(`\n> [${executor}] ${job.agentError}\n`);
|
|
854
784
|
} else {
|
|
855
785
|
job.exitCode = 0;
|
|
856
786
|
job.status = 'done';
|
|
857
|
-
sub.status = 'done';
|
|
858
|
-
// 把这个 sub 的输出累积到前序上下文,喂给下一个 sub
|
|
859
|
-
if (priorOutputs) priorOutputs.push({ title: sub.title, output: job.output || '' });
|
|
860
787
|
}
|
|
861
788
|
} catch (err) {
|
|
862
789
|
const errMsg = err && err.message ? err.message : String(err);
|
|
863
790
|
job.error = errMsg;
|
|
864
791
|
job.status = 'error';
|
|
865
|
-
sub.status = 'error';
|
|
866
|
-
sub.error = errMsg;
|
|
867
|
-
sub.errorAt = nowIso();
|
|
868
792
|
} finally {
|
|
869
793
|
// 工具调用收口:进程都退了,还挂在 running 的调用不会再有下文。先标终态再推 job:update,
|
|
870
|
-
//
|
|
794
|
+
// 否则前端那些工具块会一直转圈。
|
|
871
795
|
toolTracker?.seal(job.status);
|
|
872
796
|
// 移除 child 引用——避免后续被 SSE 序列化到前端
|
|
873
797
|
delete job.child;
|
|
874
798
|
publish('job:update', job);
|
|
875
|
-
publish('sub:update', { taskId: task.id, sub });
|
|
876
799
|
// 终态:await 同步落盘,确保 done/cancelled/error 全部立即归档。
|
|
877
800
|
try {
|
|
878
801
|
await flushJobsSaveNow();
|
|
@@ -880,94 +803,9 @@ ${prompt}`;
|
|
|
880
803
|
logger.warn('[workbench] flushJobsSaveNow failed (job id=' + job.id + ', status=' + job.status + '):', err && err.message || err);
|
|
881
804
|
}
|
|
882
805
|
}
|
|
883
|
-
// 把 sub 的终态返回给 runTaskQueue,用于「连续模式」判断要不要 break 整批队列
|
|
884
806
|
return job.status; // 'done' | 'cancelled' | 'error'
|
|
885
807
|
}
|
|
886
808
|
|
|
887
|
-
/**
|
|
888
|
-
* 把被取消的 sub 同步置 'cancelled' 并落盘。
|
|
889
|
-
* cancelJob 路径专用:前端 taskIsRunning/sub.is-running 都看 sub.status,不改就会出现
|
|
890
|
-
* "主任务黄点 + sub running 动效 + 右侧执行完成"三处不一致。
|
|
891
|
-
*
|
|
892
|
-
* 简单任务的虚拟 subId 不在 tasks.json 里(task.subtasks 是 complex 才有),所以这里
|
|
893
|
-
* 找不到 sub 时静默返回;简单任务的 running 状态由 job 数组单独维护(见 taskIsRunning)。
|
|
894
|
-
*
|
|
895
|
-
* @returns {{ taskId: string, sub: object } | null} 找到并更新时返回新 sub,否则 null
|
|
896
|
-
*/
|
|
897
|
-
export async function syncSubToCancelled(job) {
|
|
898
|
-
if (!job || !job.taskId || !job.subId) return null;
|
|
899
|
-
const data = await readJson(TASKS_FILE, { tasks: [] });
|
|
900
|
-
const task = (data.tasks || []).find(x => x.id === job.taskId);
|
|
901
|
-
if (!task || !Array.isArray(task.subtasks)) return null;
|
|
902
|
-
const sub = task.subtasks.find(s => s && s.id === job.subId);
|
|
903
|
-
if (!sub) return null;
|
|
904
|
-
if (sub.status === 'cancelled') return { taskId: task.id, sub }; // 已置过,幂等返回
|
|
905
|
-
sub.status = 'cancelled';
|
|
906
|
-
sub.error = '用户已停止执行';
|
|
907
|
-
sub.errorAt = nowIso();
|
|
908
|
-
task.updatedAt = nowIso();
|
|
909
|
-
await writeJson(TASKS_FILE, data);
|
|
910
|
-
publish('sub:update', { taskId: task.id, sub });
|
|
911
|
-
publish('task:update', task);
|
|
912
|
-
return { taskId: task.id, sub };
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
/** 把 task.subtasks 写回 tasks.json,并广播 task:update。runTaskQueue 和"单 sub 执行"共用。 */
|
|
916
|
-
export async function persistTaskAfterRun(task) {
|
|
917
|
-
const data = await readJson(TASKS_FILE, { tasks: [] });
|
|
918
|
-
const t = data.tasks.find(x => x.id === task.id);
|
|
919
|
-
if (t) {
|
|
920
|
-
// 仅同步 status 之外的 error/errorAt 字段,避免覆盖用户编辑过的 title/desc 等。
|
|
921
|
-
const newMap = new Map(task.subtasks.map(s => [s.id, s]));
|
|
922
|
-
t.subtasks = (t.subtasks || []).map(old => {
|
|
923
|
-
const fresh = newMap.get(old.id);
|
|
924
|
-
if (!fresh) return old;
|
|
925
|
-
return {
|
|
926
|
-
...old,
|
|
927
|
-
status: fresh.status ?? old.status,
|
|
928
|
-
error: fresh.error ?? old.error,
|
|
929
|
-
errorAt: fresh.errorAt ?? old.errorAt,
|
|
930
|
-
};
|
|
931
|
-
});
|
|
932
|
-
t.updatedAt = nowIso();
|
|
933
|
-
await writeJson(TASKS_FILE, data);
|
|
934
|
-
publish('task:update', t);
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
/**
|
|
939
|
-
* 构建单 sub 执行时的 priorOutputs:把同一 task 下"排在当前 sub 之前"且已 done 的
|
|
940
|
-
* 子任务输出摘要收集起来。这样单独跑一个 sub 时,它也能拿到前序上下文。
|
|
941
|
-
*/
|
|
942
|
-
export async function collectPriorOutputs(task, targetSub) {
|
|
943
|
-
const targetIdx = task.subtasks.findIndex(s => s.id === targetSub.id);
|
|
944
|
-
if (targetIdx < 0) return [];
|
|
945
|
-
return collectPriorOutputsUpTo(task, targetIdx);
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
/**
|
|
949
|
-
* 收集 [0, endIdx) 区间内已 done 的 sub 输出摘要,作为队列内 sub 的前序上下文。
|
|
950
|
-
* runTaskQueue 在 fromIndex>0 时调这个,让"从此处开始"也能拼上前序 done sub 的结论。
|
|
951
|
-
*/
|
|
952
|
-
export async function collectPriorOutputsUpTo(task, endIdx) {
|
|
953
|
-
// 前序 sub 可能是**另一个 g ui 实例**跑完的(或者本进程重启过),只翻内存会漏,
|
|
954
|
-
// 于是"前序结论"整段丢了。刷新有 mtime 短路,队列里每个 sub 调一次也只是个 stat。
|
|
955
|
-
await refreshJobsFromDisk();
|
|
956
|
-
const prior = [];
|
|
957
|
-
for (let i = 0; i < endIdx; i++) {
|
|
958
|
-
const s = task.subtasks[i];
|
|
959
|
-
if (s.status !== 'done') continue;
|
|
960
|
-
// 从 jobs 列表里找最近一个属于这个 sub 且 status=done 的 job,
|
|
961
|
-
// 取其 output 作为"前序上下文"。完整透传,不做字符截断。
|
|
962
|
-
const job = snapshotJobs()
|
|
963
|
-
.filter(j => j.subId === s.id && j.status === 'done')
|
|
964
|
-
.sort((a, b) => (b.endedAt || '').localeCompare(a.endedAt || ''))[0];
|
|
965
|
-
if (!job) continue;
|
|
966
|
-
prior.push({ title: s.title, output: job.output || '' });
|
|
967
|
-
}
|
|
968
|
-
return prior;
|
|
969
|
-
}
|
|
970
|
-
|
|
971
809
|
// polling 等进程退出:信号 0 探测存活;30 分钟超时兜底
|
|
972
810
|
export function waitProcessExit(pid) {
|
|
973
811
|
return new Promise(resolve => {
|
|
@@ -15,17 +15,16 @@
|
|
|
15
15
|
// 工作台后端路由入口。
|
|
16
16
|
//
|
|
17
17
|
// 本文件原本是 3539 行的巨型文件(含所有顶层工具函数 + 45 个路由端点)。
|
|
18
|
-
// 2026-06-29 已按业务域拆分到 ./workbench/
|
|
18
|
+
// 2026-06-29 已按业务域拆分到 ./workbench/ 子目录的多个模块:
|
|
19
19
|
// - shared.js 常量 + 通用工具 (nowIso, genId, readJson, writeJson, interpolate)
|
|
20
|
-
// - jsonParse.js
|
|
20
|
+
// - jsonParse.js 从 LLM 输出里抠 JSON 的纯函数
|
|
21
21
|
// - llmClient.js LLM 客户端 (callLlmJson, callLlmStream)
|
|
22
22
|
// - projectScan.js 子项目识别 (findSubProjects, detectProjectManifest)
|
|
23
23
|
// - attachmentUtils.js 附件白名单 (sanitizeExt, resolveExt, MIME_TO_EXT)
|
|
24
|
-
// - sessionStore.js AI 对话拆分会话持久化
|
|
25
24
|
// - instructionStore.js AI 指令读写
|
|
26
25
|
// - jobStore.js jobs Map + bus + 持久化 + retention
|
|
27
|
-
// - taskRunner.js 任务执行引擎 (
|
|
28
|
-
// - index.js registerWorkbenchRoutes
|
|
26
|
+
// - taskRunner.js 任务执行引擎 (runSingleSubtask)
|
|
27
|
+
// - index.js registerWorkbenchRoutes 入口聚合
|
|
29
28
|
//
|
|
30
29
|
// 此处保留 re-export 是为了让 src/ui/server/index.js 的 import 路径不变。
|
|
31
30
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
.marketplace-panel[data-v-07abd42c]{background:var(--bg-container);flex-direction:column;height:100%;min-height:0;display:flex}.mp-toolbar[data-v-07abd42c]{flex-shrink:0;align-items:center;gap:12px;padding:12px 16px 8px;display:flex}.mp-search[data-v-07abd42c]{border:1px solid var(--border-color);border-radius:var(--radius-md);background:var(--bg-panel);flex:1;align-items:center;gap:6px;max-width:420px;height:32px;padding:0 10px;transition:border-color .15s;display:flex}.mp-search[data-v-07abd42c]:focus-within{border-color:var(--color-primary)}.mp-search .mp-search-icon[data-v-07abd42c]{color:var(--text-tertiary);flex-shrink:0}.mp-search .mp-search-input[data-v-07abd42c]{color:var(--text-primary);background:0 0;border:none;outline:none;flex:1;font-family:inherit;font-size:13px}.mp-search .mp-search-input[data-v-07abd42c]::placeholder{color:var(--text-tertiary)}.mp-target[data-v-07abd42c]{flex-shrink:0}.mp-refresh[data-v-07abd42c]{border:1px solid var(--border-color);border-radius:var(--radius-md);background:var(--bg-panel);width:30px;height:30px;color:var(--text-secondary);cursor:pointer;justify-content:center;align-items:center;transition:all .15s;display:flex}.mp-refresh[data-v-07abd42c]:hover{color:var(--color-primary);border-color:var(--color-primary)}.mp-refresh.spinning svg[data-v-07abd42c]{animation:.9s linear infinite mkt-spin-07abd42c}@keyframes mkt-spin-07abd42c{to{transform:rotate(360deg)}}.mp-toolbar-sub[data-v-07abd42c]{flex-wrap:wrap;flex-shrink:0;align-items:center;gap:12px;padding:0 16px 10px;display:flex}.mp-target-hint[data-v-07abd42c]{color:var(--text-tertiary);font-size:12px}.mp-target-hint b[data-v-07abd42c]{color:var(--color-primary);font-weight:600}.mp-source-filter[data-v-07abd42c]{flex-wrap:wrap;align-items:center;gap:6px;display:flex}.mp-chip[data-v-07abd42c]{border:1px solid var(--border-color);color:var(--text-tertiary);cursor:pointer;background:0 0;border-radius:12px;padding:2px 10px;font-family:inherit;font-size:12px;transition:all .15s}.mp-chip[data-v-07abd42c]:hover{color:var(--text-secondary);border-color:var(--text-tertiary)}.mp-chip.active[data-v-07abd42c]{color:var(--color-primary);border-color:var(--color-primary);background:color-mix(in srgb, var(--color-primary) 8%, transparent)}.mp-chip.clear[data-v-07abd42c]{color:var(--text-tertiary);border-style:dashed}.mp-body[data-v-07abd42c]{flex:1;min-height:0;padding:0 16px 20px;overflow-y:auto}.mp-group[data-v-07abd42c]{margin-bottom:20px}.mp-group.installed[data-v-07abd42c]{border-top:1px dashed var(--border-color);padding-top:14px}.mp-group-header[data-v-07abd42c]{align-items:center;gap:8px;margin-bottom:10px;display:flex}.mp-group-title[data-v-07abd42c]{color:var(--text-primary);margin:0;font-size:13px;font-weight:600}.mp-group-count[data-v-07abd42c]{background:var(--bg-hover);min-width:20px;height:18px;color:var(--text-tertiary);border-radius:9px;justify-content:center;align-items:center;padding:0 6px;font-size:11px;display:inline-flex}.mp-group-error[data-v-07abd42c]{color:var(--color-warning);align-items:center;gap:4px;font-size:12px;display:inline-flex}.mp-group-link[data-v-07abd42c]{color:var(--text-tertiary);transition:color .15s;display:inline-flex}.mp-group-link[data-v-07abd42c]:hover{color:var(--color-primary)}.mp-grid[data-v-07abd42c]{grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:10px;display:grid}.mp-card[data-v-07abd42c]{border:1px solid var(--border-color);border-radius:var(--radius-md);background:var(--bg-panel);flex-direction:column;gap:6px;padding:12px;transition:border-color .15s,box-shadow .15s;display:flex}.mp-card[data-v-07abd42c]:hover{border-color:color-mix(in srgb, var(--color-primary) 40%, var(--border-color));box-shadow:var(--shadow-sm)}.mp-card-head[data-v-07abd42c]{align-items:center;gap:8px;display:flex}.mp-card-name[data-v-07abd42c]{min-width:0;color:var(--text-primary);white-space:nowrap;text-overflow:ellipsis;flex:1;font-size:13px;font-weight:600;overflow:hidden}.mp-card-metric[data-v-07abd42c]{color:var(--text-tertiary);flex-shrink:0;align-items:center;gap:3px;font-size:11px;display:inline-flex}.mp-card-desc[data-v-07abd42c]{color:var(--text-secondary);-webkit-line-clamp:2;-webkit-box-orient:vertical;min-height:36px;margin:0;font-size:12px;line-height:1.5;display:-webkit-box;overflow:hidden}.mp-card-meta[data-v-07abd42c]{flex-wrap:wrap;align-items:center;gap:6px;min-height:18px;display:flex}.mp-meta-pkg[data-v-07abd42c],.mp-meta-repo[data-v-07abd42c]{color:var(--text-tertiary);white-space:nowrap;text-overflow:ellipsis;max-width:220px;font-size:11px;overflow:hidden}.mp-tag[data-v-07abd42c]{background:var(--bg-hover);color:var(--text-tertiary);border-radius:8px;flex-shrink:0;padding:1px 6px;font-size:10px}.mp-tag.remote[data-v-07abd42c]{background:color-mix(in srgb, var(--color-info,#06b6d4) 14%, transparent);color:var(--color-info,#06b6d4)}.mp-tag.warn[data-v-07abd42c]{background:color-mix(in srgb, var(--color-warning) 14%, transparent);color:var(--color-warning)}.mp-card-foot[data-v-07abd42c]{align-items:center;gap:8px;margin-top:2px;display:flex}.mp-card-link[data-v-07abd42c]{color:var(--text-tertiary);font-size:11px;text-decoration:none}.mp-card-link[data-v-07abd42c]:hover{color:var(--color-primary);text-decoration:underline}.mp-foot-spacer[data-v-07abd42c]{flex:1}.mp-install-btn[data-v-07abd42c]{border:1px solid var(--color-primary);border-radius:var(--radius-sm);color:var(--color-primary);cursor:pointer;background:0 0;padding:4px 14px;font-family:inherit;font-size:12px;transition:all .15s}.mp-install-btn[data-v-07abd42c]:hover:not(:disabled){background:var(--color-primary);color:#fff}.mp-install-btn[data-v-07abd42c]:disabled{opacity:.5;cursor:not-allowed}.mp-installed-chip[data-v-07abd42c]{color:var(--color-success,#34d399);align-items:center;gap:3px;font-size:11px;display:inline-flex}.mp-not-installable[data-v-07abd42c]{color:var(--text-tertiary);font-size:11px}.mp-empty[data-v-07abd42c]{color:var(--text-tertiary);padding:16px 0;font-size:12px}.mp-empty.all[data-v-07abd42c]{text-align:center;padding:48px 0}.mp-installed-block[data-v-07abd42c]{margin-bottom:12px}.mp-installed-target[data-v-07abd42c]{color:var(--text-tertiary);text-transform:uppercase;letter-spacing:.4px;margin-bottom:6px;font-size:11px;font-weight:600}.mp-installed-list[data-v-07abd42c]{flex-direction:column;gap:4px;display:flex}.mp-installed-row[data-v-07abd42c]{border:1px solid var(--border-color);border-radius:var(--radius-sm);background:var(--bg-panel);align-items:center;gap:10px;padding:6px 10px;display:flex}.mp-installed-name[data-v-07abd42c]{color:var(--text-primary);white-space:nowrap;text-overflow:ellipsis;font-size:12px;font-weight:500;overflow:hidden}.mp-remove-btn[data-v-07abd42c]{border:1px solid var(--border-color);border-radius:var(--radius-xs);color:var(--text-tertiary);cursor:pointer;background:0 0;align-items:center;gap:4px;padding:3px 10px;font-family:inherit;font-size:11px;transition:all .15s;display:inline-flex}.mp-remove-btn[data-v-07abd42c]:hover:not(:disabled){color:var(--color-danger);border-color:var(--color-danger)}.mp-remove-btn[data-v-07abd42c]:disabled{opacity:.5;cursor:not-allowed}@media (prefers-reduced-motion:reduce){.mp-refresh.spinning svg[data-v-07abd42c]{animation:none}}.mkt-form[data-v-07abd42c]{flex-direction:column;gap:12px;display:flex}.mkt-form-name[data-v-07abd42c]{color:var(--text-primary);margin:0;font-size:13px;font-weight:600}.mkt-field[data-v-07abd42c]{flex-direction:column;gap:4px;display:flex}.mkt-field label[data-v-07abd42c]{color:var(--text-secondary);font-size:12px;font-weight:600}.mkt-field input[data-v-07abd42c]{border:1px solid var(--border-color);background:var(--bg-container);height:30px;color:var(--text-primary);border-radius:6px;padding:0 8px;font-family:inherit;font-size:13px;transition:border-color .15s}.mkt-field input[data-v-07abd42c]:focus{border-color:var(--color-primary);outline:none}.mkt-hint[data-v-07abd42c]{color:var(--text-tertiary);margin:0;font-size:11px}.mkt-cancel[data-v-07abd42c],.mkt-confirm[data-v-07abd42c]{border-radius:var(--radius-sm);cursor:pointer;padding:6px 16px;font-family:inherit;font-size:13px;transition:all .15s}.mkt-cancel[data-v-07abd42c]{border:1px solid var(--border-color);color:var(--text-secondary);background:0 0;margin-right:8px}.mkt-cancel[data-v-07abd42c]:hover{color:var(--text-primary);border-color:var(--text-tertiary)}.mkt-confirm[data-v-07abd42c]{border:1px solid var(--color-primary);background:var(--color-primary);color:#fff}.mkt-confirm[data-v-07abd42c]:hover{opacity:.9}.agent-view[data-v-17ac5478]{background:var(--bg-container);height:100%;min-height:0;display:flex}.agent-sidebar[data-v-17ac5478]{background:var(--bg-panel);border-right:1px solid var(--border-color);flex-direction:column;flex-shrink:0;display:flex;overflow:hidden}.sidebar-header[data-v-17ac5478]{flex-shrink:0;padding:10px 12px}.new-session-btn[data-v-17ac5478]{border:1px dashed var(--border-color);border-radius:var(--radius-md);width:100%;color:var(--text-secondary);cursor:pointer;background:0 0;justify-content:center;align-items:center;gap:6px;padding:8px 12px;font-family:inherit;font-size:13px;transition:all .15s;display:flex}.new-session-btn[data-v-17ac5478]:hover{border-color:var(--color-primary);color:var(--color-primary);background:color-mix(in srgb, var(--color-primary) 6%, transparent)}.sidebar-search[data-v-17ac5478]{flex-shrink:0;align-items:center;gap:6px;padding:0 12px 8px;display:flex}.sidebar-search .search-icon[data-v-17ac5478]{color:var(--text-tertiary);flex-shrink:0}.sidebar-search .search-input[data-v-17ac5478]{color:var(--text-primary);background:0 0;border:none;outline:none;flex:1;padding:4px 0;font-family:inherit;font-size:13px}.sidebar-search .search-input[data-v-17ac5478]::placeholder{color:var(--text-tertiary)}.session-list[data-v-17ac5478]{flex:1;min-height:0;padding:0 6px 8px;overflow-y:auto}.empty-state[data-v-17ac5478]{text-align:center;color:var(--text-tertiary);flex-direction:column;justify-content:center;align-items:center;padding:48px 16px;display:flex}.empty-state .empty-text[data-v-17ac5478]{color:var(--text-secondary);margin-top:10px;font-size:14px}.empty-state .empty-hint[data-v-17ac5478]{margin-top:4px;font-size:12px}.session-item[data-v-17ac5478]{border-radius:var(--radius-sm);cursor:pointer;align-items:flex-start;gap:4px;padding:8px 10px;transition:background .12s;display:flex;position:relative}.session-item[data-v-17ac5478]:hover{background:var(--bg-hover)}.session-item:hover .session-item-actions[data-v-17ac5478]{opacity:1}.session-item.active[data-v-17ac5478]{background:color-mix(in srgb, var(--color-primary) 10%, transparent)}.session-item.active .session-item-title[data-v-17ac5478]{color:var(--color-primary)}.session-item.active[data-v-17ac5478]:before{content:"";background:var(--color-primary);border-radius:0 2px 2px 0;width:3px;height:20px;position:absolute;top:50%;left:0;transform:translateY(-50%)}.session-item-main[data-v-17ac5478]{flex:1;min-width:0}.session-item-title[data-v-17ac5478]{color:var(--text-primary);white-space:nowrap;text-overflow:ellipsis;margin-bottom:2px;font-size:13px;font-weight:500;overflow:hidden}.session-item-meta[data-v-17ac5478]{color:var(--text-tertiary);align-items:center;gap:4px;font-size:11px;display:flex}.session-item-meta .meta-dot[data-v-17ac5478]{opacity:.5}.session-item-meta .meta-generating[data-v-17ac5478]{color:var(--color-primary);align-items:center;gap:3px;display:inline-flex}.session-item-meta .meta-source[data-v-17ac5478]{border-radius:3px;padding:0 4px;font-size:10px;font-weight:600}.session-item-meta .meta-source.cli[data-v-17ac5478]{background:color-mix(in srgb, var(--color-warning) 20%, transparent);color:var(--color-warning)}.session-item-actions[data-v-17ac5478]{opacity:0;flex-shrink:0;gap:2px;transition:opacity .12s;display:flex}.item-action-btn[data-v-17ac5478]{width:24px;height:24px;color:var(--text-tertiary);cursor:pointer;border-radius:var(--radius-xs);background:0 0;border:none;justify-content:center;align-items:center;padding:0;transition:all .12s;display:flex}.item-action-btn[data-v-17ac5478]:hover{background:var(--bg-hover);color:var(--text-secondary)}.item-action-btn.danger[data-v-17ac5478]:hover{color:var(--color-danger);background:color-mix(in srgb, var(--color-danger) 10%, transparent)}.sidebar-resizer[data-v-17ac5478]{cursor:col-resize;background:0 0;flex-shrink:0;width:4px;transition:background .15s}.sidebar-resizer[data-v-17ac5478]:hover,.sidebar-resizer.active[data-v-17ac5478]{background:var(--color-primary);opacity:.3}.agent-chat-area[data-v-17ac5478]{flex-direction:column;flex:1;min-width:0;display:flex;position:relative;overflow:hidden}.agent-tabs[data-v-17ac5478]{border-bottom:1px solid var(--border-color);background:var(--bg-panel);flex-shrink:0;align-items:center;gap:4px;padding:8px 12px 0;display:flex}.agent-tab[data-v-17ac5478]{color:var(--text-tertiary);cursor:pointer;border-radius:var(--radius-sm) var(--radius-sm) 0 0;background:0 0;border:none;border-bottom:2px solid #0000;align-items:center;gap:6px;padding:7px 14px 9px;font-family:inherit;font-size:13px;transition:color .15s,border-color .15s,background .15s;display:inline-flex}.agent-tab[data-v-17ac5478]:hover{color:var(--text-secondary);background:var(--bg-hover)}.agent-tab.active[data-v-17ac5478]{color:var(--color-primary);border-bottom-color:var(--color-primary);font-weight:600}.agent-tab-body[data-v-17ac5478]{flex-direction:column;flex:1;min-height:0;display:flex;overflow:hidden}.agent-chat-pane[data-v-17ac5478]{flex-direction:column;flex:1;min-height:0;display:flex;position:relative;overflow:hidden}.agent-tab-body[data-v-17ac5478]>.marketplace-panel{flex:1;min-height:0}.chat-loading[data-v-17ac5478]{color:var(--text-tertiary);flex-direction:column;flex:1;justify-content:center;align-items:center;gap:12px;font-size:14px;display:flex}.ask-user-panel[data-v-17ac5478]{z-index:9;border:1px solid var(--color-primary);border-radius:var(--radius-md);background:var(--bg-container);flex-direction:column;gap:10px;padding:12px 14px;display:flex;position:absolute;bottom:132px;left:16px;right:16px;box-shadow:0 4px 16px #00000029}.ask-user-title[data-v-17ac5478]{color:var(--color-primary);align-items:center;gap:6px;font-size:12px;font-weight:600;display:flex}.ask-user-question[data-v-17ac5478]{color:var(--text-primary);white-space:pre-wrap;overflow-wrap:anywhere;font-size:14px;line-height:1.45}.ask-user-options[data-v-17ac5478]{flex-wrap:wrap;gap:8px;display:flex}.ask-user-option[data-v-17ac5478],.ask-user-form button[data-v-17ac5478]{border:1px solid var(--border-color);border-radius:var(--radius-sm);background:var(--bg-hover);min-height:32px;color:var(--text-primary);font:inherit;cursor:pointer;justify-content:center;align-items:center;gap:5px;padding:5px 10px;font-size:13px;transition:border-color .15s,color .15s,background .15s;display:inline-flex}.ask-user-option[data-v-17ac5478]:hover:not(:disabled),.ask-user-form button[data-v-17ac5478]:hover:not(:disabled){border-color:var(--color-primary);color:var(--color-primary);background:color-mix(in srgb, var(--color-primary) 8%, var(--bg-hover))}.ask-user-option[data-v-17ac5478]:disabled,.ask-user-form button[data-v-17ac5478]:disabled{cursor:wait;opacity:.6}.ask-user-form[data-v-17ac5478]{gap:8px;display:flex}.ask-user-form input[data-v-17ac5478]{border:1px solid var(--border-color);border-radius:var(--radius-sm);background:var(--bg-input);min-width:0;height:32px;color:var(--text-primary);font:inherit;outline:none;flex:1;padding:5px 9px;font-size:13px}.ask-user-form input[data-v-17ac5478]:focus{border-color:var(--color-primary)}.stop-button-bar[data-v-17ac5478]{z-index:10;position:absolute;bottom:80px;left:50%;transform:translate(-50%)}.stop-button[data-v-17ac5478]{border:1px solid var(--color-danger);background:var(--bg-container);color:var(--color-danger);cursor:pointer;border-radius:20px;align-items:center;gap:6px;padding:6px 16px;font-family:inherit;font-size:13px;transition:all .15s;display:flex;box-shadow:0 2px 8px #0000001f}.stop-button[data-v-17ac5478]:hover{background:var(--color-danger);color:#fff}.fade-enter-active[data-v-17ac5478],.fade-leave-active[data-v-17ac5478]{transition:opacity .2s}.fade-enter-from[data-v-17ac5478],.fade-leave-to[data-v-17ac5478]{opacity:0}[data-theme=dark]{background:var(--bg-panel)}
|