codexmate 0.1.23 → 0.1.25

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 (37) hide show
  1. package/cli.js +4 -1
  2. package/package.json +1 -1
  3. package/plugins/prompt-templates/computed.mjs +1 -1
  4. package/plugins/prompt-templates/index.mjs +3 -3
  5. package/plugins/prompt-templates/manifest.mjs +5 -3
  6. package/plugins/prompt-templates/methods.mjs +9 -2
  7. package/plugins/registry.mjs +5 -2
  8. package/plugins/text-tools/computed.mjs +23 -0
  9. package/plugins/text-tools/index.mjs +14 -0
  10. package/plugins/text-tools/manifest.mjs +20 -0
  11. package/plugins/text-tools/methods.mjs +24 -0
  12. package/plugins/text-tools/overview.mjs +12 -0
  13. package/plugins/text-tools/ownership.mjs +7 -0
  14. package/web-ui/app.js +5 -2
  15. package/web-ui/index.html +0 -1
  16. package/web-ui/logic.markdown-editor.mjs +5 -57
  17. package/web-ui/logic.prompts-change-axis.mjs +3 -88
  18. package/web-ui/logic.prompts-path-reference.mjs +118 -0
  19. package/web-ui/modules/app.methods.agents.mjs +1 -0
  20. package/web-ui/modules/app.methods.index.mjs +1 -1
  21. package/web-ui/modules/app.methods.navigation.mjs +79 -3
  22. package/web-ui/modules/app.methods.prompts-editor.mjs +140 -145
  23. package/web-ui/modules/app.methods.web-ui-preferences.mjs +7 -1
  24. package/web-ui/modules/i18n/locales/en.mjs +22 -1
  25. package/web-ui/modules/i18n/locales/ja.mjs +22 -1
  26. package/web-ui/modules/i18n/locales/vi.mjs +22 -1
  27. package/web-ui/modules/i18n/locales/zh-tw.mjs +22 -1
  28. package/web-ui/modules/i18n/locales/zh.mjs +22 -1
  29. package/web-ui/modules/plugins.computed.mjs +8 -2
  30. package/web-ui/modules/plugins.methods.mjs +8 -2
  31. package/web-ui/partials/index/layout-header.html +19 -4
  32. package/web-ui/partials/index/panel-plugins.html +45 -5
  33. package/web-ui/partials/index/panel-prompts.html +24 -31
  34. package/web-ui/res/web-ui-render.precompiled.js +142 -130
  35. package/web-ui/session-helpers.mjs +0 -1
  36. package/web-ui/source-bundle.cjs +1 -0
  37. package/web-ui/styles/prompts-editor.css +20 -91
package/cli.js CHANGED
@@ -1098,7 +1098,10 @@ function normalizeWebUiPreferences(value = {}) {
1098
1098
  skillsTargetApp: navigation.skillsTargetApp === 'claude' || navigation.skillsTargetApp === 'pi'
1099
1099
  ? navigation.skillsTargetApp
1100
1100
  : 'codex',
1101
- promptTemplatesMode: navigation.promptTemplatesMode === 'manage' ? 'manage' : 'compose'
1101
+ promptTemplatesMode: navigation.promptTemplatesMode === 'manage' ? 'manage' : 'compose',
1102
+ pluginsActiveId: typeof navigation.pluginsActiveId === 'string' && navigation.pluginsActiveId.trim()
1103
+ ? navigation.pluginsActiveId.trim()
1104
+ : 'prompt-templates'
1102
1105
  }
1103
1106
  };
1104
1107
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codexmate",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "description": "Codex/Claude Code/OpenClaw 配置、会话与任务编排 CLI + Web 工具",
5
5
  "main": "cli.js",
6
6
  "bin": {
@@ -158,7 +158,7 @@ function localizeBuiltinPromptTemplate(item, t) {
158
158
 
159
159
  import { pluginsRegistry } from '../registry.mjs';
160
160
 
161
- export function createPluginsComputed() {
161
+ export function createPromptTemplatesComputed() {
162
162
  return {
163
163
  pluginsCatalog() {
164
164
  return pluginsRegistry
@@ -1,6 +1,6 @@
1
- export { createPluginsMethods } from './methods.mjs';
2
- export { createPluginsComputed } from './computed.mjs';
3
- export { pluginMeta } from './manifest.mjs';
1
+ export { createPromptTemplatesMethods as createPluginsMethods } from './methods.mjs';
2
+ export { createPromptTemplatesComputed as createPluginsComputed } from './computed.mjs';
3
+ export { promptTemplatesPluginMeta as pluginMeta } from './manifest.mjs';
4
4
  export {
5
5
  readPromptTemplatesFromStorage,
6
6
  persistPromptTemplatesToStorage,
@@ -1,6 +1,6 @@
1
1
  import { pluginOwnership } from './ownership.mjs';
2
2
 
3
- const baseMeta = {
3
+ const promptTemplatesBaseMeta = {
4
4
  id: 'prompt-templates',
5
5
  title: 'Prompt Templates',
6
6
  titleKey: 'plugins.catalog.promptTemplates.title',
@@ -11,8 +11,10 @@ const baseMeta = {
11
11
  tone: 'configured'
12
12
  };
13
13
 
14
- export const pluginMeta = {
15
- ...baseMeta,
14
+ export const promptTemplatesPluginMeta = {
15
+ ...promptTemplatesBaseMeta,
16
16
  createdBy: pluginOwnership && typeof pluginOwnership.createdBy === 'string' ? pluginOwnership.createdBy : '',
17
17
  maintainers: pluginOwnership && Array.isArray(pluginOwnership.maintainers) ? pluginOwnership.maintainers : []
18
18
  };
19
+
20
+ export { promptTemplatesPluginMeta as pluginMeta };
@@ -79,7 +79,7 @@ function normalizePromptTemplateDraft(draft) {
79
79
  };
80
80
  }
81
81
 
82
- export function createPluginsMethods() {
82
+ export function createPromptTemplatesMethods() {
83
83
  return {
84
84
  resetPromptComposerVarValues() {
85
85
  this.promptComposerVarValuesRaw = {};
@@ -280,8 +280,15 @@ export function createPluginsMethods() {
280
280
  selectPlugin(pluginId) {
281
281
  const id = typeof pluginId === 'string' ? pluginId.trim() : '';
282
282
  if (!id) return;
283
- if (!getPluginEntry(id)) return;
283
+ const entry = getPluginEntry(id);
284
+ if (!entry) return;
284
285
  this.pluginsActiveId = id;
286
+ if (typeof this.saveNavState === 'function') {
287
+ this.saveNavState();
288
+ }
289
+ if (typeof entry.loadOverview === 'function' && typeof this.loadPluginsOverview === 'function') {
290
+ void Promise.resolve(this.loadPluginsOverview({ silent: true })).catch(() => {});
291
+ }
285
292
  },
286
293
 
287
294
  async loadPluginsOverview(options = {}) {
@@ -1,8 +1,11 @@
1
- import { pluginMeta as promptTemplatesMeta } from './prompt-templates/manifest.mjs';
1
+ import { promptTemplatesPluginMeta } from './prompt-templates/manifest.mjs';
2
2
  import { loadPromptTemplatesOverview } from './prompt-templates/overview.mjs';
3
+ import { textToolsPluginMeta } from './text-tools/manifest.mjs';
4
+ import { loadTextToolsOverview } from './text-tools/overview.mjs';
3
5
 
4
6
  export const pluginsRegistry = [
5
- { id: promptTemplatesMeta.id, meta: promptTemplatesMeta, loadOverview: loadPromptTemplatesOverview }
7
+ { id: promptTemplatesPluginMeta.id, meta: promptTemplatesPluginMeta, loadOverview: loadPromptTemplatesOverview },
8
+ { id: textToolsPluginMeta.id, meta: textToolsPluginMeta, loadOverview: loadTextToolsOverview }
6
9
  ];
7
10
 
8
11
  export function getFirstPluginId() {
@@ -0,0 +1,23 @@
1
+ import { textTools, DEFAULT_TEXT_TOOL_ID } from './index.mjs';
2
+
3
+ export function createTextToolsComputed() {
4
+ return {
5
+ textToolsList() {
6
+ const t = typeof this.t === 'function' ? this.t : null;
7
+ return textTools.map((tool) => ({
8
+ id: tool.id,
9
+ labelKey: tool.labelKey,
10
+ label: t ? t(tool.labelKey) : tool.labelKey
11
+ }));
12
+ },
13
+
14
+ textToolsOutput() {
15
+ const activeId = typeof this.textToolsActiveToolId === 'string' ? this.textToolsActiveToolId.trim() : '';
16
+ const tool = textTools.find((item) => item && item.id === activeId)
17
+ || textTools.find((item) => item && item.id === DEFAULT_TEXT_TOOL_ID)
18
+ || null;
19
+ const input = typeof this.textToolsInput === 'string' ? this.textToolsInput : '';
20
+ return tool && typeof tool.run === 'function' ? tool.run(input) : '';
21
+ }
22
+ };
23
+ }
@@ -0,0 +1,14 @@
1
+ export const DEFAULT_TEXT_TOOL_ID = 'compress';
2
+
3
+ export function compressTextToSingleLine(input) {
4
+ if (typeof input !== 'string') return '';
5
+ return input.replace(/\s+/g, ' ').trim();
6
+ }
7
+
8
+ export const textTools = [
9
+ {
10
+ id: 'compress',
11
+ labelKey: 'plugins.textTools.tools.compress',
12
+ run: compressTextToSingleLine
13
+ }
14
+ ];
@@ -0,0 +1,20 @@
1
+ import { textToolsPluginOwnership } from './ownership.mjs';
2
+
3
+ const textToolsBaseMeta = {
4
+ id: 'text-tools',
5
+ title: 'Text Tools',
6
+ titleKey: 'plugins.catalog.textTools.title',
7
+ description: 'Lightweight in-browser text utilities; the first tool compresses text into a single line.',
8
+ descriptionKey: 'plugins.catalog.textTools.description',
9
+ statusLabel: 'standard',
10
+ statusLabelKey: 'plugins.status.standard',
11
+ tone: 'configured'
12
+ };
13
+
14
+ export const textToolsPluginMeta = {
15
+ ...textToolsBaseMeta,
16
+ createdBy: textToolsPluginOwnership && typeof textToolsPluginOwnership.createdBy === 'string' ? textToolsPluginOwnership.createdBy : '',
17
+ maintainers: textToolsPluginOwnership && Array.isArray(textToolsPluginOwnership.maintainers) ? textToolsPluginOwnership.maintainers : []
18
+ };
19
+
20
+ export { textToolsPluginMeta as pluginMeta };
@@ -0,0 +1,24 @@
1
+ export function createTextToolsMethods() {
2
+ return {
3
+ async copyTextToolsOutput() {
4
+ const text = typeof this.textToolsOutput === 'string' ? this.textToolsOutput.trim() : '';
5
+ if (!text) {
6
+ this.showMessage(typeof this.t === 'function' ? this.t('toast.copy.empty') : 'Nothing to copy', 'info');
7
+ return;
8
+ }
9
+ try {
10
+ if (navigator.clipboard && window.isSecureContext) {
11
+ await navigator.clipboard.writeText(text);
12
+ this.showMessage(typeof this.t === 'function' ? this.t('toast.copy.ok') : 'Copied', 'success');
13
+ return;
14
+ }
15
+ } catch (_) {}
16
+ const ok = typeof this.fallbackCopyText === 'function' ? this.fallbackCopyText(text) : false;
17
+ if (ok) {
18
+ this.showMessage(typeof this.t === 'function' ? this.t('toast.copy.ok') : 'Copied', 'success');
19
+ return;
20
+ }
21
+ this.showMessage(typeof this.t === 'function' ? this.t('toast.copy.fail') : 'Copy failed', 'error');
22
+ }
23
+ };
24
+ }
@@ -0,0 +1,12 @@
1
+ import { textTools, DEFAULT_TEXT_TOOL_ID } from './index.mjs';
2
+
3
+ export async function loadTextToolsOverview(ctx) {
4
+ const app = ctx && typeof ctx === 'object' ? ctx : {};
5
+ const activeId = typeof app.textToolsActiveToolId === 'string' ? app.textToolsActiveToolId.trim() : '';
6
+ const exists = textTools.some((tool) => tool && tool.id === activeId);
7
+ app.textToolsActiveToolId = exists ? activeId : DEFAULT_TEXT_TOOL_ID;
8
+ if (typeof app.textToolsInput !== 'string') {
9
+ app.textToolsInput = '';
10
+ }
11
+ return true;
12
+ }
@@ -0,0 +1,7 @@
1
+ export const textToolsPluginOwnership = {
2
+ pluginId: 'text-tools',
3
+ createdBy: 'ymkiux',
4
+ maintainers: ['ymkiux']
5
+ };
6
+
7
+ export { textToolsPluginOwnership as pluginOwnership };
package/web-ui/app.js CHANGED
@@ -108,8 +108,9 @@ document.addEventListener('DOMContentLoaded', () => {
108
108
  sysChangeAxis: { mode: 'edit', ticks: [], truncated: false, totalLines: 0, added: 0, removed: 0 },
109
109
  promptsMobileView: 'edit',
110
110
  promptsPreviewLibsMissing: false,
111
- agentsHighlightHtml: '',
112
- sysPromptHighlightHtml: '',
111
+ promptsPathReferencePaths: {},
112
+ promptsPathReferenceLoading: false,
113
+ promptsPathReferenceCacheKey: '',
113
114
  agentsPreviewHtml: '',
114
115
  sysPromptPreviewHtml: '',
115
116
  projectClaudeMdPath: '',
@@ -169,6 +170,8 @@ document.addEventListener('DOMContentLoaded', () => {
169
170
  pluginsActiveId: 'prompt-templates',
170
171
  pluginsLoading: false,
171
172
  pluginsError: '',
173
+ textToolsActiveToolId: '',
174
+ textToolsInput: '',
172
175
  promptTemplatesListRaw: [],
173
176
  promptTemplatesLoadedOnce: false,
174
177
  promptTemplatesKeyword: '',
package/web-ui/index.html CHANGED
@@ -8,7 +8,6 @@
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
11
  <script src="/res/marked.min.js"></script>
13
12
  <script src="/res/purify.min.js"></script>
14
13
  <link rel="stylesheet" href="/web-ui/styles.css">
@@ -2,67 +2,15 @@
2
2
  * Pure logic for the prompts-panel Markdown editor.
3
3
  * No DOM access in this module so every function stays unit-testable.
4
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.
5
+ * Preview rendering delegates to the vendored marked + DOMPurify pair; raw
6
+ * markdown may embed arbitrary HTML, so sanitize is mandatory before the
7
+ * result reaches v-html.
11
8
  */
12
9
 
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
10
  /**
63
11
  * 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.
12
+ * the UI can surface "libs missing" and "empty input" explicitly instead of
13
+ * faking a successful render.
66
14
  */
67
15
  export function buildMarkdownPreviewHtml(text, marked, purify) {
68
16
  const source = typeof text === 'string' ? text : '';
@@ -1,15 +1,9 @@
1
1
  /**
2
- * Prompts-panel change axis: aggregates the line-level diff between the
3
- * saved original content and the current editor content (or between the
4
- * rendered diff rows) into clickable tick positions. Pure functions only.
5
- *
6
- * The diff engine itself is reused from logic.agents-diff.mjs (LCS for
7
- * small inputs, window-sync for large ones) so hunk line numbers stay
8
- * consistent with the red/green preview flow.
2
+ * Prompts-panel change axis: aggregates the rendered diff rows into
3
+ * clickable tick positions for the red/green diff preview view.
4
+ * Pure functions only.
9
5
  */
10
6
 
11
- import { buildLineDiff } from './logic.agents-diff.mjs';
12
-
13
7
  const AXIS_MAX_TICKS = 200;
14
8
  const AXIS_MIN_TICK_HEIGHT_PERCENT = 1.2;
15
9
 
@@ -55,64 +49,6 @@ function capAxisTicks(runs, positioning) {
55
49
  };
56
50
  }
57
51
 
58
- /**
59
- * Build change-axis ticks for the editor view.
60
- *
61
- * @param {string} baseText saved original content
62
- * @param {string} currentText current editor content
63
- * @returns {{ ticks: Array<object>, totalLines: number, truncated: boolean,
64
- * stats: { added: number, removed: number, unchanged: number } }}
65
- */
66
- export function buildPromptsChangeAxis(baseText, currentText) {
67
- const diff = buildLineDiff(baseText, currentText);
68
- const totalLines = Math.max(1, diff.newLineCount);
69
- const runs = [];
70
- let run = null;
71
- let newCursor = 0;
72
- const flushRun = () => {
73
- if (run) {
74
- runs.push(run);
75
- run = null;
76
- }
77
- };
78
-
79
- for (const line of diff.lines) {
80
- if (!line || !isChangedDiffType(line.type)) {
81
- flushRun();
82
- if (line && line.type === 'context' && typeof line.newNumber === 'number') {
83
- newCursor = line.newNumber;
84
- }
85
- continue;
86
- }
87
- if (!run) {
88
- run = {
89
- anchor: line.type === 'add' && typeof line.newNumber === 'number'
90
- ? line.newNumber - 1
91
- : newCursor,
92
- startLine: line.type === 'add' && typeof line.newNumber === 'number'
93
- ? line.newNumber
94
- : newCursor + 1,
95
- added: 0,
96
- removed: 0
97
- };
98
- }
99
- if (line.type === 'add') {
100
- run.added += 1;
101
- } else {
102
- run.removed += 1;
103
- }
104
- }
105
- flushRun();
106
-
107
- const { ticks, truncated } = capAxisTicks(runs, { total: totalLines });
108
- return {
109
- ticks,
110
- totalLines,
111
- truncated,
112
- stats: diff.stats
113
- };
114
- }
115
-
116
52
  /**
117
53
  * Build change-axis ticks for the diff preview view. Tick positions map to
118
54
  * rendered row indexes so clicks scroll the diff list to the exact row.
@@ -164,24 +100,3 @@ export function buildPromptsDiffAxis(diffLines) {
164
100
  };
165
101
  }
166
102
 
167
- /**
168
- * Proportional scroll position for jumping to a line inside the editor
169
- * textarea. The textarea scrolls internally (min-height contract in
170
- * prompts-editor.css), so scrollTop maps linearly onto the line space.
171
- *
172
- * @param {number} scrollHeight textarea scrollHeight
173
- * @param {number} clientHeight textarea clientHeight
174
- * @param {number} lineIndex 1-based target line in the current content
175
- * @param {number} totalLines total line count of the current content
176
- * @returns {number} clamped scrollTop value
177
- */
178
- export function computePromptsAxisScrollTop(scrollHeight, clientHeight, lineIndex, totalLines) {
179
- const maxScroll = Math.max(0, (Number(scrollHeight) || 0) - (Number(clientHeight) || 0));
180
- if (maxScroll <= 0) {
181
- return 0;
182
- }
183
- const safeTotal = Math.max(1, Number(totalLines) || 1);
184
- const safeLine = Math.max(1, Number(lineIndex) || 1);
185
- const ratio = Math.min(1, Math.max(0, (safeLine - 1) / Math.max(1, safeTotal - 1)));
186
- return Math.min(maxScroll, Math.round(maxScroll * ratio));
187
- }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Prompts-panel "path reference" logic: build the dropdown entries that
3
+ * point to the OTHER instruction-file tabs' real absolute paths, and
4
+ * splice the rendered reference sentence into editor content at the caret.
5
+ *
6
+ * The inserted sentence is instruction-file CONTENT (not UI text), so it
7
+ * is intentionally a fixed English template, independent of the UI locale.
8
+ *
9
+ * Pure functions only: no Vue, no DOM, no api access.
10
+ */
11
+
12
+ export const PROMPT_PATH_REFERENCE_TEMPLATE = 'Always follow the rules defined in `{path}`.';
13
+
14
+ const PROMPT_PATH_REFERENCE_ENTRY_ORDER = ['codex', 'opencode', 'claudeGlobal', 'claudeProject', 'system'];
15
+
16
+ // promptsSubTab value -> entry id excluded from that tab's own dropdown.
17
+ const CURRENT_TAB_EXCLUDED_ENTRY = {
18
+ 'codex': 'codex',
19
+ 'opencode': 'opencode',
20
+ 'claude-project': 'claudeProject',
21
+ 'system': 'system'
22
+ };
23
+
24
+ export function buildPromptPathReferenceSentence(path) {
25
+ const target = typeof path === 'string' ? path.trim() : '';
26
+ if (!target) {
27
+ return '';
28
+ }
29
+ return PROMPT_PATH_REFERENCE_TEMPLATE.replace('{path}', target);
30
+ }
31
+
32
+ /**
33
+ * Splice `text` into `content` at the given selection, replacing the
34
+ * selected range (same contract as typing over a selection). Out-of-range
35
+ * indices are clamped; invalid indices fall back to "insert at end".
36
+ */
37
+ export function insertPromptPathReferenceText(content, selectionStart, selectionEnd, text) {
38
+ const source = typeof content === 'string' ? content : '';
39
+ const inserted = typeof text === 'string' ? text : '';
40
+ const fallback = source.length;
41
+ const rawStart = Number.isFinite(selectionStart) ? Number(selectionStart) : fallback;
42
+ const rawEnd = Number.isFinite(selectionEnd) ? Number(selectionEnd) : fallback;
43
+ const start = Math.min(Math.max(rawStart, 0), source.length);
44
+ const end = Math.min(Math.max(rawEnd, start), source.length);
45
+ return {
46
+ content: source.slice(0, start) + inserted + source.slice(end),
47
+ caret: start + inserted.length
48
+ };
49
+ }
50
+
51
+ /**
52
+ * Build the dropdown option list for the prompts editors.
53
+ *
54
+ * config:
55
+ * - currentTab: promptsSubTab value ('codex' | 'opencode' | 'claude-project' | 'system')
56
+ * - paths: map entry id -> { path, exists, error } (server-resolved absolute paths)
57
+ * - t: i18n translator, signature t(key, params)
58
+ * - systemFile: file name shown for the Pi system-prompt entry (SYSTEM.md / APPEND_SYSTEM.md)
59
+ *
60
+ * Each option: { id, label, path, exists, disabled, disabledReason }.
61
+ * Missing/error entries stay in the list but render disabled, so a failed
62
+ * fetch is visible instead of silently hiding a tab.
63
+ */
64
+ export function buildPromptPathReferenceOptions(config) {
65
+ const cfg = config || {};
66
+ const currentTab = CURRENT_TAB_EXCLUDED_ENTRY[cfg.currentTab] ? cfg.currentTab : 'codex';
67
+ const excludedEntry = CURRENT_TAB_EXCLUDED_ENTRY[currentTab];
68
+ const paths = cfg.paths || {};
69
+ const t = typeof cfg.t === 'function' ? cfg.t : ((key) => key);
70
+ const systemFile = typeof cfg.systemFile === 'string' && cfg.systemFile.trim()
71
+ ? cfg.systemFile.trim()
72
+ : 'SYSTEM.md';
73
+
74
+ const labelKeys = {
75
+ codex: 'prompts.pathReference.codex',
76
+ opencode: 'prompts.pathReference.opencode',
77
+ claudeGlobal: 'prompts.pathReference.claudeGlobal',
78
+ claudeProject: 'prompts.pathReference.claudeProject',
79
+ system: 'prompts.pathReference.system'
80
+ };
81
+
82
+ const options = [];
83
+ for (const id of PROMPT_PATH_REFERENCE_ENTRY_ORDER) {
84
+ if (id === excludedEntry) {
85
+ continue;
86
+ }
87
+ const entry = paths[id] || null;
88
+ let label = id === 'system'
89
+ ? t(labelKeys[id], { file: systemFile })
90
+ : t(labelKeys[id]);
91
+ let disabled = false;
92
+ let disabledReason = '';
93
+ if (id === 'claudeProject' && !(entry && entry.path)) {
94
+ disabled = true;
95
+ disabledReason = t('prompts.pathReference.projectMissing');
96
+ }
97
+ if (!entry) {
98
+ disabled = true;
99
+ if (!disabledReason) {
100
+ disabledReason = t('prompts.pathReference.notLoaded');
101
+ }
102
+ } else if (entry.error) {
103
+ disabled = true;
104
+ disabledReason = entry.error;
105
+ } else if (entry.path && entry.exists === false) {
106
+ label += ' · ' + t('common.notExistsWillCreateOnSave');
107
+ }
108
+ options.push({
109
+ id,
110
+ label,
111
+ path: entry && entry.path ? entry.path : '',
112
+ exists: entry ? !!entry.exists : false,
113
+ disabled,
114
+ disabledReason
115
+ });
116
+ }
117
+ return options;
118
+ }
@@ -1020,6 +1020,7 @@ export function createAgentsMethods(options = {}) {
1020
1020
  },
1021
1021
 
1022
1022
  loadPromptsTabContent() {
1023
+ if (typeof this.loadPromptsPathReferences === "function") this.loadPromptsPathReferences();
1023
1024
  if (this.promptsSubTab === 'system') {
1024
1025
  if (typeof this.loadSystemPrompt === 'function') this.loadSystemPrompt();
1025
1026
  } else {
@@ -52,7 +52,7 @@ import {
52
52
  export function createAppMethods() {
53
53
  return {
54
54
  ...createI18nMethods(),
55
- ...createPromptsEditorMethods(),
55
+ ...createPromptsEditorMethods({ api }),
56
56
  ...createWebhookMethods(),
57
57
  ...createStartupClaudeMethods({
58
58
  api,