pi-sdk-web 0.3.11 → 0.3.12
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/static/app.js +180 -7
- package/dist/ui-context.js +61 -18
- package/package.json +1 -1
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;
|
|
@@ -869,7 +1031,10 @@ class PiWebClient {
|
|
|
869
1031
|
widgetsEl.appendChild(el);
|
|
870
1032
|
}
|
|
871
1033
|
el.querySelector('.widget-title').textContent = key;
|
|
872
|
-
|
|
1034
|
+
// ANSI escapes (extension theme) render as colored spans per line.
|
|
1035
|
+
el.querySelector('.widget-body').innerHTML = (req.widgetLines || [])
|
|
1036
|
+
.map((line) => ansiToHtml(String(line)))
|
|
1037
|
+
.join('<br>');
|
|
873
1038
|
widgetsEl.style.display = 'block';
|
|
874
1039
|
// Layout change: keep pinned to bottom if already there
|
|
875
1040
|
if (wasAtBottom) this.scrollToBottom();
|
|
@@ -1028,9 +1193,11 @@ class PiWebClient {
|
|
|
1028
1193
|
if (!expanded && lines.length > limit) {
|
|
1029
1194
|
const visible = lines.slice(0, limit).join('\n');
|
|
1030
1195
|
const hidden = lines.length - limit;
|
|
1031
|
-
|
|
1196
|
+
// ANSI escapes (from tool outputs) render as colored spans; the
|
|
1197
|
+
// truncated marker stays plain text.
|
|
1198
|
+
out.innerHTML = ansiToHtml(visible) + `<div class="tool-truncated">... (${hidden} more lines, click to expand)</div>`;
|
|
1032
1199
|
} else {
|
|
1033
|
-
out.
|
|
1200
|
+
out.innerHTML = ansiToHtml(full);
|
|
1034
1201
|
}
|
|
1035
1202
|
}
|
|
1036
1203
|
|
|
@@ -1225,9 +1392,12 @@ class PiWebClient {
|
|
|
1225
1392
|
const text = this.inputEl.value.trim();
|
|
1226
1393
|
if (!text) return;
|
|
1227
1394
|
if (text.startsWith('!')) {
|
|
1228
|
-
|
|
1395
|
+
// TUI parity: "!cmd" runs bash (output goes to LLM context);
|
|
1396
|
+
// "!!cmd" runs bash with excludeFromContext (output NOT sent to LLM).
|
|
1397
|
+
const isExcluded = text.startsWith('!!');
|
|
1398
|
+
const command = (isExcluded ? text.slice(2) : text.slice(1)).trim();
|
|
1229
1399
|
if (command) {
|
|
1230
|
-
this.send({ type: 'bash', command: command });
|
|
1400
|
+
this.send({ type: 'bash', command: command, excludeFromContext: isExcluded });
|
|
1231
1401
|
}
|
|
1232
1402
|
} else if (text.startsWith('/')) {
|
|
1233
1403
|
// Skill commands (/skill:name args) go through prompt expansion in Pi -
|
|
@@ -1473,7 +1643,9 @@ class PiWebClient {
|
|
|
1473
1643
|
const el = document.getElementById('ext-status');
|
|
1474
1644
|
if (!el) return;
|
|
1475
1645
|
const entries = Object.entries(this.extStatus);
|
|
1476
|
-
|
|
1646
|
+
// ANSI escapes (from extension theme.fg/bg) render as colored spans.
|
|
1647
|
+
// ansiToHtml escapes the value's HTML; the key is escaped separately.
|
|
1648
|
+
el.innerHTML = entries.map(([k, v]) => `${this.escapeHtml(k)}: ${ansiToHtml(String(v))}`).join(' · ');
|
|
1477
1649
|
el.style.display = entries.length ? 'block' : 'none';
|
|
1478
1650
|
}
|
|
1479
1651
|
|
|
@@ -1578,7 +1750,8 @@ class PiWebClient {
|
|
|
1578
1750
|
if (!container) return;
|
|
1579
1751
|
const toast = document.createElement('div');
|
|
1580
1752
|
toast.className = 'toast' + (type ? ` toast-${type}` : '');
|
|
1581
|
-
|
|
1753
|
+
// Extension notify messages may carry ANSI escapes (theme.fg); render them.
|
|
1754
|
+
toast.innerHTML = ansiToHtml(String(message));
|
|
1582
1755
|
container.appendChild(toast);
|
|
1583
1756
|
setTimeout(() => {
|
|
1584
1757
|
toast.classList.add('toast-hide');
|
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
|
+
* Build the official Pi theme (colors from the shipped dark.json) so
|
|
7
|
+
* extensions calling ui.theme.fg("accent", text) / .bg(...) get REAL ANSI
|
|
8
|
+
* escapes — identical to TUI (interactive-mode.ts returns the same Theme
|
|
9
|
+
* instance from ctx.ui.theme). The browser renders the escapes as colored
|
|
10
|
+
* spans (see app.js ansiToHtml), acting as the "terminal".
|
|
11
|
+
*
|
|
12
|
+
* The Theme class and initTheme are exported by the Pi SDK; dark.json ships
|
|
13
|
+
* in the package dist. Constructing the Theme directly keeps us independent
|
|
14
|
+
* of 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() {
|
|
17
|
+
try {
|
|
18
|
+
const mainEntry = fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"));
|
|
19
|
+
const darkJson = JSON.parse(readFileSync(join(dirname(mainEntry), "modes", "interactive", "theme", "dark.json"), "utf8"));
|
|
20
|
+
const fgColors = {};
|
|
21
|
+
const bgColors = {};
|
|
22
|
+
// dark.json 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(darkJson.colors)) {
|
|
37
|
+
const resolved = value.startsWith("#") ? value : darkJson.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: "dark" });
|
|
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 [];
|