codexmate 0.1.21 → 0.1.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -26,16 +26,6 @@
26
26
 
27
27
  <br />
28
28
 
29
- <p>
30
- <img src="site/.vitepress/public/images/readme/config-codex.png" alt="Codex Mate Codex provider configuration" width="100%" />
31
- </p>
32
- <p>
33
- <img src="site/.vitepress/public/images/readme/config-opencode.png" alt="Codex Mate OpenCode provider configuration" width="100%" />
34
- </p>
35
- <p>
36
- <img src="site/.vitepress/public/images/readme/sessions.png" alt="Codex Mate unified session browser" width="100%" />
37
- </p>
38
-
39
29
  </div>
40
30
 
41
31
  ---
package/README.vi.md CHANGED
@@ -26,8 +26,6 @@
26
26
 
27
27
  <br />
28
28
 
29
- <img src="site/.vitepress/public/images/readme-hero.png" alt="Giao diện Codex Mate" width="100%" />
30
-
31
29
  </div>
32
30
 
33
31
  ---
package/README.zh.md CHANGED
@@ -25,16 +25,6 @@
25
25
 
26
26
  <br />
27
27
 
28
- <p>
29
- <img src="site/.vitepress/public/images/readme/config-codex.png" alt="Codex Mate Codex Provider 配置" width="100%" />
30
- </p>
31
- <p>
32
- <img src="site/.vitepress/public/images/readme/config-opencode.png" alt="Codex Mate OpenCode Provider 配置" width="100%" />
33
- </p>
34
- <p>
35
- <img src="site/.vitepress/public/images/readme/sessions.png" alt="Codex Mate 统一会话浏览器" width="100%" />
36
- </p>
37
-
38
28
  </div>
39
29
 
40
30
  ---
package/cli.js CHANGED
@@ -1076,6 +1076,7 @@ function normalizeWebUiPreferences(value = {}) {
1076
1076
  configTemplateDiffConfirmEnabled: normalizeBooleanPreference(source.configTemplateDiffConfirmEnabled, true),
1077
1077
  sessionsUsageTimeRange: normalizeUsageTimeRangePreference(source.sessionsUsageTimeRange),
1078
1078
  promptsSubTab: normalizePromptsSubTabPreference(source.promptsSubTab),
1079
+ promptsPreviewCollapsed: normalizeBooleanPreference(source.promptsPreviewCollapsed, true),
1079
1080
  sysPromptScope: normalizeSysPromptScopePreference(source.sysPromptScope),
1080
1081
  sysPromptMode: normalizeSysPromptModePreference(source.sysPromptMode),
1081
1082
  projectClaudeMdPath: typeof source.projectClaudeMdPath === 'string' ? source.projectClaudeMdPath : '',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codexmate",
3
- "version": "0.1.21",
3
+ "version": "0.1.23",
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,16 @@ document.addEventListener('DOMContentLoaded', () => {
102
102
  showConfigTemplateModal: false,
103
103
  showAgentsModal: false,
104
104
  promptsSubTab: 'codex',
105
+ promptsPreviewEnabled: true,
106
+ promptsPreviewCollapsed: true,
107
+ agentsChangeAxis: { mode: 'edit', ticks: [], truncated: false, totalLines: 0, added: 0, removed: 0 },
108
+ sysChangeAxis: { mode: 'edit', ticks: [], truncated: false, totalLines: 0, added: 0, removed: 0 },
109
+ promptsMobileView: 'edit',
110
+ promptsPreviewLibsMissing: false,
111
+ agentsHighlightHtml: '',
112
+ sysPromptHighlightHtml: '',
113
+ agentsPreviewHtml: '',
114
+ sysPromptPreviewHtml: '',
105
115
  projectClaudeMdPath: '',
106
116
  promptHistoryVisible: false,
107
117
  promptHistoryLoading: false,
@@ -812,6 +822,30 @@ document.addEventListener('DOMContentLoaded', () => {
812
822
  if (typeof this.loadPromptsTabContent === 'function') this.loadPromptsTabContent();
813
823
  }
814
824
  },
825
+ agentsContent() {
826
+ if (typeof this.schedulePromptsEditorRefresh === 'function') this.schedulePromptsEditorRefresh('agents');
827
+ },
828
+ sysPromptContent() {
829
+ if (typeof this.schedulePromptsEditorRefresh === 'function') this.schedulePromptsEditorRefresh('sys');
830
+ },
831
+ agentsOriginalContent() {
832
+ if (typeof this.schedulePromptsChangeAxis === 'function') this.schedulePromptsChangeAxis('agents');
833
+ },
834
+ sysPromptOriginalContent() {
835
+ if (typeof this.schedulePromptsChangeAxis === 'function') this.schedulePromptsChangeAxis('sys');
836
+ },
837
+ agentsDiffVisible() {
838
+ if (typeof this.refreshPromptsChangeAxis === 'function') this.refreshPromptsChangeAxis('agents');
839
+ },
840
+ sysPromptDiffVisible() {
841
+ if (typeof this.refreshPromptsChangeAxis === 'function') this.refreshPromptsChangeAxis('sys');
842
+ },
843
+ agentsDiffLines() {
844
+ if (this.agentsDiffVisible && typeof this.refreshPromptsChangeAxis === 'function') this.refreshPromptsChangeAxis('agents');
845
+ },
846
+ sysPromptDiffLines() {
847
+ if (this.sysPromptDiffVisible && typeof this.refreshPromptsChangeAxis === 'function') this.refreshPromptsChangeAxis('sys');
848
+ },
815
849
  projectClaudeMdPath(newPath) {
816
850
  if (typeof this.persistWebUiPreferences === 'function') {
817
851
  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
+ }
@@ -0,0 +1,187 @@
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.
9
+ */
10
+
11
+ import { buildLineDiff } from './logic.agents-diff.mjs';
12
+
13
+ const AXIS_MAX_TICKS = 200;
14
+ const AXIS_MIN_TICK_HEIGHT_PERCENT = 1.2;
15
+
16
+ function isChangedDiffType(type) {
17
+ return type === 'add' || type === 'del';
18
+ }
19
+
20
+ function clampAxisPercent(value) {
21
+ if (!Number.isFinite(value) || value < 0) return 0;
22
+ if (value > 100) return 100;
23
+ return value;
24
+ }
25
+
26
+ function tickKindForRun(added, removed) {
27
+ if (added && removed) return 'mixed';
28
+ return added ? 'add' : 'del';
29
+ }
30
+
31
+ function buildTickFromRun(run, positioning) {
32
+ const height = clampAxisPercent(Math.max(
33
+ AXIS_MIN_TICK_HEIGHT_PERCENT,
34
+ (Math.max(run.added, run.removed) / Math.max(1, positioning.total)) * 100
35
+ ));
36
+ const top = Math.min(clampAxisPercent((run.anchor / Math.max(1, positioning.total)) * 100), 100 - height);
37
+ return {
38
+ kind: tickKindForRun(run.added, run.removed),
39
+ startLine: run.startLine,
40
+ endLine: run.startLine + Math.max(run.added, run.removed) - 1,
41
+ added: run.added,
42
+ removed: run.removed,
43
+ top,
44
+ height,
45
+ rowIndex: typeof run.rowIndex === 'number' ? run.rowIndex : null
46
+ };
47
+ }
48
+
49
+ function capAxisTicks(runs, positioning) {
50
+ const truncated = runs.length > AXIS_MAX_TICKS;
51
+ const visibleRuns = truncated ? runs.slice(0, AXIS_MAX_TICKS) : runs;
52
+ return {
53
+ ticks: visibleRuns.map((run) => buildTickFromRun(run, positioning)),
54
+ truncated
55
+ };
56
+ }
57
+
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
+ /**
117
+ * Build change-axis ticks for the diff preview view. Tick positions map to
118
+ * rendered row indexes so clicks scroll the diff list to the exact row.
119
+ *
120
+ * @param {Array<object>} diffLines rendered diff rows ({type, value, oldNumber, newNumber})
121
+ * @returns {{ ticks: Array<object>, rowCount: number, truncated: boolean }}
122
+ */
123
+ export function buildPromptsDiffAxis(diffLines) {
124
+ const rows = Array.isArray(diffLines) ? diffLines.filter((line) => line && line.type) : [];
125
+ const rowCount = rows.length;
126
+ const runs = [];
127
+ let run = null;
128
+ const flushRun = () => {
129
+ if (run) {
130
+ runs.push(run);
131
+ run = null;
132
+ }
133
+ };
134
+
135
+ rows.forEach((line, index) => {
136
+ if (!isChangedDiffType(line.type)) {
137
+ flushRun();
138
+ return;
139
+ }
140
+ if (!run) {
141
+ run = {
142
+ anchor: index,
143
+ rowIndex: index,
144
+ startLine: line.type === 'add' && typeof line.newNumber === 'number'
145
+ ? line.newNumber
146
+ : (typeof line.oldNumber === 'number' ? line.oldNumber : index + 1),
147
+ added: 0,
148
+ removed: 0
149
+ };
150
+ }
151
+ if (line.type === 'add') {
152
+ run.added += 1;
153
+ } else {
154
+ run.removed += 1;
155
+ }
156
+ });
157
+ flushRun();
158
+
159
+ const { ticks, truncated } = capAxisTicks(runs, { total: Math.max(1, rowCount) });
160
+ return {
161
+ ticks,
162
+ rowCount,
163
+ truncated
164
+ };
165
+ }
166
+
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
+ }
@@ -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
  },
@@ -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,