pi-sdk-web 0.3.11 → 0.3.13
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/server.js +10 -0
- package/dist/static/app.js +211 -15
- package/dist/static/index.html +1 -1
- package/dist/static/style.css +2 -2
- package/dist/ui-context.js +65 -18
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -418,6 +418,16 @@ export class PiWebServer {
|
|
|
418
418
|
// Used by the sidebar Tools refresh button
|
|
419
419
|
this.broadcastState();
|
|
420
420
|
break;
|
|
421
|
+
case "set_theme": {
|
|
422
|
+
// Browser theme switch (dark/light): swap the extension ANSI theme.
|
|
423
|
+
// CSS switches instantly on the client; the server theme only
|
|
424
|
+
// affects extension setStatus/setWidget generated after this point
|
|
425
|
+
// (same as TUI - status text is fixed at setStatus time).
|
|
426
|
+
const name = data.name === "light" ? "light" : "dark";
|
|
427
|
+
this.uiContext.setWebTheme(name);
|
|
428
|
+
this.broadcast({ type: "theme_set", data: { name } });
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
421
431
|
case "bash": {
|
|
422
432
|
const command = typeof data.command === "string" ? data.command : "";
|
|
423
433
|
if (!command)
|
package/dist/static/app.js
CHANGED
|
@@ -16,6 +16,168 @@ const BUILTIN_COMMANDS = [
|
|
|
16
16
|
{ name: 'settings', description: 'Open settings menu (not supported in web)', builtin: true, unsupported: true },
|
|
17
17
|
];
|
|
18
18
|
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// ANSI escape code to HTML converter.
|
|
21
|
+
//
|
|
22
|
+
// Extensions build status/widget/notify strings with ANSI escapes (via
|
|
23
|
+
// ui.theme.fg/bg, e.g. magic-context's status line). The browser has no
|
|
24
|
+
// terminal to render them, so we convert escapes to inline-styled <span>
|
|
25
|
+
// here — the browser acts as the "terminal". Pi's own export-html does the
|
|
26
|
+
// same conversion server-side (core/export-html/ansi-to-html.ts); this is a
|
|
27
|
+
// port of that pure function, matching its behavior.
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
// Standard ANSI color palette (0-15)
|
|
31
|
+
const ANSI_COLORS = [
|
|
32
|
+
'#000000', // 0: black
|
|
33
|
+
'#800000', // 1: red
|
|
34
|
+
'#008000', // 2: green
|
|
35
|
+
'#808000', // 3: yellow
|
|
36
|
+
'#000080', // 4: blue
|
|
37
|
+
'#800080', // 5: magenta
|
|
38
|
+
'#008080', // 6: cyan
|
|
39
|
+
'#c0c0c0', // 7: white
|
|
40
|
+
'#808080', // 8: bright black
|
|
41
|
+
'#ff0000', // 9: bright red
|
|
42
|
+
'#00ff00', // 10: bright green
|
|
43
|
+
'#ffff00', // 11: bright yellow
|
|
44
|
+
'#0000ff', // 12: bright blue
|
|
45
|
+
'#ff00ff', // 13: bright magenta
|
|
46
|
+
'#00ffff', // 14: bright cyan
|
|
47
|
+
'#ffffff', // 15: bright white
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
/** Convert a 256-color index (0-255) to hex. */
|
|
51
|
+
function color256ToHex(index) {
|
|
52
|
+
if (index < 16) return ANSI_COLORS[index];
|
|
53
|
+
if (index < 232) {
|
|
54
|
+
// Color cube (16-231): 6x6x6 = 216 colors
|
|
55
|
+
const cubeIndex = index - 16;
|
|
56
|
+
const r = Math.floor(cubeIndex / 36);
|
|
57
|
+
const g = Math.floor((cubeIndex % 36) / 6);
|
|
58
|
+
const b = cubeIndex % 6;
|
|
59
|
+
const toComponent = (n) => (n === 0 ? 0 : 55 + n * 40);
|
|
60
|
+
const toHex = (n) => toComponent(n).toString(16).padStart(2, '0');
|
|
61
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
|
62
|
+
}
|
|
63
|
+
// Grayscale (232-255): 24 shades
|
|
64
|
+
const gray = 8 + (index - 232) * 10;
|
|
65
|
+
const grayHex = gray.toString(16).padStart(2, '0');
|
|
66
|
+
return `#${grayHex}${grayHex}${grayHex}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function escapeHtmlAnsi(text) {
|
|
70
|
+
return text
|
|
71
|
+
.replace(/&/g, '&')
|
|
72
|
+
.replace(/</g, '<')
|
|
73
|
+
.replace(/>/g, '>')
|
|
74
|
+
.replace(/"/g, '"')
|
|
75
|
+
.replace(/'/g, ''');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function createEmptyStyle() {
|
|
79
|
+
return { fg: null, bg: null, bold: false, dim: false, italic: false, underline: false };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function styleToInlineCSS(style) {
|
|
83
|
+
const parts = [];
|
|
84
|
+
if (style.fg) parts.push(`color:${style.fg}`);
|
|
85
|
+
if (style.bg) parts.push(`background-color:${style.bg}`);
|
|
86
|
+
if (style.bold) parts.push('font-weight:bold');
|
|
87
|
+
if (style.dim) parts.push('opacity:0.6');
|
|
88
|
+
if (style.italic) parts.push('font-style:italic');
|
|
89
|
+
if (style.underline) parts.push('text-decoration:underline');
|
|
90
|
+
return parts.join(';');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function hasStyle(style) {
|
|
94
|
+
return style.fg !== null || style.bg !== null || style.bold || style.dim || style.italic || style.underline;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function applySgrCode(params, style) {
|
|
98
|
+
let i = 0;
|
|
99
|
+
while (i < params.length) {
|
|
100
|
+
const code = params[i];
|
|
101
|
+
if (code === 0) {
|
|
102
|
+
style.fg = null; style.bg = null; style.bold = false;
|
|
103
|
+
style.dim = false; style.italic = false; style.underline = false;
|
|
104
|
+
} else if (code === 1) {
|
|
105
|
+
style.bold = true;
|
|
106
|
+
} else if (code === 2) {
|
|
107
|
+
style.dim = true;
|
|
108
|
+
} else if (code === 3) {
|
|
109
|
+
style.italic = true;
|
|
110
|
+
} else if (code === 4) {
|
|
111
|
+
style.underline = true;
|
|
112
|
+
} else if (code === 22) {
|
|
113
|
+
style.bold = false; style.dim = false;
|
|
114
|
+
} else if (code === 23) {
|
|
115
|
+
style.italic = false;
|
|
116
|
+
} else if (code === 24) {
|
|
117
|
+
style.underline = false;
|
|
118
|
+
} else if (code >= 30 && code <= 37) {
|
|
119
|
+
style.fg = ANSI_COLORS[code - 30];
|
|
120
|
+
} else if (code === 38) {
|
|
121
|
+
if (params[i + 1] === 5 && params.length > i + 2) {
|
|
122
|
+
style.fg = color256ToHex(params[i + 2]);
|
|
123
|
+
i += 2;
|
|
124
|
+
} else if (params[i + 1] === 2 && params.length > i + 4) {
|
|
125
|
+
style.fg = `rgb(${params[i + 2]},${params[i + 3]},${params[i + 4]})`;
|
|
126
|
+
i += 4;
|
|
127
|
+
}
|
|
128
|
+
} else if (code === 39) {
|
|
129
|
+
style.fg = null;
|
|
130
|
+
} else if (code >= 40 && code <= 47) {
|
|
131
|
+
style.bg = ANSI_COLORS[code - 40];
|
|
132
|
+
} else if (code === 48) {
|
|
133
|
+
if (params[i + 1] === 5 && params.length > i + 2) {
|
|
134
|
+
style.bg = color256ToHex(params[i + 2]);
|
|
135
|
+
i += 2;
|
|
136
|
+
} else if (params[i + 1] === 2 && params.length > i + 4) {
|
|
137
|
+
style.bg = `rgb(${params[i + 2]},${params[i + 3]},${params[i + 4]})`;
|
|
138
|
+
i += 4;
|
|
139
|
+
}
|
|
140
|
+
} else if (code === 49) {
|
|
141
|
+
style.bg = null;
|
|
142
|
+
} else if (code >= 90 && code <= 97) {
|
|
143
|
+
style.fg = ANSI_COLORS[code - 90 + 8];
|
|
144
|
+
} else if (code >= 100 && code <= 107) {
|
|
145
|
+
style.bg = ANSI_COLORS[code - 100 + 8];
|
|
146
|
+
}
|
|
147
|
+
i++;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Match ANSI escape sequences: ESC[ followed by params and ending with 'm'
|
|
152
|
+
const ANSI_REGEX = /\x1b\[([\d;]*)m/g;
|
|
153
|
+
|
|
154
|
+
/** Convert ANSI-escaped text to HTML with inline styles. */
|
|
155
|
+
function ansiToHtml(text) {
|
|
156
|
+
const style = createEmptyStyle();
|
|
157
|
+
let result = '';
|
|
158
|
+
let lastIndex = 0;
|
|
159
|
+
let inSpan = false;
|
|
160
|
+
ANSI_REGEX.lastIndex = 0;
|
|
161
|
+
let match = ANSI_REGEX.exec(text);
|
|
162
|
+
while (match !== null) {
|
|
163
|
+
const beforeText = text.slice(lastIndex, match.index);
|
|
164
|
+
if (beforeText) result += escapeHtmlAnsi(beforeText);
|
|
165
|
+
const params = match[1] ? match[1].split(';').map((p) => parseInt(p, 10) || 0) : [0];
|
|
166
|
+
if (inSpan) { result += '</span>'; inSpan = false; }
|
|
167
|
+
applySgrCode(params, style);
|
|
168
|
+
if (hasStyle(style)) {
|
|
169
|
+
result += `<span style="${styleToInlineCSS(style)}">`;
|
|
170
|
+
inSpan = true;
|
|
171
|
+
}
|
|
172
|
+
lastIndex = match.index + match[0].length;
|
|
173
|
+
match = ANSI_REGEX.exec(text);
|
|
174
|
+
}
|
|
175
|
+
const remainingText = text.slice(lastIndex);
|
|
176
|
+
if (remainingText) result += escapeHtmlAnsi(remainingText);
|
|
177
|
+
if (inSpan) result += '</span>';
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
|
|
19
181
|
class PiWebClient {
|
|
20
182
|
constructor() {
|
|
21
183
|
this.ws = null;
|
|
@@ -84,6 +246,10 @@ class PiWebClient {
|
|
|
84
246
|
return;
|
|
85
247
|
}
|
|
86
248
|
this.hasConnectedBefore = true;
|
|
249
|
+
// Sync the persisted theme to the server on first connection so
|
|
250
|
+
// extension ANSI colors match the CSS theme (server defaults to dark).
|
|
251
|
+
const stored = localStorage.getItem('piweb-theme') === 'bright' ? 'light' : (localStorage.getItem('piweb-theme') || 'light');
|
|
252
|
+
this.send({ type: 'set_theme', name: stored });
|
|
87
253
|
};
|
|
88
254
|
ws.onmessage = (ev) => this.handleMessage(ev.data);
|
|
89
255
|
ws.onclose = () => {
|
|
@@ -108,20 +274,33 @@ class PiWebClient {
|
|
|
108
274
|
// ------------------------------------------------------------------
|
|
109
275
|
|
|
110
276
|
initThemeSwitch() {
|
|
111
|
-
|
|
112
|
-
|
|
277
|
+
// Migrate the old 'bright' key to 'light' (renamed for TUI parity).
|
|
278
|
+
const stored = localStorage.getItem('piweb-theme') === 'bright' ? 'light' : (localStorage.getItem('piweb-theme') || 'light');
|
|
279
|
+
this.applyTheme(stored, true);
|
|
113
280
|
document.querySelectorAll('.theme-option').forEach((el) => {
|
|
114
|
-
el.addEventListener('click', () => this.applyTheme(el.dataset.theme));
|
|
281
|
+
el.addEventListener('click', () => this.applyTheme(el.dataset.theme, false));
|
|
115
282
|
});
|
|
116
283
|
}
|
|
117
284
|
|
|
118
|
-
applyTheme(theme) {
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
285
|
+
applyTheme(theme, initial) {
|
|
286
|
+
const light = theme === 'light';
|
|
287
|
+
const name = light ? 'light' : 'dark';
|
|
288
|
+
// CSS variables switch instantly (the "interface frame", like TUI's
|
|
289
|
+
// requestRender on theme change).
|
|
290
|
+
document.body.classList.toggle('theme-light', light);
|
|
291
|
+
localStorage.setItem('piweb-theme', name);
|
|
122
292
|
document.querySelectorAll('.theme-option').forEach((el) => {
|
|
123
|
-
el.classList.toggle('active', el.dataset.theme ===
|
|
293
|
+
el.classList.toggle('active', el.dataset.theme === name);
|
|
124
294
|
});
|
|
295
|
+
if (initial) return;
|
|
296
|
+
// Tell the server to swap the extension ANSI theme: extensions that
|
|
297
|
+
// setStatus/setWidget AFTER this point generate colors with the new
|
|
298
|
+
// theme. Already-rendered status text keeps its old colors until the
|
|
299
|
+
// extension updates it (same as TUI - status text is fixed at setStatus
|
|
300
|
+
// time; theme change only repaints the frame).
|
|
301
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
302
|
+
this.send({ type: 'set_theme', name: name });
|
|
303
|
+
}
|
|
125
304
|
}
|
|
126
305
|
|
|
127
306
|
send(obj) {
|
|
@@ -148,6 +327,12 @@ class PiWebClient {
|
|
|
148
327
|
case 'state':
|
|
149
328
|
this.renderState(data.data);
|
|
150
329
|
break;
|
|
330
|
+
case 'theme_set':
|
|
331
|
+
// Server acknowledged the theme switch. No reload needed: CSS
|
|
332
|
+
// variables already switched instantly and the server theme only
|
|
333
|
+
// affects extension setStatus/setWidget generated after this point
|
|
334
|
+
// (same as TUI).
|
|
335
|
+
break;
|
|
151
336
|
case 'history':
|
|
152
337
|
this.renderHistory(data.data);
|
|
153
338
|
break;
|
|
@@ -869,7 +1054,10 @@ class PiWebClient {
|
|
|
869
1054
|
widgetsEl.appendChild(el);
|
|
870
1055
|
}
|
|
871
1056
|
el.querySelector('.widget-title').textContent = key;
|
|
872
|
-
|
|
1057
|
+
// ANSI escapes (extension theme) render as colored spans per line.
|
|
1058
|
+
el.querySelector('.widget-body').innerHTML = (req.widgetLines || [])
|
|
1059
|
+
.map((line) => ansiToHtml(String(line)))
|
|
1060
|
+
.join('<br>');
|
|
873
1061
|
widgetsEl.style.display = 'block';
|
|
874
1062
|
// Layout change: keep pinned to bottom if already there
|
|
875
1063
|
if (wasAtBottom) this.scrollToBottom();
|
|
@@ -1028,9 +1216,11 @@ class PiWebClient {
|
|
|
1028
1216
|
if (!expanded && lines.length > limit) {
|
|
1029
1217
|
const visible = lines.slice(0, limit).join('\n');
|
|
1030
1218
|
const hidden = lines.length - limit;
|
|
1031
|
-
|
|
1219
|
+
// ANSI escapes (from tool outputs) render as colored spans; the
|
|
1220
|
+
// truncated marker stays plain text.
|
|
1221
|
+
out.innerHTML = ansiToHtml(visible) + `<div class="tool-truncated">... (${hidden} more lines, click to expand)</div>`;
|
|
1032
1222
|
} else {
|
|
1033
|
-
out.
|
|
1223
|
+
out.innerHTML = ansiToHtml(full);
|
|
1034
1224
|
}
|
|
1035
1225
|
}
|
|
1036
1226
|
|
|
@@ -1225,9 +1415,12 @@ class PiWebClient {
|
|
|
1225
1415
|
const text = this.inputEl.value.trim();
|
|
1226
1416
|
if (!text) return;
|
|
1227
1417
|
if (text.startsWith('!')) {
|
|
1228
|
-
|
|
1418
|
+
// TUI parity: "!cmd" runs bash (output goes to LLM context);
|
|
1419
|
+
// "!!cmd" runs bash with excludeFromContext (output NOT sent to LLM).
|
|
1420
|
+
const isExcluded = text.startsWith('!!');
|
|
1421
|
+
const command = (isExcluded ? text.slice(2) : text.slice(1)).trim();
|
|
1229
1422
|
if (command) {
|
|
1230
|
-
this.send({ type: 'bash', command: command });
|
|
1423
|
+
this.send({ type: 'bash', command: command, excludeFromContext: isExcluded });
|
|
1231
1424
|
}
|
|
1232
1425
|
} else if (text.startsWith('/')) {
|
|
1233
1426
|
// Skill commands (/skill:name args) go through prompt expansion in Pi -
|
|
@@ -1473,7 +1666,9 @@ class PiWebClient {
|
|
|
1473
1666
|
const el = document.getElementById('ext-status');
|
|
1474
1667
|
if (!el) return;
|
|
1475
1668
|
const entries = Object.entries(this.extStatus);
|
|
1476
|
-
|
|
1669
|
+
// ANSI escapes (from extension theme.fg/bg) render as colored spans.
|
|
1670
|
+
// ansiToHtml escapes the value's HTML; the key is escaped separately.
|
|
1671
|
+
el.innerHTML = entries.map(([k, v]) => `${this.escapeHtml(k)}: ${ansiToHtml(String(v))}`).join(' · ');
|
|
1477
1672
|
el.style.display = entries.length ? 'block' : 'none';
|
|
1478
1673
|
}
|
|
1479
1674
|
|
|
@@ -1578,7 +1773,8 @@ class PiWebClient {
|
|
|
1578
1773
|
if (!container) return;
|
|
1579
1774
|
const toast = document.createElement('div');
|
|
1580
1775
|
toast.className = 'toast' + (type ? ` toast-${type}` : '');
|
|
1581
|
-
|
|
1776
|
+
// Extension notify messages may carry ANSI escapes (theme.fg); render them.
|
|
1777
|
+
toast.innerHTML = ansiToHtml(String(message));
|
|
1582
1778
|
container.appendChild(toast);
|
|
1583
1779
|
setTimeout(() => {
|
|
1584
1780
|
toast.classList.add('toast-hide');
|
package/dist/static/index.html
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
<span id="theme-switch">
|
|
15
15
|
<span class="theme-option" data-theme="dark">Dark</span>
|
|
16
16
|
<span class="theme-sep">|</span>
|
|
17
|
-
<span class="theme-option" data-theme="
|
|
17
|
+
<span class="theme-option" data-theme="light">Light</span>
|
|
18
18
|
</span>
|
|
19
19
|
<span id="msg-nav">
|
|
20
20
|
<span class="nav-btn" id="nav-prev" title="Jump to previous user message">↑</span>
|
package/dist/static/style.css
CHANGED
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
--table-head-bg: rgba(128, 128, 128, 0.1);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
/*
|
|
32
|
-
body.theme-
|
|
31
|
+
/* Light theme (colors aligned with Pi's light theme) */
|
|
32
|
+
body.theme-light {
|
|
33
33
|
--bg: #f5f5f5;
|
|
34
34
|
--text: #1f2328;
|
|
35
35
|
--dim: #767676;
|
package/dist/ui-context.js
CHANGED
|
@@ -1,26 +1,69 @@
|
|
|
1
|
+
import { Theme as PiTheme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
1
5
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
+
* Load a Pi theme (dark.json/light.json colors) so extensions calling
|
|
7
|
+
* ui.theme.fg("accent", text) / .bg(...) get REAL ANSI escapes — identical
|
|
8
|
+
* to TUI (interactive-mode.ts returns the same Theme instance from
|
|
9
|
+
* ctx.ui.theme). The browser renders the escapes as colored spans (see
|
|
10
|
+
* app.js ansiToHtml), acting as the "terminal".
|
|
11
|
+
*
|
|
12
|
+
* The Theme class is exported by the Pi SDK and the theme JSONs ship in the
|
|
13
|
+
* package dist. Constructing the Theme directly keeps us independent of
|
|
14
|
+
* initTheme's global side effects and gives the same result.
|
|
6
15
|
*/
|
|
7
|
-
function
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
function createWebTheme(name = "dark") {
|
|
17
|
+
try {
|
|
18
|
+
const mainEntry = fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"));
|
|
19
|
+
const themeJson = JSON.parse(readFileSync(join(dirname(mainEntry), "modes", "interactive", "theme", `${name}.json`), "utf8"));
|
|
20
|
+
const fgColors = {};
|
|
21
|
+
const bgColors = {};
|
|
22
|
+
// theme colors refer to vars by name (e.g. "accent" -> "#8abeb7")
|
|
23
|
+
// or carry a literal hex. Split background keys from foreground keys by
|
|
24
|
+
// the explicit ThemeBg set (mirrors Pi's createTheme in theme.ts) rather
|
|
25
|
+
// than a "Bg" suffix: scrollbarThumb is a ThemeBg without that suffix.
|
|
26
|
+
const bgKeys = new Set([
|
|
27
|
+
"selectedBg",
|
|
28
|
+
"scrollbarThumb",
|
|
29
|
+
"searchMatchBg",
|
|
30
|
+
"userMessageBg",
|
|
31
|
+
"customMessageBg",
|
|
32
|
+
"toolPendingBg",
|
|
33
|
+
"toolSuccessBg",
|
|
34
|
+
"toolErrorBg",
|
|
35
|
+
]);
|
|
36
|
+
for (const [key, value] of Object.entries(themeJson.colors)) {
|
|
37
|
+
const resolved = value.startsWith("#") ? value : themeJson.vars[value] ?? value;
|
|
38
|
+
if (bgKeys.has(key)) {
|
|
39
|
+
bgColors[key] = resolved;
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
fgColors[key] = resolved;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return new PiTheme(fgColors, bgColors, "truecolor", { name });
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Last-resort identity: return the text argument unchanged (no ANSI).
|
|
49
|
+
return new Proxy({}, {
|
|
50
|
+
get(_target, prop) {
|
|
51
|
+
if (prop === "name")
|
|
52
|
+
return "dark";
|
|
53
|
+
if (prop === "isDark")
|
|
54
|
+
return true;
|
|
55
|
+
return (...args) => {
|
|
56
|
+
const textArg = args.length > 1 ? args[1] : args[0];
|
|
57
|
+
return typeof textArg === "string" ? textArg : "";
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
}
|
|
19
62
|
}
|
|
20
63
|
export class WebUIContext {
|
|
21
64
|
pending = new Map();
|
|
22
65
|
sink;
|
|
23
|
-
|
|
66
|
+
webTheme = createWebTheme();
|
|
24
67
|
/** Latest setStatus values per key, so late-connecting browsers get current state */
|
|
25
68
|
statusMap = new Map();
|
|
26
69
|
constructor(sink) {
|
|
@@ -148,7 +191,7 @@ export class WebUIContext {
|
|
|
148
191
|
// Theme
|
|
149
192
|
// ------------------------------------------------------------------
|
|
150
193
|
get theme() {
|
|
151
|
-
return this.
|
|
194
|
+
return this.webTheme;
|
|
152
195
|
}
|
|
153
196
|
getAllThemes() {
|
|
154
197
|
return [];
|
|
@@ -159,6 +202,10 @@ export class WebUIContext {
|
|
|
159
202
|
setTheme() {
|
|
160
203
|
return { success: false, error: "Theme switching not supported in web mode" };
|
|
161
204
|
}
|
|
205
|
+
/** Switch the theme used for extension ANSI colors (dark/light). */
|
|
206
|
+
setWebTheme(name) {
|
|
207
|
+
this.webTheme = createWebTheme(name);
|
|
208
|
+
}
|
|
162
209
|
// ------------------------------------------------------------------
|
|
163
210
|
// Tool output expansion (web always shows expandable blocks)
|
|
164
211
|
// ------------------------------------------------------------------
|