codexmate 0.1.24 → 0.1.27

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 (50) hide show
  1. package/cli/session-usage.js +10 -4
  2. package/cli.js +737 -200
  3. package/lib/cli-sessions.js +1 -1
  4. package/package.json +3 -3
  5. package/plugins/prompt-templates/computed.mjs +1 -1
  6. package/plugins/prompt-templates/index.mjs +3 -3
  7. package/plugins/prompt-templates/manifest.mjs +5 -3
  8. package/plugins/prompt-templates/methods.mjs +9 -2
  9. package/plugins/registry.mjs +5 -2
  10. package/plugins/text-tools/computed.mjs +23 -0
  11. package/plugins/text-tools/index.mjs +14 -0
  12. package/plugins/text-tools/manifest.mjs +20 -0
  13. package/plugins/text-tools/methods.mjs +24 -0
  14. package/plugins/text-tools/overview.mjs +12 -0
  15. package/plugins/text-tools/ownership.mjs +7 -0
  16. package/tools/dev/launch.js +147 -0
  17. package/web-ui/app.js +15 -5
  18. package/web-ui/index.html +1 -2
  19. package/web-ui/logic.markdown-editor.mjs +5 -57
  20. package/web-ui/logic.prompts-token-count.mjs +49 -0
  21. package/web-ui/logic.sessions.mjs +30 -15
  22. package/web-ui/modules/app.computed.dashboard.mjs +1 -0
  23. package/web-ui/modules/app.computed.prompts.mjs +18 -0
  24. package/web-ui/modules/app.computed.session.mjs +3 -1
  25. package/web-ui/modules/app.methods.navigation.mjs +79 -3
  26. package/web-ui/modules/app.methods.prompts-editor.mjs +6 -64
  27. package/web-ui/modules/app.methods.session-actions.mjs +10 -4
  28. package/web-ui/modules/app.methods.session-browser.mjs +34 -45
  29. package/web-ui/modules/app.methods.session-timeline.mjs +1 -0
  30. package/web-ui/modules/app.methods.web-ui-preferences.mjs +7 -1
  31. package/web-ui/modules/i18n/locales/en.mjs +20 -1
  32. package/web-ui/modules/i18n/locales/ja.mjs +20 -1
  33. package/web-ui/modules/i18n/locales/vi.mjs +20 -1
  34. package/web-ui/modules/i18n/locales/zh-tw.mjs +20 -1
  35. package/web-ui/modules/i18n/locales/zh.mjs +20 -1
  36. package/web-ui/modules/plugins.computed.mjs +8 -2
  37. package/web-ui/modules/plugins.methods.mjs +8 -2
  38. package/web-ui/partials/index/layout-header.html +23 -6
  39. package/web-ui/partials/index/panel-plugins.html +45 -5
  40. package/web-ui/partials/index/panel-prompts.html +16 -17
  41. package/web-ui/partials/index/panel-sessions.html +19 -4
  42. package/web-ui/res/web-ui-render.precompiled.js +193 -93
  43. package/web-ui/session-helpers.mjs +1 -2
  44. package/web-ui/source-bundle.cjs +1 -0
  45. package/web-ui/styles/base-theme.css +24 -0
  46. package/web-ui/styles/modals-core.css +48 -0
  47. package/web-ui/styles/prompts-editor.css +3 -66
  48. package/web-ui/styles/sessions-list.css +1 -1
  49. package/web-ui/styles/sessions-preview.css +56 -7
  50. package/web-ui/styles/sessions-toolbar-trash.css +1 -7
@@ -192,7 +192,7 @@ function extractMessageFromRecord(record, source) {
192
192
  }
193
193
  return null;
194
194
  }
195
- if (source === 'codebuddy') {
195
+ if (source === 'codebuddy' || source === 'workbuddy') {
196
196
  if (record.type === 'message') {
197
197
  const role = normalizeRole(record.role);
198
198
  const content = record.message ? record.message.content : record.content;
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "codexmate",
3
- "version": "0.1.24",
3
+ "version": "0.1.27",
4
4
  "description": "Codex/Claude Code/OpenClaw 配置、会话与任务编排 CLI + Web 工具",
5
5
  "main": "cli.js",
6
6
  "bin": {
7
- "codexmate": "./cli.js"
7
+ "codexmate": "./tools/dev/launch.js"
8
8
  },
9
9
  "files": [
10
10
  "cli.js",
@@ -26,7 +26,7 @@
26
26
  "./res/*": "./web-ui/res/*"
27
27
  },
28
28
  "scripts": {
29
- "dev": "node cli.js run",
29
+ "dev": "node tools/dev/launch.js run",
30
30
  "start": "node cli.js",
31
31
  "reset": "node tools/dev/reset-main.js",
32
32
  "release:npm": "node tools/release/publish-npm.js",
@@ -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 };
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ /**
4
+ * codexmate 启动包装器(launcher)
5
+ * 作用:以 spawn 方式原样转发 `node cli.js <argv>`;
6
+ * 仅当子进程因「package.json 中已声明的依赖缺失」(Cannot find module /
7
+ * MODULE_NOT_FOUND)而失败时,在包根目录自动执行一次 npm install,
8
+ * 然后重跑原命令一次;其余任何错误原样透传退出码,行为与直跑 cli.js 一致。
9
+ * 本文件必须保持零第三方依赖(它要能跑在 npm install 之前)。
10
+ */
11
+ const { spawn } = require('child_process');
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+
15
+ const PACKAGE_ROOT = path.resolve(__dirname, '..', '..');
16
+ const CLI_ENTRY = path.join(PACKAGE_ROOT, 'cli.js');
17
+ const RETRY_FLAG = 'CODEXMATE_LAUNCH_RETRIED';
18
+ const STDERR_KEEP_BYTES = 128 * 1024;
19
+ // 匹配 "Cannot find module 'X'" / "Cannot find module \"X\"" / MODULE_NOT_FOUND 路径
20
+ const MISSING_MODULE_RE = /(?:Cannot find module|MODULE_NOT_FOUND)(?:[^\S\r\n]*['"]([^'"\r\n]+)['"])?/;
21
+
22
+ function isWindows() {
23
+ return process.platform === 'win32';
24
+ }
25
+
26
+ function forwardSignalsTo(child) {
27
+ const signals = ['SIGINT', 'SIGTERM', 'SIGHUP'];
28
+ signals.forEach(function (sig) {
29
+ process.on(sig, function () {
30
+ try { child.kill(sig); } catch (e) { /* 子进程可能已退出 */ }
31
+ });
32
+ });
33
+ }
34
+
35
+ function extractMissingPackageName(stderrText) {
36
+ const m = MISSING_MODULE_RE.exec(stderrText);
37
+ if (!m || !m[1]) return null;
38
+ const raw = m[1];
39
+ // 相对/绝对路径的 require 失败属于代码 bug,不属于环境缺依赖,不触发重试
40
+ if (raw.startsWith('.') || raw.startsWith('/') || path.isAbsolute(raw)) return null;
41
+ const parts = raw.split('/');
42
+ // scoped 包(@scope/name)取前两段,普通包取第一段
43
+ const pkgName = raw.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0];
44
+ return pkgName;
45
+ }
46
+
47
+ function isDeclaredDependency(pkgName) {
48
+ try {
49
+ const pkg = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, 'package.json'), 'utf8'));
50
+ const deps = Object.assign({}, pkg.dependencies, pkg.devDependencies);
51
+ return Object.prototype.hasOwnProperty.call(deps, pkgName);
52
+ } catch (e) {
53
+ return false;
54
+ }
55
+ }
56
+
57
+ function runNpmInstall(cb) {
58
+ const child = spawn('npm', ['install'], {
59
+ cwd: PACKAGE_ROOT,
60
+ stdio: 'inherit',
61
+ shell: isWindows()
62
+ });
63
+ forwardSignalsTo(child);
64
+ child.on('error', function (err) {
65
+ process.stderr.write('[codexmate-launch] npm install 无法启动: ' + err.message + '\n');
66
+ cb(false);
67
+ });
68
+ child.on('exit', function (code) {
69
+ cb(code === 0);
70
+ });
71
+ }
72
+
73
+ function spawnCli(args, opts, onExit) {
74
+ const pipeStderr = !!opts.pipeStderr;
75
+ const child = spawn(process.execPath, args, {
76
+ cwd: process.cwd(),
77
+ env: opts.env || process.env,
78
+ stdio: ['inherit', 'inherit', pipeStderr ? 'pipe' : 'inherit']
79
+ });
80
+ let stderrText = '';
81
+ if (pipeStderr) {
82
+ child.stderr.on('data', function (chunk) {
83
+ process.stderr.write(chunk); // 实时原样转发,绝不静默吞掉
84
+ stderrText += chunk;
85
+ if (stderrText.length > STDERR_KEEP_BYTES) {
86
+ stderrText = stderrText.slice(-STDERR_KEEP_BYTES);
87
+ }
88
+ });
89
+ }
90
+ forwardSignalsTo(child);
91
+ child.on('error', function (err) {
92
+ process.stderr.write('[codexmate-launch] 无法启动 cli.js: ' + err.message + '\n');
93
+ process.exit(127);
94
+ });
95
+ child.on('exit', function (code, signal) {
96
+ onExit(code, signal, stderrText);
97
+ });
98
+ return child;
99
+ }
100
+
101
+ function runFinal(args) {
102
+ const env = Object.assign({}, process.env);
103
+ env[RETRY_FLAG] = '1';
104
+ spawnCli(args, { env: env, pipeStderr: false }, function (code, signal) {
105
+ if (signal) {
106
+ process.exit(128 + 15);
107
+ }
108
+ process.exit(code === null ? 1 : code);
109
+ });
110
+ }
111
+
112
+ function main() {
113
+ const args = [CLI_ENTRY].concat(process.argv.slice(2));
114
+
115
+ // 重入护栏:已被 launcher 重跑过的进程不再重试,杜绝死循环
116
+ if (process.env[RETRY_FLAG] === '1') {
117
+ runFinal(args);
118
+ return;
119
+ }
120
+
121
+ spawnCli(args, { pipeStderr: true }, function (code, signal, stderrText) {
122
+ if (signal) {
123
+ process.exit(128 + 15);
124
+ }
125
+ const exitCode = code === null ? 1 : code;
126
+ if (exitCode === 0) {
127
+ process.exit(0);
128
+ }
129
+ const missing = extractMissingPackageName(stderrText);
130
+ if (!missing || !isDeclaredDependency(missing)) {
131
+ process.exit(exitCode); // 非环境性缺依赖:退出码原样透传
132
+ }
133
+ process.stderr.write(
134
+ '[codexmate-launch] 检测到已声明的依赖缺失: ' + missing +
135
+ ',自动执行 npm install 后重试一次...\n'
136
+ );
137
+ runNpmInstall(function (ok) {
138
+ if (!ok) {
139
+ process.stderr.write('[codexmate-launch] npm install 失败,保留上方原始报错并以原退出码退出。\n');
140
+ process.exit(exitCode);
141
+ }
142
+ runFinal(args);
143
+ });
144
+ });
145
+ }
146
+
147
+ main();
package/web-ui/app.js CHANGED
@@ -111,8 +111,6 @@ document.addEventListener('DOMContentLoaded', () => {
111
111
  promptsPathReferencePaths: {},
112
112
  promptsPathReferenceLoading: false,
113
113
  promptsPathReferenceCacheKey: '',
114
- agentsHighlightHtml: '',
115
- sysPromptHighlightHtml: '',
116
114
  agentsPreviewHtml: '',
117
115
  sysPromptPreviewHtml: '',
118
116
  projectClaudeMdPath: '',
@@ -172,6 +170,8 @@ document.addEventListener('DOMContentLoaded', () => {
172
170
  pluginsActiveId: 'prompt-templates',
173
171
  pluginsLoading: false,
174
172
  pluginsError: '',
173
+ textToolsActiveToolId: '',
174
+ textToolsInput: '',
175
175
  promptTemplatesListRaw: [],
176
176
  promptTemplatesLoadedOnce: false,
177
177
  promptTemplatesKeyword: '',
@@ -299,6 +299,7 @@ document.addEventListener('DOMContentLoaded', () => {
299
299
  sessionFilterSource: 'all',
300
300
  sessionPathFilter: '',
301
301
  sessionQuery: '',
302
+ sessionPreviewScrollAtTop: true,
302
303
  sessionRoleFilter: 'all',
303
304
  sessionTimePreset: 'all',
304
305
  sessionSortMode: 'time',
@@ -308,19 +309,28 @@ document.addEventListener('DOMContentLoaded', () => {
308
309
  all: [],
309
310
  codex: [],
310
311
  claude: [],
311
- gemini: []
312
+ gemini: [],
313
+ codebuddy: [],
314
+ pi: [],
315
+ opencode: []
312
316
  },
313
317
  sessionPathOptionsLoadedMap: {
314
318
  all: false,
315
319
  codex: false,
316
320
  claude: false,
317
- gemini: false
321
+ gemini: false,
322
+ codebuddy: false,
323
+ pi: false,
324
+ opencode: false
318
325
  },
319
326
  sessionPathRequestSeqMap: {
320
327
  all: 0,
321
328
  codex: 0,
322
329
  claude: 0,
323
- gemini: 0
330
+ gemini: 0,
331
+ codebuddy: 0,
332
+ pi: 0,
333
+ opencode: 0
324
334
  },
325
335
  sessionExporting: {},
326
336
  sessionConverting: {},
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">
@@ -39,6 +38,6 @@
39
38
  <!-- @include ./partials/index/modal-skills.html -->
40
39
  <!-- @include ./partials/index/modal-health-check.html -->
41
40
  <!-- @include ./partials/index/modal-confirm-toast.html -->
42
- <script type="module" src="/web-ui/app.js?cb=3"></script>
41
+ <script type="module" src="/web-ui/app.js"></script>
43
42
  </body>
44
43
  </html>
@@ -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 : '';
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Heuristic token-count estimation for the prompts panel.
3
+ *
4
+ * The Web UI ships zero npm dependencies and all frontend libraries are
5
+ * vendored single files, so a real BPE tokenizer (2-3 MB per encoding) is not
6
+ * an acceptable trade-off for this feature. Instead we use a lightweight
7
+ * heuristic that is accurate enough for "is my instruction file 2k or 20k
8
+ * tokens" style decisions:
9
+ *
10
+ * - CJK ideographs / kana / fullwidth forms count as ~1 token per
11
+ * character (matches the common ~1 token per CJK char behavior of
12
+ * mainstream tokenizers);
13
+ * - everything else (Latin words, markdown syntax, whitespace, code) is
14
+ * estimated at ~4 characters per token.
15
+ *
16
+ * Values are meant to be displayed with an "approx." qualifier in the UI.
17
+ */
18
+
19
+ // CJK ideographs, Hangul jamo/syllables, kana, CJK punctuation/symbols and
20
+ // fullwidth forms all tokenize at roughly one token per character.
21
+ const CJK_TOKEN_RE = /[\u1100-\u11FF\u2E80-\u2EFF\u3000-\u303F\u3040-\u30FF\u3130-\u318F\u3400-\u4DBF\u4E00-\u9FFF\uAC00-\uD7A3\uF900-\uFAFF\uFE30-\uFE4F\uFF00-\uFFEF]/g;
22
+
23
+ // Non-CJK text averages roughly 4 characters per token for typical
24
+ // English + markdown + code instruction files.
25
+ const NON_CJK_CHARS_PER_TOKEN = 4;
26
+
27
+ export function estimatePromptTokenCount(text) {
28
+ const source = typeof text === 'string' ? text : '';
29
+ if (!source) {
30
+ return 0;
31
+ }
32
+ const cjkMatches = source.match(CJK_TOKEN_RE);
33
+ const cjkTokenCount = cjkMatches ? cjkMatches.length : 0;
34
+ const nonCjkCharCount = source.length - cjkTokenCount;
35
+ return cjkTokenCount + Math.ceil(nonCjkCharCount / NON_CJK_CHARS_PER_TOKEN);
36
+ }
37
+
38
+ export function buildPromptTokenDiffStats(originalText, currentText) {
39
+ const before = estimatePromptTokenCount(originalText);
40
+ const current = estimatePromptTokenCount(currentText);
41
+ const delta = current - before;
42
+ return {
43
+ before,
44
+ current,
45
+ delta,
46
+ deltaLabel: delta === 0 ? '±0' : (delta > 0 ? `+${delta}` : String(delta)),
47
+ deltaKind: delta === 0 ? 'same' : (delta > 0 ? 'add' : 'del')
48
+ };
49
+ }