codexmate 0.1.20 → 0.1.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.
package/README.md CHANGED
@@ -63,7 +63,7 @@ Unlike simple wrappers, Codex Mate acts as a **Local Agent Bridge**:
63
63
  - **Pi Provider Config**: Manage Pi providers from the Web UI with lossless round-trip writes to `~/.pi/agent/models.json`, auto-fetched remote model lists, and one-click default provider/model switching via `~/.pi/agent/settings.json`.
64
64
  - **Provider Health Cleanup**: Probe local Codex and Claude provider routes, surface failed configs in one modal, and bulk-clean selected broken providers without touching healthy or protected entries.
65
65
  - **Skills Marketplace**: A local-first market to share and import skills between different agent apps.
66
- - **Prompt File Editor**: Unified editor for global and project-level `CLAUDE.md` and `AGENTS.md` with auto-detection of project paths, plus a shared preset pool for reusable prompts.
66
+ - **Prompt File Editor**: Unified editor for global and project-level `CLAUDE.md` and `AGENTS.md` (including OpenCode's `~/.config/opencode/AGENTS.md`) with auto-detection of project paths, plus a shared preset pool for reusable prompts.
67
67
  - **Task Orchestrator**: Plan and execute complex tasks with dependency tracking.
68
68
 
69
69
  ---
package/README.vi.md CHANGED
@@ -69,7 +69,7 @@ Khác với các wrapper đơn giản, Codex Mate hoạt động như một **Ag
69
69
  | **Bridge OpenAI** | ✅ | Chuyển đổi Codex Responses API sang định dạng OpenAI chuẩn |
70
70
  | **Kiểm tra sức khỏe Provider** | ✅ | Kiểm tra route provider Codex/Claude, đánh dấu cấu hình lỗi và xóa hàng loạt các provider hỏng đã chọn một cách an toàn |
71
71
  | **Mẫu Prompt** | ✅ | Plugin prompt tái sử dụng được với hỗ trợ biến |
72
- | **Trình sửa tệp prompt** | ✅ | Chỉnh sửa CLAUDE.md / AGENTS.md và dùng kho preset chung. Áp dụng preset chỉ cập nhật editor; cần lưu thủ công để ghi vào tệp. |
72
+ | **Trình sửa tệp prompt** | ✅ | Chỉnh sửa CLAUDE.md / AGENTS.md (bao gồm `~/.config/opencode/AGENTS.md` của OpenCode) và dùng kho preset chung. Áp dụng preset chỉ cập nhật editor; cần lưu thủ công để ghi vào tệp. |
73
73
  | **Tích hợp MCP** | ✅ | Expose tool và resource cục bộ qua MCP stdio |
74
74
  | **Tự động cập nhật** | ✅ | Cập nhật nhanh qua `codexmate update` |
75
75
 
package/README.zh.md CHANGED
@@ -61,7 +61,7 @@
61
61
  - **Pi Provider 配置**:在 Web UI 中管理 Pi 供应商,写入 `~/.pi/agent/models.json` 时无损保留未知字段,支持自动拉取远端模型列表,并可一键切换 `~/.pi/agent/settings.json` 中的默认供应商/模型。
62
62
  - **Provider 健康清理**:探测本地 Codex 与 Claude provider 路由,在同一个弹窗里集中展示失败配置,并批量清理已选坏配置,不影响正常或受保护项。
63
63
  - **Skills 市场**:本地优先的市场,支持在不同的智能体应用之间共享和导入 Skills。
64
- - **提示词文件编辑器**:统一编辑全局和项目级 `CLAUDE.md` 与 `AGENTS.md`,支持项目路径自动检测,并提供通用预设池复用 prompt。
64
+ - **提示词文件编辑器**:统一编辑全局和项目级 `CLAUDE.md` 与 `AGENTS.md`(含 OpenCode 的 `~/.config/opencode/AGENTS.md`),支持项目路径自动检测,并提供通用预设池复用 prompt。
65
65
  - **任务编排器**:支持带有依赖跟踪的复杂任务规划与执行。
66
66
 
67
67
  ---
@@ -15,6 +15,7 @@ function createAgentsFileController(deps = {}) {
15
15
  CLAUDE_MD_FILE_NAME,
16
16
  readOpenclawAgentsFile,
17
17
  readOpenclawWorkspaceFile,
18
+ OPENCODE_CONFIG_DIR,
18
19
  backupPromptBeforeWrite
19
20
  } = deps;
20
21
 
@@ -33,6 +34,7 @@ function createAgentsFileController(deps = {}) {
33
34
  if (typeof CLAUDE_MD_FILE_NAME !== 'string' || !CLAUDE_MD_FILE_NAME) throw new Error('createAgentsFileController 缺少 CLAUDE_MD_FILE_NAME');
34
35
  if (typeof readOpenclawAgentsFile !== 'function') throw new Error('createAgentsFileController 缺少 readOpenclawAgentsFile');
35
36
  if (typeof readOpenclawWorkspaceFile !== 'function') throw new Error('createAgentsFileController 缺少 readOpenclawWorkspaceFile');
37
+ if (typeof OPENCODE_CONFIG_DIR !== 'string' || !OPENCODE_CONFIG_DIR) throw new Error('createAgentsFileController 缺少 OPENCODE_CONFIG_DIR');
36
38
  if (typeof backupPromptBeforeWrite !== 'function' && typeof backupPromptBeforeWrite !== 'undefined') throw new Error('createAgentsFileController 备份回调无效');
37
39
 
38
40
  function resolveAgentsFilePath(params = {}) {
@@ -236,7 +238,9 @@ function createAgentsFileController(deps = {}) {
236
238
  }
237
239
  let agentsHistoryBucket = '';
238
240
  if (typeof backupPromptBeforeWrite === 'function') {
239
- const bucket = 'codex_' + sanitizeHistoryId(String(params.baseDir || '').trim() || 'global');
241
+ const explicitBucket = typeof params.historyBucket === 'string' ? params.historyBucket.trim() : '';
242
+ const bucket = explicitBucket
243
+ || ('codex_' + sanitizeHistoryId(String(params.baseDir || '').trim() || 'global'));
240
244
  backupPromptBeforeWrite(bucket, filePath);
241
245
  agentsHistoryBucket = bucket;
242
246
  }
@@ -254,6 +258,43 @@ function createAgentsFileController(deps = {}) {
254
258
  }
255
259
  }
256
260
 
261
+
262
+ function readOpencodeAgentsFile(params = {}) {
263
+ const baseDir = OPENCODE_CONFIG_DIR;
264
+ const filePath = path.join(baseDir, AGENTS_FILE_NAME);
265
+ const lineEndingFallback = os.EOL === '\r\n' ? '\r\n' : '\n';
266
+
267
+ if (!fs.existsSync(baseDir)) {
268
+ return {
269
+ exists: false,
270
+ path: filePath,
271
+ content: '',
272
+ lineEnding: lineEndingFallback,
273
+ configDir: baseDir,
274
+ baseDirMissing: true
275
+ };
276
+ }
277
+
278
+ const readResult = readAgentsFile({ baseDir, metaOnly: !!params.metaOnly });
279
+ return {
280
+ ...readResult,
281
+ configDir: baseDir
282
+ };
283
+ }
284
+
285
+ function applyOpencodeAgentsFile(params = {}) {
286
+ const baseDir = OPENCODE_CONFIG_DIR;
287
+ ensureDir(baseDir);
288
+ const result = applyAgentsFile({
289
+ ...params,
290
+ baseDir,
291
+ historyBucket: 'opencode_global'
292
+ });
293
+ return {
294
+ ...result,
295
+ configDir: baseDir
296
+ };
297
+ }
257
298
  function normalizeDiffText(input) {
258
299
  const safe = typeof input === 'string' ? input : '';
259
300
  return normalizeLineEnding(stripUtf8Bom(safe), '\n');
@@ -273,6 +314,8 @@ function createAgentsFileController(deps = {}) {
273
314
  readResult = readOpenclawAgentsFile({ metaOnly });
274
315
  } else if (context === 'openclaw-workspace') {
275
316
  readResult = readOpenclawWorkspaceFile({ ...params, metaOnly });
317
+ } else if (context === 'opencode') {
318
+ readResult = readOpencodeAgentsFile({ metaOnly });
276
319
  } else if (context === 'codex') {
277
320
  readResult = readAgentsFile({ ...params, metaOnly });
278
321
  } else {
@@ -310,6 +353,8 @@ function createAgentsFileController(deps = {}) {
310
353
  applyClaudeMdFile,
311
354
  readAgentsFile,
312
355
  applyAgentsFile,
356
+ readOpencodeAgentsFile,
357
+ applyOpencodeAgentsFile,
313
358
  normalizeDiffText,
314
359
  buildAgentsDiff
315
360
  };
package/cli.js CHANGED
@@ -1190,7 +1190,8 @@ function getApiToolConfigWriteTarget(action) {
1190
1190
  ]);
1191
1191
  const opencodeWriteActions = new Set([
1192
1192
  'apply-opencode-config',
1193
- 'update-opencode-selection'
1193
+ 'update-opencode-selection',
1194
+ 'apply-opencode-agents-file'
1194
1195
  ]);
1195
1196
  const kilocodeWriteActions = new Set([
1196
1197
  'apply-kilocode-config',
@@ -2372,6 +2373,8 @@ const {
2372
2373
  applyClaudeMdFile,
2373
2374
  readAgentsFile,
2374
2375
  applyAgentsFile,
2376
+ readOpencodeAgentsFile,
2377
+ applyOpencodeAgentsFile,
2375
2378
  normalizeDiffText,
2376
2379
  buildAgentsDiff
2377
2380
  } = createAgentsFileController({
@@ -2394,7 +2397,8 @@ const {
2394
2397
  },
2395
2398
  readOpenclawWorkspaceFile() {
2396
2399
  return readOpenclawWorkspaceFile(...arguments);
2397
- }
2400
+ },
2401
+ OPENCODE_CONFIG_DIR
2398
2402
  });
2399
2403
 
2400
2404
  const {
@@ -13694,6 +13698,12 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
13694
13698
  case 'get-openclaw-agents-file':
13695
13699
  result = readOpenclawAgentsFile();
13696
13700
  break;
13701
+ case 'get-opencode-agents-file':
13702
+ result = readOpencodeAgentsFile(params || {});
13703
+ break;
13704
+ case 'apply-opencode-agents-file':
13705
+ result = applyOpencodeAgentsFile(params || {});
13706
+ break;
13697
13707
  case 'apply-openclaw-agents-file':
13698
13708
  result = applyOpenclawAgentsFile(params || {});
13699
13709
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codexmate",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
4
4
  "description": "Codex/Claude Code/OpenClaw 配置、会话与任务编排 CLI + Web 工具",
5
5
  "main": "cli.js",
6
6
  "bin": {
package/web-ui/app.js CHANGED
@@ -102,6 +102,14 @@ document.addEventListener('DOMContentLoaded', () => {
102
102
  showConfigTemplateModal: false,
103
103
  showAgentsModal: false,
104
104
  promptsSubTab: 'codex',
105
+ promptsPreviewEnabled: true,
106
+ promptsPreviewCollapsed: false,
107
+ promptsMobileView: 'edit',
108
+ promptsPreviewLibsMissing: false,
109
+ agentsHighlightHtml: '',
110
+ sysPromptHighlightHtml: '',
111
+ agentsPreviewHtml: '',
112
+ sysPromptPreviewHtml: '',
105
113
  projectClaudeMdPath: '',
106
114
  promptHistoryVisible: false,
107
115
  promptHistoryLoading: false,
@@ -812,6 +820,12 @@ document.addEventListener('DOMContentLoaded', () => {
812
820
  if (typeof this.loadPromptsTabContent === 'function') this.loadPromptsTabContent();
813
821
  }
814
822
  },
823
+ agentsContent() {
824
+ if (typeof this.schedulePromptsEditorRefresh === 'function') this.schedulePromptsEditorRefresh('agents');
825
+ },
826
+ sysPromptContent() {
827
+ if (typeof this.schedulePromptsEditorRefresh === 'function') this.schedulePromptsEditorRefresh('sys');
828
+ },
815
829
  projectClaudeMdPath(newPath) {
816
830
  if (typeof this.persistWebUiPreferences === 'function') {
817
831
  this.persistWebUiPreferences({ projectClaudeMdPath: newPath || '' });
package/web-ui/index.html CHANGED
@@ -8,6 +8,9 @@
8
8
  <link rel="apple-touch-icon" href="/res/logo-pack.webp">
9
9
  <script src="/res/vue.runtime.global.prod.js"></script>
10
10
  <script src="/res/json5.min.js"></script>
11
+ <script src="/res/highlight.min.js"></script>
12
+ <script src="/res/marked.min.js"></script>
13
+ <script src="/res/purify.min.js"></script>
11
14
  <link rel="stylesheet" href="/web-ui/styles.css">
12
15
  </head>
13
16
  <body>
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Pure logic for the prompts-panel Markdown editor.
3
+ * No DOM access in this module so every function stays unit-testable.
4
+ *
5
+ * Syntax highlighting delegates to the vendored highlight.js UMD build
6
+ * (web-ui/res/highlight.min.js, "markdown" grammar is included in the
7
+ * common build) and the rendered highlight markup is layered behind a
8
+ * transparent textarea. Preview rendering delegates to the vendored
9
+ * marked + DOMPurify pair; raw markdown may embed arbitrary HTML, so
10
+ * sanitize is mandatory before the result reaches v-html.
11
+ */
12
+
13
+ const HIGHLIGHT_MAX_CHARS = 200000;
14
+ const HTML_ESCAPE_MAP = {
15
+ '&': '&amp;',
16
+ '<': '&lt;',
17
+ '>': '&gt;',
18
+ '"': '&quot;',
19
+ "'": '&#39;'
20
+ };
21
+
22
+
23
+ let hasWarnedHighlightFallback = false;
24
+
25
+ export function escapeHtmlText(text) {
26
+ return String(text === null || text === undefined ? '' : text)
27
+ .replace(/[&<>"']/g, (ch) => HTML_ESCAPE_MAP[ch]);
28
+ }
29
+
30
+ function plainHighlightFallback(text) {
31
+ if (!hasWarnedHighlightFallback) {
32
+ hasWarnedHighlightFallback = true;
33
+ // eslint-disable-next-line no-console
34
+ console.warn('prompts editor: highlight.js unavailable, editor falls back to plain text rendering');
35
+ }
36
+ return escapeHtmlText(text) + '\n';
37
+ }
38
+
39
+ /**
40
+ * Produce the highlighted HTML layer for a markdown source string.
41
+ * highlight.js escapes HTML entities itself; the trailing newline keeps
42
+ * the <pre> backdrop visually aligned with the textarea's final line.
43
+ * Returns escaped plain text when hljs is missing or the input exceeds
44
+ * the highlight budget (observable degradation, never a fake success).
45
+ */
46
+ export function highlightMarkdownText(text, hljs) {
47
+ const source = typeof text === 'string' ? text : '';
48
+ if (!hljs || typeof hljs.highlight !== 'function' || typeof hljs.getLanguage !== 'function' || !hljs.getLanguage('markdown')) {
49
+ return plainHighlightFallback(source);
50
+ }
51
+ if (source.length > HIGHLIGHT_MAX_CHARS) {
52
+ return plainHighlightFallback(source);
53
+ }
54
+ try {
55
+ return hljs.highlight(source, { language: 'markdown', ignoreUnescapedHTML: true }).value + '\n';
56
+ } catch (e) {
57
+ return plainHighlightFallback(source);
58
+ }
59
+ }
60
+
61
+
62
+ /**
63
+ * Render a markdown preview. Returns a state object instead of raw HTML so
64
+ * the UI can surface "libs missing" and "empty input" explicitly instead
65
+ * of faking a successful render.
66
+ */
67
+ export function buildMarkdownPreviewHtml(text, marked, purify) {
68
+ const source = typeof text === 'string' ? text : '';
69
+ if (!source.trim()) {
70
+ return { empty: true, html: '' };
71
+ }
72
+ if (!marked || typeof marked.parse !== 'function' || !purify || typeof purify.sanitize !== 'function') {
73
+ return { unavailable: true, html: '' };
74
+ }
75
+ const rawHtml = marked.parse(source, { gfm: true, breaks: false });
76
+ return { html: purify.sanitize(String(rawHtml)) };
77
+ }
@@ -112,30 +112,39 @@ function extractWorkspaceArtifacts(line, target) {
112
112
  }
113
113
  }
114
114
 
115
- function buildWorkspaceBriefText(summary, labels = {}) {
115
+ function buildWorkspaceBriefText(summary, labels = {}, metricLabels = {}) {
116
116
  if (!summary || !summary.available) return '';
117
117
  const label = (key, fallback) => (labels && labels[key]) || fallback;
118
+ const metricLabel = (key, fallback) => (metricLabels && metricLabels[key]) || fallback;
118
119
  const lines = [
119
120
  `# ${label('title', 'Session workspace brief')}`,
120
121
  '',
121
122
  `${label('source', 'Source')}: ${summary.sourceLabel || summary.source || '-'}`,
122
123
  `${label('messages', 'Messages')}: ${summary.messageCount}`,
124
+ `${metricLabel('userLabel', 'User')}: ${summary.roleCounts.user}`,
125
+ `${metricLabel('assistantLabel', 'Assistant')}: ${summary.roleCounts.assistant}`,
126
+ `${metricLabel('commandsLabel', 'Commands')}: ${summary.commands.length}`,
127
+ `${metricLabel('artifactsLabel', 'Artifacts')}: ${summary.files.length + summary.links.length}`,
128
+ `${metricLabel('risksLabel', 'Risks')}: ${summary.risks.length + summary.nextSteps.length}`,
123
129
  `${label('path', 'Path')}: ${summary.cwd || '-'}`,
124
130
  ''
125
131
  ];
126
132
  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
+ ['signals', label('signals', 'Signals'), summary.signals, label('noneSignals', 'No strong signals yet')],
134
+ ['commands', label('commands', 'Reusable commands'), summary.commands, label('noneCommands', 'No command signals')],
135
+ ['files', label('files', 'Files'), summary.files, label('noneFiles', 'No file signals')],
136
+ ['links', label('links', 'Links'), summary.links, label('noneLinks', 'No link signals')],
137
+ ['risks', label('risks', 'Risks / blockers'), summary.risks, label('noneRisks', 'No risks or todos detected')],
138
+ ['nextSteps', label('nextSteps', 'Next steps'), summary.nextSteps, label('noneRisks', 'No risks or todos detected')]
133
139
  ];
134
- for (const [, sectionTitle, items] of sections) {
135
- if (!Array.isArray(items) || !items.length) continue;
140
+ for (const [, sectionTitle, items, emptyText] of sections) {
136
141
  lines.push(`## ${sectionTitle}`);
137
- for (const item of items) {
138
- lines.push(`- ${item}`);
142
+ if (Array.isArray(items) && items.length) {
143
+ for (const item of items) {
144
+ lines.push(`- ${item}`);
145
+ }
146
+ } else {
147
+ lines.push(`- ${emptyText}`);
139
148
  }
140
149
  lines.push('');
141
150
  }
@@ -231,7 +240,13 @@ export function buildSessionWorkspaceSummary(session, messages, options = {}) {
231
240
  risks,
232
241
  nextSteps
233
242
  };
234
- summary.briefText = buildWorkspaceBriefText(summary, options.briefLabels || {});
243
+ summary.briefText = buildWorkspaceBriefText(summary, options.briefLabels || {}, {
244
+ userLabel: options.userLabel,
245
+ assistantLabel: options.assistantLabel,
246
+ commandsLabel: options.commandsLabel,
247
+ artifactsLabel: options.artifactsLabel,
248
+ risksLabel: options.risksLabel
249
+ });
235
250
  return summary;
236
251
  }
237
252
 
@@ -474,7 +489,12 @@ export function createSessionComputed() {
474
489
  files: t ? t('sessions.workspace.files') : 'Files',
475
490
  links: t ? t('sessions.workspace.links') : 'Links',
476
491
  risks: t ? t('sessions.workspace.risks') : 'Risks / blockers',
477
- nextSteps: t ? t('sessions.workspace.nextSteps') : 'Next steps'
492
+ nextSteps: t ? t('sessions.workspace.nextSteps') : 'Next steps',
493
+ noneSignals: t ? t('sessions.workspace.empty') : 'No strong signals yet',
494
+ noneCommands: t ? t('sessions.workspace.noneCommands') : 'No command signals',
495
+ noneFiles: t ? t('sessions.workspace.noneFiles') : 'No file signals',
496
+ noneLinks: t ? t('sessions.workspace.noneLinks') : 'No link signals',
497
+ noneRisks: t ? t('sessions.workspace.noneRisks') : 'No risks or todos detected'
478
498
  }
479
499
  });
480
500
  },
@@ -33,6 +33,9 @@ function resolveAgentsHistoryBucket(instance) {
33
33
  const projectPath = (instance.projectClaudeMdPath || '').trim();
34
34
  return 'claude-project_' + sanitizePromptHistoryId(projectPath || 'global');
35
35
  }
36
+ if (subTab === 'opencode') {
37
+ return 'opencode_global';
38
+ }
36
39
  return 'codex_global';
37
40
  }
38
41
 
@@ -216,6 +219,13 @@ export function createAgentsMethods(options = {}) {
216
219
  this.agentsModalHint = tr('modal.agents.hint.openclawWorkspaceFile', `Workspace / ${fileName}`, { fileName });
217
220
  return;
218
221
  }
222
+ if (context === 'opencode') {
223
+ this.agentsContext = 'opencode';
224
+ this.agentsWorkspaceFileName = '';
225
+ this.agentsModalTitle = tr('modal.agents.title.opencode', 'OpenCode AGENTS.md 编辑器');
226
+ this.agentsModalHint = tr('modal.agents.hint.opencode', '保存后会写入 ~/.config/opencode/AGENTS.md。');
227
+ return;
228
+ }
219
229
  this.agentsContext = context === 'openclaw' ? 'openclaw' : 'codex';
220
230
  if (this.agentsContext === 'openclaw') {
221
231
  this.agentsModalTitle = tr('modal.agents.title.openclaw', 'OpenClaw AGENTS.md 编辑器');
@@ -700,6 +710,8 @@ export function createAgentsMethods(options = {}) {
700
710
  }
701
711
  } else if (this.agentsContext === 'openclaw') {
702
712
  action = 'apply-openclaw-agents-file';
713
+ } else if (this.agentsContext === 'opencode') {
714
+ action = 'apply-opencode-agents-file';
703
715
  } else if (this.agentsContext === 'openclaw-workspace') {
704
716
  action = 'apply-openclaw-workspace-file';
705
717
  params.fileName = this.agentsWorkspaceFileName;
@@ -716,7 +728,11 @@ export function createAgentsMethods(options = {}) {
716
728
  ? this.t('toast.agents.saved.workspace', { name: this.agentsWorkspaceFileName || '' }).replace(/:\s*$/, '')
717
729
  : (this.agentsContext === 'claude-project'
718
730
  ? this.t('toast.agents.saved.claudeProject')
719
- : (this.agentsContext === 'openclaw' ? this.t('toast.agents.saved.openclaw') : this.t('toast.agents.saved.agents')));
731
+ : (this.agentsContext === 'openclaw'
732
+ ? this.t('toast.agents.saved.openclaw')
733
+ : (this.agentsContext === 'opencode'
734
+ ? this.t('toast.agents.saved.opencode')
735
+ : this.t('toast.agents.saved.agents'))));
720
736
  this.showMessage(successLabel, 'success');
721
737
  if (this.mainTab === 'prompts') {
722
738
  this.loadPromptsContent();
@@ -987,7 +1003,7 @@ export function createAgentsMethods(options = {}) {
987
1003
  },
988
1004
 
989
1005
  switchPromptsSubTab(subTab) {
990
- const normalized = subTab === 'claude-project' || subTab === 'system' ? subTab : 'codex';
1006
+ const normalized = subTab === 'claude-project' || subTab === 'system' || subTab === 'opencode' ? subTab : 'codex';
991
1007
  if (normalized === 'claude-project' && !this.projectPathOptions.length && !this.projectPathOptionsLoading) {
992
1008
  this.loadProjectPathOptions();
993
1009
  }
@@ -1035,6 +1051,8 @@ export function createAgentsMethods(options = {}) {
1035
1051
  if (projectPath) {
1036
1052
  rpcParams.baseDir = projectPath;
1037
1053
  }
1054
+ } else if (subTab === 'opencode') {
1055
+ action = 'get-opencode-agents-file';
1038
1056
  } else {
1039
1057
  action = 'get-agents-file';
1040
1058
  }
@@ -1051,7 +1069,7 @@ export function createAgentsMethods(options = {}) {
1051
1069
  this.agentsPath = res.path || '';
1052
1070
  this.agentsExists = !!res.exists;
1053
1071
  this.agentsLineEnding = res.lineEnding === '\r\n' ? '\r\n' : '\n';
1054
- this.agentsContext = subTab === 'claude-project' ? 'claude-project' : 'codex';
1072
+ this.agentsContext = subTab === 'claude-project' ? 'claude-project' : (subTab === 'opencode' ? 'opencode' : 'codex');
1055
1073
  } catch (e) {
1056
1074
  if (!isLatestRequestToken(this, '_agentsOpenRequestToken', requestToken)) {
1057
1075
  return;
@@ -13,6 +13,7 @@ import {
13
13
  } from './app.constants.mjs';
14
14
  import { createAgentsMethods } from './app.methods.agents.mjs';
15
15
  import { createSystemPromptMethods } from './app.methods.system-prompt.mjs';
16
+ import { createPromptsEditorMethods } from './app.methods.prompts-editor.mjs';
16
17
  import { createClaudeConfigMethods } from './app.methods.claude-config.mjs';
17
18
  import { createCodexConfigMethods } from './app.methods.codex-config.mjs';
18
19
  import { createInstallMethods } from './app.methods.install.mjs';
@@ -51,6 +52,7 @@ import {
51
52
  export function createAppMethods() {
52
53
  return {
53
54
  ...createI18nMethods(),
55
+ ...createPromptsEditorMethods(),
54
56
  ...createWebhookMethods(),
55
57
  ...createStartupClaudeMethods({
56
58
  api,
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Prompts-panel Markdown editor methods: overlay highlight sync,
3
+ * mobile view switching, and the live split preview (marked + DOMPurify,
4
+ * debounced). One implementation is shared by the agentsContent editor
5
+ * and the sysPromptContent editor.
6
+ *
7
+ * State contract (declared in app.js data()):
8
+ * promptsPreviewEnabled, promptsPreviewCollapsed, promptsMobileView,
9
+ * promptsPreviewLibsMissing, agentsHighlightHtml, sysPromptHighlightHtml,
10
+ * agentsPreviewHtml, sysPromptPreviewHtml
11
+ * Template contract:
12
+ * refs promptsAgentsTextarea / promptsSysTextarea on the two textareas,
13
+ * refs promptsAgentsHighlight / promptsSysHighlight on the backdrop <pre>.
14
+ */
15
+
16
+ import {
17
+ buildMarkdownPreviewHtml,
18
+ highlightMarkdownText
19
+ } from '../logic.markdown-editor.mjs';
20
+
21
+ const PROMPTS_HIGHLIGHT_DEBOUNCE_MS = 150;
22
+ const PROMPTS_PREVIEW_DEBOUNCE_MS = 300;
23
+
24
+ const PROMPTS_EDITOR_META = {
25
+ agents: {
26
+ contentField: 'agentsContent',
27
+ highlightField: 'agentsHighlightHtml',
28
+ previewField: 'agentsPreviewHtml',
29
+ textareaRef: 'promptsAgentsTextarea',
30
+ highlightRef: 'promptsAgentsHighlight'
31
+ },
32
+ sys: {
33
+ contentField: 'sysPromptContent',
34
+ highlightField: 'sysPromptHighlightHtml',
35
+ previewField: 'sysPromptPreviewHtml',
36
+ textareaRef: 'promptsSysTextarea',
37
+ highlightRef: 'promptsSysHighlight'
38
+ }
39
+ };
40
+
41
+ function resolvePromptsHljs() {
42
+ if (typeof window !== 'undefined' && window.hljs) {
43
+ return window.hljs;
44
+ }
45
+ return null;
46
+ }
47
+
48
+ function resolvePromptsPreviewLibs() {
49
+ if (typeof window === 'undefined') {
50
+ return { marked: null, purify: null };
51
+ }
52
+ return { marked: window.marked || null, purify: window.DOMPurify || null };
53
+ }
54
+
55
+ export function createPromptsEditorMethods() {
56
+ return {
57
+ promptsEditorMeta(key) {
58
+ return PROMPTS_EDITOR_META[key] || null;
59
+ },
60
+
61
+
62
+ refreshPromptsHighlight(key) {
63
+ const meta = this.promptsEditorMeta(key);
64
+ if (!meta) {
65
+ return;
66
+ }
67
+ this[meta.highlightField] = highlightMarkdownText(this[meta.contentField], resolvePromptsHljs());
68
+ this.syncPromptsOverlayScroll(key);
69
+ },
70
+
71
+ schedulePromptsHighlight(key) {
72
+ const meta = this.promptsEditorMeta(key);
73
+ if (!meta) {
74
+ return;
75
+ }
76
+ if (!this._promptsHighlightTimers) {
77
+ this._promptsHighlightTimers = {};
78
+ }
79
+ if (this._promptsHighlightTimers[key]) {
80
+ clearTimeout(this._promptsHighlightTimers[key]);
81
+ }
82
+ this._promptsHighlightTimers[key] = setTimeout(() => {
83
+ this._promptsHighlightTimers[key] = null;
84
+ this.refreshPromptsHighlight(key);
85
+ }, PROMPTS_HIGHLIGHT_DEBOUNCE_MS);
86
+ },
87
+
88
+ syncPromptsOverlayScroll(key) {
89
+ const meta = this.promptsEditorMeta(key);
90
+ if (!meta || typeof this.$refs !== 'object' || !this.$refs) {
91
+ return;
92
+ }
93
+ const textarea = this.$refs[meta.textareaRef];
94
+ const backdrop = this.$refs[meta.highlightRef];
95
+ if (!textarea || !backdrop) {
96
+ return;
97
+ }
98
+ backdrop.scrollTop = textarea.scrollTop;
99
+ backdrop.scrollLeft = textarea.scrollLeft;
100
+ },
101
+
102
+ refreshPromptsPreview(key) {
103
+ const meta = this.promptsEditorMeta(key);
104
+ if (!meta) {
105
+ return;
106
+ }
107
+ if (!this.promptsPreviewEnabled) {
108
+ return;
109
+ }
110
+ const libs = resolvePromptsPreviewLibs();
111
+ const state = buildMarkdownPreviewHtml(this[meta.contentField], libs.marked, libs.purify);
112
+ if (state.unavailable) {
113
+ this.promptsPreviewLibsMissing = true;
114
+ this[meta.previewField] = '';
115
+ return;
116
+ }
117
+ this.promptsPreviewLibsMissing = false;
118
+ this[meta.previewField] = state.html;
119
+ },
120
+
121
+ schedulePromptsPreview(key) {
122
+ const meta = this.promptsEditorMeta(key);
123
+ if (!meta) {
124
+ return;
125
+ }
126
+ if (!this._promptsPreviewTimers) {
127
+ this._promptsPreviewTimers = {};
128
+ }
129
+ if (this._promptsPreviewTimers[key]) {
130
+ clearTimeout(this._promptsPreviewTimers[key]);
131
+ }
132
+ this._promptsPreviewTimers[key] = setTimeout(() => {
133
+ this._promptsPreviewTimers[key] = null;
134
+ this.refreshPromptsPreview(key);
135
+ }, PROMPTS_PREVIEW_DEBOUNCE_MS);
136
+ },
137
+
138
+ schedulePromptsEditorRefresh(key) {
139
+ this.schedulePromptsHighlight(key);
140
+ this.schedulePromptsPreview(key);
141
+ },
142
+
143
+ setPromptsMobileView(view) {
144
+ if (view === 'edit' || view === 'preview') {
145
+ this.promptsMobileView = view;
146
+ }
147
+ },
148
+
149
+ togglePromptsPreviewCollapsed() {
150
+ this.promptsPreviewCollapsed = !this.promptsPreviewCollapsed;
151
+ }
152
+ };
153
+ }