@mjasnikovs/pi-task 0.17.20 → 0.17.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -21
- package/README.md +10 -2
- package/dist/remote/history.d.ts +2 -0
- package/dist/remote/session-state.d.ts +3 -0
- package/dist/remote/session-state.js +11 -1
- package/dist/remote/ui-highlight.d.ts +2 -0
- package/dist/remote/ui-highlight.js +119 -0
- package/dist/remote/ui-render.d.ts +2 -0
- package/dist/remote/ui-render.js +169 -0
- package/dist/remote/ui-script.js +53 -124
- package/dist/remote/ui-styles.d.ts +1 -1
- package/dist/remote/ui-styles.js +59 -5
- package/dist/remote/ui-tools.d.ts +2 -0
- package/dist/remote/ui-tools.js +113 -0
- package/dist/remote/ui.js +6 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
**Deterministic spec-orchestration for local models — with bundled web, docs, fetch, and worker sub-agent tools.**
|
|
8
8
|
|
|
9
9
|
[](https://www.npmjs.com/package/@mjasnikovs/pi-task)
|
|
10
|
-
[](./LICENSE)
|
|
11
11
|
[](https://www.npmjs.com/package/@earendil-works/pi-coding-agent)
|
|
12
12
|
[](#development)
|
|
13
13
|
[](./tsconfig.json)
|
|
@@ -177,4 +177,12 @@ Built with [Bun](https://bun.sh), TypeScript (strict), and [TypeBox](https://git
|
|
|
177
177
|
|
|
178
178
|
## License
|
|
179
179
|
|
|
180
|
-
[
|
|
180
|
+
[AGPL-3.0-only](./LICENSE) © Edgars Mjasnikovs
|
|
181
|
+
|
|
182
|
+
Free and open source under the GNU Affero General Public License v3.0: you may
|
|
183
|
+
use, modify, and redistribute it, but any modified version you run — **including
|
|
184
|
+
over a network as a hosted service** — must make its complete source available
|
|
185
|
+
under the same license. Contributions are accepted under the
|
|
186
|
+
[Contributor License Agreement](./CLA.md), which allows dual-licensing;
|
|
187
|
+
for a commercial license that does not carry the AGPL's copyleft obligations,
|
|
188
|
+
contact the author.
|
package/dist/remote/history.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export interface ToolPart {
|
|
|
11
11
|
isError: boolean;
|
|
12
12
|
/** false while the tool is still running (result not in yet). */
|
|
13
13
|
done: boolean;
|
|
14
|
+
/** Wall-clock duration once finished, shown dimly in the tool summary. */
|
|
15
|
+
elapsedMs?: number;
|
|
14
16
|
}
|
|
15
17
|
export type Part = TextPart | ToolPart;
|
|
16
18
|
export interface Turn {
|
|
@@ -24,6 +24,9 @@ interface SessionState {
|
|
|
24
24
|
taskWidget: string[] | null;
|
|
25
25
|
prompt: PromptMessage | null;
|
|
26
26
|
context: ContextUsage | null;
|
|
27
|
+
/** tool start timestamps (ms), keyed by toolCallId — kept off the serialized
|
|
28
|
+
* parts so it never reaches the client; used only to compute elapsedMs. */
|
|
29
|
+
toolStarts: Record<string, number>;
|
|
27
30
|
/** Broadcast sink — swapped in tests via _setSink. */
|
|
28
31
|
sink: (msg: unknown) => void;
|
|
29
32
|
}
|
|
@@ -20,6 +20,7 @@ function fresh() {
|
|
|
20
20
|
taskWidget: null,
|
|
21
21
|
prompt: null,
|
|
22
22
|
context: null,
|
|
23
|
+
toolStarts: {},
|
|
23
24
|
sink: wsBroadcast
|
|
24
25
|
};
|
|
25
26
|
}
|
|
@@ -76,6 +77,7 @@ export function startTool(toolCallId, toolName, args) {
|
|
|
76
77
|
isError: false,
|
|
77
78
|
done: false
|
|
78
79
|
});
|
|
80
|
+
s.toolStarts[toolCallId] = Date.now();
|
|
79
81
|
s.sink({ type: 'tool_start', toolCallId, toolName, args });
|
|
80
82
|
}
|
|
81
83
|
export function updateTool(toolCallId, partialResult) {
|
|
@@ -85,10 +87,18 @@ export function endTool(toolCallId, toolName, result, isError) {
|
|
|
85
87
|
const s = getState();
|
|
86
88
|
const live = ensureLive(s);
|
|
87
89
|
const part = live.parts.find((p) => p.kind === 'tool' && p.toolCallId === toolCallId);
|
|
90
|
+
const startedAt = s.toolStarts[toolCallId];
|
|
91
|
+
let elapsedMs;
|
|
92
|
+
if (startedAt != null) {
|
|
93
|
+
elapsedMs = Date.now() - startedAt;
|
|
94
|
+
delete s.toolStarts[toolCallId];
|
|
95
|
+
}
|
|
88
96
|
if (part) {
|
|
89
97
|
part.result = result;
|
|
90
98
|
part.isError = isError;
|
|
91
99
|
part.done = true;
|
|
100
|
+
if (elapsedMs != null)
|
|
101
|
+
part.elapsedMs = elapsedMs;
|
|
92
102
|
}
|
|
93
103
|
else {
|
|
94
104
|
// tool_end without a matching start (shouldn't happen) — append a done tool.
|
|
@@ -102,7 +112,7 @@ export function endTool(toolCallId, toolName, result, isError) {
|
|
|
102
112
|
done: true
|
|
103
113
|
});
|
|
104
114
|
}
|
|
105
|
-
s.sink({ type: 'tool_end', toolCallId, toolName, result, isError });
|
|
115
|
+
s.sink({ type: 'tool_end', toolCallId, toolName, result, isError, elapsedMs });
|
|
106
116
|
}
|
|
107
117
|
export function agentEnd(context) {
|
|
108
118
|
const s = getState();
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// Generalized single-pass syntax highlighter, shipped as a JS string and
|
|
2
|
+
// concatenated into the page by ui.ts. It grew out of the client's original
|
|
3
|
+
// JS-only tokenizer: the string/comment/number/identifier logic is now shared and
|
|
4
|
+
// parameterized by a per-language config (keyword set + comment markers + string
|
|
5
|
+
// flavors). Unknown languages fall back to escaped plain text (unchanged).
|
|
6
|
+
//
|
|
7
|
+
// Depends on escHtml (ui-render.ts) — both land in the same <script>.
|
|
8
|
+
/** Emitted client JS (top-level function declarations + language tables). */
|
|
9
|
+
export function highlightModule() {
|
|
10
|
+
return ` var HL_JS_KW = new Set(('break case catch class const continue debugger default delete do else export '
|
|
11
|
+
+ 'extends finally for from function if import in instanceof let new of return static super switch this '
|
|
12
|
+
+ 'throw try typeof var void while with yield async await type interface enum implements abstract as declare '
|
|
13
|
+
+ 'namespace readonly undefined null true false override satisfies public private protected get set').split(' '));
|
|
14
|
+
var HL_PY_KW = new Set(('def return if elif else for while in not and or import from as class try except finally '
|
|
15
|
+
+ 'raise with lambda yield pass break continue global nonlocal assert del None True False async await is print self').split(' '));
|
|
16
|
+
var HL_SH_KW = new Set(('if then else elif fi for while do done case esac in function return echo export local read '
|
|
17
|
+
+ 'cd set unset source alias exit test').split(' '));
|
|
18
|
+
var HL_SQL_KW = new Set(('select from where insert into values update set delete create table drop alter add column '
|
|
19
|
+
+ 'join left right inner outer full on group by order having limit offset union and or not null is as distinct '
|
|
20
|
+
+ 'count sum avg min max primary key foreign references index unique default int integer text varchar char boolean '
|
|
21
|
+
+ 'timestamp date time serial begin commit rollback').split(' '));
|
|
22
|
+
var HL_GO_KW = new Set(('func package import var const type struct interface map chan go defer return if else for range '
|
|
23
|
+
+ 'switch case default break continue select nil true false make new append len cap string int int64 float64 bool byte rune error').split(' '));
|
|
24
|
+
var HL_RUST_KW = new Set(('fn let mut const static struct enum impl trait pub use mod match if else for while loop return '
|
|
25
|
+
+ 'break continue self Self super crate as where move ref dyn async await unsafe true false None Some Ok Err String Vec Option Result Box').split(' '));
|
|
26
|
+
var HL_YAML_KW = new Set('true false null yes no on off'.split(' '));
|
|
27
|
+
var HL_JSON_KW = new Set('true false null'.split(' '));
|
|
28
|
+
|
|
29
|
+
var HL_LANGS = {
|
|
30
|
+
js: {kw: HL_JS_KW, line: '//', block: ['/*', '*/'], tmpl: true},
|
|
31
|
+
ts: {kw: HL_JS_KW, line: '//', block: ['/*', '*/'], tmpl: true},
|
|
32
|
+
python: {kw: HL_PY_KW, line: '#', block: null, triple: true},
|
|
33
|
+
sh: {kw: HL_SH_KW, line: '#', block: null},
|
|
34
|
+
sql: {kw: HL_SQL_KW, line: '--', block: ['/*', '*/'], ci: true},
|
|
35
|
+
go: {kw: HL_GO_KW, line: '//', block: ['/*', '*/'], tmpl: true},
|
|
36
|
+
rust: {kw: HL_RUST_KW, line: '//', block: ['/*', '*/']},
|
|
37
|
+
yaml: {kw: HL_YAML_KW, line: '#', block: null},
|
|
38
|
+
json: {kw: HL_JSON_KW, line: null, block: null},
|
|
39
|
+
css: {kw: null, line: null, block: ['/*', '*/']}
|
|
40
|
+
};
|
|
41
|
+
var HL_ALIAS = {javascript: 'js', jsx: 'js', mjs: 'js', cjs: 'js', node: 'js', typescript: 'ts', tsx: 'ts',
|
|
42
|
+
py: 'python', python3: 'python', bash: 'sh', shell: 'sh', shellscript: 'sh', zsh: 'sh', console: 'sh',
|
|
43
|
+
yml: 'yaml', rs: 'rust', golang: 'go', htm: 'html', xml: 'html', svg: 'html', scss: 'css'};
|
|
44
|
+
|
|
45
|
+
function hlSpan(cls, text) { return '<span class="' + cls + '">' + escHtml(text) + '</span>'; }
|
|
46
|
+
|
|
47
|
+
function tokenize(code, cfg) {
|
|
48
|
+
var BT = String.fromCharCode(96);
|
|
49
|
+
var r = '', i = 0, n = code.length;
|
|
50
|
+
while (i < n) {
|
|
51
|
+
var ch = code[i];
|
|
52
|
+
if (cfg.block && code.startsWith(cfg.block[0], i)) {
|
|
53
|
+
var end = code.indexOf(cfg.block[1], i + cfg.block[0].length);
|
|
54
|
+
var je = end < 0 ? n : end + cfg.block[1].length;
|
|
55
|
+
r += hlSpan('hl-cmt', code.slice(i, je)); i = je; continue;
|
|
56
|
+
}
|
|
57
|
+
if (cfg.line && code.startsWith(cfg.line, i)) {
|
|
58
|
+
var jl = i; while (jl < n && code[jl] !== '\\n') jl++;
|
|
59
|
+
r += hlSpan('hl-cmt', code.slice(i, jl)); i = jl; continue;
|
|
60
|
+
}
|
|
61
|
+
if (ch === '"' || ch === "'" || (cfg.tmpl && ch === BT)) {
|
|
62
|
+
if (cfg.triple && code[i + 1] === ch && code[i + 2] === ch) {
|
|
63
|
+
var close = code.indexOf(ch + ch + ch, i + 3);
|
|
64
|
+
var jt = close < 0 ? n : close + 3;
|
|
65
|
+
r += hlSpan('hl-str', code.slice(i, jt)); i = jt; continue;
|
|
66
|
+
}
|
|
67
|
+
var js = i + 1;
|
|
68
|
+
while (js < n) { if (code[js] === '\\\\') { js += 2; continue; } if (code[js] === ch || code[js] === '\\n') break; js++; }
|
|
69
|
+
if (code[js] === ch) js++;
|
|
70
|
+
r += hlSpan('hl-str', code.slice(i, js)); i = js; continue;
|
|
71
|
+
}
|
|
72
|
+
if (ch >= '0' && ch <= '9') {
|
|
73
|
+
var jn = i;
|
|
74
|
+
if (code[i] === '0' && /[xXoObB]/.test(code[i + 1] || '')) {
|
|
75
|
+
jn += 2; while (jn < n && /[0-9a-fA-F_]/.test(code[jn])) jn++;
|
|
76
|
+
} else {
|
|
77
|
+
while (jn < n && ((code[jn] >= '0' && code[jn] <= '9') || code[jn] === '_')) jn++;
|
|
78
|
+
if (code[jn] === '.') { jn++; while (jn < n && code[jn] >= '0' && code[jn] <= '9') jn++; }
|
|
79
|
+
if (code[jn] === 'e' || code[jn] === 'E') { jn++; if (code[jn] === '+' || code[jn] === '-') jn++; while (jn < n && code[jn] >= '0' && code[jn] <= '9') jn++; }
|
|
80
|
+
if (code[jn] === 'n') jn++;
|
|
81
|
+
}
|
|
82
|
+
r += hlSpan('hl-num', code.slice(i, jn)); i = jn; continue;
|
|
83
|
+
}
|
|
84
|
+
if (/[a-zA-Z_$]/.test(ch)) {
|
|
85
|
+
var jw = i; while (jw < n && /[a-zA-Z0-9_$]/.test(code[jw])) jw++;
|
|
86
|
+
var word = code.slice(i, jw);
|
|
87
|
+
var kwHit = cfg.kw && (cfg.kw.has(word) || (cfg.ci && cfg.kw.has(word.toLowerCase())));
|
|
88
|
+
if (kwHit) r += hlSpan('hl-kw', word);
|
|
89
|
+
else if (code[jw] === '(') r += hlSpan('hl-fn', word);
|
|
90
|
+
else r += escHtml(word);
|
|
91
|
+
i = jw; continue;
|
|
92
|
+
}
|
|
93
|
+
r += escHtml(ch); i++;
|
|
94
|
+
}
|
|
95
|
+
return r;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Markup languages don't fit the identifier tokenizer; highlight tag names and
|
|
99
|
+
// attribute names off the already-escaped text.
|
|
100
|
+
function highlightMarkup(code) {
|
|
101
|
+
var e = escHtml(code);
|
|
102
|
+
// Attribute names first (before any spans exist), then tag names — otherwise
|
|
103
|
+
// the attribute pass would re-match the class="" inside the spans it inserted.
|
|
104
|
+
e = e.replace(/([a-zA-Z-]+)=("|")/g, function (_, a, q) { return '<span class="hl-fn">' + a + '</span>=' + q; });
|
|
105
|
+
e = e.replace(/(<\\/?)([a-zA-Z][\\w-]*)/g, function (_, p, t) { return p + '<span class="hl-kw">' + t + '</span>'; });
|
|
106
|
+
return e;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function syntaxHighlight(code, lang) {
|
|
110
|
+
code = String(code == null ? '' : code);
|
|
111
|
+
lang = (lang || '').toLowerCase();
|
|
112
|
+
lang = HL_ALIAS[lang] || lang;
|
|
113
|
+
if (lang === 'html') return highlightMarkup(code);
|
|
114
|
+
var cfg = HL_LANGS[lang];
|
|
115
|
+
if (!cfg) return escHtml(code);
|
|
116
|
+
return tokenize(code, cfg);
|
|
117
|
+
}
|
|
118
|
+
`;
|
|
119
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// Pure string→HTML render helpers, shipped to the browser as a JS string and
|
|
2
|
+
// concatenated into the page by ui.ts BEFORE clientScript. Splitting these out
|
|
3
|
+
// of the 900-line client template gives them a testability seam: each emitted
|
|
4
|
+
// function is `new Function`-eval'd in a unit test and asserted on its return
|
|
5
|
+
// value, with no DOM needed. (The shipped fence bug survived because the client
|
|
6
|
+
// JS was never executed by any test — only string-matched.)
|
|
7
|
+
//
|
|
8
|
+
// Exposes (as top-level fn declarations in the page's single <script>):
|
|
9
|
+
// escHtml(s) — HTML-escape & < >
|
|
10
|
+
// parseBlocks(text) — line-based fence scanner (text vs code segments)
|
|
11
|
+
// renderInline(s) — inline markdown (bold/italic/strike/code/links)
|
|
12
|
+
// renderMarkdown(text) — full block-level markdown → HTML string
|
|
13
|
+
// renderMarkdown calls syntaxHighlight (ui-highlight.ts); both land in the same
|
|
14
|
+
// <script>, so declaration order across the concatenated modules doesn't matter.
|
|
15
|
+
/** Emitted client JS (top-level function declarations). */
|
|
16
|
+
export function renderModule() {
|
|
17
|
+
return ` function escHtml(s) {
|
|
18
|
+
return String(s == null ? '' : s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Split text into ordered {type:'text'|'code', ...} blocks by scanning lines
|
|
22
|
+
// for a fenced code marker at line start. This replaces a RegExp that was built
|
|
23
|
+
// from a string where '[\\\\s\\\\S]' collapsed to '[sS]', so it matched nothing
|
|
24
|
+
// and every code block rendered as raw text. A line scanner also tolerates an
|
|
25
|
+
// unclosed trailing fence, which the regex never could.
|
|
26
|
+
function parseBlocks(text) {
|
|
27
|
+
var FENCE = String.fromCharCode(96, 96, 96);
|
|
28
|
+
var lines = String(text == null ? '' : text).split('\\n');
|
|
29
|
+
var blocks = [];
|
|
30
|
+
var textBuf = [];
|
|
31
|
+
function flushText() {
|
|
32
|
+
if (textBuf.length) { blocks.push({type: 'text', content: textBuf.join('\\n')}); textBuf = []; }
|
|
33
|
+
}
|
|
34
|
+
var i = 0;
|
|
35
|
+
while (i < lines.length) {
|
|
36
|
+
var line = lines[i];
|
|
37
|
+
if (line.slice(0, 3) === FENCE) {
|
|
38
|
+
flushText();
|
|
39
|
+
var lang = line.slice(3).trim();
|
|
40
|
+
var codeLines = [];
|
|
41
|
+
i++;
|
|
42
|
+
while (i < lines.length && lines[i].trim() !== FENCE) { codeLines.push(lines[i]); i++; }
|
|
43
|
+
if (i < lines.length) i++; // consume the closing fence; tolerate EOF (unclosed)
|
|
44
|
+
blocks.push({type: 'code', lang: lang, content: codeLines.join('\\n')});
|
|
45
|
+
} else {
|
|
46
|
+
textBuf.push(line);
|
|
47
|
+
i++;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
flushText();
|
|
51
|
+
return blocks;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Inline formatting on ONE line's worth of text. Code spans are extracted first
|
|
55
|
+
// (and protected with a NUL placeholder) so emphasis inside them is left alone.
|
|
56
|
+
function renderInline(s) {
|
|
57
|
+
s = escHtml(String(s == null ? '' : s));
|
|
58
|
+
var BT = String.fromCharCode(96);
|
|
59
|
+
var codes = [];
|
|
60
|
+
var codeRe = new RegExp(BT + '([^' + BT + ']+)' + BT, 'g');
|
|
61
|
+
s = s.replace(codeRe, function (_, c) { codes.push(c); return '\\u0000' + (codes.length - 1) + '\\u0000'; });
|
|
62
|
+
s = s.replace(/\\[([^\\]]+)\\]\\(([^)\\s]+)\\)/g, function (_, t, u) {
|
|
63
|
+
var safe = /^(https?:|mailto:)/i.test(u) ? u : '#';
|
|
64
|
+
return '<a href="' + safe + '" target="_blank" rel="noopener">' + t + '</a>';
|
|
65
|
+
});
|
|
66
|
+
s = s.replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>');
|
|
67
|
+
s = s.replace(/__([^_]+)__/g, '<strong>$1</strong>');
|
|
68
|
+
s = s.replace(/\\*([^*]+)\\*/g, '<em>$1</em>');
|
|
69
|
+
s = s.replace(/~~([^~]+)~~/g, '<del>$1</del>');
|
|
70
|
+
s = s.replace(/\\u0000(\\d+)\\u0000/g, function (_, n) { return '<code class="md-code">' + codes[n] + '</code>'; });
|
|
71
|
+
return s;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// A line that begins a block-level construct — used to stop paragraph grouping.
|
|
75
|
+
function mdIsSpecial(l) {
|
|
76
|
+
return /^(#{1,6})\\s+/.test(l)
|
|
77
|
+
|| /^\\s*>\\s?/.test(l)
|
|
78
|
+
|| /^\\s*([-*+]|\\d+\\.)\\s+/.test(l)
|
|
79
|
+
|| /^\\s*([-*_])\\s*(\\1\\s*){2,}$/.test(l);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function mdParseList(lines, i) {
|
|
83
|
+
var ordered = /^\\s*\\d+\\.\\s+/.test(lines[i]);
|
|
84
|
+
var tag = ordered ? 'ol' : 'ul';
|
|
85
|
+
var items = '';
|
|
86
|
+
while (i < lines.length && /^\\s*([-*+]|\\d+\\.)\\s+/.test(lines[i])) {
|
|
87
|
+
var content = lines[i].replace(/^\\s*([-*+]|\\d+\\.)\\s+/, '');
|
|
88
|
+
var task = /^\\[([ xX])\\]\\s+(.*)$/.exec(content);
|
|
89
|
+
if (task) {
|
|
90
|
+
var checked = task[1].toLowerCase() === 'x';
|
|
91
|
+
items += '<li class="md-task"><span class="md-check">' + (checked ? '\\u2611' : '\\u2610') + '</span> ' + renderInline(task[2]) + '</li>';
|
|
92
|
+
} else {
|
|
93
|
+
items += '<li>' + renderInline(content) + '</li>';
|
|
94
|
+
}
|
|
95
|
+
i++;
|
|
96
|
+
}
|
|
97
|
+
return {html: '<' + tag + ' class="md-list">' + items + '</' + tag + '>', next: i};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function mdParseTable(lines, i) {
|
|
101
|
+
function cells(l) {
|
|
102
|
+
var s = l.trim().replace(/^\\|/, '').replace(/\\|$/, '');
|
|
103
|
+
return s.split('|').map(function (c) { return c.trim(); });
|
|
104
|
+
}
|
|
105
|
+
var head = cells(lines[i]);
|
|
106
|
+
i += 2; // skip the header row and the |---| separator
|
|
107
|
+
var body = '';
|
|
108
|
+
while (i < lines.length && /\\|/.test(lines[i]) && lines[i].trim() !== '') {
|
|
109
|
+
var row = cells(lines[i]);
|
|
110
|
+
var tds = '';
|
|
111
|
+
for (var c = 0; c < row.length; c++) tds += '<td>' + renderInline(row[c]) + '</td>';
|
|
112
|
+
body += '<tr>' + tds + '</tr>';
|
|
113
|
+
i++;
|
|
114
|
+
}
|
|
115
|
+
var ths = '';
|
|
116
|
+
for (var c2 = 0; c2 < head.length; c2++) ths += '<th>' + renderInline(head[c2]) + '</th>';
|
|
117
|
+
return {html: '<table class="md-table"><thead><tr>' + ths + '</tr></thead><tbody>' + body + '</tbody></table>', next: i};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Block-level markdown for one text segment (no fences — those are split off by
|
|
121
|
+
// renderMarkdown first). Headers, hr, blockquotes, tables, lists, paragraphs.
|
|
122
|
+
function renderProse(src) {
|
|
123
|
+
var lines = String(src == null ? '' : src).split('\\n');
|
|
124
|
+
var html = '';
|
|
125
|
+
var i = 0;
|
|
126
|
+
while (i < lines.length) {
|
|
127
|
+
var line = lines[i];
|
|
128
|
+
if (line.trim() === '') { i++; continue; }
|
|
129
|
+
var h = /^(#{1,6})\\s+(.*)$/.exec(line);
|
|
130
|
+
if (h) { var lvl = h[1].length; html += '<h' + lvl + ' class="md-h md-h' + lvl + '">' + renderInline(h[2]) + '</h' + lvl + '>'; i++; continue; }
|
|
131
|
+
if (/^\\s*([-*_])\\s*(\\1\\s*){2,}$/.test(line)) { html += '<hr class="md-hr">'; i++; continue; }
|
|
132
|
+
if (/^\\s*>\\s?/.test(line)) {
|
|
133
|
+
var q = [];
|
|
134
|
+
while (i < lines.length && /^\\s*>\\s?/.test(lines[i])) { q.push(lines[i].replace(/^\\s*>\\s?/, '')); i++; }
|
|
135
|
+
html += '<blockquote class="md-quote">' + renderProse(q.join('\\n')) + '</blockquote>';
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (/\\|/.test(line) && i + 1 < lines.length && /^\\s*\\|?[\\s:|-]*-[\\s:|-]*\\|?\\s*$/.test(lines[i + 1])) {
|
|
139
|
+
var t = mdParseTable(lines, i); html += t.html; i = t.next; continue;
|
|
140
|
+
}
|
|
141
|
+
if (/^\\s*([-*+]|\\d+\\.)\\s+/.test(line)) {
|
|
142
|
+
var lst = mdParseList(lines, i); html += lst.html; i = lst.next; continue;
|
|
143
|
+
}
|
|
144
|
+
var para = [];
|
|
145
|
+
while (i < lines.length && lines[i].trim() !== '' && !mdIsSpecial(lines[i])) { para.push(lines[i]); i++; }
|
|
146
|
+
var inner = [];
|
|
147
|
+
for (var p = 0; p < para.length; p++) inner.push(renderInline(para[p]));
|
|
148
|
+
html += '<p class="md-p">' + inner.join('<br>') + '</p>';
|
|
149
|
+
}
|
|
150
|
+
return html;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function renderMarkdown(text) {
|
|
154
|
+
var blocks = parseBlocks(text);
|
|
155
|
+
var out = '';
|
|
156
|
+
for (var k = 0; k < blocks.length; k++) {
|
|
157
|
+
var b = blocks[k];
|
|
158
|
+
if (b.type === 'code') {
|
|
159
|
+
out += '<div class="code-block">'
|
|
160
|
+
+ (b.lang ? '<div class="code-lang">' + escHtml(b.lang) + '</div>' : '')
|
|
161
|
+
+ '<pre><code>' + syntaxHighlight(b.content, b.lang) + '</code></pre></div>';
|
|
162
|
+
} else {
|
|
163
|
+
out += renderProse(b.content);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return out;
|
|
167
|
+
}
|
|
168
|
+
`;
|
|
169
|
+
}
|
package/dist/remote/ui-script.js
CHANGED
|
@@ -56,119 +56,14 @@ export function clientScript(wsUrl) {
|
|
|
56
56
|
let reconnectTimer = null;
|
|
57
57
|
let ws = null;
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
'this','throw','try','typeof','var','void','while','with','yield','async','await',
|
|
65
|
-
'type','interface','enum','implements','abstract','as','declare','namespace',
|
|
66
|
-
'readonly','undefined','null','true','false','override','satisfies']);
|
|
67
|
-
|
|
68
|
-
function escHtml(s) {
|
|
69
|
-
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function syntaxHighlight(code, lang) {
|
|
73
|
-
if (!JS_LANGS.has((lang || '').toLowerCase())) return escHtml(code);
|
|
74
|
-
let r = '', i = 0;
|
|
75
|
-
while (i < code.length) {
|
|
76
|
-
const ch = code[i];
|
|
77
|
-
// Template literal
|
|
78
|
-
if (ch === BT) {
|
|
79
|
-
let j = i + 1;
|
|
80
|
-
while (j < code.length) {
|
|
81
|
-
if (code[j] === '\\\\') { j += 2; continue; }
|
|
82
|
-
if (code[j] === BT) { j++; break; }
|
|
83
|
-
j++;
|
|
84
|
-
}
|
|
85
|
-
r += '<span class="hl-str">' + escHtml(code.slice(i, j)) + '</span>';
|
|
86
|
-
i = j; continue;
|
|
87
|
-
}
|
|
88
|
-
// Single / double quoted string
|
|
89
|
-
if (ch === '"' || ch === "'") {
|
|
90
|
-
let j = i + 1;
|
|
91
|
-
while (j < code.length) {
|
|
92
|
-
if (code[j] === '\\\\') { j += 2; continue; }
|
|
93
|
-
if (code[j] === ch || code[j] === '\\n') break;
|
|
94
|
-
j++;
|
|
95
|
-
}
|
|
96
|
-
if (code[j] === ch) j++;
|
|
97
|
-
r += '<span class="hl-str">' + escHtml(code.slice(i, j)) + '</span>';
|
|
98
|
-
i = j; continue;
|
|
99
|
-
}
|
|
100
|
-
// Line comment
|
|
101
|
-
if (ch === '/' && code[i + 1] === '/') {
|
|
102
|
-
let j = i + 2;
|
|
103
|
-
while (j < code.length && code[j] !== '\\n') j++;
|
|
104
|
-
r += '<span class="hl-cmt">' + escHtml(code.slice(i, j)) + '</span>';
|
|
105
|
-
i = j; continue;
|
|
106
|
-
}
|
|
107
|
-
// Block comment
|
|
108
|
-
if (ch === '/' && code[i + 1] === '*') {
|
|
109
|
-
let j = i + 2;
|
|
110
|
-
while (j < code.length && !(code[j] === '*' && code[j + 1] === '/')) j++;
|
|
111
|
-
j += 2;
|
|
112
|
-
r += '<span class="hl-cmt">' + escHtml(code.slice(i, j)) + '</span>';
|
|
113
|
-
i = j; continue;
|
|
114
|
-
}
|
|
115
|
-
// Number
|
|
116
|
-
if (ch >= '0' && ch <= '9') {
|
|
117
|
-
let j = i;
|
|
118
|
-
if (code[i] === '0' && /[xXoObB]/.test(code[i + 1] || '')) {
|
|
119
|
-
j += 2; while (j < code.length && /[0-9a-fA-F_]/.test(code[j])) j++;
|
|
120
|
-
} else {
|
|
121
|
-
while (j < code.length && (code[j] >= '0' && code[j] <= '9' || code[j] === '_')) j++;
|
|
122
|
-
if (code[j] === '.') { j++; while (j < code.length && code[j] >= '0' && code[j] <= '9') j++; }
|
|
123
|
-
if (code[j] === 'e' || code[j] === 'E') {
|
|
124
|
-
j++; if (code[j] === '+' || code[j] === '-') j++;
|
|
125
|
-
while (j < code.length && code[j] >= '0' && code[j] <= '9') j++;
|
|
126
|
-
}
|
|
127
|
-
if (code[j] === 'n') j++;
|
|
128
|
-
}
|
|
129
|
-
r += '<span class="hl-num">' + escHtml(code.slice(i, j)) + '</span>';
|
|
130
|
-
i = j; continue;
|
|
131
|
-
}
|
|
132
|
-
// Identifier / keyword / function call
|
|
133
|
-
if (/[a-zA-Z_$]/.test(ch)) {
|
|
134
|
-
let j = i;
|
|
135
|
-
while (j < code.length && /[a-zA-Z0-9_$]/.test(code[j])) j++;
|
|
136
|
-
const word = code.slice(i, j);
|
|
137
|
-
if (JS_KW.has(word)) {
|
|
138
|
-
r += '<span class="hl-kw">' + word + '</span>';
|
|
139
|
-
} else if (code[j] === '(') {
|
|
140
|
-
r += '<span class="hl-fn">' + escHtml(word) + '</span>';
|
|
141
|
-
} else {
|
|
142
|
-
r += escHtml(word);
|
|
143
|
-
}
|
|
144
|
-
i = j; continue;
|
|
145
|
-
}
|
|
146
|
-
r += escHtml(ch); i++;
|
|
147
|
-
}
|
|
148
|
-
return r;
|
|
149
|
-
}
|
|
150
|
-
|
|
59
|
+
// Render assistant text as markdown (headers, lists, tables, emphasis, links)
|
|
60
|
+
// with fenced code blocks syntax-highlighted. renderMarkdown/syntaxHighlight are
|
|
61
|
+
// the pure functions from ui-render.ts / ui-highlight.ts, concatenated into this
|
|
62
|
+
// same <script> ahead of clientScript. innerHTML is safe because renderMarkdown
|
|
63
|
+
// escapes every span of literal text and only emits a fixed set of known tags.
|
|
151
64
|
function setContent(el, text) {
|
|
152
|
-
el.
|
|
153
|
-
|
|
154
|
-
const re = new RegExp(BT3 + '([^\\n' + BT + ']*)\\n([\\s\\S]*?)' + BT3, 'g');
|
|
155
|
-
let last = 0, m;
|
|
156
|
-
while ((m = re.exec(text)) !== null) {
|
|
157
|
-
if (m.index > last) el.appendChild(document.createTextNode(text.slice(last, m.index)));
|
|
158
|
-
const lang = m[1].trim();
|
|
159
|
-
const code = m[2];
|
|
160
|
-
const wrap = document.createElement('div');
|
|
161
|
-
wrap.className = 'code-block';
|
|
162
|
-
if (lang) { const lb = document.createElement('div'); lb.className = 'code-lang'; lb.textContent = lang; wrap.appendChild(lb); }
|
|
163
|
-
const pre = document.createElement('pre');
|
|
164
|
-
const codeEl = document.createElement('code');
|
|
165
|
-
codeEl.innerHTML = syntaxHighlight(code, lang);
|
|
166
|
-
pre.appendChild(codeEl);
|
|
167
|
-
wrap.appendChild(pre);
|
|
168
|
-
el.appendChild(wrap);
|
|
169
|
-
last = m.index + m[0].length;
|
|
170
|
-
}
|
|
171
|
-
if (last < text.length) el.appendChild(document.createTextNode(text.slice(last)));
|
|
65
|
+
el.classList.add('md');
|
|
66
|
+
el.innerHTML = renderMarkdown(text);
|
|
172
67
|
}
|
|
173
68
|
|
|
174
69
|
const COMMANDS = [
|
|
@@ -247,7 +142,10 @@ export function clientScript(wsUrl) {
|
|
|
247
142
|
function addBubble(role, text) {
|
|
248
143
|
const el = document.createElement('div');
|
|
249
144
|
el.className = 'bubble ' + role;
|
|
250
|
-
|
|
145
|
+
// Only assistant text is markdown-rendered; user/error bubbles stay plain so a
|
|
146
|
+
// user's literal *asterisks* or backticks show verbatim.
|
|
147
|
+
if (role === 'assistant') setContent(el, text);
|
|
148
|
+
else el.textContent = text;
|
|
251
149
|
chatLog.appendChild(el);
|
|
252
150
|
scrollBottom();
|
|
253
151
|
return el;
|
|
@@ -293,20 +191,50 @@ export function clientScript(wsUrl) {
|
|
|
293
191
|
stopSpinIfIdle();
|
|
294
192
|
}
|
|
295
193
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
194
|
+
// A finished/running tool card. args is the RAW args (object or JSON string) —
|
|
195
|
+
// toolSummary/toolBadge/toolDiffHtml (ui-tools.ts) turn it into a readable
|
|
196
|
+
// one-line summary, a +N −M badge, and (for edit/write) an expandable line diff.
|
|
197
|
+
function addToolCall(toolName, args, isError) {
|
|
300
198
|
const d = document.createElement('details');
|
|
301
199
|
d.className = 'tool-call' + (isError ? ' error' : '');
|
|
302
200
|
const s = document.createElement('summary');
|
|
303
|
-
|
|
201
|
+
const label = document.createElement('span');
|
|
202
|
+
label.className = 'tool-label';
|
|
203
|
+
label.textContent = toolSummary(toolName, args);
|
|
204
|
+
s.title = label.textContent;
|
|
205
|
+
s.appendChild(label);
|
|
206
|
+
const badge = toolBadge(toolName, args);
|
|
207
|
+
if (badge && (badge.added || badge.removed)) {
|
|
208
|
+
const b = document.createElement('span');
|
|
209
|
+
b.className = 'tool-badge';
|
|
210
|
+
b.textContent = '+' + badge.added + ' \\u2212' + badge.removed;
|
|
211
|
+
s.appendChild(b);
|
|
212
|
+
}
|
|
304
213
|
d.appendChild(s);
|
|
214
|
+
const diffHtml = toolDiffHtml(toolName, args);
|
|
215
|
+
if (diffHtml) {
|
|
216
|
+
const dv = document.createElement('div');
|
|
217
|
+
dv.className = 'tool-diff';
|
|
218
|
+
dv.innerHTML = diffHtml;
|
|
219
|
+
d.appendChild(dv);
|
|
220
|
+
}
|
|
305
221
|
chatLog.appendChild(d);
|
|
306
222
|
scrollBottom();
|
|
307
223
|
return d;
|
|
308
224
|
}
|
|
309
225
|
|
|
226
|
+
// Dim "· 1.2s" appended to a finished tool summary.
|
|
227
|
+
function appendElapsed(d, elapsedMs) {
|
|
228
|
+
const txt = fmtElapsed(elapsedMs);
|
|
229
|
+
if (!txt) return;
|
|
230
|
+
const s = d.querySelector('summary');
|
|
231
|
+
if (!s || s.querySelector('.tool-elapsed')) return;
|
|
232
|
+
const e = document.createElement('span');
|
|
233
|
+
e.className = 'tool-elapsed';
|
|
234
|
+
e.textContent = '\\u00B7 ' + txt;
|
|
235
|
+
s.appendChild(e);
|
|
236
|
+
}
|
|
237
|
+
|
|
310
238
|
function setEnabled(on) {
|
|
311
239
|
const allow = on && activePromptId === null;
|
|
312
240
|
inputEl.disabled = !allow;
|
|
@@ -345,9 +273,9 @@ export function clientScript(wsUrl) {
|
|
|
345
273
|
|
|
346
274
|
// Render one tool part from the ordered parts list (running or finished).
|
|
347
275
|
function renderToolPart(p) {
|
|
348
|
-
const
|
|
349
|
-
const d = addToolCall(p.toolName, argsStr, p.isError);
|
|
276
|
+
const d = addToolCall(p.toolName, p.args, p.isError);
|
|
350
277
|
if (p.done) {
|
|
278
|
+
appendElapsed(d, p.elapsedMs);
|
|
351
279
|
const pre = document.createElement('pre');
|
|
352
280
|
pre.textContent = toolResultText(p.result);
|
|
353
281
|
d.appendChild(pre);
|
|
@@ -604,8 +532,9 @@ export function clientScript(wsUrl) {
|
|
|
604
532
|
activeRecommended = msg.recommended || '';
|
|
605
533
|
activeRecommended2 = msg.recommended2 || '';
|
|
606
534
|
if (msg.recommended) {
|
|
607
|
-
// Mode A: recommendation(s) present.
|
|
608
|
-
|
|
535
|
+
// Mode A: recommendation(s) present. Render markdown in the panel so a
|
|
536
|
+
// recommendation with code/emphasis reads the same as an assistant bubble.
|
|
537
|
+
setContent(promptRecText, msg.recommended);
|
|
609
538
|
showRecommendation();
|
|
610
539
|
} else {
|
|
611
540
|
// Mode B: no recommendation — the user must type an answer (or skip).
|
|
@@ -685,8 +614,7 @@ export function clientScript(wsUrl) {
|
|
|
685
614
|
break;
|
|
686
615
|
case 'tool_start': {
|
|
687
616
|
hideThinking();
|
|
688
|
-
const
|
|
689
|
-
const d = addToolCall(msg.toolName, argsStr, false);
|
|
617
|
+
const d = addToolCall(msg.toolName, msg.args, false);
|
|
690
618
|
const sp = document.createElement('span');
|
|
691
619
|
sp.className = 'tool-spin spin';
|
|
692
620
|
d.querySelector('summary').appendChild(sp);
|
|
@@ -700,6 +628,7 @@ export function clientScript(wsUrl) {
|
|
|
700
628
|
const sp = d.querySelector('.tool-spin');
|
|
701
629
|
if (sp) { sp.remove(); stopSpinIfIdle(); }
|
|
702
630
|
if (msg.isError) d.classList.add('error');
|
|
631
|
+
appendElapsed(d, msg.elapsedMs);
|
|
703
632
|
const pre = document.createElement('pre');
|
|
704
633
|
pre.textContent = toolResultText(msg.result);
|
|
705
634
|
d.appendChild(pre);
|