codexmate 0.1.10 → 0.1.12

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 (32) hide show
  1. package/README.md +0 -4
  2. package/README.vi.md +0 -4
  3. package/README.zh.md +0 -4
  4. package/cli/agents-files.js +27 -3
  5. package/cli/prompt-history.js +171 -0
  6. package/cli/system-prompt-files.js +20 -2
  7. package/cli.js +23 -1
  8. package/package.json +1 -1
  9. package/web-ui/app.js +15 -0
  10. package/web-ui/modules/app.methods.agents.mjs +90 -0
  11. package/web-ui/modules/app.methods.navigation.mjs +7 -1
  12. package/web-ui/modules/app.methods.session-actions.mjs +57 -0
  13. package/web-ui/modules/app.methods.system-prompt.mjs +81 -0
  14. package/web-ui/modules/app.methods.web-ui-preferences.mjs +6 -0
  15. package/web-ui/modules/i18n/locales/en.mjs +9 -0
  16. package/web-ui/modules/i18n/locales/ja.mjs +9 -0
  17. package/web-ui/modules/i18n/locales/vi.mjs +9 -0
  18. package/web-ui/modules/i18n/locales/zh-tw.mjs +9 -0
  19. package/web-ui/modules/i18n/locales/zh.mjs +9 -0
  20. package/web-ui/partials/index/layout-header.html +5 -0
  21. package/web-ui/partials/index/panel-config-claude.html +3 -3
  22. package/web-ui/partials/index/panel-config-codex.html +3 -3
  23. package/web-ui/partials/index/panel-config-kilocode.html +5 -5
  24. package/web-ui/partials/index/panel-config-openclaw.html +3 -3
  25. package/web-ui/partials/index/panel-config-opencode.html +5 -5
  26. package/web-ui/partials/index/panel-prompts.html +52 -0
  27. package/web-ui/partials/index/panel-sessions.html +12 -0
  28. package/web-ui/partials/index/panel-settings.html +35 -0
  29. package/web-ui/res/web-ui-render.precompiled.js +517 -186
  30. package/web-ui/styles/modals-core.css +86 -0
  31. package/web-ui/styles/sessions-preview.css +38 -0
  32. package/web-ui/styles/titles-cards.css +2 -2
package/README.md CHANGED
@@ -184,10 +184,6 @@ Special thanks to all contributors for their contributions to Codex Mate ❤️
184
184
  <img src="https://contrib.rocks/image?repo=SakuraByteCore/codexmate" />
185
185
  </a>
186
186
 
187
- ## Star History
188
-
189
- [![Star History Chart](https://api.star-history.com/svg?repos=SakuraByteCore/codexmate&type=Date)](https://star-history.com/#SakuraByteCore/codexmate&Date)
190
-
191
187
  ## License
192
188
 
193
189
  Apache-2.0
package/README.vi.md CHANGED
@@ -167,10 +167,6 @@ Cảm ơn tất cả những người đóng góp cho Codex Mate ❤️
167
167
  <img src="https://contrib.rocks/image?repo=SakuraByteCore/codexmate" alt="Contributors" />
168
168
  </a>
169
169
 
170
- ## Lịch sử Star
171
-
172
- [![Star History Chart](https://api.star-history.com/svg?repos=SakuraByteCore/codexmate&type=Date)](https://star-history.com/#SakuraByteCore/codexmate&Date)
173
-
174
170
  ## Giấy phép
175
171
 
176
172
  Apache-2.0
package/README.zh.md CHANGED
@@ -181,10 +181,6 @@ flowchart TD
181
181
  <img src="https://contrib.rocks/image?repo=SakuraByteCore/codexmate" />
182
182
  </a>
183
183
 
184
- ## Star 历史
185
-
186
- [![Star History Chart](https://api.star-history.com/svg?repos=SakuraByteCore/codexmate&type=Date)](https://star-history.com/#SakuraByteCore/codexmate&Date)
187
-
188
184
  ## 开源协议
189
185
 
190
186
  Apache-2.0
@@ -14,7 +14,8 @@ function createAgentsFileController(deps = {}) {
14
14
  CLAUDE_DIR,
15
15
  CLAUDE_MD_FILE_NAME,
16
16
  readOpenclawAgentsFile,
17
- readOpenclawWorkspaceFile
17
+ readOpenclawWorkspaceFile,
18
+ backupPromptBeforeWrite
18
19
  } = deps;
19
20
 
20
21
  if (!fs) throw new Error('createAgentsFileController 缺少 fs');
@@ -32,6 +33,7 @@ function createAgentsFileController(deps = {}) {
32
33
  if (typeof CLAUDE_MD_FILE_NAME !== 'string' || !CLAUDE_MD_FILE_NAME) throw new Error('createAgentsFileController 缺少 CLAUDE_MD_FILE_NAME');
33
34
  if (typeof readOpenclawAgentsFile !== 'function') throw new Error('createAgentsFileController 缺少 readOpenclawAgentsFile');
34
35
  if (typeof readOpenclawWorkspaceFile !== 'function') throw new Error('createAgentsFileController 缺少 readOpenclawWorkspaceFile');
36
+ if (typeof backupPromptBeforeWrite !== 'function' && typeof backupPromptBeforeWrite !== 'undefined') throw new Error('createAgentsFileController 备份回调无效');
35
37
 
36
38
  function resolveAgentsFilePath(params = {}) {
37
39
  const baseDir = typeof params.baseDir === 'string' && params.baseDir.trim()
@@ -40,6 +42,12 @@ function createAgentsFileController(deps = {}) {
40
42
  return path.join(baseDir, AGENTS_FILE_NAME);
41
43
  }
42
44
 
45
+ function sanitizeHistoryId(raw) {
46
+ const safe = typeof raw === 'string' ? raw.trim() : '';
47
+ if (!safe) return 'global';
48
+ return safe.replace(/[^A-Za-z0-9_.-]/g, '_').slice(0, 64) || 'global';
49
+ }
50
+
43
51
  function validateAgentsBaseDir(filePath) {
44
52
  const dirPath = path.dirname(filePath);
45
53
  try {
@@ -152,6 +160,13 @@ function createAgentsFileController(deps = {}) {
152
160
  if (content.length > 2 * 1024 * 1024) {
153
161
  return { error: 'content too large (max 2MB)' };
154
162
  }
163
+ if (typeof backupPromptBeforeWrite === 'function') {
164
+ var bucket = resolved.isProject
165
+ ? 'claude-project_' + sanitizeHistoryId(resolved.projectPath)
166
+ : 'claude-global';
167
+ backupPromptBeforeWrite(bucket, filePath);
168
+ resolved.historyBucket = bucket;
169
+ }
155
170
  var lineEnding = params.lineEnding === '\r\n' ? '\r\n' : '\n';
156
171
  var normalized = normalizeLineEnding(content, lineEnding);
157
172
  var finalContent = ensureUtf8Bom(normalized);
@@ -159,6 +174,7 @@ function createAgentsFileController(deps = {}) {
159
174
  ensureDir(path.dirname(filePath));
160
175
  fs.writeFileSync(filePath, finalContent, 'utf-8');
161
176
  var result = { success: true, path: filePath };
177
+ if (resolved.historyBucket) result.historyBucket = resolved.historyBucket;
162
178
  if (resolved.isProject) {
163
179
  result.projectPath = resolved.projectPath;
164
180
  result.detectionSource = resolved.detectionSource;
@@ -218,13 +234,21 @@ function createAgentsFileController(deps = {}) {
218
234
  if (content.length > 2 * 1024 * 1024) {
219
235
  return { error: '内容过大(最大 2MB)' };
220
236
  }
237
+ let agentsHistoryBucket = '';
238
+ if (typeof backupPromptBeforeWrite === 'function') {
239
+ const bucket = 'codex_' + sanitizeHistoryId(String(params.baseDir || '').trim() || 'global');
240
+ backupPromptBeforeWrite(bucket, filePath);
241
+ agentsHistoryBucket = bucket;
242
+ }
221
243
  const lineEnding = params.lineEnding === '\r\n' ? '\r\n' : '\n';
222
- const normalized = normalizeLineEnding(content, lineEnding);
244
+ var normalized = normalizeLineEnding(content, lineEnding);
223
245
  const finalContent = ensureUtf8Bom(normalized);
224
246
 
225
247
  try {
226
248
  fs.writeFileSync(filePath, finalContent, 'utf-8');
227
- return { success: true, path: filePath };
249
+ const agentsResult = { success: true, path: filePath };
250
+ if (agentsHistoryBucket) agentsResult.historyBucket = agentsHistoryBucket;
251
+ return agentsResult;
228
252
  } catch (e) {
229
253
  return { error: `写入 AGENTS.md 失败: ${e.message}` };
230
254
  }
@@ -0,0 +1,171 @@
1
+ 'use strict';
2
+
3
+ function createPromptHistoryController(deps = {}) {
4
+ const {
5
+ fs,
6
+ path,
7
+ CONFIG_DIR,
8
+ MAX_HISTORY = 20
9
+ } = deps;
10
+
11
+ if (!fs) throw new Error('createPromptHistoryController 缺少 fs');
12
+ if (!path) throw new Error('createPromptHistoryController 缺少 path');
13
+ if (typeof CONFIG_DIR !== 'string' || !CONFIG_DIR) throw new Error('createPromptHistoryController 缺少 CONFIG_DIR');
14
+
15
+ const HISTORY_ROOT = path.join(CONFIG_DIR, 'codexmate-prompt-history');
16
+ const STAMP_RE = /^(\d{8})-(\d{6})-(\d{3})\.bak$/;
17
+
18
+ function sanitizeBucket(bucket) {
19
+ const raw = typeof bucket === 'string' ? bucket.trim() : '';
20
+ if (!raw) throw new Error('prompt history bucket 不能为空');
21
+ const safe = raw.replace(/[^A-Za-z0-9_.-]/g, '_');
22
+ if (!safe) throw new Error('prompt history bucket 无效: ' + bucket);
23
+ return safe;
24
+ }
25
+
26
+ function resolveBucketDir(bucket) {
27
+ const safe = sanitizeBucket(bucket);
28
+ return path.join(HISTORY_ROOT, safe);
29
+ }
30
+
31
+ function buildTimestamp(d = new Date()) {
32
+ const pad2 = (n) => String(n).padStart(2, '0');
33
+ const pad3 = (n) => String(n).padStart(3, '0');
34
+ return `${d.getFullYear()}${pad2(d.getMonth() + 1)}${pad2(d.getDate())}`
35
+ + `-${pad2(d.getHours())}${pad2(d.getMinutes())}${pad2(d.getSeconds())}`
36
+ + `-${pad3(d.getMilliseconds())}`;
37
+ }
38
+
39
+ function parseStamp(name) {
40
+ return STAMP_RE.test(name) ? name.slice(0, name.length - 4) : null;
41
+ }
42
+
43
+ function ensureDirSync(dir) {
44
+ fs.mkdirSync(dir, { recursive: true });
45
+ }
46
+
47
+ function rmtreeSync(dir) {
48
+ try {
49
+ fs.rmSync(dir, { recursive: true, force: true });
50
+ } catch (_) {}
51
+ }
52
+
53
+ function backupPromptBeforeWrite(bucket, currentFilePath) {
54
+ const dir = resolveBucketDir(bucket);
55
+ const exists = fs.existsSync(currentFilePath);
56
+ if (!exists) return { backedUp: false, entries: listPromptHistory(bucket) };
57
+ let raw;
58
+ try {
59
+ raw = fs.readFileSync(currentFilePath, 'utf-8');
60
+ } catch (e) {
61
+ return { backedUp: false, error: '读取当前文件失败: ' + e.message };
62
+ }
63
+ ensureDirSync(dir);
64
+ const stamp = buildTimestamp();
65
+ const dest = path.join(dir, stamp + '.bak');
66
+ try {
67
+ fs.writeFileSync(dest, raw, 'utf-8');
68
+ } catch (e) {
69
+ return { backedUp: false, error: '写入备份失败: ' + e.message };
70
+ }
71
+ trimHistory(dir);
72
+ return { backedUp: true, entries: listPromptHistory(bucket) };
73
+ }
74
+
75
+ function listPromptHistory(bucket) {
76
+ const dir = resolveBucketDir(bucket);
77
+ if (!fs.existsSync(dir)) return [];
78
+ let names;
79
+ try {
80
+ names = fs.readdirSync(dir);
81
+ } catch (_) {
82
+ return [];
83
+ }
84
+ const items = [];
85
+ for (const name of names) {
86
+ const stamp = parseStamp(name);
87
+ if (!stamp) continue;
88
+ let stat;
89
+ try {
90
+ stat = fs.statSync(path.join(dir, name));
91
+ } catch (_) {
92
+ continue;
93
+ }
94
+ items.push({
95
+ id: stamp,
96
+ bucket: sanitizeBucket(bucket),
97
+ size: Number(stat.size) || 0,
98
+ mtimeMs: Number(stat.mtimeMs) || 0
99
+ });
100
+ }
101
+ items.sort((a, b) => b.mtimeMs - a.mtimeMs);
102
+ return items;
103
+ }
104
+
105
+ function readPromptHistory(bucket, id) {
106
+ const dir = resolveBucketDir(bucket);
107
+ const stamp = typeof id === 'string' ? id.trim() : '';
108
+ const fullName = stamp + '.bak';
109
+ if (!stamp || !STAMP_RE.test(fullName)) {
110
+ return { error: 'history id invalid' };
111
+ }
112
+ const file = path.join(dir, fullName);
113
+ if (!fs.existsSync(file)) return { error: 'history entry 不存在' };
114
+ try {
115
+ const content = fs.readFileSync(file, 'utf-8');
116
+ return { id: stamp, content, bucket: sanitizeBucket(bucket) };
117
+ } catch (e) {
118
+ return { error: '读取 history 失败: ' + e.message };
119
+ }
120
+ }
121
+
122
+ function trimHistory(dir) {
123
+ let names;
124
+ try {
125
+ names = fs.readdirSync(dir);
126
+ } catch (_) {
127
+ return;
128
+ }
129
+ const stamped = [];
130
+ for (const name of names) {
131
+ const stamp = parseStamp(name);
132
+ if (!stamp) continue;
133
+ let stat;
134
+ try {
135
+ stat = fs.statSync(path.join(dir, name));
136
+ } catch (_) {
137
+ continue;
138
+ }
139
+ stamped.push({ name, mtimeMs: Number(stat.mtimeMs) || 0 });
140
+ }
141
+ stamped.sort((a, b) => b.mtimeMs - a.mtimeMs);
142
+ if (stamped.length <= MAX_HISTORY) return;
143
+ for (let i = MAX_HISTORY; i < stamped.length; i++) {
144
+ try {
145
+ fs.unlinkSync(path.join(dir, stamped[i].name));
146
+ } catch (_) {}
147
+ }
148
+ }
149
+
150
+ function clearPromptHistory(bucket) {
151
+ if (!bucket) {
152
+ rmtreeSync(HISTORY_ROOT);
153
+ return { cleared: true };
154
+ }
155
+ const dir = resolveBucketDir(bucket);
156
+ rmtreeSync(dir);
157
+ return { cleared: true };
158
+ }
159
+
160
+ return {
161
+ sanitizeBucket,
162
+ backupPromptBeforeWrite,
163
+ listPromptHistory,
164
+ readPromptHistory,
165
+ clearPromptHistory
166
+ };
167
+ }
168
+
169
+ module.exports = {
170
+ createPromptHistoryController
171
+ };
@@ -8,7 +8,8 @@ function createSystemPromptFileController(deps = {}) {
8
8
  crypto,
9
9
  buildLineDiff,
10
10
  CONFIG_DIR,
11
- PI_AGENT_DIR
11
+ PI_AGENT_DIR,
12
+ backupPromptBeforeWrite
12
13
  } = deps;
13
14
 
14
15
  if (!fs) throw new Error('createSystemPromptFileController 缺少 fs');
@@ -18,6 +19,7 @@ function createSystemPromptFileController(deps = {}) {
18
19
  if (typeof buildLineDiff !== 'function') throw new Error('createSystemPromptFileController 缺少 buildLineDiff');
19
20
  if (typeof CONFIG_DIR !== 'string' || !CONFIG_DIR) throw new Error('createSystemPromptFileController 缺少 CONFIG_DIR');
20
21
  if (typeof PI_AGENT_DIR !== 'string' || !PI_AGENT_DIR) throw new Error('createSystemPromptFileController 缺少 PI_AGENT_DIR');
22
+ if (typeof backupPromptBeforeWrite !== 'function' && typeof backupPromptBeforeWrite !== 'undefined') throw new Error('createSystemPromptFileController 备份回调无效');
21
23
 
22
24
  const MODES = {
23
25
  system: 'SYSTEM.md',
@@ -34,6 +36,12 @@ function createSystemPromptFileController(deps = {}) {
34
36
  return key;
35
37
  }
36
38
 
39
+ function sanitizeSystemHistoryId(raw) {
40
+ const safe = typeof raw === 'string' ? raw.trim() : '';
41
+ if (!safe) return 'global';
42
+ return safe.replace(/[^A-Za-z0-9_.-]/g, '_').slice(0, 64) || 'global';
43
+ }
44
+
37
45
  function normalizeScope(scope) {
38
46
  const key = typeof scope === 'string' ? scope.trim() : '';
39
47
  if (!SCOPES.includes(key)) {
@@ -104,16 +112,26 @@ function createSystemPromptFileController(deps = {}) {
104
112
  if (baseHash && baseHash !== current.hash) {
105
113
  return { error: '文件已被外部修改,请重新加载后再保存' };
106
114
  }
115
+ let sysHistoryBucket = '';
107
116
  try {
108
117
  const dir = path.dirname(target.path);
109
118
  fs.mkdirSync(dir, { recursive: true });
119
+ if (typeof backupPromptBeforeWrite === 'function') {
120
+ const bucket = 'system_' + target.scope + '_' + (target.scope === 'project'
121
+ ? sanitizeSystemHistoryId(path.dirname(target.path))
122
+ : 'global');
123
+ backupPromptBeforeWrite(bucket, target.path);
124
+ sysHistoryBucket = bucket;
125
+ }
110
126
  const finalContent = content.endsWith('\n') ? content : content + '\n';
111
127
  fs.writeFileSync(target.path, finalContent, { encoding: 'utf8', mode: 0o600 });
112
128
  try { fs.chmodSync(target.path, 0o600); } catch (_) {}
113
129
  } catch (e) {
114
130
  return { error: '写入 system prompt 失败: ' + e.message };
115
131
  }
116
- return readSystemPromptFilePath(target);
132
+ const saved = readSystemPromptFilePath(target);
133
+ if (sysHistoryBucket) saved.historyBucket = sysHistoryBucket;
134
+ return saved;
117
135
  }
118
136
 
119
137
  function readSystemPromptFilePath(resolved) {
package/cli.js CHANGED
@@ -108,6 +108,9 @@ const {
108
108
  const {
109
109
  createSystemPromptFileController
110
110
  } = require('./cli/system-prompt-files');
111
+ const {
112
+ createPromptHistoryController
113
+ } = require('./cli/prompt-history');
111
114
  const {
112
115
  createArchiveHelperController
113
116
  } = require('./cli/archive-helpers');
@@ -2057,6 +2060,17 @@ async function fetchProviderModels(providerName, overrides = {}) {
2057
2060
  }
2058
2061
 
2059
2062
  // buildAgentsDiff keeps the metaOnly optimization inside cli/agents-files.js.
2063
+ const {
2064
+ backupPromptBeforeWrite: historyBackup,
2065
+ listPromptHistory,
2066
+ readPromptHistory,
2067
+ clearPromptHistory
2068
+ } = createPromptHistoryController({
2069
+ fs,
2070
+ path,
2071
+ CONFIG_DIR
2072
+ });
2073
+
2060
2074
  const {
2061
2075
  resolveAgentsFilePath,
2062
2076
  validateAgentsBaseDir,
@@ -2083,6 +2097,7 @@ const {
2083
2097
  AGENTS_FILE_NAME,
2084
2098
  CLAUDE_DIR,
2085
2099
  CLAUDE_MD_FILE_NAME,
2100
+ backupPromptBeforeWrite: historyBackup,
2086
2101
  readOpenclawAgentsFile() {
2087
2102
  return readOpenclawAgentsFile(...arguments);
2088
2103
  },
@@ -2102,7 +2117,8 @@ const {
2102
2117
  crypto,
2103
2118
  buildLineDiff,
2104
2119
  CONFIG_DIR,
2105
- PI_AGENT_DIR
2120
+ PI_AGENT_DIR,
2121
+ backupPromptBeforeWrite: historyBackup
2106
2122
  });
2107
2123
 
2108
2124
  const {
@@ -13378,6 +13394,12 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
13378
13394
  case 'preview-system-prompt-diff':
13379
13395
  result = buildSystemPromptDiff(params || {});
13380
13396
  break;
13397
+ case 'list-prompt-history':
13398
+ result = listPromptHistory((params && params.bucket) || '');
13399
+ break;
13400
+ case 'get-prompt-history':
13401
+ result = readPromptHistory((params && params.bucket) || '', (params && params.id) || '');
13402
+ break;
13381
13403
  case 'switch':
13382
13404
  case 'use':
13383
13405
  case 'add':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codexmate",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "Codex/Claude Code/OpenClaw 配置、会话与任务编排 CLI + Web 工具",
5
5
  "main": "cli.js",
6
6
  "bin": {
package/web-ui/app.js CHANGED
@@ -74,6 +74,13 @@ document.addEventListener('DOMContentLoaded', () => {
74
74
  showAgentsModal: false,
75
75
  promptsSubTab: 'codex',
76
76
  projectClaudeMdPath: '',
77
+ promptHistoryVisible: false,
78
+ promptHistoryLoading: false,
79
+ promptHistoryBucket: '',
80
+ promptHistoryItems: [],
81
+ promptHistoryPreviewId: '',
82
+ promptHistoryPreviewContent: '',
83
+ promptHistoryError: '',
77
84
  promptPresets: [],
78
85
  selectedPromptPresetId: '',
79
86
  promptPresetNameDraft: '',
@@ -130,6 +137,7 @@ document.addEventListener('DOMContentLoaded', () => {
130
137
  configTemplateDiffFingerprint: '',
131
138
  _configTemplateDiffPreviewRequestToken: null,
132
139
  configTemplateDiffConfirmEnabled: true,
140
+ configModeVisibility: { codex: true, claude: true, openclaw: true, opencode: true, kilocode: true },
133
141
  codexApplying: false,
134
142
  _pendingCodexApplyOptions: null,
135
143
  agentsContent: '',
@@ -161,6 +169,13 @@ document.addEventListener('DOMContentLoaded', () => {
161
169
  sysPromptPath: '',
162
170
  sysPromptExists: false,
163
171
  sysPromptHash: '',
172
+ sysHistoryBucket: '',
173
+ sysHistoryVisible: false,
174
+ sysHistoryLoading: false,
175
+ sysHistoryItems: [],
176
+ sysHistoryPreviewId: '',
177
+ sysHistoryPreviewContent: '',
178
+ sysHistoryError: '',
164
179
  sysPromptLoading: false,
165
180
  sysPromptSaving: false,
166
181
  sysPromptDiffVisible: false,
@@ -20,6 +20,22 @@ function isValidOpenclawWorkspaceFileName(fileName) {
20
20
  return true;
21
21
  }
22
22
 
23
+ function sanitizePromptHistoryId(raw) {
24
+ const safe = typeof raw === 'string' ? raw.trim() : '';
25
+ if (!safe) return 'global';
26
+ const replaced = safe.replace(/[^A-Za-z0-9_.-]/g, '_');
27
+ return (replaced || 'global').slice(0, 64);
28
+ }
29
+
30
+ function resolveAgentsHistoryBucket(instance) {
31
+ const subTab = typeof instance.promptsSubTab === 'string' ? instance.promptsSubTab : 'codex';
32
+ if (subTab === 'claude-project') {
33
+ const projectPath = (instance.projectClaudeMdPath || '').trim();
34
+ return 'claude-project_' + sanitizePromptHistoryId(projectPath || 'global');
35
+ }
36
+ return 'codex_global';
37
+ }
38
+
23
39
  export function createAgentsMethods(options = {}) {
24
40
  const {
25
41
  api,
@@ -681,6 +697,9 @@ export function createAgentsMethods(options = {}) {
681
697
  this.showMessage(res.error, 'error');
682
698
  return;
683
699
  }
700
+ if (typeof res.historyBucket === 'string' && res.historyBucket) {
701
+ this.promptHistoryBucket = res.historyBucket;
702
+ }
684
703
  const successLabel = this.agentsContext === 'openclaw-workspace'
685
704
  ? this.t('toast.agents.saved.workspace', { name: this.agentsWorkspaceFileName || '' }).replace(/:\s*$/, '')
686
705
  : (this.agentsContext === 'claude-project'
@@ -884,6 +903,77 @@ export function createAgentsMethods(options = {}) {
884
903
  await this.persistPromptPresets();
885
904
  this.showMessage(this.t('prompts.presets.toast.deleted'), 'success');
886
905
  },
906
+ async openPromptHistory() {
907
+ if (this.promptHistoryLoading) return;
908
+ const bucket = this.promptHistoryBucket || resolveAgentsHistoryBucket(this);
909
+ this.promptHistoryVisible = true;
910
+ this.promptHistoryError = '';
911
+ await this.loadPromptHistory(bucket);
912
+ },
913
+
914
+ async loadPromptHistory(bucketArg) {
915
+ if (this.promptHistoryLoading) return;
916
+ const bucket = typeof bucketArg === 'string' && bucketArg.trim()
917
+ ? bucketArg.trim()
918
+ : (this.promptHistoryBucket || resolveAgentsHistoryBucket(this));
919
+ this.promptHistoryBucket = bucket;
920
+ this.promptHistoryLoading = true;
921
+ this.promptHistoryError = '';
922
+ this.promptHistoryItems = [];
923
+ this.promptHistoryPreviewId = '';
924
+ this.promptHistoryPreviewContent = '';
925
+ try {
926
+ const res = await api('list-prompt-history', { bucket });
927
+ if (res && res.error) {
928
+ this.promptHistoryError = res.error;
929
+ return;
930
+ }
931
+ this.promptHistoryItems = Array.isArray(res) ? res : [];
932
+ } catch (e) {
933
+ this.promptHistoryError = this.t('toast.load.fail');
934
+ } finally {
935
+ this.promptHistoryLoading = false;
936
+ }
937
+ },
938
+
939
+ closePromptHistory() {
940
+ this.promptHistoryVisible = false;
941
+ this.promptHistoryPreviewId = '';
942
+ this.promptHistoryPreviewContent = '';
943
+ this.promptHistoryError = '';
944
+ },
945
+
946
+ async viewPromptHistoryItem(item) {
947
+ if (!item || !item.id) return;
948
+ if (this.promptHistoryLoading) return;
949
+ if (this.promptHistoryPreviewId === item.id && this.promptHistoryPreviewContent) return;
950
+ this.promptHistoryPreviewId = item.id;
951
+ this.promptHistoryPreviewContent = '';
952
+ try {
953
+ const res = await api('get-prompt-history', { bucket: this.promptHistoryBucket, id: item.id });
954
+ if (res && res.error) {
955
+ this.promptHistoryError = res.error;
956
+ this.promptHistoryPreviewId = '';
957
+ return;
958
+ }
959
+ this.promptHistoryPreviewContent = typeof res.content === 'string' ? res.content : '';
960
+ } catch (e) {
961
+ this.promptHistoryError = this.t('toast.load.fail');
962
+ this.promptHistoryPreviewId = '';
963
+ }
964
+ },
965
+
966
+ applyPromptHistoryToEditor() {
967
+ if (!this.promptHistoryPreviewContent) return;
968
+ if (this.agentsSaving || this.agentsDiffVisible) return;
969
+ this.agentsContent = this.promptHistoryPreviewContent;
970
+ if (typeof this.onAgentsContentInput === 'function') this.onAgentsContentInput();
971
+ this.promptHistoryVisible = false;
972
+ this.promptHistoryPreviewId = '';
973
+ this.promptHistoryPreviewContent = '';
974
+ this.showMessage(this.t('toast.history.restored'), 'success');
975
+ },
976
+
887
977
  switchPromptsSubTab(subTab) {
888
978
  const normalized = subTab === 'claude-project' || subTab === 'system' ? subTab : 'codex';
889
979
  if (normalized === 'claude-project' && !this.projectPathOptions.length && !this.projectPathOptionsLoading) {
@@ -145,7 +145,13 @@
145
145
  if (typeof this.ensureMainTabSwitchState === 'function') {
146
146
  this.ensureMainTabSwitchState().pendingConfigMode = '';
147
147
  }
148
- this.configMode = configModeSet.has(normalizedMode) ? normalizedMode : 'codex';
148
+ const resolvedMode = configModeSet.has(normalizedMode) ? normalizedMode : 'codex';
149
+ if (typeof this.isConfigModeVisible === 'function' && !this.isConfigModeVisible(resolvedMode)) {
150
+ const fallback = ['codex', 'claude', 'openclaw', 'opencode', 'kilocode'].find(m => this.isConfigModeVisible(m)) || 'codex';
151
+ this.configMode = configModeSet.has(fallback) ? fallback : 'codex';
152
+ } else {
153
+ this.configMode = resolvedMode;
154
+ }
149
155
  if (this.mainTab === 'config') {
150
156
  if (this.configMode === 'kilocode' && typeof this.loadKilocodeConfig === 'function') {
151
157
  this.loadKilocodeConfig();
@@ -357,6 +357,42 @@ export function createSessionActionMethods(options = {}) {
357
357
  }
358
358
  },
359
359
 
360
+ normalizeConfigModeVisibility(value) {
361
+ const source = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
362
+ const result = {};
363
+ for (const mode of ['codex', 'claude', 'openclaw', 'opencode', 'kilocode']) {
364
+ result[mode] = typeof source[mode] === 'boolean' ? source[mode] : true;
365
+ }
366
+ return result;
367
+ },
368
+
369
+ isConfigModeVisible(mode) {
370
+ const normalizedMode = typeof mode === 'string' ? mode.trim().toLowerCase() : '';
371
+ if (!this.configModeVisibility) return true;
372
+ return this.configModeVisibility[normalizedMode] !== false;
373
+ },
374
+
375
+ setConfigModeVisibility(mode, visible) {
376
+ const normalizedMode = typeof mode === 'string' ? mode.trim().toLowerCase() : '';
377
+ if (!normalizedMode || !['codex', 'claude', 'openclaw', 'opencode', 'kilocode'].includes(normalizedMode)) return;
378
+ const visibility = this.configModeVisibility || { codex: true, claude: true, openclaw: true, opencode: true, kilocode: true };
379
+ const next = { ...visibility, [normalizedMode]: !!visible };
380
+ const visibleCount = Object.values(next).filter(Boolean).length;
381
+ if (visibleCount === 0) {
382
+ this.showMessage(typeof this.t === 'function' ? this.t('settings.configTabs.minRequired') : 'At least one tab must remain visible', 'info');
383
+ return;
384
+ }
385
+ this.configModeVisibility = next;
386
+ if (typeof this.persistWebUiPreferences === 'function') {
387
+ this.persistWebUiPreferences({ configModeVisibility: next });
388
+ }
389
+ if (this.mainTab === 'config' && this.configMode === normalizedMode && !next[normalizedMode]) {
390
+ const fallback = ['codex', 'claude', 'openclaw', 'opencode', 'kilocode'].find(m => next[m] !== false);
391
+ if (fallback) this.switchConfigMode(fallback);
392
+ }
393
+ },
394
+
395
+
360
396
  getShareCommandPrefixInvocation() {
361
397
  const prefix = this.normalizeShareCommandPrefix(this.shareCommandPrefix);
362
398
  return prefix === 'codexmate' ? 'codexmate' : 'npm start --';
@@ -393,6 +429,27 @@ export function createSessionActionMethods(options = {}) {
393
429
  }
394
430
  },
395
431
 
432
+ async copySessionMessage(msg) {
433
+ const text = msg && typeof msg.text === 'string' ? msg.text : '';
434
+ if (!text) {
435
+ this.showMessage(typeof this.t === 'function' ? this.t('toast.copy.empty') : 'Nothing to copy', 'info');
436
+ return;
437
+ }
438
+ const ok = this.fallbackCopyText(text);
439
+ if (ok) {
440
+ this.showMessage(typeof this.t === 'function' ? this.t('toast.copy.ok') : 'Copied', 'success');
441
+ return;
442
+ }
443
+ try {
444
+ if (navigator.clipboard && window.isSecureContext) {
445
+ await navigator.clipboard.writeText(text);
446
+ this.showMessage(typeof this.t === 'function' ? this.t('toast.copy.ok') : 'Copied', 'success');
447
+ return;
448
+ }
449
+ } catch (_) {}
450
+ this.showMessage(typeof this.t === 'function' ? this.t('toast.copy.fail') : 'Copy failed', 'error');
451
+ },
452
+
396
453
  copyAgentsContent() {
397
454
  const text = typeof this.agentsContent === 'string' ? this.agentsContent : '';
398
455
  if (!text) {