codexmate 0.0.55 → 0.0.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +2 -0
  2. package/README.vi.md +2 -0
  3. package/README.zh.md +2 -0
  4. package/cli/local-bridge.js +221 -22
  5. package/cli.js +797 -134
  6. package/lib/task-orchestrator.js +90 -21
  7. package/lib/task-workspace-chat.js +292 -0
  8. package/package.json +2 -2
  9. package/web-ui/app.js +57 -129
  10. package/web-ui/modules/app.computed.main-tabs.mjs +304 -7
  11. package/web-ui/modules/app.computed.session.mjs +210 -0
  12. package/web-ui/modules/app.methods.agents.mjs +3 -0
  13. package/web-ui/modules/app.methods.claude-config.mjs +178 -22
  14. package/web-ui/modules/app.methods.codex-config.mjs +294 -65
  15. package/web-ui/modules/app.methods.navigation.mjs +71 -84
  16. package/web-ui/modules/app.methods.openclaw-editing.mjs +3 -6
  17. package/web-ui/modules/app.methods.providers.mjs +15 -1
  18. package/web-ui/modules/app.methods.runtime.mjs +7 -2
  19. package/web-ui/modules/app.methods.session-actions.mjs +25 -12
  20. package/web-ui/modules/app.methods.session-browser.mjs +23 -54
  21. package/web-ui/modules/app.methods.session-trash.mjs +0 -1
  22. package/web-ui/modules/app.methods.startup-claude.mjs +35 -17
  23. package/web-ui/modules/app.methods.task-orchestration.mjs +347 -8
  24. package/web-ui/modules/app.methods.tool-config-permissions.mjs +0 -3
  25. package/web-ui/modules/app.methods.web-ui-preferences.mjs +415 -68
  26. package/web-ui/modules/config-template-confirm-pref.mjs +2 -3
  27. package/web-ui/modules/i18n/locales/en.mjs +187 -28
  28. package/web-ui/modules/i18n/locales/ja.mjs +184 -25
  29. package/web-ui/modules/i18n/locales/vi.mjs +186 -33
  30. package/web-ui/modules/i18n/locales/zh-tw.mjs +187 -28
  31. package/web-ui/modules/i18n/locales/zh.mjs +189 -30
  32. package/web-ui/modules/i18n.mjs +5 -12
  33. package/web-ui/modules/provider-default-names.mjs +25 -0
  34. package/web-ui/modules/sessions-filters-url.mjs +1 -2
  35. package/web-ui/partials/index/layout-header.html +37 -14
  36. package/web-ui/partials/index/modal-health-check.html +69 -5
  37. package/web-ui/partials/index/panel-config-codex.html +2 -2
  38. package/web-ui/partials/index/panel-orchestration.html +489 -282
  39. package/web-ui/partials/index/panel-sessions.html +97 -17
  40. package/web-ui/res/web-ui-render.precompiled.js +1423 -732
  41. package/web-ui/session-helpers.mjs +4 -1
  42. package/web-ui/styles/layout-shell.css +157 -1
  43. package/web-ui/styles/navigation-panels.css +11 -0
  44. package/web-ui/styles/responsive.css +98 -0
  45. package/web-ui/styles/sessions-preview.css +212 -2
  46. package/web-ui/styles/sessions-toolbar-trash.css +61 -18
  47. package/web-ui/styles/skills-list.css +122 -0
  48. package/web-ui/styles/task-orchestration.css +2161 -4
  49. package/web-ui/styles/titles-cards.css +52 -0
@@ -12,7 +12,8 @@ function readTaskOrchestrationDraftMetrics(taskOrchestration) {
12
12
  const title = String(state.title || '').trim();
13
13
  const workflowIds = normalizeTaskDraftLines(state.workflowIdsText);
14
14
  const followUps = normalizeTaskDraftLines(state.followUpsText);
15
- const engine = String(state.selectedEngine || 'codex').trim().toLowerCase() === 'workflow' ? 'workflow' : 'codex';
15
+ const requestCount = target ? followUps.length + 1 : 0;
16
+ const engine = String(state.selectedEngine || 'openai-chat').trim().toLowerCase() === 'workflow' ? 'workflow' : 'openai-chat';
16
17
  const runMode = String(state.runMode || 'write').trim().toLowerCase();
17
18
  const allowWrite = runMode === 'write';
18
19
  const dryRun = runMode === 'dry-run';
@@ -37,12 +38,234 @@ function readTaskOrchestrationDraftMetrics(taskOrchestration) {
37
38
  planWarnings,
38
39
  workflowCount: workflowIds.length,
39
40
  followUpCount: followUps.length,
41
+ requestCount,
42
+ hasSequentialFollowUps: followUps.length > 0,
40
43
  planNodeCount: planNodes.length,
41
44
  allowWrite,
42
45
  dryRun
43
46
  };
44
47
  }
45
48
 
49
+
50
+ function formatTaskConversationMeta(items) {
51
+ return items.filter(Boolean).join(' · ');
52
+ }
53
+
54
+ function collectTaskRunWorkspaceFileLines(detail) {
55
+ const run = detail && detail.run && typeof detail.run === 'object' ? detail.run : null;
56
+ const nodes = Array.isArray(detail && detail.nodes)
57
+ ? detail.nodes
58
+ : (Array.isArray(run && run.nodes) ? run.nodes : []);
59
+ const files = [];
60
+ for (const node of nodes) {
61
+ const output = node && node.output && typeof node.output === 'object' ? node.output : null;
62
+ const workspaceFiles = Array.isArray(output && output.workspaceFiles) ? output.workspaceFiles : [];
63
+ const materializedFiles = Array.isArray(output && output.materializedFiles) ? output.materializedFiles : [];
64
+ for (const file of workspaceFiles) {
65
+ if (!file || typeof file !== 'object') continue;
66
+ const op = String(file.operation || '').trim().toUpperCase() || 'FILE';
67
+ const filePath = String(file.relativePath || file.path || '').trim();
68
+ if (filePath) files.push(`${op} ${filePath}`);
69
+ }
70
+ for (const file of materializedFiles) {
71
+ if (!file || typeof file !== 'object') continue;
72
+ const filePath = String(file.relativePath || file.path || '').trim();
73
+ if (filePath) files.push(`WRITE ${filePath}`);
74
+ }
75
+ }
76
+ return Array.from(new Set(files)).slice(0, 6);
77
+ }
78
+
79
+ function normalizeTaskWorkspacePath(value) {
80
+ return String(value || '').trim().replace(/\\+/g, '/').replace(/\/$/, '');
81
+ }
82
+
83
+ function readTaskWorkspacePathFromItem(item) {
84
+ if (!item || typeof item !== 'object') return '';
85
+ return normalizeTaskWorkspacePath(item.cwd || item.workspacePath || item.projectPath || item.path);
86
+ }
87
+
88
+ function formatTaskWorkspaceLabel(workspacePath, fallback = 'Auto workspace') {
89
+ const normalized = normalizeTaskWorkspacePath(workspacePath);
90
+ if (!normalized) return fallback;
91
+ const parts = normalized.split('/').filter(Boolean);
92
+ return parts.length ? parts[parts.length - 1] : normalized;
93
+ }
94
+
95
+ function readTaskItemTimestamp(item) {
96
+ if (!item || typeof item !== 'object') return '';
97
+ return String(item.updatedAt || item.completedAt || item.startedAt || item.createdAt || item.enqueuedAt || '').trim();
98
+ }
99
+
100
+ function addTaskWorkspaceCandidate(workspaces, workspacePath, source, timestamp = '') {
101
+ const normalized = normalizeTaskWorkspacePath(workspacePath);
102
+ if (!normalized) return;
103
+ const key = normalized.toLowerCase();
104
+ const current = workspaces.get(key) || {
105
+ key,
106
+ path: normalized,
107
+ label: formatTaskWorkspaceLabel(normalized),
108
+ meta: normalized,
109
+ queueCount: 0,
110
+ runCount: 0,
111
+ lastActivity: ''
112
+ };
113
+ if (source === 'queue') current.queueCount += 1;
114
+ if (source === 'run') current.runCount += 1;
115
+ if (timestamp && (!current.lastActivity || timestamp > current.lastActivity)) {
116
+ current.lastActivity = timestamp;
117
+ }
118
+ workspaces.set(key, current);
119
+ }
120
+
121
+ function taskItemMatchesWorkspace(item, workspacePath) {
122
+ const selectedWorkspacePath = normalizeTaskWorkspacePath(workspacePath);
123
+ if (!selectedWorkspacePath) return true;
124
+ return readTaskWorkspacePathFromItem(item) === selectedWorkspacePath;
125
+ }
126
+
127
+ function createTaskWorkspaceItems(taskOrchestration) {
128
+ const state = taskOrchestration && typeof taskOrchestration === 'object' ? taskOrchestration : {};
129
+ const workspaces = new Map();
130
+ addTaskWorkspaceCandidate(workspaces, state.workspacePath, 'draft');
131
+ const detail = state.selectedRunDetail && typeof state.selectedRunDetail === 'object' ? state.selectedRunDetail : null;
132
+ if (detail) addTaskWorkspaceCandidate(workspaces, detail.cwd, 'detail');
133
+ const queue = Array.isArray(state.queue) ? state.queue : [];
134
+ const runs = Array.isArray(state.runs) ? state.runs : [];
135
+ queue.forEach((item) => addTaskWorkspaceCandidate(workspaces, readTaskWorkspacePathFromItem(item), 'queue', readTaskItemTimestamp(item)));
136
+ runs.forEach((item) => addTaskWorkspaceCandidate(workspaces, readTaskWorkspacePathFromItem(item), 'run', readTaskItemTimestamp(item)));
137
+ const items = Array.from(workspaces.values()).sort((a, b) => {
138
+ const byActivity = String(b.lastActivity || '').localeCompare(String(a.lastActivity || ''));
139
+ if (byActivity !== 0) return byActivity;
140
+ return a.label.localeCompare(b.label);
141
+ });
142
+ if (items.length === 0) {
143
+ items.push({
144
+ key: '__auto__',
145
+ path: '',
146
+ label: 'Auto workspace',
147
+ meta: 'Server default cwd',
148
+ queueCount: queue.length,
149
+ runCount: runs.length,
150
+ lastActivity: ''
151
+ });
152
+ }
153
+ return items;
154
+ }
155
+
156
+ function createTaskWorkspaceSessions(taskOrchestration, workspacePath) {
157
+ const state = taskOrchestration && typeof taskOrchestration === 'object' ? taskOrchestration : {};
158
+ const queue = Array.isArray(state.queue) ? state.queue : [];
159
+ const runs = Array.isArray(state.runs) ? state.runs : [];
160
+ const queueSessions = queue
161
+ .filter((item) => taskItemMatchesWorkspace(item, workspacePath))
162
+ .map((item) => ({
163
+ id: item.taskId || item.lastRunId || `queue-${readTaskItemTimestamp(item)}`,
164
+ type: 'queue',
165
+ status: item.status || item.runStatus || 'queued',
166
+ title: item.title || item.target || item.taskId || 'Queued task',
167
+ meta: item.threadId || item.cwd || item.taskId || '',
168
+ taskId: item.taskId || '',
169
+ runId: item.lastRunId || '',
170
+ threadId: item.threadId || '',
171
+ cwd: readTaskWorkspacePathFromItem(item),
172
+ timestamp: readTaskItemTimestamp(item),
173
+ active: true
174
+ }));
175
+ const runSessions = runs
176
+ .filter((item) => taskItemMatchesWorkspace(item, workspacePath))
177
+ .map((item) => ({
178
+ id: item.runId || item.taskId || `run-${readTaskItemTimestamp(item)}`,
179
+ type: 'run',
180
+ status: item.status || 'completed',
181
+ title: item.title || item.summary || item.taskId || item.runId || 'Run',
182
+ meta: item.threadId || item.cwd || `${item.durationMs || 0}ms`,
183
+ taskId: item.taskId || '',
184
+ runId: item.runId || '',
185
+ threadId: item.threadId || '',
186
+ cwd: readTaskWorkspacePathFromItem(item),
187
+ timestamp: readTaskItemTimestamp(item),
188
+ active: false
189
+ }));
190
+ return queueSessions.concat(runSessions).sort((a, b) => {
191
+ if (a.active !== b.active) return a.active ? -1 : 1;
192
+ return String(b.timestamp || '').localeCompare(String(a.timestamp || ''));
193
+ });
194
+ }
195
+
196
+ function createTaskConversationMessages(taskOrchestration, t = null) {
197
+ const state = taskOrchestration && typeof taskOrchestration === 'object' ? taskOrchestration : {};
198
+ const messages = [];
199
+ const target = String(state.target || '').trim();
200
+ const followUps = normalizeTaskDraftLines(state.followUpsText);
201
+ const detail = state.selectedRunDetail && typeof state.selectedRunDetail === 'object' ? state.selectedRunDetail : null;
202
+ const detailRun = detail && detail.run && typeof detail.run === 'object' ? detail.run : null;
203
+ if (detail) {
204
+ const fileLines = collectTaskRunWorkspaceFileLines(detail);
205
+ const resultLines = [];
206
+ if (detailRun && detailRun.summary) {
207
+ resultLines.push(detailRun.summary);
208
+ } else {
209
+ resultLines.push(translateTaskText(t, 'orchestration.chat.assistant.contextFallback', '已选中一个历史任务,继续时会继承它的线程和工作区。'));
210
+ }
211
+ if (fileLines.length > 0) {
212
+ resultLines.push(translateTaskText(t, 'orchestration.chat.assistant.filesSummary', `文件操作:${fileLines.join(';')}`, { files: fileLines.join(';') }));
213
+ }
214
+ messages.push({
215
+ id: 'context-run',
216
+ role: 'assistant',
217
+ label: detailRun && detailRun.status === 'success'
218
+ ? translateTaskText(t, 'orchestration.chat.assistant.resultLabel', '实现结果')
219
+ : translateTaskText(t, 'orchestration.chat.assistant.contextLabel', '上一轮上下文'),
220
+ text: resultLines.join('\n'),
221
+ meta: formatTaskConversationMeta([
222
+ detail.threadId ? translateTaskText(t, 'orchestration.chat.meta.thread', `线程 ${detail.threadId}`, { value: detail.threadId }) : '',
223
+ detail.cwd ? translateTaskText(t, 'orchestration.chat.meta.workspace', `工作区 ${detail.cwd}`, { value: detail.cwd }) : '',
224
+ detailRun && detailRun.status ? detailRun.status : ''
225
+ ])
226
+ });
227
+ } else if (!target && followUps.length === 0) {
228
+ messages.push({
229
+ id: 'assistant-empty',
230
+ role: 'assistant',
231
+ label: translateTaskText(t, 'orchestration.chat.assistant.readyLabel', 'Codexmate'),
232
+ text: translateTaskText(t, 'orchestration.chat.assistant.empty', '先发第一个需求;如果还有第二个需求,继续发送,Codexmate 会按顺序处理并保留上下文。'),
233
+ meta: translateTaskText(t, 'orchestration.chat.meta.order', '顺序执行 · 保留上下文')
234
+ });
235
+ }
236
+ if (target) {
237
+ messages.push({
238
+ id: 'user-target',
239
+ role: 'user',
240
+ label: translateTaskText(t, 'orchestration.chat.user.step', '需求 {count}', { count: 1 }),
241
+ text: target,
242
+ meta: translateTaskText(t, 'orchestration.chat.meta.first', '先完成这一条')
243
+ });
244
+ }
245
+ followUps.forEach((item, index) => {
246
+ const step = index + 2;
247
+ messages.push({
248
+ id: `user-follow-up-${index}`,
249
+ role: 'user',
250
+ label: translateTaskText(t, 'orchestration.chat.user.step', '需求 {count}', { count: step }),
251
+ text: item,
252
+ meta: translateTaskText(t, 'orchestration.chat.meta.afterPrevious', '等待前一条完成后继续')
253
+ });
254
+ });
255
+ if (!state.plan && target) {
256
+ messages.push({
257
+ id: 'assistant-next',
258
+ role: 'assistant',
259
+ label: translateTaskText(t, 'orchestration.chat.assistant.readyLabel', 'Codexmate'),
260
+ text: followUps.length > 0
261
+ ? translateTaskText(t, 'orchestration.chat.assistant.sequenceReady', '已收到多条需求;执行时会先完成需求 1,再带着上下文继续后续需求。')
262
+ : translateTaskText(t, 'orchestration.chat.assistant.singleReady', '已收到第一条需求。可以继续补需求 2,或直接预览并执行。'),
263
+ meta: translateTaskText(t, 'orchestration.chat.meta.previewNext', '下一步:预览计划')
264
+ });
265
+ }
266
+ return messages;
267
+ }
268
+
46
269
  function translateTaskText(t, key, fallback, params = null) {
47
270
  if (typeof t !== 'function') return fallback;
48
271
  const translated = t(key, params);
@@ -60,13 +283,23 @@ function createTaskDraftChecklist(metrics, t = null) {
60
283
  done: metrics.hasTarget,
61
284
  detail: metrics.hasTarget ? translateTaskText(t, 'orchestration.readiness.target.done', '已写目标') : translateTaskText(t, 'orchestration.readiness.target.missing', '还没写目标')
62
285
  },
286
+ {
287
+ key: 'sequence',
288
+ label: translateTaskText(t, 'orchestration.readiness.sequence.label', '顺序'),
289
+ done: metrics.hasTarget,
290
+ detail: !metrics.hasTarget
291
+ ? translateTaskText(t, 'orchestration.readiness.sequence.missing', '先发送需求 1')
292
+ : (metrics.hasSequentialFollowUps
293
+ ? translateTaskText(t, 'orchestration.readiness.sequence.multiple', '{count} 条需求会按顺序执行:先完成需求 1,再继续需求 2。', { count: metrics.requestCount })
294
+ : translateTaskText(t, 'orchestration.readiness.sequence.single', '当前只有需求 1;继续发送会变成需求 2。'))
295
+ },
63
296
  {
64
297
  key: 'engine',
65
298
  label: metrics.engine === 'workflow' ? 'Workflow' : translateTaskText(t, 'orchestration.readiness.engine.label', '执行策略'),
66
299
  done: workflowReady,
67
300
  detail: metrics.engine === 'workflow'
68
301
  ? (metrics.workflowCount > 0 ? translateTaskText(t, 'orchestration.readiness.workflow.done', `已选 ${metrics.workflowCount} 个 Workflow`, { count: metrics.workflowCount }) : translateTaskText(t, 'orchestration.readiness.workflow.missing', '还没选 Workflow ID'))
69
- : translateTaskText(t, 'orchestration.readiness.engine.codex', '使用 Codex 规划节点')
302
+ : translateTaskText(t, 'orchestration.readiness.engine.openaiChat', '使用 OpenAI Chat-compatible 节点')
70
303
  },
71
304
  {
72
305
  key: 'scope',
@@ -81,7 +314,7 @@ function createTaskDraftChecklist(metrics, t = null) {
81
314
  label: translateTaskText(t, 'orchestration.readiness.preview.label', '预览'),
82
315
  done: previewReady,
83
316
  detail: !metrics.hasPlan
84
- ? translateTaskText(t, 'orchestration.readiness.preview.missing', '还没生成计划')
317
+ ? translateTaskText(t, 'orchestration.readiness.preview.missing', '还没发送 /plan')
85
318
  : (metrics.planIssues.length > 0 ? translateTaskText(t, 'orchestration.readiness.preview.blocked', `有 ${metrics.planIssues.length} 个阻塞项`, { count: metrics.planIssues.length }) : translateTaskText(t, 'orchestration.readiness.preview.ready', `计划可用,${metrics.planNodeCount} 个节点`, { count: metrics.planNodeCount }))
86
319
  }
87
320
  ];
@@ -106,7 +339,9 @@ function createTaskDraftReadiness(metrics, t = null) {
106
339
  return {
107
340
  tone: 'warn',
108
341
  title: translateTaskText(t, 'orchestration.readiness.preview.title', '建议先预览'),
109
- summary: translateTaskText(t, 'orchestration.readiness.preview.summary', '草稿已成形,先生成一次计划,确认节点和依赖再执行。')
342
+ summary: metrics.hasSequentialFollowUps
343
+ ? translateTaskText(t, 'orchestration.readiness.preview.sequenceSummary', '草稿已成形,已锁定 {count} 条顺序需求:先完成需求 1,再继续需求 2。', { count: metrics.requestCount })
344
+ : translateTaskText(t, 'orchestration.readiness.preview.summary', '草稿已成形,发送 /plan 预览方案,确认节点和依赖再执行。')
110
345
  };
111
346
  }
112
347
  if (metrics.planIssues.length > 0) {
@@ -120,7 +355,7 @@ function createTaskDraftReadiness(metrics, t = null) {
120
355
  return {
121
356
  tone: 'warn',
122
357
  title: translateTaskText(t, 'orchestration.readiness.warn.title', '可以执行,但有提醒'),
123
- summary: translateTaskText(t, 'orchestration.readiness.warn.summary', `计划已生成,但还有 ${metrics.planWarnings.length} 条提醒值得先看一眼。`, { count: metrics.planWarnings.length })
358
+ summary: translateTaskText(t, 'orchestration.readiness.warn.summary', `/plan 方案已生成,但还有 ${metrics.planWarnings.length} 条提醒值得先看一眼。`, { count: metrics.planWarnings.length })
124
359
  };
125
360
  }
126
361
  if (metrics.dryRun) {
@@ -134,7 +369,7 @@ function createTaskDraftReadiness(metrics, t = null) {
134
369
  tone: 'success',
135
370
  title: translateTaskText(t, 'orchestration.readiness.ready.title', '可以执行'),
136
371
  summary: metrics.followUpCount > 0
137
- ? translateTaskText(t, 'orchestration.readiness.ready.withFollowUps', `主目标和收尾动作都已具备,可以直接执行或入队。`)
372
+ ? translateTaskText(t, 'orchestration.readiness.ready.withFollowUps', `已锁定 ${metrics.requestCount} 条顺序需求:先完成需求 1,再带上下文继续需求 2。`, { count: metrics.requestCount })
138
373
  : translateTaskText(t, 'orchestration.readiness.ready.summary', '主目标已经够清楚了,可以直接执行或入队。')
139
374
  };
140
375
  }
@@ -191,13 +426,72 @@ export function createMainTabsComputed() {
191
426
  if (detail && Array.isArray(detail.nodes)) return detail.nodes;
192
427
  return Array.isArray(run.nodes) ? run.nodes : [];
193
428
  },
429
+ taskOrchestrationActiveQueue() {
430
+ const queue = this.taskOrchestration && Array.isArray(this.taskOrchestration.queue)
431
+ ? this.taskOrchestration.queue
432
+ : [];
433
+ return queue.filter((item) => {
434
+ const status = String((item && (item.status || item.runStatus)) || '').trim().toLowerCase();
435
+ return status === 'queued' || status === 'running';
436
+ });
437
+ },
438
+ taskOrchestrationEngineLabel() {
439
+ const state = this.taskOrchestration && typeof this.taskOrchestration === 'object'
440
+ ? this.taskOrchestration
441
+ : {};
442
+ return state.selectedEngine === 'workflow' ? 'Workflow' : 'Codex';
443
+ },
444
+ taskOrchestrationWorkbenchVisible() {
445
+ const state = this.taskOrchestration && typeof this.taskOrchestration === 'object'
446
+ ? this.taskOrchestration
447
+ : {};
448
+ const activeQueue = Array.isArray(this.taskOrchestrationActiveQueue)
449
+ ? this.taskOrchestrationActiveQueue
450
+ : [];
451
+ return activeQueue.length > 0
452
+ || !!String(state.selectedRunId || '').trim()
453
+ || !!String(state.selectedRunError || '').trim()
454
+ || (state.workspaceTab === 'runs' && Array.isArray(state.runs) && state.runs.length > 0);
455
+ },
456
+ taskOrchestrationWorkspacePath() {
457
+ const state = this.taskOrchestration && typeof this.taskOrchestration === 'object'
458
+ ? this.taskOrchestration
459
+ : {};
460
+ const detail = state.selectedRunDetail && typeof state.selectedRunDetail === 'object'
461
+ ? state.selectedRunDetail
462
+ : null;
463
+ return normalizeTaskWorkspacePath(state.workspacePath || (detail && detail.cwd) || '');
464
+ },
465
+ taskOrchestrationWorkspaceItems() {
466
+ const items = createTaskWorkspaceItems(this.taskOrchestration);
467
+ const selectedPath = this.taskOrchestrationWorkspacePath;
468
+ return items.map((item) => ({
469
+ ...item,
470
+ active: normalizeTaskWorkspacePath(item.path) === selectedPath || (!selectedPath && !item.path)
471
+ }));
472
+ },
473
+ taskOrchestrationWorkspaceQueue() {
474
+ const queue = this.taskOrchestration && Array.isArray(this.taskOrchestration.queue)
475
+ ? this.taskOrchestration.queue
476
+ : [];
477
+ return queue.filter((item) => taskItemMatchesWorkspace(item, this.taskOrchestrationWorkspacePath));
478
+ },
479
+ taskOrchestrationWorkspaceRuns() {
480
+ const runs = this.taskOrchestration && Array.isArray(this.taskOrchestration.runs)
481
+ ? this.taskOrchestration.runs
482
+ : [];
483
+ return runs.filter((item) => taskItemMatchesWorkspace(item, this.taskOrchestrationWorkspacePath));
484
+ },
485
+ taskOrchestrationWorkspaceSessions() {
486
+ return createTaskWorkspaceSessions(this.taskOrchestration, this.taskOrchestrationWorkspacePath);
487
+ },
194
488
  taskOrchestrationQueueStats() {
195
489
  const queue = this.taskOrchestration && Array.isArray(this.taskOrchestration.queue)
196
490
  ? this.taskOrchestration.queue
197
491
  : [];
198
492
  const stats = { queued: 0, running: 0, failed: 0 };
199
493
  for (const item of queue) {
200
- const status = String(item && item.status || '').trim().toLowerCase();
494
+ const status = String((item && (item.status || item.runStatus)) || '').trim().toLowerCase();
201
495
  if (status === 'queued') stats.queued += 1;
202
496
  else if (status === 'running') stats.running += 1;
203
497
  else if (status === 'failed') stats.failed += 1;
@@ -212,6 +506,9 @@ export function createMainTabsComputed() {
212
506
  },
213
507
  taskOrchestrationDraftReadiness() {
214
508
  return createTaskDraftReadiness(this.taskOrchestrationDraftMetrics, this.t && this.t.bind(this));
509
+ },
510
+ taskOrchestrationConversationMessages() {
511
+ return createTaskConversationMessages(this.taskOrchestration, this.t && this.t.bind(this));
215
512
  }
216
513
  };
217
514
  }
@@ -51,6 +51,190 @@ function formatUsageRangeLabel(range, t) {
51
51
  return '近 7 天';
52
52
  }
53
53
 
54
+ function normalizeWorkspaceText(value) {
55
+ if (value == null) {
56
+ return '';
57
+ }
58
+ return String(value)
59
+ .replace(/<[^>]*>/g, ' ')
60
+ .replace(/&nbsp;/gi, ' ')
61
+ .replace(/&lt;/gi, '<')
62
+ .replace(/&gt;/gi, '>')
63
+ .replace(/&amp;/gi, '&')
64
+ .replace(/\s+/g, ' ')
65
+ .trim();
66
+ }
67
+
68
+ function trimWorkspaceLine(value, maxLength = 160) {
69
+ const text = normalizeWorkspaceText(value);
70
+ if (!text) return '';
71
+ const limit = Number.isFinite(Number(maxLength)) ? Math.max(20, Math.floor(Number(maxLength))) : 160;
72
+ return text.length > limit ? `${text.slice(0, Math.max(0, limit - 1)).trimEnd()}…` : text;
73
+ }
74
+
75
+ function pushUniqueWorkspaceItem(items, value, limit = 6, maxLength = 160) {
76
+ if (!Array.isArray(items) || items.length >= limit) return;
77
+ const text = trimWorkspaceLine(value, maxLength);
78
+ if (!text) return;
79
+ const key = text.toLowerCase();
80
+ if (items.some(item => String(item || '').toLowerCase() === key)) return;
81
+ items.push(text);
82
+ }
83
+
84
+ function extractWorkspaceLines(text) {
85
+ return String(text || '')
86
+ .split(/\r?\n|(?:^|\s)(?:[-*•]|\d+[.)])\s+|[。;;]\s*/g)
87
+ .map(line => normalizeWorkspaceText(line))
88
+ .filter(Boolean);
89
+ }
90
+
91
+ function isWorkspaceCommandLine(line) {
92
+ return /^\$\s+/.test(line)
93
+ || /^(?:npm|pnpm|yarn|bun|node|npx|git|gh|codex|claude|gemini|codebuddy|openclaw|pytest|go test|cargo test|make|docker)\b/i.test(line);
94
+ }
95
+
96
+ function extractInlineWorkspaceCommands(text) {
97
+ const source = normalizeWorkspaceText(text);
98
+ if (!source) return [];
99
+ const matches = source.match(/(?:^|[^A-Za-z0-9_])(?:\$\s*)?(?:(?:npm|pnpm|yarn|bun|node|npx|git|gh|codex|claude|gemini|codebuddy|openclaw|pytest|make|docker)\s+(?:run\s+)?[A-Za-z0-9:_./@-]+(?:\s+&&\s+(?:npm|pnpm|yarn|bun|node|npx|git|gh|codex|claude|gemini|codebuddy|openclaw|pytest|make|docker)\s+(?:run\s+)?[A-Za-z0-9:_./@-]+)?|go\s+test(?:\s+[A-Za-z0-9:_./@-]+)?|cargo\s+test(?:\s+[A-Za-z0-9:_./@-]+)?)(?=$|\s|[,。;,;])/gi) || [];
100
+ return matches.map(match => match.replace(/^[^A-Za-z0-9_$]*\$?\s*/, '').trim());
101
+ }
102
+
103
+ function extractWorkspaceArtifacts(line, target) {
104
+ if (!target || typeof line !== 'string') return;
105
+ const urlMatches = line.match(/https?:\/\/[^\s)\]"'<>,。;、]+/gi) || [];
106
+ for (const url of urlMatches) {
107
+ pushUniqueWorkspaceItem(target.links, url.replace(/[,。;、.,;:!?]+$/g, ''), 5, 140);
108
+ }
109
+ const fileMatches = line.match(/(?:[A-Za-z]:[\\/]|\/)?(?:[A-Za-z0-9_.@+-]+[\\/])*[A-Za-z0-9_.@+-]+\.(?:mjs|js|cjs|ts|tsx|jsx|vue|css|scss|html|json|jsonl|md|yml|yaml|toml|lock|py|go|rs|sh|txt)/g) || [];
110
+ for (const filePath of fileMatches) {
111
+ pushUniqueWorkspaceItem(target.files, filePath, 8, 120);
112
+ }
113
+ }
114
+
115
+ function buildWorkspaceBriefText(summary, labels = {}) {
116
+ if (!summary || !summary.available) return '';
117
+ const label = (key, fallback) => (labels && labels[key]) || fallback;
118
+ const lines = [
119
+ `# ${label('title', 'Session workspace brief')}`,
120
+ '',
121
+ `${label('source', 'Source')}: ${summary.sourceLabel || summary.source || '-'}`,
122
+ `${label('messages', 'Messages')}: ${summary.messageCount}`,
123
+ `${label('path', 'Path')}: ${summary.cwd || '-'}`,
124
+ ''
125
+ ];
126
+ const sections = [
127
+ ['signals', label('signals', 'Signals'), summary.signals],
128
+ ['commands', label('commands', 'Reusable commands'), summary.commands],
129
+ ['files', label('files', 'Files'), summary.files],
130
+ ['links', label('links', 'Links'), summary.links],
131
+ ['risks', label('risks', 'Risks / blockers'), summary.risks],
132
+ ['nextSteps', label('nextSteps', 'Next steps'), summary.nextSteps]
133
+ ];
134
+ for (const [, sectionTitle, items] of sections) {
135
+ if (!Array.isArray(items) || !items.length) continue;
136
+ lines.push(`## ${sectionTitle}`);
137
+ for (const item of items) {
138
+ lines.push(`- ${item}`);
139
+ }
140
+ lines.push('');
141
+ }
142
+ return lines.join('\n').trimEnd();
143
+ }
144
+
145
+ export function buildSessionWorkspaceSummary(session, messages, options = {}) {
146
+ const safeMessages = Array.isArray(messages) ? messages : [];
147
+ const roleCounts = { user: 0, assistant: 0, system: 0, other: 0 };
148
+ const signals = [];
149
+ const commands = [];
150
+ const files = [];
151
+ const links = [];
152
+ const risks = [];
153
+ const nextSteps = [];
154
+ const target = { files, links };
155
+ let firstUser = '';
156
+ let lastUser = '';
157
+ let lastAssistant = '';
158
+ let firstTimestamp = '';
159
+ let lastTimestamp = '';
160
+
161
+ safeMessages.forEach((message) => {
162
+ if (!message || typeof message !== 'object') return;
163
+ const normalizedRole = String(message.normalizedRole || message.role || '').trim().toLowerCase();
164
+ const role = normalizedRole === 'user' || normalizedRole === 'assistant' || normalizedRole === 'system'
165
+ ? normalizedRole
166
+ : 'other';
167
+ roleCounts[role] += 1;
168
+ const timestamp = normalizeWorkspaceText(message.timestamp || '');
169
+ if (timestamp && !firstTimestamp) firstTimestamp = timestamp;
170
+ if (timestamp) lastTimestamp = timestamp;
171
+ const text = normalizeWorkspaceText(message.text || message.content || '');
172
+ if (!text) return;
173
+ if (role === 'user') {
174
+ if (!firstUser) firstUser = text;
175
+ lastUser = text;
176
+ } else if (role === 'assistant') {
177
+ lastAssistant = text;
178
+ }
179
+ const lines = extractWorkspaceLines(message.text || message.content || text);
180
+ for (const command of extractInlineWorkspaceCommands(message.text || message.content || text)) {
181
+ pushUniqueWorkspaceItem(commands, command, 6, 140);
182
+ }
183
+ for (const line of lines) {
184
+ extractWorkspaceArtifacts(line, target);
185
+ if (isWorkspaceCommandLine(line)) {
186
+ pushUniqueWorkspaceItem(commands, line.replace(/^\$\s+/, ''), 6, 140);
187
+ }
188
+ if (/(?:blocker|blocked|risk|failed|failure|error|timeout|regression|冲突|失败|错误|阻塞|风险|不可合|超时|回归)/i.test(line)) {
189
+ pushUniqueWorkspaceItem(risks, line, 5, 150);
190
+ }
191
+ if (/(?:todo|next|follow[- ]?up|remaining|后续|下一步|待办|继续|剩余)/i.test(line)) {
192
+ pushUniqueWorkspaceItem(nextSteps, line, 5, 150);
193
+ }
194
+ }
195
+ });
196
+
197
+ pushUniqueWorkspaceItem(signals, firstUser, 3, 150);
198
+ if (lastUser && lastUser !== firstUser) {
199
+ pushUniqueWorkspaceItem(signals, lastUser, 3, 150);
200
+ }
201
+ pushUniqueWorkspaceItem(signals, lastAssistant, 3, 150);
202
+
203
+ const source = session && typeof session.source === 'string' ? session.source : '';
204
+ const sourceLabel = session && typeof session.sourceLabel === 'string' ? session.sourceLabel : source;
205
+ const cwd = session && typeof session.cwd === 'string' ? session.cwd : '';
206
+ const messageCount = safeMessages.length;
207
+ const available = messageCount > 0;
208
+ const metrics = [
209
+ { key: 'messages', value: String(messageCount), label: options.messagesLabel || 'Messages' },
210
+ { key: 'user', value: String(roleCounts.user), label: options.userLabel || 'User' },
211
+ { key: 'assistant', value: String(roleCounts.assistant), label: options.assistantLabel || 'Assistant' },
212
+ { key: 'commands', value: String(commands.length), label: options.commandsLabel || 'Commands' },
213
+ { key: 'artifacts', value: String(files.length + links.length), label: options.artifactsLabel || 'Artifacts' },
214
+ { key: 'risks', value: String(risks.length + nextSteps.length), label: options.risksLabel || 'Risks' }
215
+ ];
216
+
217
+ const summary = {
218
+ available,
219
+ source,
220
+ sourceLabel,
221
+ cwd,
222
+ firstTimestamp,
223
+ lastTimestamp,
224
+ messageCount,
225
+ roleCounts,
226
+ metrics,
227
+ signals,
228
+ commands,
229
+ files,
230
+ links,
231
+ risks,
232
+ nextSteps
233
+ };
234
+ summary.briefText = buildWorkspaceBriefText(summary, options.briefLabels || {});
235
+ return summary;
236
+ }
237
+
54
238
  function formatUsageDuration(value, options = {}) {
55
239
  const normalizedLang = typeof options.lang === 'string' ? options.lang.trim().toLowerCase() : '';
56
240
  const isEn = normalizedLang === 'en';
@@ -267,6 +451,32 @@ export function createSessionComputed() {
267
451
  if (index < 0) return 0;
268
452
  return Math.round(((index + 1) / this.sessionTimelineNodes.length) * 100);
269
453
  },
454
+ activeSessionWorkspaceSummary() {
455
+ if (this.mainTab !== 'sessions' || !this.activeSession) {
456
+ return buildSessionWorkspaceSummary(null, []);
457
+ }
458
+ const t = typeof this.t === 'function' ? this.t : null;
459
+ return buildSessionWorkspaceSummary(this.activeSession, this.activeSessionMessages, {
460
+ messagesLabel: t ? t('sessions.workspace.metric.messages') : 'Messages',
461
+ userLabel: t ? t('sessions.workspace.metric.user') : 'User',
462
+ assistantLabel: t ? t('sessions.workspace.metric.assistant') : 'Assistant',
463
+ commandsLabel: t ? t('sessions.workspace.metric.commands') : 'Commands',
464
+ artifactsLabel: t ? t('sessions.workspace.metric.artifacts') : 'Artifacts',
465
+ risksLabel: t ? t('sessions.workspace.metric.risks') : 'Risks',
466
+ briefLabels: {
467
+ title: t ? t('sessions.workspace.copy.title') : 'Session workspace brief',
468
+ source: t ? t('sessions.workspace.copy.source') : 'Source',
469
+ messages: t ? t('sessions.workspace.metric.messages') : 'Messages',
470
+ path: t ? t('sessions.workspace.copy.path') : 'Path',
471
+ signals: t ? t('sessions.workspace.signals') : 'Signals',
472
+ commands: t ? t('sessions.workspace.commands') : 'Reusable commands',
473
+ files: t ? t('sessions.workspace.files') : 'Files',
474
+ links: t ? t('sessions.workspace.links') : 'Links',
475
+ risks: t ? t('sessions.workspace.risks') : 'Risks / blockers',
476
+ nextSteps: t ? t('sessions.workspace.nextSteps') : 'Next steps'
477
+ }
478
+ });
479
+ },
270
480
  sessionQueryPlaceholder() {
271
481
  if (this.isSessionQueryEnabled) {
272
482
  return typeof this.t === 'function'
@@ -354,6 +354,9 @@ export function createAgentsMethods(options = {}) {
354
354
  return !!this.agentsSaving || this.hasAgentsContentChanged() || this.agentsDiffVisible;
355
355
  },
356
356
  handleBeforeUnload(event) {
357
+ if (typeof this.flushWebUiPreferences === 'function') {
358
+ this.flushWebUiPreferences();
359
+ }
357
360
  if (!this.hasPendingAgentsDraft()) {
358
361
  return;
359
362
  }