codexmate 0.0.22 → 0.0.23

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/README.md CHANGED
@@ -31,6 +31,7 @@ Codex Mate is a local-first CLI + Web UI for unified management of:
31
31
  - Local skills market for Codex / Claude Code (target switching, local skills management, cross-app import, ZIP distribution)
32
32
  - Local Codex/Claude sessions (list/filter/export/delete) with Usage analytics overview
33
33
  - Plugins (Prompt templates): reusable templates with variables and one-click copy
34
+ - Task orchestration: plan/queue/run/review local tasks
34
35
 
35
36
  It works on local files directly and does not require cloud hosting. The skills market is also local-first: it operates on local directories and does not depend on a remote marketplace.
36
37
 
package/README.zh.md CHANGED
@@ -31,7 +31,7 @@ Codex Mate 提供一套本地优先的 CLI + Web UI,用于统一管理:
31
31
  - Codex / Claude Code Skills 市场(安装目标切换、本地 skills 管理、跨应用导入、ZIP 分发)
32
32
  - Codex / Claude 本地会话浏览、筛选、导出、删除与 Usage 统计概览
33
33
  - 插件(提示词模板):模板复用、变量填写、一键复制
34
- - 任务编排(规划中,未开放)
34
+ - 任务编排:规划 / 排队 / 执行 / 回看
35
35
 
36
36
  项目不依赖云端托管,配置写入你的本地文件,便于审计和回滚。Skills 市场同样坚持本地优先,只操作本地目录,不依赖远程在线市场。
37
37
 
@@ -75,8 +75,9 @@ Codex Mate 提供一套本地优先的 CLI + Web UI,用于统一管理:
75
75
  - 提示词模板:本地保存/编辑/复用(支持变量)
76
76
  - 编写 → 填参 → 一键复制的工作流(模板数据保存在浏览器存储)
77
77
 
78
- **任务编排(规划中,未开放)**
79
- - 当前版本暂未开放
78
+ **任务编排**
79
+ - DAG 节点拆分与波次并发
80
+ - 支持计划预览、执行、队列与运行详情
80
81
 
81
82
  **工程能力**
82
83
  - MCP stdio 能力(tools/resources/prompts)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codexmate",
3
- "version": "0.0.22",
3
+ "version": "0.0.23",
4
4
  "description": "Codex/Claude Code/OpenClaw 配置、会话与任务编排 CLI + Web 工具",
5
5
  "main": "cli.js",
6
6
  "bin": {
package/web-ui/app.js CHANGED
@@ -69,6 +69,7 @@ document.addEventListener('DOMContentLoaded', () => {
69
69
  // Plugins
70
70
  pluginsActiveId: 'prompt-templates',
71
71
  pluginsLoading: false,
72
+ pluginsError: '',
72
73
  promptTemplatesListRaw: [],
73
74
  promptTemplatesLoadedOnce: false,
74
75
  promptTemplatesKeyword: '',
@@ -161,6 +162,7 @@ document.addEventListener('DOMContentLoaded', () => {
161
162
  sessionsUsageTimeRange: '7d',
162
163
  sessionsUsageList: [],
163
164
  sessionsUsageLoadedOnce: false,
165
+ sessionsUsageLoadedLimit: 0,
164
166
  sessionsUsageLoading: false,
165
167
  sessionsUsageError: '',
166
168
  sessionsList: [],
@@ -382,7 +384,7 @@ document.addEventListener('DOMContentLoaded', () => {
382
384
  codexImportLoading: false,
383
385
  codexAuthProfiles: [],
384
386
  forceCompactLayout: false,
385
- taskOrchestrationTabEnabled: false,
387
+ taskOrchestrationTabEnabled: true,
386
388
  taskOrchestration: {
387
389
  loading: false,
388
390
  planning: false,
@@ -396,11 +398,11 @@ document.addEventListener('DOMContentLoaded', () => {
396
398
  followUpsText: '',
397
399
  workflowIdsText: '',
398
400
  selectedEngine: 'codex',
399
- allowWrite: false,
400
- dryRun: false,
401
+ runMode: 'write',
401
402
  concurrency: 2,
402
403
  autoFixRounds: 1,
403
404
  plan: null,
405
+ planFingerprint: '',
404
406
  planIssues: [],
405
407
  planWarnings: [],
406
408
  overviewWarnings: [],
@@ -430,6 +432,55 @@ document.addEventListener('DOMContentLoaded', () => {
430
432
  this.agentsModalTitle = this.t('modal.agents.title');
431
433
  this.agentsModalHint = this.t('modal.agents.hint');
432
434
  }
435
+ {
436
+ const NAV_STATE_STORAGE_KEY = 'codexmateNavState.v1';
437
+ const mainTabSet = new Set(['config', 'sessions', 'usage', 'orchestration', 'market', 'plugins', 'docs', 'settings']);
438
+ let restored = null;
439
+ try {
440
+ const raw = localStorage.getItem(NAV_STATE_STORAGE_KEY) || '';
441
+ restored = raw ? JSON.parse(raw) : null;
442
+ } catch (_) {
443
+ restored = null;
444
+ }
445
+ const nextMainTab = restored && typeof restored.mainTab === 'string'
446
+ ? restored.mainTab.trim().toLowerCase()
447
+ : '';
448
+ const nextConfigMode = restored && typeof restored.configMode === 'string'
449
+ ? restored.configMode.trim().toLowerCase()
450
+ : '';
451
+ let urlMainTab = '';
452
+ try {
453
+ const url = new URL(window.location.href);
454
+ if (url.pathname !== '/session') {
455
+ urlMainTab = String(url.searchParams.get('tab') || '').trim().toLowerCase();
456
+ }
457
+ } catch (_) {
458
+ urlMainTab = '';
459
+ }
460
+ const resolvedMainTab = urlMainTab && mainTabSet.has(urlMainTab)
461
+ ? urlMainTab
462
+ : nextMainTab;
463
+ if (nextConfigMode && typeof this.switchConfigMode === 'function') {
464
+ this.__navStateRestoring = true;
465
+ try {
466
+ if (nextConfigMode === 'codex' || nextConfigMode === 'claude' || nextConfigMode === 'openclaw') {
467
+ this.configMode = nextConfigMode;
468
+ }
469
+ if (resolvedMainTab && mainTabSet.has(resolvedMainTab) && resolvedMainTab !== this.mainTab) {
470
+ this.switchMainTab(resolvedMainTab);
471
+ }
472
+ } finally {
473
+ this.__navStateRestoring = false;
474
+ }
475
+ } else if (resolvedMainTab && mainTabSet.has(resolvedMainTab) && resolvedMainTab !== this.mainTab) {
476
+ this.__navStateRestoring = true;
477
+ try {
478
+ this.switchMainTab(resolvedMainTab);
479
+ } finally {
480
+ this.__navStateRestoring = false;
481
+ }
482
+ }
483
+ }
433
484
  this.initSessionStandalone();
434
485
  this.updateCompactLayoutMode();
435
486
  if (!this.taskOrchestrationTabEnabled && this.mainTab === 'orchestration') {
@@ -13,12 +13,16 @@ function readTaskOrchestrationDraftMetrics(taskOrchestration) {
13
13
  const workflowIds = normalizeTaskDraftLines(state.workflowIdsText);
14
14
  const followUps = normalizeTaskDraftLines(state.followUpsText);
15
15
  const engine = String(state.selectedEngine || 'codex').trim().toLowerCase() === 'workflow' ? 'workflow' : 'codex';
16
+ const runMode = String(state.runMode || 'write').trim().toLowerCase();
17
+ const allowWrite = runMode === 'write';
18
+ const dryRun = runMode === 'dry-run';
16
19
  const plan = state.plan && typeof state.plan === 'object' ? state.plan : null;
17
20
  const planNodes = Array.isArray(plan && plan.nodes) ? plan.nodes : [];
18
21
  const planIssues = Array.isArray(state.planIssues) ? state.planIssues : [];
19
22
  const planWarnings = Array.isArray(state.planWarnings) ? state.planWarnings : [];
20
23
  return {
21
24
  engine,
25
+ runMode,
22
26
  title,
23
27
  target,
24
28
  notes,
@@ -34,8 +38,8 @@ function readTaskOrchestrationDraftMetrics(taskOrchestration) {
34
38
  workflowCount: workflowIds.length,
35
39
  followUpCount: followUps.length,
36
40
  planNodeCount: planNodes.length,
37
- allowWrite: state.allowWrite === true,
38
- dryRun: state.dryRun === true
41
+ allowWrite,
42
+ dryRun
39
43
  };
40
44
  }
41
45
 
@@ -156,6 +156,22 @@ export function createInstallMethods() {
156
156
 
157
157
  setInstallRegistryPreset(presetName) {
158
158
  this.installRegistryPreset = this.normalizeInstallRegistryPreset(presetName);
159
+ },
160
+
161
+ getInstallStatusTarget(targetId) {
162
+ const key = typeof targetId === 'string' ? targetId.trim() : '';
163
+ if (!key) return null;
164
+ const list = Array.isArray(this.installStatusTargets) ? this.installStatusTargets : [];
165
+ return list.find((item) => item && item.id === key) || null;
166
+ },
167
+
168
+ isInstallTargetInstalled(targetId) {
169
+ const target = this.getInstallStatusTarget(targetId);
170
+ return !!(target && target.installed === true);
171
+ },
172
+
173
+ shouldShowCliInstallPlaceholder(targetId) {
174
+ return Array.isArray(this.installStatusTargets) && !this.isInstallTargetInstalled(targetId);
159
175
  }
160
176
  };
161
177
  }
@@ -4,6 +4,46 @@ export function createNavigationMethods(options = {}) {
4
4
  switchMainTabHelper,
5
5
  loadMoreSessionMessagesHelper
6
6
  } = options;
7
+ const NAV_STATE_STORAGE_KEY = 'codexmateNavState.v1';
8
+ const MAIN_TAB_SET = new Set([
9
+ 'config',
10
+ 'sessions',
11
+ 'usage',
12
+ 'orchestration',
13
+ 'market',
14
+ 'plugins',
15
+ 'docs',
16
+ 'settings'
17
+ ]);
18
+ const readNavState = () => {
19
+ if (typeof localStorage === 'undefined') return null;
20
+ let raw = '';
21
+ try {
22
+ raw = localStorage.getItem(NAV_STATE_STORAGE_KEY) || '';
23
+ } catch (_) {
24
+ raw = '';
25
+ }
26
+ if (!raw) return null;
27
+ try {
28
+ const parsed = JSON.parse(raw);
29
+ return parsed && typeof parsed === 'object' ? parsed : null;
30
+ } catch (_) {
31
+ return null;
32
+ }
33
+ };
34
+ const persistNavState = (vm) => {
35
+ if (!vm || vm.__navStateRestoring) return;
36
+ if (typeof localStorage === 'undefined') return;
37
+ const mainTab = typeof vm.mainTab === 'string' ? vm.mainTab.trim().toLowerCase() : '';
38
+ const configMode = typeof vm.configMode === 'string' ? vm.configMode.trim().toLowerCase() : '';
39
+ const snapshot = {
40
+ mainTab: MAIN_TAB_SET.has(mainTab) ? mainTab : 'docs',
41
+ configMode: configModeSet && configModeSet.has(configMode) ? configMode : 'codex'
42
+ };
43
+ try {
44
+ localStorage.setItem(NAV_STATE_STORAGE_KEY, JSON.stringify(snapshot));
45
+ } catch (_) {}
46
+ };
7
47
 
8
48
  return {
9
49
  switchConfigMode(mode) {
@@ -34,6 +74,7 @@ export function createNavigationMethods(options = {}) {
34
74
  this.scheduleAfterFrame(() => {
35
75
  this.clearMainTabSwitchIntent('config');
36
76
  });
77
+ persistNavState(this);
37
78
  return;
38
79
  }
39
80
  this.switchMainTab('config');
@@ -324,6 +365,7 @@ export function createNavigationMethods(options = {}) {
324
365
  switchState.ticket += 1;
325
366
  switchState.pendingTarget = '';
326
367
  const result = switchMainTabHelper.call(this, targetTab);
368
+ persistNavState(this);
327
369
  this.scheduleAfterFrame(() => {
328
370
  this.clearMainTabSwitchIntent(normalizedTab);
329
371
  });
@@ -338,6 +380,7 @@ export function createNavigationMethods(options = {}) {
338
380
  const pendingTarget = liveState.pendingTarget || targetTab;
339
381
  liveState.pendingTarget = '';
340
382
  switchMainTabHelper.call(this, pendingTarget);
383
+ persistNavState(this);
341
384
  this.clearMainTabSwitchIntent(normalizedTab);
342
385
  });
343
386
  },
@@ -4,6 +4,14 @@ import {
4
4
  normalizeSessionMessageRole,
5
5
  normalizeSessionPathFilter
6
6
  } from '../logic.mjs';
7
+ import {
8
+ applySessionsFilterUrlState,
9
+ buildSessionsFilterShareUrl,
10
+ normalizeSessionRoleFilter,
11
+ normalizeSessionTimePreset,
12
+ readSessionsFilterUrlState,
13
+ syncSessionsFilterUrl
14
+ } from './sessions-filters-url.mjs';
7
15
 
8
16
  function isSessionLoadNativeDialogEnabled(vm) {
9
17
  if (vm && typeof vm.isSessionLoadNativeDialogEnabled === 'function' && vm.isSessionLoadNativeDialogEnabled !== isSessionLoadNativeDialogEnabled) {
@@ -241,11 +249,22 @@ export function createSessionBrowserMethods(options = {}) {
241
249
  },
242
250
 
243
251
  restoreSessionFilterCache() {
252
+ const urlState = readSessionsFilterUrlState();
253
+ if (urlState) {
254
+ applySessionsFilterUrlState(this, urlState);
255
+ return;
256
+ }
244
257
  const sourceCache = localStorage.getItem('codexmateSessionFilterSource');
245
258
  const pathCache = localStorage.getItem('codexmateSessionPathFilter');
246
259
  const cached = buildSessionFilterCacheState(sourceCache, pathCache);
247
260
  this.sessionFilterSource = cached.source;
248
261
  this.sessionPathFilter = cached.pathFilter;
262
+ const queryCache = localStorage.getItem('codexmateSessionQuery');
263
+ const roleCache = localStorage.getItem('codexmateSessionRoleFilter');
264
+ const timeCache = localStorage.getItem('codexmateSessionTimePreset');
265
+ this.sessionQuery = typeof queryCache === 'string' ? queryCache : '';
266
+ this.sessionRoleFilter = normalizeSessionRoleFilter(roleCache);
267
+ this.sessionTimePreset = normalizeSessionTimePreset(timeCache);
249
268
  this.refreshSessionPathOptions(this.sessionFilterSource);
250
269
  },
251
270
 
@@ -257,6 +276,13 @@ export function createSessionBrowserMethods(options = {}) {
257
276
  } else {
258
277
  localStorage.removeItem('codexmateSessionPathFilter');
259
278
  }
279
+ if (this.sessionQuery && isSessionQueryEnabled(this.sessionFilterSource)) {
280
+ localStorage.setItem('codexmateSessionQuery', this.sessionQuery);
281
+ } else {
282
+ localStorage.removeItem('codexmateSessionQuery');
283
+ }
284
+ localStorage.setItem('codexmateSessionRoleFilter', normalizeSessionRoleFilter(this.sessionRoleFilter));
285
+ localStorage.setItem('codexmateSessionTimePreset', normalizeSessionTimePreset(this.sessionTimePreset));
260
286
  },
261
287
 
262
288
  normalizeSessionPinnedMap(raw) {
@@ -389,15 +415,19 @@ export function createSessionBrowserMethods(options = {}) {
389
415
  async onSessionSourceChange() {
390
416
  this.refreshSessionPathOptions(this.sessionFilterSource);
391
417
  this.persistSessionFilterCache();
418
+ syncSessionsFilterUrl(this);
392
419
  await this.loadSessions();
393
420
  },
394
421
 
395
422
  async onSessionPathFilterChange() {
396
423
  this.persistSessionFilterCache();
424
+ syncSessionsFilterUrl(this);
397
425
  await this.loadSessions();
398
426
  },
399
427
 
400
428
  async onSessionFilterChange() {
429
+ this.persistSessionFilterCache();
430
+ syncSessionsFilterUrl(this);
401
431
  await this.loadSessions();
402
432
  },
403
433
 
@@ -408,9 +438,24 @@ export function createSessionBrowserMethods(options = {}) {
408
438
  this.sessionRoleFilter = 'all';
409
439
  this.sessionTimePreset = 'all';
410
440
  this.persistSessionFilterCache();
441
+ syncSessionsFilterUrl(this);
411
442
  await this.onSessionSourceChange();
412
443
  },
413
444
 
445
+ copySessionsFilterShareUrl() {
446
+ const url = buildSessionsFilterShareUrl(this);
447
+ if (!url) {
448
+ this.showMessage('无法生成链接', 'error');
449
+ return;
450
+ }
451
+ const ok = typeof this.fallbackCopyText === 'function' ? this.fallbackCopyText(url) : false;
452
+ if (ok) {
453
+ this.showMessage(typeof this.t === 'function' ? this.t('toast.copy.ok') : 'Copied', 'success');
454
+ return;
455
+ }
456
+ this.showMessage(typeof this.t === 'function' ? this.t('toast.copy.fail') : 'Copy failed', 'error');
457
+ },
458
+
414
459
  normalizeSessionMessage(message) {
415
460
  const fallback = {
416
461
  role: 'assistant',
@@ -464,21 +509,40 @@ export function createSessionBrowserMethods(options = {}) {
464
509
 
465
510
  invalidateSessionsUsageData(options = {}) {
466
511
  this.sessionsUsageLoadedOnce = false;
512
+ this.sessionsUsageLoadedLimit = 0;
467
513
  this.sessionsUsageError = '';
468
514
  if (options.preserveList !== true) {
469
515
  this.sessionsUsageList = [];
470
516
  }
471
517
  },
472
518
 
519
+ setSessionsUsageTimeRange(nextRange) {
520
+ const normalized = typeof nextRange === 'string' ? nextRange.trim().toLowerCase() : '';
521
+ const range = normalized === 'all' ? 'all' : (normalized === '30d' ? '30d' : '7d');
522
+ this.sessionsUsageTimeRange = range;
523
+ void this.loadSessionsUsage({ range });
524
+ },
525
+
473
526
  async loadSessionsUsage(options = {}) {
474
527
  if (this.sessionsUsageLoading) return;
528
+ const normalizedRange = typeof options.range === 'string'
529
+ ? options.range.trim().toLowerCase()
530
+ : (typeof this.sessionsUsageTimeRange === 'string' ? this.sessionsUsageTimeRange.trim().toLowerCase() : '');
531
+ const range = normalizedRange === 'all' ? 'all' : (normalizedRange === '30d' ? '30d' : '7d');
532
+ const defaultLimit = range === 'all' ? 2000 : (range === '30d' ? 1200 : 600);
533
+ const rawLimit = Number(options.limit);
534
+ const limit = Number.isFinite(rawLimit) ? Math.max(1, Math.min(rawLimit, 2000)) : defaultLimit;
535
+ const loadedLimit = Number(this.sessionsUsageLoadedLimit || 0);
536
+ if (this.sessionsUsageLoadedOnce && !options.forceRefresh && loadedLimit >= limit) {
537
+ return;
538
+ }
475
539
  this.sessionsUsageLoading = true;
476
540
  this.sessionsUsageError = '';
477
541
  let loadSucceeded = false;
478
542
  try {
479
543
  const res = await api('list-sessions-usage', {
480
544
  source: 'all',
481
- limit: 2000,
545
+ limit,
482
546
  forceRefresh: !!options.forceRefresh
483
547
  });
484
548
  if (res.error) {
@@ -495,6 +559,7 @@ export function createSessionBrowserMethods(options = {}) {
495
559
  this.sessionsUsageLoading = false;
496
560
  if (loadSucceeded) {
497
561
  this.sessionsUsageLoadedOnce = true;
562
+ this.sessionsUsageLoadedLimit = limit;
498
563
  }
499
564
  }
500
565
  },
@@ -34,6 +34,18 @@ export function createStartupClaudeMethods(options = {}) {
34
34
  startupOk = true;
35
35
  this.currentProvider = statusRes.provider;
36
36
  this.currentModel = statusRes.model;
37
+ try {
38
+ const installRes = await api('install-status');
39
+ if (installRes && !installRes.error) {
40
+ const targets = Array.isArray(installRes.targets) ? installRes.targets : null;
41
+ if (targets) {
42
+ this.installStatusTargets = targets;
43
+ }
44
+ if (typeof installRes.packageManager === 'string' && typeof this.normalizeInstallPackageManager === 'function') {
45
+ this.installPackageManager = this.normalizeInstallPackageManager(installRes.packageManager);
46
+ }
47
+ }
48
+ } catch (_) {}
37
49
  {
38
50
  const tier = typeof statusRes.serviceTier === 'string'
39
51
  ? statusRes.serviceTier.trim().toLowerCase()
@@ -12,11 +12,11 @@ function createDefaultTaskOrchestrationState() {
12
12
  followUpsText: '',
13
13
  workflowIdsText: '',
14
14
  selectedEngine: 'codex',
15
- allowWrite: false,
16
- dryRun: false,
15
+ runMode: 'write',
17
16
  concurrency: 2,
18
17
  autoFixRounds: 1,
19
18
  plan: null,
19
+ planFingerprint: '',
20
20
  planIssues: [],
21
21
  planWarnings: [],
22
22
  overviewWarnings: [],
@@ -62,6 +62,22 @@ function isActiveStatus(status) {
62
62
  return normalized === 'running' || normalized === 'queued';
63
63
  }
64
64
 
65
+ function normalizeTaskRunMode(value) {
66
+ const normalized = String(value || '').trim().toLowerCase();
67
+ if (normalized === 'read') return 'read';
68
+ if (normalized === 'dry-run' || normalized === 'dryrun' || normalized === 'plan') return 'dry-run';
69
+ return 'write';
70
+ }
71
+
72
+ function buildRunModeFlags(runMode) {
73
+ const normalized = normalizeTaskRunMode(runMode);
74
+ return {
75
+ runMode: normalized,
76
+ allowWrite: normalized === 'write',
77
+ dryRun: normalized === 'dry-run'
78
+ };
79
+ }
80
+
65
81
  export function createTaskOrchestrationMethods(options = {}) {
66
82
  const { api } = options;
67
83
 
@@ -75,6 +91,7 @@ export function createTaskOrchestrationMethods(options = {}) {
75
91
  current[key] = value;
76
92
  }
77
93
  }
94
+ current.runMode = normalizeTaskRunMode(current.runMode);
78
95
  return current;
79
96
  }
80
97
  this.taskOrchestration = createDefaultTaskOrchestrationState();
@@ -83,6 +100,7 @@ export function createTaskOrchestrationMethods(options = {}) {
83
100
 
84
101
  buildTaskOrchestrationRequest() {
85
102
  const state = this.ensureTaskOrchestrationState();
103
+ const flags = buildRunModeFlags(state.runMode);
86
104
  return {
87
105
  title: String(state.title || '').trim(),
88
106
  target: String(state.target || '').trim(),
@@ -90,13 +108,29 @@ export function createTaskOrchestrationMethods(options = {}) {
90
108
  followUps: normalizeLines(state.followUpsText),
91
109
  workflowIds: normalizeLines(state.workflowIdsText),
92
110
  engine: String(state.selectedEngine || 'codex').trim().toLowerCase() === 'workflow' ? 'workflow' : 'codex',
93
- allowWrite: state.allowWrite === true,
94
- dryRun: state.dryRun === true,
111
+ allowWrite: flags.allowWrite,
112
+ dryRun: flags.dryRun,
95
113
  concurrency: normalizePositiveInteger(state.concurrency, 2, 1, 8),
96
114
  autoFixRounds: normalizePositiveInteger(state.autoFixRounds, 1, 0, 5)
97
115
  };
98
116
  },
99
117
 
118
+ buildTaskOrchestrationFingerprint() {
119
+ const req = this.buildTaskOrchestrationRequest();
120
+ return JSON.stringify({
121
+ title: req.title,
122
+ target: req.target,
123
+ notes: req.notes,
124
+ followUps: req.followUps,
125
+ workflowIds: req.workflowIds,
126
+ engine: req.engine,
127
+ allowWrite: req.allowWrite,
128
+ dryRun: req.dryRun,
129
+ concurrency: req.concurrency,
130
+ autoFixRounds: req.autoFixRounds
131
+ });
132
+ },
133
+
100
134
  taskRunStatusTone(status) {
101
135
  return normalizeTaskStatusTone(status);
102
136
  },
@@ -190,6 +224,7 @@ export function createTaskOrchestrationMethods(options = {}) {
190
224
  state.plan = res && res.plan ? res.plan : null;
191
225
  state.planIssues = Array.isArray(res && res.issues) ? res.issues : [];
192
226
  state.planWarnings = Array.isArray(res && res.warnings) ? res.warnings : [];
227
+ state.planFingerprint = state.plan ? this.buildTaskOrchestrationFingerprint() : '';
193
228
  if (res && res.error) {
194
229
  if (!options.silent) {
195
230
  this.showMessage(res.error, 'error');
@@ -246,7 +281,7 @@ export function createTaskOrchestrationMethods(options = {}) {
246
281
  }
247
282
  },
248
283
 
249
- async addTaskOrchestrationToQueue() {
284
+ async addTaskOrchestrationToQueue(options = {}) {
250
285
  const state = this.ensureTaskOrchestrationState();
251
286
  if (state.queueAdding) {
252
287
  return null;
@@ -255,21 +290,72 @@ export function createTaskOrchestrationMethods(options = {}) {
255
290
  try {
256
291
  const res = await api('task-queue-add', this.buildTaskOrchestrationRequest());
257
292
  if (res && res.error) {
258
- this.showMessage(res.error, 'error');
293
+ if (!options.silent) {
294
+ this.showMessage(res.error, 'error');
295
+ }
259
296
  return res;
260
297
  }
261
- await this.loadTaskOrchestrationOverview({ silent: true, includeDetail: false });
262
- this.showMessage(`已加入队列: ${res && res.task ? res.task.taskId : ''}`.trim(), 'success');
298
+ if (!options.deferRefresh) {
299
+ await this.loadTaskOrchestrationOverview({ silent: true, includeDetail: false });
300
+ }
301
+ if (!options.silent) {
302
+ this.showMessage(`已加入队列: ${res && res.task ? res.task.taskId : ''}`.trim(), 'success');
303
+ }
263
304
  return res;
264
305
  } catch (error) {
265
306
  const message = error && error.message ? error.message : '加入队列失败';
266
- this.showMessage(message, 'error');
307
+ if (!options.silent) {
308
+ this.showMessage(message, 'error');
309
+ }
267
310
  return { error: message };
268
311
  } finally {
269
312
  state.queueAdding = false;
270
313
  }
271
314
  },
272
315
 
316
+ async planAndRunTaskOrchestration() {
317
+ const state = this.ensureTaskOrchestrationState();
318
+ if (state.running || state.planning) {
319
+ return null;
320
+ }
321
+ if (!String(state.target || '').trim()) {
322
+ return null;
323
+ }
324
+ if (buildRunModeFlags(state.runMode).dryRun) {
325
+ return this.previewTaskPlan({ silent: false });
326
+ }
327
+ const fingerprint = this.buildTaskOrchestrationFingerprint();
328
+ const shouldPreview = !state.plan || state.planFingerprint !== fingerprint;
329
+ if (shouldPreview) {
330
+ const preview = await this.previewTaskPlan({ silent: true });
331
+ if (preview && preview.error) {
332
+ this.showMessage(preview.error, 'error');
333
+ return preview;
334
+ }
335
+ }
336
+ if (state.planIssues && state.planIssues.length) {
337
+ this.showMessage('计划存在问题,请先修复再执行', 'error');
338
+ return { error: 'Plan has blocking issues' };
339
+ }
340
+ return this.runTaskOrchestration();
341
+ },
342
+
343
+ async queueTaskOrchestrationAndStart() {
344
+ const state = this.ensureTaskOrchestrationState();
345
+ if (state.queueAdding || state.queueStarting || state.planning || state.running) {
346
+ return null;
347
+ }
348
+ if (!String(state.target || '').trim()) {
349
+ return null;
350
+ }
351
+ const queued = await this.addTaskOrchestrationToQueue({ silent: true, deferRefresh: true });
352
+ if (queued && queued.error) {
353
+ this.showMessage(queued.error, 'error');
354
+ return queued;
355
+ }
356
+ return this.startTaskQueueRunner();
357
+ },
358
+
273
359
  async startTaskQueueRunner() {
274
360
  const state = this.ensureTaskOrchestrationState();
275
361
  if (state.queueStarting) {
@@ -457,8 +543,7 @@ export function createTaskOrchestrationMethods(options = {}) {
457
543
  state.followUpsText = '';
458
544
  state.workflowIdsText = '';
459
545
  state.selectedEngine = 'codex';
460
- state.allowWrite = false;
461
- state.dryRun = false;
546
+ state.runMode = 'write';
462
547
  state.concurrency = 2;
463
548
  state.autoFixRounds = 1;
464
549
  state.plan = null;