codexmate 0.0.20 → 0.0.22

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 (96) hide show
  1. package/README.md +289 -152
  2. package/README.zh.md +321 -0
  3. package/cli/agents-files.js +224 -0
  4. package/cli/archive-helpers.js +446 -0
  5. package/cli/auth-profiles.js +359 -0
  6. package/cli/builtin-proxy.js +1044 -0
  7. package/cli/claude-proxy.js +998 -0
  8. package/cli/config-bootstrap.js +384 -0
  9. package/cli/openai-bridge.js +950 -0
  10. package/cli/openclaw-config.js +629 -0
  11. package/cli/session-usage.concurrent.js +28 -0
  12. package/cli/session-usage.js +112 -0
  13. package/cli/session-usage.models.js +176 -0
  14. package/cli/skills.js +1141 -0
  15. package/cli/zip-commands.js +510 -0
  16. package/cli.js +9408 -9719
  17. package/lib/cli-models-utils.js +109 -1
  18. package/lib/cli-path-utils.js +69 -0
  19. package/lib/cli-sessions.js +386 -0
  20. package/lib/download-artifacts.js +77 -0
  21. package/lib/task-orchestrator.js +869 -0
  22. package/package.json +14 -10
  23. package/res/logo.png +0 -0
  24. package/res/vue.global.prod.js +13 -0
  25. package/web-ui/app.js +193 -15
  26. package/web-ui/index.html +5 -1
  27. package/web-ui/logic.agents-diff.mjs +1 -1
  28. package/web-ui/logic.claude.mjs +60 -0
  29. package/web-ui/logic.runtime.mjs +11 -7
  30. package/web-ui/logic.sessions.mjs +372 -21
  31. package/web-ui/modules/api.mjs +22 -1
  32. package/web-ui/modules/app.computed.dashboard.mjs +23 -10
  33. package/web-ui/modules/app.computed.index.mjs +4 -0
  34. package/web-ui/modules/app.computed.main-tabs.mjs +198 -0
  35. package/web-ui/modules/app.computed.session.mjs +521 -9
  36. package/web-ui/modules/app.methods.agents.mjs +62 -11
  37. package/web-ui/modules/app.methods.codex-config.mjs +189 -34
  38. package/web-ui/modules/app.methods.index.mjs +7 -1
  39. package/web-ui/modules/app.methods.install.mjs +24 -20
  40. package/web-ui/modules/app.methods.navigation.mjs +142 -1
  41. package/web-ui/modules/app.methods.openclaw-core.mjs +339 -39
  42. package/web-ui/modules/app.methods.openclaw-editing.mjs +39 -4
  43. package/web-ui/modules/app.methods.openclaw-persist.mjs +122 -4
  44. package/web-ui/modules/app.methods.providers.mjs +192 -53
  45. package/web-ui/modules/app.methods.session-actions.mjs +99 -19
  46. package/web-ui/modules/app.methods.session-browser.mjs +196 -5
  47. package/web-ui/modules/app.methods.session-timeline.mjs +22 -15
  48. package/web-ui/modules/app.methods.session-trash.mjs +3 -0
  49. package/web-ui/modules/app.methods.startup-claude.mjs +70 -71
  50. package/web-ui/modules/app.methods.task-orchestration.mjs +471 -0
  51. package/web-ui/modules/config-mode.computed.mjs +2 -0
  52. package/web-ui/modules/config-template-confirm-pref.mjs +33 -0
  53. package/web-ui/modules/i18n.mjs +1609 -0
  54. package/web-ui/modules/plugins.computed.mjs +220 -0
  55. package/web-ui/modules/plugins.methods.mjs +620 -0
  56. package/web-ui/modules/plugins.storage.mjs +37 -0
  57. package/web-ui/partials/index/layout-footer.html +1 -57
  58. package/web-ui/partials/index/layout-header.html +299 -175
  59. package/web-ui/partials/index/modal-config-template-agents.html +79 -29
  60. package/web-ui/partials/index/modal-confirm-toast.html +1 -1
  61. package/web-ui/partials/index/modal-health-check.html +14 -14
  62. package/web-ui/partials/index/modal-openclaw-config.html +47 -42
  63. package/web-ui/partials/index/modal-skills.html +130 -114
  64. package/web-ui/partials/index/modals-basic.html +71 -102
  65. package/web-ui/partials/index/panel-config-claude.html +50 -12
  66. package/web-ui/partials/index/panel-config-codex.html +34 -37
  67. package/web-ui/partials/index/panel-config-openclaw.html +10 -16
  68. package/web-ui/partials/index/panel-docs.html +147 -0
  69. package/web-ui/partials/index/panel-market.html +38 -38
  70. package/web-ui/partials/index/panel-orchestration.html +397 -0
  71. package/web-ui/partials/index/panel-plugins.html +243 -0
  72. package/web-ui/partials/index/panel-sessions.html +51 -146
  73. package/web-ui/partials/index/panel-settings.html +188 -96
  74. package/web-ui/partials/index/panel-usage.html +353 -0
  75. package/web-ui/session-helpers.mjs +221 -10
  76. package/web-ui/styles/base-theme.css +120 -229
  77. package/web-ui/styles/controls-forms.css +59 -51
  78. package/web-ui/styles/docs-panel.css +247 -0
  79. package/web-ui/styles/layout-shell.css +394 -128
  80. package/web-ui/styles/modals-core.css +18 -3
  81. package/web-ui/styles/navigation-panels.css +184 -183
  82. package/web-ui/styles/plugins-panel.css +518 -0
  83. package/web-ui/styles/responsive.css +102 -62
  84. package/web-ui/styles/sessions-list.css +13 -27
  85. package/web-ui/styles/sessions-preview.css +13 -7
  86. package/web-ui/styles/sessions-toolbar-trash.css +25 -0
  87. package/web-ui/styles/sessions-usage.css +581 -6
  88. package/web-ui/styles/settings-panel.css +166 -0
  89. package/web-ui/styles/skills-list.css +16 -11
  90. package/web-ui/styles/skills-market.css +63 -2
  91. package/web-ui/styles/task-orchestration.css +776 -0
  92. package/web-ui/styles/titles-cards.css +67 -66
  93. package/web-ui/styles.css +4 -0
  94. package/README.en.md +0 -259
  95. package/res/screenshot.png +0 -0
  96. package/res/vue.global.js +0 -18552
@@ -36,6 +36,38 @@ export function createAgentsMethods(options = {}) {
36
36
  } = options;
37
37
 
38
38
  return {
39
+ async openClaudeMdEditor() {
40
+ this.setAgentsModalContext('claude-md');
41
+ const requestToken = issueLatestRequestToken(this, '_agentsOpenRequestToken');
42
+ this.agentsLoading = true;
43
+ try {
44
+ const res = await api('get-claude-md-file');
45
+ if (!isLatestRequestToken(this, '_agentsOpenRequestToken', requestToken)) {
46
+ return;
47
+ }
48
+ if (res.error) {
49
+ this.showMessage(res.error, 'error');
50
+ return;
51
+ }
52
+ this.agentsContent = res.content || '';
53
+ this.agentsOriginalContent = this.agentsContent;
54
+ this.agentsPath = res.path || '';
55
+ this.agentsExists = !!res.exists;
56
+ this.agentsLineEnding = res.lineEnding === '\r\n' ? '\r\n' : '\n';
57
+ this.resetAgentsDiffState();
58
+ this.showAgentsModal = true;
59
+ } catch (e) {
60
+ if (!isLatestRequestToken(this, '_agentsOpenRequestToken', requestToken)) {
61
+ return;
62
+ }
63
+ this.showMessage('加载文件失败', 'error');
64
+ } finally {
65
+ if (isLatestRequestToken(this, '_agentsOpenRequestToken', requestToken)) {
66
+ this.agentsLoading = false;
67
+ }
68
+ }
69
+ },
70
+
39
71
  async openAgentsEditor() {
40
72
  this.setAgentsModalContext('codex');
41
73
  const requestToken = issueLatestRequestToken(this, '_agentsOpenRequestToken');
@@ -148,6 +180,15 @@ export function createAgentsMethods(options = {}) {
148
180
  },
149
181
 
150
182
  setAgentsModalContext(context, options = {}) {
183
+ const t = typeof this.t === 'function' ? this.t : null;
184
+ const tr = (key, fallback, params = null) => (t ? t(key, params) : fallback);
185
+ if (context === 'claude-md') {
186
+ this.agentsContext = 'claude-md';
187
+ this.agentsWorkspaceFileName = '';
188
+ this.agentsModalTitle = tr('modal.agents.title.claudeMd', 'CLAUDE.md 编辑器');
189
+ this.agentsModalHint = tr('modal.agents.hint.claudeMd', '保存后会写入 ~/.claude/CLAUDE.md。');
190
+ return;
191
+ }
151
192
  if (context === 'openclaw-workspace') {
152
193
  const fileName = (options.fileName || this.openclawWorkspaceFileName || 'AGENTS.md').trim();
153
194
  this.agentsContext = 'openclaw-workspace';
@@ -158,11 +199,11 @@ export function createAgentsMethods(options = {}) {
158
199
  }
159
200
  this.agentsContext = context === 'openclaw' ? 'openclaw' : 'codex';
160
201
  if (this.agentsContext === 'openclaw') {
161
- this.agentsModalTitle = 'OpenClaw AGENTS.md 编辑器';
162
- this.agentsModalHint = '保存后会写入 OpenClaw Workspace 下的 AGENTS.md。';
202
+ this.agentsModalTitle = tr('modal.agents.title.openclaw', 'OpenClaw AGENTS.md 编辑器');
203
+ this.agentsModalHint = tr('modal.agents.hint.openclaw', '保存后会写入 OpenClaw Workspace 下的 AGENTS.md。');
163
204
  } else {
164
- this.agentsModalTitle = 'AGENTS.md 编辑器';
165
- this.agentsModalHint = '保存后会写入目标 AGENTS.md(与 config.toml 同级)。';
205
+ this.agentsModalTitle = tr('modal.agents.title.default', 'AGENTS.md 编辑器');
206
+ this.agentsModalHint = tr('modal.agents.hint.default', '保存后会写入目标 AGENTS.md(与 config.toml 同级)。');
166
207
  }
167
208
  this.agentsWorkspaceFileName = '';
168
209
  },
@@ -232,16 +273,17 @@ export function createAgentsMethods(options = {}) {
232
273
  this.confirmDialogResolver(false);
233
274
  }
234
275
  const confirmDisabled = options.confirmDisabled;
276
+ const t = typeof this.t === 'function' ? this.t : null;
235
277
  this.confirmDialogTitle = typeof options.title === 'string' && options.title.trim()
236
278
  ? options.title.trim()
237
- : '请确认操作';
279
+ : (t ? t('confirm.title.default') : '请确认操作');
238
280
  this.confirmDialogMessage = typeof options.message === 'string' ? options.message : '';
239
281
  this.confirmDialogConfirmText = typeof options.confirmText === 'string' && options.confirmText.trim()
240
282
  ? options.confirmText.trim()
241
- : '确认';
283
+ : (t ? t('confirm.ok') : '确认');
242
284
  this.confirmDialogCancelText = typeof options.cancelText === 'string' && options.cancelText.trim()
243
285
  ? options.cancelText.trim()
244
- : '取消';
286
+ : (t ? t('confirm.cancel') : '取消');
245
287
  this.confirmDialogDanger = !!options.danger;
246
288
  this.confirmDialogConfirmDisabled = typeof confirmDisabled === 'function' ? false : !!confirmDisabled;
247
289
  this.confirmDialogDisableWhen = typeof confirmDisabled === 'function' ? confirmDisabled : null;
@@ -267,8 +309,13 @@ export function createAgentsMethods(options = {}) {
267
309
  this.showConfirmDialog = false;
268
310
  this.confirmDialogTitle = '';
269
311
  this.confirmDialogMessage = '';
270
- this.confirmDialogConfirmText = '确认';
271
- this.confirmDialogCancelText = '取消';
312
+ if (typeof this.t === 'function') {
313
+ this.confirmDialogConfirmText = this.t('confirm.ok');
314
+ this.confirmDialogCancelText = this.t('confirm.cancel');
315
+ } else {
316
+ this.confirmDialogConfirmText = '确认';
317
+ this.confirmDialogCancelText = '取消';
318
+ }
272
319
  this.confirmDialogDanger = false;
273
320
  this.confirmDialogConfirmDisabled = false;
274
321
  this.confirmDialogDisableWhen = null;
@@ -467,7 +514,9 @@ export function createAgentsMethods(options = {}) {
467
514
  content: this.agentsContent,
468
515
  lineEnding: this.agentsLineEnding
469
516
  };
470
- if (this.agentsContext === 'openclaw') {
517
+ if (this.agentsContext === 'claude-md') {
518
+ action = 'apply-claude-md-file';
519
+ } else if (this.agentsContext === 'openclaw') {
471
520
  action = 'apply-openclaw-agents-file';
472
521
  } else if (this.agentsContext === 'openclaw-workspace') {
473
522
  action = 'apply-openclaw-workspace-file';
@@ -480,7 +529,9 @@ export function createAgentsMethods(options = {}) {
480
529
  }
481
530
  const successLabel = this.agentsContext === 'openclaw-workspace'
482
531
  ? `工作区文件已保存${this.agentsWorkspaceFileName ? `: ${this.agentsWorkspaceFileName}` : ''}`
483
- : (this.agentsContext === 'openclaw' ? 'OpenClaw AGENTS.md 已保存' : 'AGENTS.md 已保存');
532
+ : (this.agentsContext === 'claude-md'
533
+ ? 'CLAUDE.md 已保存'
534
+ : (this.agentsContext === 'openclaw' ? 'OpenClaw AGENTS.md 已保存' : 'AGENTS.md 已保存'));
484
535
  this.showMessage(successLabel, 'success');
485
536
  this.closeAgentsModal({ force: true });
486
537
  } catch (e) {
@@ -1,4 +1,5 @@
1
1
  import { runLatestOnlyQueue } from '../logic.mjs';
2
+ import { normalizeConfigTemplateDiffConfirmEnabled } from './config-template-confirm-pref.mjs';
2
3
 
3
4
  function hasResponseError(response) {
4
5
  if (!response || typeof response !== 'object') {
@@ -104,23 +105,42 @@ export function createCodexConfigMethods(options = {}) {
104
105
  const previousModelsSource = this.modelsSource;
105
106
  const previousModelsHasCurrent = this.modelsHasCurrent;
106
107
  this.currentProvider = name;
107
- await this.loadModelsForProvider(name);
108
- if (this.modelsSource === 'error') {
109
- this.currentProvider = previousProvider;
110
- this.currentModel = previousModel;
111
- this.models = previousModels;
112
- this.modelsSource = previousModelsSource;
113
- this.modelsHasCurrent = previousModelsHasCurrent;
114
- return;
115
- }
108
+ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
109
+
110
+ // 不要把“切换提供商”强绑定到 /models 成功与否:
111
+ // 部分 OpenAI 兼容服务 /models 不可用或很慢,但用户仍希望一次点击即可完成切换。
112
+ // 这里做“短等待 + 后台补齐”:
113
+ // 1) 先启动 models 拉取(静默)
114
+ // 2) 给一个很短的窗口等待它完成,以便能立即选到第一个模型
115
+ // 3) 无论 models 是否成功,先应用 provider 切换
116
+ // 4) models 后续若补齐并发现当前 model 不在列表,则自动切到首个 model 并再应用一次
117
+ const modelsTask = this.loadModelsForProvider(name, { silentError: true })
118
+ .catch(() => {});
119
+
120
+ await Promise.race([modelsTask, delay(250)]);
121
+
116
122
  if (this.modelsSource === 'remote' && this.models.length > 0 && !this.models.includes(this.currentModel)) {
117
123
  this.currentModel = this.models[0];
118
124
  this.modelsHasCurrent = true;
119
125
  }
126
+
120
127
  if (getProviderConfigModeMeta(this.configMode)) {
121
128
  await this.waitForCodexApplyIdle();
122
129
  await this.applyCodexConfigDirect({ silent: true });
123
130
  }
131
+
132
+ await modelsTask;
133
+
134
+ if (this.currentProvider === name) {
135
+ if (this.modelsSource === 'remote' && this.models.length > 0 && !this.models.includes(this.currentModel)) {
136
+ this.currentModel = this.models[0];
137
+ this.modelsHasCurrent = true;
138
+ if (getProviderConfigModeMeta(this.configMode)) {
139
+ await this.waitForCodexApplyIdle();
140
+ await this.applyCodexConfigDirect({ silent: true });
141
+ }
142
+ }
143
+ }
124
144
  },
125
145
 
126
146
  async switchProvider(name) {
@@ -191,7 +211,7 @@ export function createCodexConfigMethods(options = {}) {
191
211
  const fallbackText = fallback === '' ? '' : String(fallback).trim();
192
212
  const raw = typeof value === 'string'
193
213
  ? value.trim()
194
- : String(value ?? '').trim();
214
+ : String(value == null ? '' : value).trim();
195
215
  const text = raw || fallbackText;
196
216
  if (!text) {
197
217
  return { ok: true, value: null, text: '' };
@@ -329,7 +349,7 @@ export function createCodexConfigMethods(options = {}) {
329
349
  },
330
350
 
331
351
  buildDefaultHealthCheckPrompt() {
332
- return '请简短回复:当前提供商连接正常。';
352
+ return '请简短回复:连接正常。';
333
353
  },
334
354
 
335
355
  openHealthCheckDialog(options = {}) {
@@ -337,6 +357,12 @@ export function createCodexConfigMethods(options = {}) {
337
357
  ? options.providerName.trim()
338
358
  : '';
339
359
  const locked = !!options.locked && !!providerName;
360
+ if (locked && providerName && providerName !== String(this.currentProvider || '').trim()) {
361
+ if (typeof this.showMessage === 'function') {
362
+ this.showMessage('请先切换到该提供商再进行健康聊天测试', 'info');
363
+ }
364
+ return;
365
+ }
340
366
  const nextProvider = providerName
341
367
  || String(this.healthCheckDialogSelectedProvider || '').trim()
342
368
  || String(this.currentProvider || '').trim()
@@ -376,7 +402,7 @@ export function createCodexConfigMethods(options = {}) {
376
402
  return;
377
403
  }
378
404
  if (!prompt) {
379
- this.showMessage('请输入对话内容', 'error');
405
+ this.showMessage('请输入消息内容', 'error');
380
406
  return;
381
407
  }
382
408
 
@@ -396,7 +422,7 @@ export function createCodexConfigMethods(options = {}) {
396
422
  this.healthCheckDialogLastResult = res;
397
423
 
398
424
  if (hasResponseError(res) || res.ok === false) {
399
- const message = getResponseMessage(res, '健康检测失败');
425
+ const message = getResponseMessage(res, '健康聊天测试失败');
400
426
  this.healthCheckDialogMessages.push({
401
427
  id: `assistant-${Date.now()}`,
402
428
  role: 'assistant',
@@ -413,7 +439,7 @@ export function createCodexConfigMethods(options = {}) {
413
439
 
414
440
  const reply = typeof res.reply === 'string' && res.reply.trim()
415
441
  ? res.reply.trim()
416
- : '已收到响应,但未解析到可展示文本。';
442
+ : '已收到回复,但未解析到可展示文本。';
417
443
  this.healthCheckDialogMessages.push({
418
444
  id: `assistant-${Date.now()}`,
419
445
  role: 'assistant',
@@ -426,7 +452,7 @@ export function createCodexConfigMethods(options = {}) {
426
452
  });
427
453
  this.healthCheckDialogPrompt = '';
428
454
  } catch (e) {
429
- const message = e && e.message ? e.message : '健康检测失败';
455
+ const message = e && e.message ? e.message : '健康聊天测试失败';
430
456
  this.healthCheckDialogMessages.push({
431
457
  id: `assistant-${Date.now()}`,
432
458
  role: 'assistant',
@@ -451,6 +477,7 @@ export function createCodexConfigMethods(options = {}) {
451
477
  },
452
478
 
453
479
  async openConfigTemplateEditor(options = {}) {
480
+ this.resetConfigTemplateDiffState();
454
481
  const modelContextWindow = this.normalizePositiveIntegerInput(
455
482
  this.modelContextWindowInput,
456
483
  'model_context_window',
@@ -598,43 +625,171 @@ export function createCodexConfigMethods(options = {}) {
598
625
 
599
626
  closeConfigTemplateModal(options = {}) {
600
627
  const force = !!options.force;
601
- if (!force && this.configTemplateApplying) {
628
+ if (!force && (this.configTemplateApplying || this.configTemplateDiffLoading)) {
602
629
  return;
603
630
  }
604
631
  this.showConfigTemplateModal = false;
605
632
  this.configTemplateContent = '';
633
+ this.resetConfigTemplateDiffState();
606
634
  },
607
635
 
608
- async applyConfigTemplate() {
609
- if (this.configTemplateApplying) {
610
- return;
636
+ resetConfigTemplateDiffState() {
637
+ this.configTemplateDiffVisible = false;
638
+ this.configTemplateDiffLoading = false;
639
+ this.configTemplateDiffError = '';
640
+ this.configTemplateDiffLines = [];
641
+ this.configTemplateDiffStats = { added: 0, removed: 0, unchanged: 0 };
642
+ this.configTemplateDiffHasChangesValue = false;
643
+ this.configTemplateDiffFingerprint = '';
644
+ this._configTemplateDiffPreviewRequestToken = null;
645
+ },
646
+
647
+ onConfigTemplateContentInput() {
648
+ if (this.configTemplateDiffVisible || (this.configTemplateDiffLines && this.configTemplateDiffLines.length)) {
649
+ this.resetConfigTemplateDiffState();
611
650
  }
612
- if (!this.configTemplateContent || !this.configTemplateContent.trim()) {
613
- this.showMessage('模板不能为空', 'error');
614
- return;
651
+ },
652
+
653
+ buildConfigTemplateDiffFingerprint() {
654
+ const content = typeof this.configTemplateContent === 'string' ? this.configTemplateContent : '';
655
+ return `${content.length}::${content}`;
656
+ },
657
+
658
+ hasConfigTemplateDiffChanges() {
659
+ if (this.configTemplateDiffHasChangesValue !== undefined && this.configTemplateDiffHasChangesValue !== null) {
660
+ return !!this.configTemplateDiffHasChangesValue;
615
661
  }
662
+ const stats = this.configTemplateDiffStats && typeof this.configTemplateDiffStats === 'object'
663
+ ? this.configTemplateDiffStats
664
+ : {};
665
+ const added = Number(stats.added || 0);
666
+ const removed = Number(stats.removed || 0);
667
+ return added > 0 || removed > 0;
668
+ },
616
669
 
617
- this.configTemplateApplying = true;
670
+ async prepareConfigTemplateDiff() {
671
+ const requestFingerprint = this.buildConfigTemplateDiffFingerprint();
672
+ const requestToken = Symbol('config-template-diff-preview');
673
+ this._configTemplateDiffPreviewRequestToken = requestToken;
674
+ this.configTemplateDiffVisible = true;
675
+ this.configTemplateDiffLoading = true;
676
+ this.configTemplateDiffError = '';
677
+ this.configTemplateDiffLines = [];
678
+ this.configTemplateDiffStats = { added: 0, removed: 0, unchanged: 0 };
679
+ this.configTemplateDiffHasChangesValue = false;
618
680
  try {
619
- const res = await api('apply-config-template', {
681
+ const shouldApply = () => (
682
+ this.configTemplateDiffVisible
683
+ && this._configTemplateDiffPreviewRequestToken === requestToken
684
+ && this.buildConfigTemplateDiffFingerprint() === requestFingerprint
685
+ );
686
+ const res = await api('preview-config-template-diff', {
620
687
  template: this.configTemplateContent
621
688
  });
689
+ if (!shouldApply()) {
690
+ return;
691
+ }
622
692
  if (res.error) {
623
- this.showMessage(res.error, 'error');
693
+ this.configTemplateDiffError = res.error;
624
694
  return;
625
695
  }
626
- this.showMessage('模板已应用', 'success');
627
- this.closeConfigTemplateModal({ force: true });
628
- try {
629
- await this.loadAll();
630
- } catch (_) {
631
- this.showMessage('模板已应用,但界面刷新失败,请手动刷新', 'error');
696
+ const diff = res.diff && typeof res.diff === 'object' ? res.diff : {};
697
+ const lines = Array.isArray(diff.lines) ? diff.lines : [];
698
+ this.configTemplateDiffLines = lines.filter(line => line && line.type);
699
+ const stats = diff.stats && typeof diff.stats === 'object' ? diff.stats : null;
700
+ if (stats) {
701
+ this.configTemplateDiffStats = {
702
+ added: Number(stats.added || 0),
703
+ removed: Number(stats.removed || 0),
704
+ unchanged: Number(stats.unchanged || 0)
705
+ };
706
+ } else {
707
+ const nextStats = { added: 0, removed: 0, unchanged: 0 };
708
+ for (const line of this.configTemplateDiffLines) {
709
+ if (line && line.type === 'add') nextStats.added += 1;
710
+ else if (line && line.type === 'del') nextStats.removed += 1;
711
+ else nextStats.unchanged += 1;
712
+ }
713
+ this.configTemplateDiffStats = nextStats;
714
+ }
715
+ this.configTemplateDiffHasChangesValue = !!diff.hasChanges;
716
+ this.configTemplateDiffFingerprint = requestFingerprint;
717
+ } catch (_) {
718
+ if (this._configTemplateDiffPreviewRequestToken === requestToken) {
719
+ this.configTemplateDiffError = '生成差异失败';
632
720
  }
633
- } catch (e) {
634
- this.showMessage('应用模板失败', 'error');
635
721
  } finally {
636
- this.configTemplateApplying = false;
722
+ if (this._configTemplateDiffPreviewRequestToken === requestToken) {
723
+ this.configTemplateDiffLoading = false;
724
+ }
725
+ }
726
+ },
727
+
728
+ async applyConfigTemplate() {
729
+ if (this.configTemplateApplying) {
730
+ return;
731
+ }
732
+ if (!this.configTemplateContent || !this.configTemplateContent.trim()) {
733
+ this.showMessage('模板不能为空', 'error');
734
+ return;
637
735
  }
736
+
737
+ // Default to two-step confirmation when the setting is unset.
738
+ // (The normalize helper lives in session-actions; keep a safe fallback here.)
739
+ const shouldUseTwoStepConfirm = normalizeConfigTemplateDiffConfirmEnabled(this.configTemplateDiffConfirmEnabled);
740
+
741
+ const performApply = async () => {
742
+ this.configTemplateApplying = true;
743
+ try {
744
+ const res = await api('apply-config-template', {
745
+ template: this.configTemplateContent
746
+ });
747
+ if (res.error) {
748
+ this.showMessage(res.error, 'error');
749
+ return;
750
+ }
751
+ this.showMessage('模板已应用', 'success');
752
+ this.closeConfigTemplateModal({ force: true });
753
+ try {
754
+ await this.loadAll();
755
+ } catch (_) {
756
+ this.showMessage('模板已应用,但界面刷新失败,请手动刷新', 'error');
757
+ }
758
+ } catch (e) {
759
+ this.showMessage('应用模板失败', 'error');
760
+ } finally {
761
+ this.configTemplateApplying = false;
762
+ }
763
+ };
764
+
765
+ // One-step mode: apply immediately unless user explicitly entered the diff preview state.
766
+ if (!shouldUseTwoStepConfirm && !this.configTemplateDiffVisible) {
767
+ await performApply();
768
+ return;
769
+ }
770
+
771
+ if (!this.configTemplateDiffVisible) {
772
+ await this.prepareConfigTemplateDiff();
773
+ return;
774
+ }
775
+ if (this.configTemplateDiffLoading) {
776
+ return;
777
+ }
778
+ if (this.configTemplateDiffError) {
779
+ this.showMessage(this.configTemplateDiffError, 'error');
780
+ return;
781
+ }
782
+ const fingerprint = this.buildConfigTemplateDiffFingerprint();
783
+ if (this.configTemplateDiffFingerprint !== fingerprint) {
784
+ await this.prepareConfigTemplateDiff();
785
+ return;
786
+ }
787
+ if (!this.hasConfigTemplateDiffChanges()) {
788
+ this.showMessage('未检测到改动', 'info');
789
+ return;
790
+ }
791
+
792
+ await performApply();
638
793
  }
639
794
  };
640
795
  }
@@ -20,12 +20,15 @@ import { createOpenclawEditingMethods } from './app.methods.openclaw-editing.mjs
20
20
  import { createOpenclawPersistMethods } from './app.methods.openclaw-persist.mjs';
21
21
  import { createProvidersMethods } from './app.methods.providers.mjs';
22
22
  import { createRuntimeMethods } from './app.methods.runtime.mjs';
23
+ import { createTaskOrchestrationMethods } from './app.methods.task-orchestration.mjs';
23
24
  import { createSessionActionMethods } from './app.methods.session-actions.mjs';
24
25
  import { createSessionBrowserMethods } from './app.methods.session-browser.mjs';
25
26
  import { createSessionTimelineMethods } from './app.methods.session-timeline.mjs';
26
27
  import { createSessionTrashMethods } from './app.methods.session-trash.mjs';
27
28
  import { createStartupClaudeMethods } from './app.methods.startup-claude.mjs';
28
29
  import { createSkillsMethods } from './skills.methods.mjs';
30
+ import { createPluginsMethods } from './plugins.methods.mjs';
31
+ import { createI18nMethods } from './i18n.mjs';
29
32
  import {
30
33
  CONFIG_MODE_SET,
31
34
  getProviderConfigModeMeta
@@ -39,6 +42,7 @@ import {
39
42
 
40
43
  export function createAppMethods() {
41
44
  return {
45
+ ...createI18nMethods(),
42
46
  ...createStartupClaudeMethods({
43
47
  api,
44
48
  defaultModelContextWindow: DEFAULT_MODEL_CONTEXT_WINDOW,
@@ -71,6 +75,7 @@ export function createAppMethods() {
71
75
  getProviderConfigModeMeta
72
76
  }),
73
77
  ...createSkillsMethods({ api }),
78
+ ...createPluginsMethods(),
74
79
  ...createAgentsMethods({ api, apiWithMeta }),
75
80
  ...createProvidersMethods({ api }),
76
81
  ...createClaudeConfigMethods({ api }),
@@ -81,6 +86,7 @@ export function createAppMethods() {
81
86
  defaultOpenclawTemplate: DEFAULT_OPENCLAW_TEMPLATE
82
87
  }),
83
88
  ...createInstallMethods(),
84
- ...createRuntimeMethods({ api })
89
+ ...createRuntimeMethods({ api }),
90
+ ...createTaskOrchestrationMethods({ api })
85
91
  };
86
92
  }
@@ -78,6 +78,13 @@ export function createInstallMethods() {
78
78
  },
79
79
 
80
80
  resolveInstallPlatform() {
81
+ const navUserAgent = typeof navigator !== 'undefined' && typeof navigator.userAgent === 'string'
82
+ ? navigator.userAgent.trim().toLowerCase()
83
+ : '';
84
+ // Termux runs on Android; Codex CLI needs a Termux-friendly build.
85
+ if (navUserAgent.includes('termux') || navUserAgent.includes('android')) {
86
+ return 'termux';
87
+ }
81
88
  const navPlatform = typeof navigator !== 'undefined' && typeof navigator.platform === 'string'
82
89
  ? navigator.platform.trim().toLowerCase()
83
90
  : '';
@@ -86,8 +93,11 @@ export function createInstallMethods() {
86
93
  return 'linux';
87
94
  },
88
95
 
89
- buildInstallCommandMatrix(packageManager) {
96
+ buildInstallCommandMatrix(packageManager, platformOverride = '') {
90
97
  const manager = this.normalizeInstallPackageManager(packageManager);
98
+ const platform = platformOverride ? String(platformOverride).trim().toLowerCase() : this.resolveInstallPlatform();
99
+ const codexPackage = platform === 'termux' ? '@mmmbuto/codex-cli-termux' : '@openai/codex';
100
+ const codexInstallPackage = platform === 'termux' ? '@mmmbuto/codex-cli-termux@latest' : '@openai/codex';
91
101
  const matrix = {
92
102
  claude: {
93
103
  install: '',
@@ -104,34 +114,36 @@ export function createInstallMethods() {
104
114
  matrix.claude.install = 'pnpm add -g @anthropic-ai/claude-code';
105
115
  matrix.claude.update = 'pnpm up -g @anthropic-ai/claude-code';
106
116
  matrix.claude.uninstall = 'pnpm remove -g @anthropic-ai/claude-code';
107
- matrix.codex.install = 'pnpm add -g @openai/codex';
108
- matrix.codex.update = 'pnpm up -g @openai/codex';
109
- matrix.codex.uninstall = 'pnpm remove -g @openai/codex';
117
+ matrix.codex.install = `pnpm add -g ${codexInstallPackage}`;
118
+ matrix.codex.update = `pnpm up -g ${codexPackage}`;
119
+ matrix.codex.uninstall = `pnpm remove -g ${codexPackage}`;
110
120
  return matrix;
111
121
  }
112
122
  if (manager === 'bun') {
113
123
  matrix.claude.install = 'bun add -g @anthropic-ai/claude-code';
114
124
  matrix.claude.update = 'bun update -g @anthropic-ai/claude-code';
115
125
  matrix.claude.uninstall = 'bun remove -g @anthropic-ai/claude-code';
116
- matrix.codex.install = 'bun add -g @openai/codex';
117
- matrix.codex.update = 'bun update -g @openai/codex';
118
- matrix.codex.uninstall = 'bun remove -g @openai/codex';
126
+ matrix.codex.install = `bun add -g ${codexInstallPackage}`;
127
+ matrix.codex.update = `bun update -g ${codexPackage}`;
128
+ matrix.codex.uninstall = `bun remove -g ${codexPackage}`;
119
129
  return matrix;
120
130
  }
121
131
  matrix.claude.install = 'npm install -g @anthropic-ai/claude-code';
122
132
  matrix.claude.update = 'npm update -g @anthropic-ai/claude-code';
123
133
  matrix.claude.uninstall = 'npm uninstall -g @anthropic-ai/claude-code';
124
- matrix.codex.install = 'npm install -g @openai/codex';
125
- matrix.codex.update = 'npm update -g @openai/codex';
126
- matrix.codex.uninstall = 'npm uninstall -g @openai/codex';
134
+ matrix.codex.install = `npm install -g ${codexInstallPackage}`;
135
+ matrix.codex.update = platform === 'termux'
136
+ ? `npm install -g ${codexInstallPackage}`
137
+ : `npm update -g ${codexPackage}`;
138
+ matrix.codex.uninstall = `npm uninstall -g ${codexPackage}`;
127
139
  return matrix;
128
140
  },
129
141
 
130
- getInstallCommand(targetId, actionName) {
142
+ getInstallCommand(targetId, actionName, platformOverride = '') {
131
143
  const targetKey = typeof targetId === 'string' ? targetId.trim() : '';
132
144
  if (!targetKey) return '';
133
145
  const action = this.normalizeInstallAction(actionName);
134
- const currentMap = this.buildInstallCommandMatrix(this.installPackageManager);
146
+ const currentMap = this.buildInstallCommandMatrix(this.installPackageManager, platformOverride);
135
147
  const current = currentMap[targetKey] && typeof currentMap[targetKey][action] === 'string'
136
148
  ? currentMap[targetKey][action]
137
149
  : '';
@@ -144,14 +156,6 @@ export function createInstallMethods() {
144
156
 
145
157
  setInstallRegistryPreset(presetName) {
146
158
  this.installRegistryPreset = this.normalizeInstallRegistryPreset(presetName);
147
- },
148
-
149
- openInstallModal() {
150
- this.showInstallModal = true;
151
- },
152
-
153
- closeInstallModal() {
154
- this.showInstallModal = false;
155
159
  }
156
160
  };
157
161
  }