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.
- package/cli/session-usage.js +10 -4
- package/cli.js +737 -200
- package/lib/cli-sessions.js +1 -1
- package/package.json +3 -3
- package/plugins/prompt-templates/computed.mjs +1 -1
- package/plugins/prompt-templates/index.mjs +3 -3
- package/plugins/prompt-templates/manifest.mjs +5 -3
- package/plugins/prompt-templates/methods.mjs +9 -2
- package/plugins/registry.mjs +5 -2
- package/plugins/text-tools/computed.mjs +23 -0
- package/plugins/text-tools/index.mjs +14 -0
- package/plugins/text-tools/manifest.mjs +20 -0
- package/plugins/text-tools/methods.mjs +24 -0
- package/plugins/text-tools/overview.mjs +12 -0
- package/plugins/text-tools/ownership.mjs +7 -0
- package/tools/dev/launch.js +147 -0
- package/web-ui/app.js +15 -5
- package/web-ui/index.html +1 -2
- package/web-ui/logic.markdown-editor.mjs +5 -57
- package/web-ui/logic.prompts-token-count.mjs +49 -0
- package/web-ui/logic.sessions.mjs +30 -15
- package/web-ui/modules/app.computed.dashboard.mjs +1 -0
- package/web-ui/modules/app.computed.prompts.mjs +18 -0
- package/web-ui/modules/app.computed.session.mjs +3 -1
- package/web-ui/modules/app.methods.navigation.mjs +79 -3
- package/web-ui/modules/app.methods.prompts-editor.mjs +6 -64
- package/web-ui/modules/app.methods.session-actions.mjs +10 -4
- package/web-ui/modules/app.methods.session-browser.mjs +34 -45
- package/web-ui/modules/app.methods.session-timeline.mjs +1 -0
- package/web-ui/modules/app.methods.web-ui-preferences.mjs +7 -1
- package/web-ui/modules/i18n/locales/en.mjs +20 -1
- package/web-ui/modules/i18n/locales/ja.mjs +20 -1
- package/web-ui/modules/i18n/locales/vi.mjs +20 -1
- package/web-ui/modules/i18n/locales/zh-tw.mjs +20 -1
- package/web-ui/modules/i18n/locales/zh.mjs +20 -1
- package/web-ui/modules/plugins.computed.mjs +8 -2
- package/web-ui/modules/plugins.methods.mjs +8 -2
- package/web-ui/partials/index/layout-header.html +23 -6
- package/web-ui/partials/index/panel-plugins.html +45 -5
- package/web-ui/partials/index/panel-prompts.html +16 -17
- package/web-ui/partials/index/panel-sessions.html +19 -4
- package/web-ui/res/web-ui-render.precompiled.js +193 -93
- package/web-ui/session-helpers.mjs +1 -2
- package/web-ui/source-bundle.cjs +1 -0
- package/web-ui/styles/base-theme.css +24 -0
- package/web-ui/styles/modals-core.css +48 -0
- package/web-ui/styles/prompts-editor.css +3 -66
- package/web-ui/styles/sessions-list.css +1 -1
- package/web-ui/styles/sessions-preview.css +56 -7
- package/web-ui/styles/sessions-toolbar-trash.css +1 -7
package/lib/cli-sessions.js
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "0.1.27",
|
|
4
4
|
"description": "Codex/Claude Code/OpenClaw 配置、会话与任务编排 CLI + Web 工具",
|
|
5
5
|
"main": "cli.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"codexmate": "./
|
|
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
|
|
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
|
|
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
|
|
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
|
|
15
|
-
...
|
|
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
|
|
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
|
-
|
|
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 = {}) {
|
package/plugins/registry.mjs
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import {
|
|
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:
|
|
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,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
|
|
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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
'&': '&',
|
|
16
|
-
'<': '<',
|
|
17
|
-
'>': '>',
|
|
18
|
-
'"': '"',
|
|
19
|
-
"'": '''
|
|
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
|
-
*
|
|
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
|
+
}
|