@mjasnikovs/pi-task 0.17.21 → 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/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 +1 -1
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);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const STYLES = " :root {\n --base: #1e1e2e; --mantle: #181825; --crust: #11111b;\n --surface0: #313244; --surface1: #45475a; --surface2: #585b70;\n --text: #cdd6f4; --subtext1: #a6adc8; --subtext0: #7f849c;\n --mauve: #cba6f7; --blue: #89b4fa; --green: #a6e3a1; --red: #f38ba8;\n --yellow: #f9e2af; --peach: #fab387; --teal: #94e2d5;\n }\n *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n body {\n background: var(--base); color: var(--text);\n font-family: ui-monospace, monospace;\n /* --app-h is set from window.innerHeight (see setAppHeight) so the column\n height is a stable pixel value across an orientation change. 100dvh is a\n fallback for first paint / no-JS: iOS Safari interpolates dvh during the\n rotation animation, which makes the whole flex column resize repeatedly\n (\"spazzing out\") \u2014 a fixed px height does not. */\n height: var(--app-h, 100dvh);\n display: flex; flex-direction: column; overflow: hidden;\n padding: env(safe-area-inset-top, 0px) env(safe-area-inset-right, 0px)\n 0px env(safe-area-inset-left, 0px);\n }\n #context-bar { height: 4px; background: var(--surface0); flex-shrink: 0; }\n #context-bar-fill { height: 100%; background: var(--mauve); width: 0%; transition: width 0.4s ease; }\n #header {\n background: var(--mantle); padding: 8px 16px;\n display: flex; justify-content: space-between; align-items: center;\n font-size: 13px; flex-shrink: 0; border-bottom: 1px solid var(--surface0);\n }\n #header .title { font-weight: bold; color: var(--mauve); letter-spacing: 0.05em;\n position: relative; animation: glitch 5s steps(1) infinite; }\n @keyframes glitch {\n 0%, 88%, 100% { text-shadow: none; transform: translate(0, 0); }\n 90% { text-shadow: -1px 0 var(--red), 1px 0 var(--teal); transform: translate(1px, -1px); }\n 92% { text-shadow: 1px 0 var(--red), -1px 0 var(--blue); transform: translate(-1px, 1px); }\n 94% { text-shadow: -1px 0 var(--blue), 1px 0 var(--red); transform: translate(1px, 0); }\n 96% { text-shadow: 1px 0 var(--teal), -1px 0 var(--red); transform: translate(-1px, 0); }\n }\n @media (prefers-reduced-motion: reduce) { #header .title { animation: none; } }\n #header .hgroup { display: flex; align-items: center; gap: 10px; }\n #bell {\n background: none; border: none; color: var(--subtext1); cursor: pointer;\n font-size: 15px; line-height: 1; padding: 2px; font-family: inherit;\n }\n #bell:hover { color: var(--text); }\n #bell.on { color: var(--mauve); }\n #chat-wrap { position: relative; flex: 1; min-height: 0; display: flex; }\n #chat-log {\n flex: 1; min-width: 0; overflow-y: auto; overflow-x: hidden; padding: 16px;\n display: flex; flex-direction: column; gap: 8px;\n }\n /* Floating jump-to-latest button \u2014 only shown when scrolled away from the\n bottom (toggled via .visible from the scroll handler). */\n #scroll-bottom {\n display: none; position: absolute; bottom: 16px; right: 16px; z-index: 40;\n width: 36px; height: 36px; border-radius: 50%; cursor: pointer;\n background: var(--surface1); color: var(--text); border: 1px solid var(--surface2);\n font-family: inherit; font-size: 18px; line-height: 1; padding: 0;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);\n }\n #scroll-bottom:hover { background: var(--surface2); color: var(--mauve); }\n #scroll-bottom.visible { display: block; }\n #chat-log::-webkit-scrollbar { width: 6px; }\n #chat-log::-webkit-scrollbar-track { background: transparent; }\n #chat-log::-webkit-scrollbar-thumb { background: var(--surface2); border-radius: 3px; }\n .bubble {\n max-width: 82%; padding: 8px 12px; border-radius: 8px;\n line-height: 1.6; white-space: pre-wrap; word-break: break-word; font-size: 13px;\n }\n .bubble.user { background: var(--surface1); color: var(--text); align-self: flex-end; }\n .bubble.assistant { background: var(--surface0); color: var(--text); align-self: flex-start; }\n .bubble.error {\n background: var(--crust); color: var(--red); align-self: stretch;\n max-width: 100%; border: 1px solid var(--red); font-size: 12px;\n }\n /* Persistent inline system note (e.g. context compaction) \u2014 a muted centered\n divider, distinct from chat bubbles. */\n .sysnote {\n align-self: center; color: var(--subtext0); font-size: 11px;\n font-family: ui-monospace, monospace; letter-spacing: 0.5px;\n padding: 2px 10px; opacity: 0.85;\n }\n .bubble.thinking {\n display: flex; gap: 5px; align-items: center; padding: 10px 14px;\n }\n .bubble.thinking .spinner {\n color: var(--mauve); font-size: 15px; line-height: 1;\n font-family: ui-monospace, monospace;\n }\n .tool-call {\n background: var(--crust); border-radius: 6px; align-self: flex-start;\n max-width: 90%; font-size: 12px; border: 1px solid var(--surface0);\n }\n .tool-call summary {\n padding: 6px 10px; color: var(--subtext1); cursor: pointer;\n user-select: none; list-style: none;\n overflow-wrap: anywhere; word-break: break-word;\n }\n .tool-call summary::-webkit-details-marker { display: none; }\n .tool-call summary::before { content: \"\u25B6 \"; }\n .tool-call[open] > summary::before { content: \"\u25BC \"; }\n .tool-call.error > summary { color: var(--red); }\n .tool-call pre {\n padding: 8px 12px; overflow-y: auto;\n color: var(--subtext1); font-size: 11px; max-height: 280px;\n border-top: 1px solid var(--surface0);\n white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word;\n }\n .tool-spin { color: var(--mauve); margin-left: 6px; font-family: ui-monospace, monospace; font-size: 13px; }\n .code-block {\n background: var(--crust); border: 1px solid var(--surface0);\n border-radius: 6px; overflow: hidden; margin: 4px 0;\n align-self: stretch; max-width: 100%; font-size: 12px;\n }\n .code-lang {\n background: var(--surface0); color: var(--subtext0);\n font-size: 10px; padding: 3px 10px; letter-spacing: 0.05em;\n }\n .code-block code {\n display: block; padding: 10px 12px; overflow-x: auto;\n color: var(--text); white-space: pre; line-height: 1.55;\n }\n .hl-kw { color: var(--mauve); }\n .hl-str { color: var(--green); }\n .hl-cmt { color: var(--subtext0); font-style: italic; }\n .hl-num { color: var(--blue); }\n .hl-fn { color: var(--yellow); }\n #input-bar {\n background: var(--mantle); padding: 10px 16px calc(10px + env(safe-area-inset-bottom, 0px));\n display: flex; gap: 8px; flex-shrink: 0;\n border-top: 1px solid var(--surface0);\n position: relative;\n }\n #cmd-suggestions {\n display: none; position: absolute; bottom: 100%; left: 16px; right: 16px;\n background: var(--mantle); border: 1px solid var(--surface1);\n border-bottom: none; border-radius: 8px 8px 0 0;\n overflow: hidden; z-index: 10;\n }\n .cmd-item {\n display: flex; align-items: baseline; gap: 10px;\n padding: 7px 12px; cursor: pointer; font-size: 12px;\n border-bottom: 1px solid var(--surface0);\n }\n .cmd-item:last-child { border-bottom: none; }\n .cmd-item:hover, .cmd-item.active { background: var(--surface0); }\n .cmd-item .cmd-name { color: var(--blue); font-weight: bold; flex-shrink: 0; }\n .cmd-item .cmd-desc { color: var(--subtext0); }\n #input {\n flex: 1; background: var(--surface0); color: var(--text);\n border: none; border-radius: 6px; padding: 8px 12px;\n font-family: inherit; font-size: 13px; resize: none;\n outline: none; line-height: 1.5; min-height: 36px; max-height: 120px;\n }\n #input::placeholder { color: var(--subtext0); }\n #input:focus { box-shadow: 0 0 0 1px var(--mauve); }\n #send {\n background: var(--blue); color: var(--crust); border: none;\n border-radius: 6px; padding: 8px 16px; font-weight: bold;\n cursor: pointer; font-size: 13px; font-family: inherit;\n white-space: nowrap; align-self: flex-end;\n }\n #send:disabled, #input:disabled { opacity: 0.45; cursor: not-allowed; }\n #reconnect-overlay {\n display: none; position: fixed; inset: 0;\n background: rgba(30,30,46,0.88); color: var(--subtext1);\n justify-content: center; align-items: center;\n font-size: 13px; z-index: 100; letter-spacing: 0.03em;\n }\n #reconnect-overlay.visible { display: flex; }\n /* Trailing stream indicator: the same braille spinner as the thinking bubble,\n inline at the end of the streaming text (not a green blinking block). */\n .cursor {\n color: var(--mauve); margin-left: 2px;\n font-family: ui-monospace, monospace;\n }\n #status-panel { padding: 6px 12px; border-bottom: 1px solid var(--surface1);\n color: var(--subtext1); white-space: pre-wrap; font-size: 13px; display: none; }\n #prompt-card { position: fixed; left: 0; right: 0; bottom: 0; background: var(--mantle);\n border-top: 2px solid var(--mauve); padding: 16px 14px calc(16px + env(safe-area-inset-bottom, 0px));\n display: none; z-index: 50; max-height: 80dvh; overflow-y: auto; }\n #prompt-card .q-label { color: var(--mauve); font-size: 11px; font-weight: 700;\n text-transform: uppercase; letter-spacing: .6px; margin-bottom: 6px; }\n #prompt-card .q { color: var(--text); margin-bottom: 12px; white-space: pre-wrap;\n font-size: 15px; line-height: 1.5; }\n #prompt-card .rec-panel { background: var(--surface0); border-left: 3px solid var(--green);\n border-radius: 6px; padding: 10px 12px; margin-bottom: 12px; }\n #prompt-card .rec-label { color: var(--green); font-size: 11px; font-weight: 700;\n text-transform: uppercase; letter-spacing: .5px; margin-bottom: 4px; }\n #prompt-card .rec-text { color: var(--text); font-size: 15px; line-height: 1.5;\n white-space: pre-wrap; overflow-wrap: anywhere; }\n #prompt-card textarea { width: 100%; background: var(--surface0); color: var(--text);\n border: 1px solid var(--surface2); border-radius: 6px; padding: 10px; font-size: 15px;\n font-family: inherit; line-height: 1.5; resize: vertical; margin-bottom: 4px; }\n #prompt-card .row { display: flex; gap: 8px; margin-top: 12px; align-items: stretch;\n flex-wrap: wrap; }\n /* Recommendation answers can be long sentences, so stack them as a readable list. */\n #prompt-card .row.stacked { flex-direction: column; align-items: stretch; }\n #prompt-card .row.stacked button { flex: none; text-align: left; }\n #prompt-card .row.stacked button.cancel { align-self: center; text-align: center; }\n #prompt-card button { padding: 11px 16px; border-radius: 8px; border: none; cursor: pointer;\n font-family: inherit; font-size: 14px; font-weight: 600; transition: filter .15s ease; }\n #prompt-card button:hover { filter: brightness(1.08); }\n #prompt-card button.primary { background: var(--green); color: var(--crust);\n font-weight: 700; flex: 1; min-width: 160px; }\n #prompt-card button.secondary { background: var(--surface1); color: var(--text);\n flex: 1; min-width: 160px; }\n #prompt-card button.cancel { margin-left: auto; align-self: center; background: transparent;\n color: var(--subtext0); font-size: 12px; font-weight: 500; padding: 8px 10px; }\n #prompt-card button.cancel:hover { color: var(--red); filter: none; }\n #prompt-card button.cancel.armed { background: var(--red); color: var(--crust); font-weight: 700; }\n .toast { position: fixed; top: calc(env(safe-area-inset-top, 0px) + 12px);\n right: calc(env(safe-area-inset-right, 0px) + 12px); max-width: calc(100vw - 24px);\n padding: 8px 12px; border-radius: 6px; overflow-wrap: anywhere; word-break: break-word;\n background: var(--surface1); color: var(--text); z-index: 60; }\n .toast.warning { background: var(--peach); color: var(--crust); }\n .toast.error { background: var(--red); color: var(--crust); }\n #viewer { position: fixed; inset: 24px; background: var(--mantle); border: 1px solid var(--surface2);\n border-radius: 8px; padding: 16px; overflow: auto; white-space: pre-wrap;\n overflow-wrap: anywhere; word-break: break-word; display: none; z-index: 70; }\n #viewer .close { position: absolute; top: 8px; right: 12px; cursor: pointer; color: var(--subtext0); }";
|
|
1
|
+
export declare const STYLES = " :root {\n --base: #1e1e2e; --mantle: #181825; --crust: #11111b;\n --surface0: #313244; --surface1: #45475a; --surface2: #585b70;\n --text: #cdd6f4; --subtext1: #a6adc8; --subtext0: #7f849c;\n --mauve: #cba6f7; --blue: #89b4fa; --green: #a6e3a1; --red: #f38ba8;\n --yellow: #f9e2af; --peach: #fab387; --teal: #94e2d5;\n }\n *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n body {\n background: var(--base); color: var(--text);\n font-family: ui-monospace, monospace;\n /* --app-h is set from window.innerHeight (see setAppHeight) so the column\n height is a stable pixel value across an orientation change. 100dvh is a\n fallback for first paint / no-JS: iOS Safari interpolates dvh during the\n rotation animation, which makes the whole flex column resize repeatedly\n (\"spazzing out\") \u2014 a fixed px height does not. */\n height: var(--app-h, 100dvh);\n display: flex; flex-direction: column; overflow: hidden;\n padding: env(safe-area-inset-top, 0px) env(safe-area-inset-right, 0px)\n 0px env(safe-area-inset-left, 0px);\n }\n #context-bar { height: 4px; background: var(--surface0); flex-shrink: 0; }\n #context-bar-fill { height: 100%; background: var(--mauve); width: 0%; transition: width 0.4s ease; }\n #header {\n background: var(--mantle); padding: 8px 16px;\n display: flex; justify-content: space-between; align-items: center;\n font-size: 13px; flex-shrink: 0; border-bottom: 1px solid var(--surface0);\n }\n #header .title { font-weight: bold; color: var(--mauve); letter-spacing: 0.05em;\n position: relative; animation: glitch 5s steps(1) infinite; }\n @keyframes glitch {\n 0%, 88%, 100% { text-shadow: none; transform: translate(0, 0); }\n 90% { text-shadow: -1px 0 var(--red), 1px 0 var(--teal); transform: translate(1px, -1px); }\n 92% { text-shadow: 1px 0 var(--red), -1px 0 var(--blue); transform: translate(-1px, 1px); }\n 94% { text-shadow: -1px 0 var(--blue), 1px 0 var(--red); transform: translate(1px, 0); }\n 96% { text-shadow: 1px 0 var(--teal), -1px 0 var(--red); transform: translate(-1px, 0); }\n }\n @media (prefers-reduced-motion: reduce) { #header .title { animation: none; } }\n #header .hgroup { display: flex; align-items: center; gap: 10px; }\n #bell {\n background: none; border: none; color: var(--subtext1); cursor: pointer;\n font-size: 15px; line-height: 1; padding: 2px; font-family: inherit;\n }\n #bell:hover { color: var(--text); }\n #bell.on { color: var(--mauve); }\n #chat-wrap { position: relative; flex: 1; min-height: 0; display: flex; }\n #chat-log {\n flex: 1; min-width: 0; overflow-y: auto; overflow-x: hidden; padding: 16px;\n display: flex; flex-direction: column; gap: 8px;\n }\n /* Floating jump-to-latest button \u2014 only shown when scrolled away from the\n bottom (toggled via .visible from the scroll handler). */\n #scroll-bottom {\n display: none; position: absolute; bottom: 16px; right: 16px; z-index: 40;\n width: 36px; height: 36px; border-radius: 50%; cursor: pointer;\n background: var(--surface1); color: var(--text); border: 1px solid var(--surface2);\n font-family: inherit; font-size: 18px; line-height: 1; padding: 0;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);\n }\n #scroll-bottom:hover { background: var(--surface2); color: var(--mauve); }\n #scroll-bottom.visible { display: block; }\n #chat-log::-webkit-scrollbar { width: 6px; }\n #chat-log::-webkit-scrollbar-track { background: transparent; }\n #chat-log::-webkit-scrollbar-thumb { background: var(--surface2); border-radius: 3px; }\n .bubble {\n max-width: 82%; padding: 8px 12px; border-radius: 8px;\n line-height: 1.6; white-space: pre-wrap; word-break: break-word; font-size: 13px;\n }\n .bubble.user { background: var(--surface1); color: var(--text); align-self: flex-end; }\n .bubble.assistant { background: var(--surface0); color: var(--text); align-self: flex-start; }\n .bubble.error {\n background: var(--crust); color: var(--red); align-self: stretch;\n max-width: 100%; border: 1px solid var(--red); font-size: 12px;\n }\n /* Persistent inline system note (e.g. context compaction) \u2014 a muted centered\n divider, distinct from chat bubbles. */\n .sysnote {\n align-self: center; color: var(--subtext0); font-size: 11px;\n font-family: ui-monospace, monospace; letter-spacing: 0.5px;\n padding: 2px 10px; opacity: 0.85;\n }\n .bubble.thinking {\n display: flex; gap: 5px; align-items: center; padding: 10px 14px;\n }\n .bubble.thinking .spinner {\n color: var(--mauve); font-size: 15px; line-height: 1;\n font-family: ui-monospace, monospace;\n }\n .tool-call {\n background: var(--crust); border-radius: 6px; align-self: flex-start;\n max-width: 90%; font-size: 12px; border: 1px solid var(--surface0);\n }\n .tool-call summary {\n padding: 6px 10px; color: var(--subtext1); cursor: pointer;\n user-select: none; list-style: none;\n display: flex; align-items: center; gap: 8px;\n }\n .tool-call summary::-webkit-details-marker { display: none; }\n .tool-call summary::before { content: \"\u25B6\"; flex-shrink: 0; font-size: 9px; color: var(--subtext0); }\n .tool-call[open] > summary::before { content: \"\u25BC\"; }\n .tool-call.error > summary { color: var(--red); }\n /* The summary is a single line: the label ellipsizes (full text in the title\n tooltip / on expand), badges and timing stay pinned to the right. */\n .tool-label {\n flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n font-family: ui-monospace, monospace;\n }\n .tool-badge { flex-shrink: 0; color: var(--subtext0); font-size: 11px; }\n .tool-elapsed { flex-shrink: 0; color: var(--subtext0); font-size: 11px; }\n .tool-call pre {\n padding: 8px 12px; overflow-y: auto;\n color: var(--subtext1); font-size: 11px; max-height: 280px;\n border-top: 1px solid var(--surface0);\n white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word;\n }\n .tool-spin { color: var(--mauve); flex-shrink: 0; font-family: ui-monospace, monospace; font-size: 13px; }\n /* Line diff for edit/write tools (tints derived from --green/--red). */\n .tool-diff { border-top: 1px solid var(--surface0); overflow-x: auto; }\n .diff { font-family: ui-monospace, monospace; font-size: 11px; padding: 4px 0; }\n .diff-line { white-space: pre; padding: 0 10px; }\n .diff-sign { display: inline-block; width: 1ch; margin-right: 8px; color: var(--subtext0); }\n .diff-add { background: color-mix(in srgb, var(--green) 14%, transparent); color: var(--green); }\n .diff-del { background: color-mix(in srgb, var(--red) 14%, transparent); color: var(--red); }\n .diff-ctx { color: var(--subtext1); }\n .code-block {\n background: var(--crust); border: 1px solid var(--surface0);\n border-radius: 6px; overflow: hidden; margin: 4px 0;\n align-self: stretch; max-width: 100%; font-size: 12px;\n }\n .code-lang {\n background: var(--surface0); color: var(--subtext0);\n font-size: 10px; padding: 3px 10px; letter-spacing: 0.05em;\n }\n .code-block code {\n display: block; padding: 10px 12px; overflow-x: auto;\n color: var(--text); white-space: pre; line-height: 1.55;\n }\n .hl-kw { color: var(--mauve); }\n .hl-str { color: var(--green); }\n .hl-cmt { color: var(--subtext0); font-style: italic; }\n .hl-num { color: var(--blue); }\n .hl-fn { color: var(--yellow); }\n /* Hand-rolled markdown, applied only to assistant bubbles + the recommendation\n panel (both get the .md class from setContent). Block elements lay themselves\n out, so switch off the container's pre-wrap for these. */\n .md { white-space: normal; }\n .md > :first-child { margin-top: 0; }\n .md > :last-child { margin-bottom: 0; }\n .md .md-h { color: var(--mauve); font-weight: 700; line-height: 1.3; margin: 10px 0 4px; }\n .md .md-h1 { font-size: 1.35em; }\n .md .md-h2 { font-size: 1.2em; }\n .md .md-h3 { font-size: 1.08em; }\n .md .md-h4, .md .md-h5, .md .md-h6 { font-size: 1em; }\n .md .md-p { margin: 6px 0; }\n .md .md-list { margin: 6px 0; padding-left: 22px; }\n .md .md-list li { margin: 2px 0; }\n .md .md-task { list-style: none; margin-left: -22px; }\n .md .md-check { color: var(--green); }\n .md .md-quote { border-left: 3px solid var(--surface2); margin: 6px 0; padding: 2px 0 2px 10px; color: var(--subtext1); }\n .md .md-hr { border: none; border-top: 1px solid var(--surface1); margin: 10px 0; }\n .md a { color: var(--blue); text-decoration: underline; }\n .md strong { color: var(--text); font-weight: 700; }\n .md em { font-style: italic; }\n .md del { color: var(--subtext0); }\n .md .md-code {\n background: var(--crust); border: 1px solid var(--surface0);\n border-radius: 4px; padding: 1px 4px; font-size: 0.92em;\n }\n .md .md-table { border-collapse: collapse; margin: 8px 0; font-size: 0.95em; display: block; overflow-x: auto; }\n .md .md-table th, .md .md-table td { border: 1px solid var(--surface1); padding: 4px 8px; text-align: left; }\n .md .md-table th { background: var(--surface0); color: var(--subtext1); }\n #input-bar {\n background: var(--mantle); padding: 10px 16px calc(10px + env(safe-area-inset-bottom, 0px));\n display: flex; gap: 8px; flex-shrink: 0;\n border-top: 1px solid var(--surface0);\n position: relative;\n }\n #cmd-suggestions {\n display: none; position: absolute; bottom: 100%; left: 16px; right: 16px;\n background: var(--mantle); border: 1px solid var(--surface1);\n border-bottom: none; border-radius: 8px 8px 0 0;\n overflow: hidden; z-index: 10;\n }\n .cmd-item {\n display: flex; align-items: baseline; gap: 10px;\n padding: 7px 12px; cursor: pointer; font-size: 12px;\n border-bottom: 1px solid var(--surface0);\n }\n .cmd-item:last-child { border-bottom: none; }\n .cmd-item:hover, .cmd-item.active { background: var(--surface0); }\n .cmd-item .cmd-name { color: var(--blue); font-weight: bold; flex-shrink: 0; }\n .cmd-item .cmd-desc { color: var(--subtext0); }\n #input {\n flex: 1; background: var(--surface0); color: var(--text);\n border: none; border-radius: 6px; padding: 8px 12px;\n font-family: inherit; font-size: 13px; resize: none;\n outline: none; line-height: 1.5; min-height: 36px; max-height: 120px;\n }\n #input::placeholder { color: var(--subtext0); }\n #input:focus { box-shadow: 0 0 0 1px var(--mauve); }\n #send {\n background: var(--blue); color: var(--crust); border: none;\n border-radius: 6px; padding: 8px 16px; font-weight: bold;\n cursor: pointer; font-size: 13px; font-family: inherit;\n white-space: nowrap; align-self: flex-end;\n }\n #send:disabled, #input:disabled { opacity: 0.45; cursor: not-allowed; }\n #reconnect-overlay {\n display: none; position: fixed; inset: 0;\n background: rgba(30,30,46,0.88); color: var(--subtext1);\n justify-content: center; align-items: center;\n font-size: 13px; z-index: 100; letter-spacing: 0.03em;\n }\n #reconnect-overlay.visible { display: flex; }\n /* Trailing stream indicator: the same braille spinner as the thinking bubble,\n inline at the end of the streaming text (not a green blinking block). */\n .cursor {\n color: var(--mauve); margin-left: 2px;\n font-family: ui-monospace, monospace;\n }\n #status-panel { padding: 6px 12px; border-bottom: 1px solid var(--surface1);\n color: var(--subtext1); white-space: pre-wrap; font-size: 13px; display: none; }\n #prompt-card { position: fixed; left: 0; right: 0; bottom: 0; background: var(--mantle);\n border-top: 2px solid var(--mauve); padding: 16px 14px calc(16px + env(safe-area-inset-bottom, 0px));\n display: none; z-index: 50; max-height: 80dvh; overflow-y: auto; }\n #prompt-card .q-label { color: var(--mauve); font-size: 11px; font-weight: 700;\n text-transform: uppercase; letter-spacing: .6px; margin-bottom: 6px; }\n #prompt-card .q { color: var(--text); margin-bottom: 12px; white-space: pre-wrap;\n font-size: 15px; line-height: 1.5; }\n #prompt-card .rec-panel { background: var(--surface0); border-left: 3px solid var(--green);\n border-radius: 6px; padding: 10px 12px; margin-bottom: 12px; }\n #prompt-card .rec-label { color: var(--green); font-size: 11px; font-weight: 700;\n text-transform: uppercase; letter-spacing: .5px; margin-bottom: 4px; }\n #prompt-card .rec-text { color: var(--text); font-size: 15px; line-height: 1.5;\n white-space: pre-wrap; overflow-wrap: anywhere; }\n #prompt-card textarea { width: 100%; background: var(--surface0); color: var(--text);\n border: 1px solid var(--surface2); border-radius: 6px; padding: 10px; font-size: 15px;\n font-family: inherit; line-height: 1.5; resize: vertical; margin-bottom: 4px; }\n #prompt-card .row { display: flex; gap: 8px; margin-top: 12px; align-items: stretch;\n flex-wrap: wrap; }\n /* Recommendation answers can be long sentences, so stack them as a readable list. */\n #prompt-card .row.stacked { flex-direction: column; align-items: stretch; }\n #prompt-card .row.stacked button { flex: none; text-align: left; }\n #prompt-card .row.stacked button.cancel { align-self: center; text-align: center; }\n #prompt-card button { padding: 11px 16px; border-radius: 8px; border: none; cursor: pointer;\n font-family: inherit; font-size: 14px; font-weight: 600; transition: filter .15s ease; }\n #prompt-card button:hover { filter: brightness(1.08); }\n #prompt-card button.primary { background: var(--green); color: var(--crust);\n font-weight: 700; flex: 1; min-width: 160px; }\n #prompt-card button.secondary { background: var(--surface1); color: var(--text);\n flex: 1; min-width: 160px; }\n #prompt-card button.cancel { margin-left: auto; align-self: center; background: transparent;\n color: var(--subtext0); font-size: 12px; font-weight: 500; padding: 8px 10px; }\n #prompt-card button.cancel:hover { color: var(--red); filter: none; }\n #prompt-card button.cancel.armed { background: var(--red); color: var(--crust); font-weight: 700; }\n .toast { position: fixed; top: calc(env(safe-area-inset-top, 0px) + 12px);\n right: calc(env(safe-area-inset-right, 0px) + 12px); max-width: calc(100vw - 24px);\n padding: 8px 12px; border-radius: 6px; overflow-wrap: anywhere; word-break: break-word;\n background: var(--surface1); color: var(--text); z-index: 60; }\n .toast.warning { background: var(--peach); color: var(--crust); }\n .toast.error { background: var(--red); color: var(--crust); }\n #viewer { position: fixed; inset: 24px; background: var(--mantle); border: 1px solid var(--surface2);\n border-radius: 8px; padding: 16px; overflow: auto; white-space: pre-wrap;\n overflow-wrap: anywhere; word-break: break-word; display: none; z-index: 70; }\n #viewer .close { position: absolute; top: 8px; right: 12px; cursor: pointer; color: var(--subtext0); }\n /* Desktop: center the transcript/status/input in a readable column instead of\n hugging the left edge. The scrollbar stays at the true window edge; only the\n content is inset. Mobile (below 960px) is unchanged. */\n @media (min-width: 960px) {\n #chat-log { padding-left: calc((100% - 920px) / 2); padding-right: calc((100% - 920px) / 2); }\n #status-panel { padding-left: calc((100% - 920px) / 2 + 12px); padding-right: calc((100% - 920px) / 2 + 12px); }\n #input-bar { padding-left: calc((100% - 920px) / 2); padding-right: calc((100% - 920px) / 2); }\n #cmd-suggestions { left: calc((100% - 920px) / 2 + 16px); right: calc((100% - 920px) / 2 + 16px); }\n }";
|
package/dist/remote/ui-styles.js
CHANGED
|
@@ -93,19 +93,35 @@ export const STYLES = ` :root {
|
|
|
93
93
|
.tool-call summary {
|
|
94
94
|
padding: 6px 10px; color: var(--subtext1); cursor: pointer;
|
|
95
95
|
user-select: none; list-style: none;
|
|
96
|
-
|
|
96
|
+
display: flex; align-items: center; gap: 8px;
|
|
97
97
|
}
|
|
98
98
|
.tool-call summary::-webkit-details-marker { display: none; }
|
|
99
|
-
.tool-call summary::before { content: "▶
|
|
100
|
-
.tool-call[open] > summary::before { content: "▼
|
|
99
|
+
.tool-call summary::before { content: "▶"; flex-shrink: 0; font-size: 9px; color: var(--subtext0); }
|
|
100
|
+
.tool-call[open] > summary::before { content: "▼"; }
|
|
101
101
|
.tool-call.error > summary { color: var(--red); }
|
|
102
|
+
/* The summary is a single line: the label ellipsizes (full text in the title
|
|
103
|
+
tooltip / on expand), badges and timing stay pinned to the right. */
|
|
104
|
+
.tool-label {
|
|
105
|
+
flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
106
|
+
font-family: ui-monospace, monospace;
|
|
107
|
+
}
|
|
108
|
+
.tool-badge { flex-shrink: 0; color: var(--subtext0); font-size: 11px; }
|
|
109
|
+
.tool-elapsed { flex-shrink: 0; color: var(--subtext0); font-size: 11px; }
|
|
102
110
|
.tool-call pre {
|
|
103
111
|
padding: 8px 12px; overflow-y: auto;
|
|
104
112
|
color: var(--subtext1); font-size: 11px; max-height: 280px;
|
|
105
113
|
border-top: 1px solid var(--surface0);
|
|
106
114
|
white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word;
|
|
107
115
|
}
|
|
108
|
-
.tool-spin { color: var(--mauve);
|
|
116
|
+
.tool-spin { color: var(--mauve); flex-shrink: 0; font-family: ui-monospace, monospace; font-size: 13px; }
|
|
117
|
+
/* Line diff for edit/write tools (tints derived from --green/--red). */
|
|
118
|
+
.tool-diff { border-top: 1px solid var(--surface0); overflow-x: auto; }
|
|
119
|
+
.diff { font-family: ui-monospace, monospace; font-size: 11px; padding: 4px 0; }
|
|
120
|
+
.diff-line { white-space: pre; padding: 0 10px; }
|
|
121
|
+
.diff-sign { display: inline-block; width: 1ch; margin-right: 8px; color: var(--subtext0); }
|
|
122
|
+
.diff-add { background: color-mix(in srgb, var(--green) 14%, transparent); color: var(--green); }
|
|
123
|
+
.diff-del { background: color-mix(in srgb, var(--red) 14%, transparent); color: var(--red); }
|
|
124
|
+
.diff-ctx { color: var(--subtext1); }
|
|
109
125
|
.code-block {
|
|
110
126
|
background: var(--crust); border: 1px solid var(--surface0);
|
|
111
127
|
border-radius: 6px; overflow: hidden; margin: 4px 0;
|
|
@@ -124,6 +140,35 @@ export const STYLES = ` :root {
|
|
|
124
140
|
.hl-cmt { color: var(--subtext0); font-style: italic; }
|
|
125
141
|
.hl-num { color: var(--blue); }
|
|
126
142
|
.hl-fn { color: var(--yellow); }
|
|
143
|
+
/* Hand-rolled markdown, applied only to assistant bubbles + the recommendation
|
|
144
|
+
panel (both get the .md class from setContent). Block elements lay themselves
|
|
145
|
+
out, so switch off the container's pre-wrap for these. */
|
|
146
|
+
.md { white-space: normal; }
|
|
147
|
+
.md > :first-child { margin-top: 0; }
|
|
148
|
+
.md > :last-child { margin-bottom: 0; }
|
|
149
|
+
.md .md-h { color: var(--mauve); font-weight: 700; line-height: 1.3; margin: 10px 0 4px; }
|
|
150
|
+
.md .md-h1 { font-size: 1.35em; }
|
|
151
|
+
.md .md-h2 { font-size: 1.2em; }
|
|
152
|
+
.md .md-h3 { font-size: 1.08em; }
|
|
153
|
+
.md .md-h4, .md .md-h5, .md .md-h6 { font-size: 1em; }
|
|
154
|
+
.md .md-p { margin: 6px 0; }
|
|
155
|
+
.md .md-list { margin: 6px 0; padding-left: 22px; }
|
|
156
|
+
.md .md-list li { margin: 2px 0; }
|
|
157
|
+
.md .md-task { list-style: none; margin-left: -22px; }
|
|
158
|
+
.md .md-check { color: var(--green); }
|
|
159
|
+
.md .md-quote { border-left: 3px solid var(--surface2); margin: 6px 0; padding: 2px 0 2px 10px; color: var(--subtext1); }
|
|
160
|
+
.md .md-hr { border: none; border-top: 1px solid var(--surface1); margin: 10px 0; }
|
|
161
|
+
.md a { color: var(--blue); text-decoration: underline; }
|
|
162
|
+
.md strong { color: var(--text); font-weight: 700; }
|
|
163
|
+
.md em { font-style: italic; }
|
|
164
|
+
.md del { color: var(--subtext0); }
|
|
165
|
+
.md .md-code {
|
|
166
|
+
background: var(--crust); border: 1px solid var(--surface0);
|
|
167
|
+
border-radius: 4px; padding: 1px 4px; font-size: 0.92em;
|
|
168
|
+
}
|
|
169
|
+
.md .md-table { border-collapse: collapse; margin: 8px 0; font-size: 0.95em; display: block; overflow-x: auto; }
|
|
170
|
+
.md .md-table th, .md .md-table td { border: 1px solid var(--surface1); padding: 4px 8px; text-align: left; }
|
|
171
|
+
.md .md-table th { background: var(--surface0); color: var(--subtext1); }
|
|
127
172
|
#input-bar {
|
|
128
173
|
background: var(--mantle); padding: 10px 16px calc(10px + env(safe-area-inset-bottom, 0px));
|
|
129
174
|
display: flex; gap: 8px; flex-shrink: 0;
|
|
@@ -217,4 +262,13 @@ export const STYLES = ` :root {
|
|
|
217
262
|
#viewer { position: fixed; inset: 24px; background: var(--mantle); border: 1px solid var(--surface2);
|
|
218
263
|
border-radius: 8px; padding: 16px; overflow: auto; white-space: pre-wrap;
|
|
219
264
|
overflow-wrap: anywhere; word-break: break-word; display: none; z-index: 70; }
|
|
220
|
-
#viewer .close { position: absolute; top: 8px; right: 12px; cursor: pointer; color: var(--subtext0); }
|
|
265
|
+
#viewer .close { position: absolute; top: 8px; right: 12px; cursor: pointer; color: var(--subtext0); }
|
|
266
|
+
/* Desktop: center the transcript/status/input in a readable column instead of
|
|
267
|
+
hugging the left edge. The scrollbar stays at the true window edge; only the
|
|
268
|
+
content is inset. Mobile (below 960px) is unchanged. */
|
|
269
|
+
@media (min-width: 960px) {
|
|
270
|
+
#chat-log { padding-left: calc((100% - 920px) / 2); padding-right: calc((100% - 920px) / 2); }
|
|
271
|
+
#status-panel { padding-left: calc((100% - 920px) / 2 + 12px); padding-right: calc((100% - 920px) / 2 + 12px); }
|
|
272
|
+
#input-bar { padding-left: calc((100% - 920px) / 2); padding-right: calc((100% - 920px) / 2); }
|
|
273
|
+
#cmd-suggestions { left: calc((100% - 920px) / 2 + 16px); right: calc((100% - 920px) / 2 + 16px); }
|
|
274
|
+
}`;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Tool-call presentation helpers, shipped as a JS string and concatenated into
|
|
2
|
+
// the page by ui.ts. Turns raw {toolName, args} into a readable one-line summary,
|
|
3
|
+
// a +N −M diff badge, and an expandable line-diff body for edit/write tools —
|
|
4
|
+
// replacing the old "name + JSON.stringify(args) hard-sliced at 64 chars".
|
|
5
|
+
//
|
|
6
|
+
// Depends on escHtml (ui-render.ts) — both land in the same <script>.
|
|
7
|
+
/** Emitted client JS (top-level function declarations). */
|
|
8
|
+
export function toolsModule() {
|
|
9
|
+
return ` // args arrives as an object (live tool_start / snapshot part) or a JSON string.
|
|
10
|
+
function toolArgs(args) {
|
|
11
|
+
if (args && typeof args === 'object') return args;
|
|
12
|
+
if (typeof args === 'string') { try { return JSON.parse(args); } catch (e) { return null; } }
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// A readable one-line summary. Falls back to the old name+JSON form (but with NO
|
|
17
|
+
// hard 64-char slice — the summary line ellipsizes via CSS, full text on hover).
|
|
18
|
+
function toolSummary(toolName, args) {
|
|
19
|
+
var name = (toolName || '').toLowerCase();
|
|
20
|
+
var a = toolArgs(args);
|
|
21
|
+
function s(v) { return v == null ? '' : String(v); }
|
|
22
|
+
if (a) {
|
|
23
|
+
var path = a.path != null ? a.path : a.file_path;
|
|
24
|
+
if (name.indexOf('bash') >= 0 && a.command != null) return '$ ' + s(a.command);
|
|
25
|
+
if (name === 'read' && path != null) return 'read ' + s(path);
|
|
26
|
+
if ((name === 'edit' || name === 'multiedit' || name === 'write' || name === 'str_replace') && path != null) return s(path);
|
|
27
|
+
if (name === 'grep' || name === 'find' || name === 'glob' || name === 'search') {
|
|
28
|
+
var pat = a.pattern != null ? a.pattern : (a.query != null ? a.query : a.glob);
|
|
29
|
+
var loc = a.path != null ? a.path : a.glob;
|
|
30
|
+
return name + ' ' + s(pat) + (loc && loc !== pat ? ' ' + s(loc) : '');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
var argStr = typeof args === 'string' ? args : (args === undefined ? '' : JSON.stringify(args));
|
|
34
|
+
return toolName + (argStr ? ': ' + argStr : '');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Line diff via LCS. Bounded: a huge pair falls back to "all removed / all added"
|
|
38
|
+
// rather than allocating an O(m*n) table that could hang the tab.
|
|
39
|
+
function lcsDiff(oldText, newText) {
|
|
40
|
+
// Treat '' / null as ZERO lines (not one empty line) so a new-file write shows
|
|
41
|
+
// as all-added with no phantom "removed blank line".
|
|
42
|
+
var a = (oldText == null || oldText === '') ? [] : String(oldText).split('\\n');
|
|
43
|
+
var b = (newText == null || newText === '') ? [] : String(newText).split('\\n');
|
|
44
|
+
var m = a.length, n = b.length;
|
|
45
|
+
var rows = [];
|
|
46
|
+
if (m * n > 250000) {
|
|
47
|
+
for (var x = 0; x < m; x++) rows.push({t: '-', s: a[x]});
|
|
48
|
+
for (var y = 0; y < n; y++) rows.push({t: '+', s: b[y]});
|
|
49
|
+
return rows;
|
|
50
|
+
}
|
|
51
|
+
var dp = [];
|
|
52
|
+
for (var i = 0; i <= m; i++) dp.push(new Array(n + 1).fill(0));
|
|
53
|
+
for (var i2 = m - 1; i2 >= 0; i2--)
|
|
54
|
+
for (var j2 = n - 1; j2 >= 0; j2--)
|
|
55
|
+
dp[i2][j2] = a[i2] === b[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
|
|
56
|
+
var i = 0, j = 0;
|
|
57
|
+
while (i < m && j < n) {
|
|
58
|
+
if (a[i] === b[j]) { rows.push({t: ' ', s: a[i]}); i++; j++; }
|
|
59
|
+
else if (dp[i + 1][j] >= dp[i][j + 1]) { rows.push({t: '-', s: a[i]}); i++; }
|
|
60
|
+
else { rows.push({t: '+', s: b[j]}); j++; }
|
|
61
|
+
}
|
|
62
|
+
while (i < m) { rows.push({t: '-', s: a[i++]}); }
|
|
63
|
+
while (j < n) { rows.push({t: '+', s: b[j++]}); }
|
|
64
|
+
return rows;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function diffStats(oldText, newText) {
|
|
68
|
+
var rows = lcsDiff(oldText, newText), add = 0, rem = 0;
|
|
69
|
+
for (var k = 0; k < rows.length; k++) { if (rows[k].t === '+') add++; else if (rows[k].t === '-') rem++; }
|
|
70
|
+
return {added: add, removed: rem};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function renderDiff(oldText, newText) {
|
|
74
|
+
var rows = lcsDiff(oldText, newText);
|
|
75
|
+
var html = '<div class="diff">';
|
|
76
|
+
for (var k = 0; k < rows.length; k++) {
|
|
77
|
+
var r = rows[k];
|
|
78
|
+
var cls = r.t === '+' ? 'diff-add' : (r.t === '-' ? 'diff-del' : 'diff-ctx');
|
|
79
|
+
html += '<div class="diff-line ' + cls + '"><span class="diff-sign">' + (r.t === ' ' ? ' ' : r.t) + '</span>' + escHtml(r.s) + '</div>';
|
|
80
|
+
}
|
|
81
|
+
return html + '</div>';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// The old/new text for an edit/write tool's diff, or null if not a diffable tool.
|
|
85
|
+
function toolDiffPair(toolName, args) {
|
|
86
|
+
var name = (toolName || '').toLowerCase();
|
|
87
|
+
var a = toolArgs(args);
|
|
88
|
+
if (!a) return null;
|
|
89
|
+
if (name === 'edit' || name === 'multiedit' || name === 'str_replace') {
|
|
90
|
+
if (a.old_string != null || a.new_string != null) return {old: a.old_string, neu: a.new_string};
|
|
91
|
+
}
|
|
92
|
+
if (name === 'write' && a.content != null) return {old: '', neu: a.content};
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function toolBadge(toolName, args) {
|
|
97
|
+
var pair = toolDiffPair(toolName, args);
|
|
98
|
+
return pair ? diffStats(pair.old, pair.neu) : null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function toolDiffHtml(toolName, args) {
|
|
102
|
+
var pair = toolDiffPair(toolName, args);
|
|
103
|
+
return pair ? renderDiff(pair.old, pair.neu) : null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function fmtElapsed(ms) {
|
|
107
|
+
if (ms == null || isNaN(ms)) return '';
|
|
108
|
+
if (ms < 1000) return Math.round(ms) + 'ms';
|
|
109
|
+
var sec = ms / 1000;
|
|
110
|
+
return (sec < 10 ? sec.toFixed(1) : Math.round(sec)) + 's';
|
|
111
|
+
}
|
|
112
|
+
`;
|
|
113
|
+
}
|
package/dist/remote/ui.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { STYLES } from './ui-styles.js';
|
|
2
2
|
import { clientScript } from './ui-script.js';
|
|
3
|
+
import { renderModule } from './ui-render.js';
|
|
4
|
+
import { highlightModule } from './ui-highlight.js';
|
|
5
|
+
import { toolsModule } from './ui-tools.js';
|
|
3
6
|
export function html(wsUrl) {
|
|
4
7
|
const iconSvg = encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180">`
|
|
5
8
|
+ `<rect width="180" height="180" rx="38" fill="#1e1e2e"/>`
|
|
@@ -61,6 +64,9 @@ ${STYLES}
|
|
|
61
64
|
</div>
|
|
62
65
|
<div id="viewer"><span class="close" id="viewer-close">✕</span><div id="viewer-body"></div></div>
|
|
63
66
|
<script>
|
|
67
|
+
${renderModule()}
|
|
68
|
+
${highlightModule()}
|
|
69
|
+
${toolsModule()}
|
|
64
70
|
${clientScript(wsUrl)}
|
|
65
71
|
</script>
|
|
66
72
|
</body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.22",
|
|
4
4
|
"description": "Deterministic task planning and spec-orchestration for local models — crash-safe /task pipelines with verify/enforce gates, a real-time remote web view, and web/docs/fetch/worker subagent tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|