pi-sdk-web 0.5.6 → 0.5.7
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 +37 -2
- package/dist/static/app.js +29 -43
- package/dist/static/index.html +0 -1
- package/dist/static/style.css +2 -43
- package/dist/usage-render.js +28 -8
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -93,7 +93,25 @@ export class PiWebServer {
|
|
|
93
93
|
// Some components (ws internals, MCP-style extensions) accumulate 'close'
|
|
94
94
|
// listeners on the server; raise the limit to avoid MaxListenersExceededWarning
|
|
95
95
|
this.httpServer.setMaxListeners(50);
|
|
96
|
-
this.wsServer = new WebSocketServer({
|
|
96
|
+
this.wsServer = new WebSocketServer({
|
|
97
|
+
server: this.httpServer,
|
|
98
|
+
path: "/ws",
|
|
99
|
+
// CSWSH guard: only accept same-origin connections (the page itself).
|
|
100
|
+
// ws://127.0.0.1:<port> has no Origin header requirement in browsers,
|
|
101
|
+
// but cross-origin pages would send their own Origin - reject those.
|
|
102
|
+
verifyClient: (info) => {
|
|
103
|
+
const origin = info.origin;
|
|
104
|
+
if (!origin)
|
|
105
|
+
return true; // non-browser clients (no Origin header)
|
|
106
|
+
try {
|
|
107
|
+
const u = new URL(origin);
|
|
108
|
+
return u.hostname === "127.0.0.1" || u.hostname === "localhost" || u.hostname === "::1";
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
});
|
|
97
115
|
this.wsServer.setMaxListeners(50);
|
|
98
116
|
this.wsServer.on("connection", (ws) => this.handleConnection(ws));
|
|
99
117
|
await new Promise((resolvePromise, reject) => {
|
|
@@ -150,6 +168,18 @@ export class PiWebServer {
|
|
|
150
168
|
// would stay hidden until the new session's first turn event.
|
|
151
169
|
const extStatus = this.uiContext.getStatusSnapshot();
|
|
152
170
|
this.broadcast({ type: "ext_status", data: extStatus });
|
|
171
|
+
// Same for widgets (e.g. todos overlay): clearContent() wipes #widgets
|
|
172
|
+
// and there is no other channel after switching.
|
|
173
|
+
for (const [key, w] of Object.entries(this.uiContext.getWidgetSnapshot())) {
|
|
174
|
+
this.broadcast({
|
|
175
|
+
type: "extension_ui_request",
|
|
176
|
+
id: crypto.randomUUID(),
|
|
177
|
+
method: "setWidget",
|
|
178
|
+
widgetKey: key,
|
|
179
|
+
widgetLines: w.lines,
|
|
180
|
+
widgetPlacement: w.placement,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
153
183
|
}
|
|
154
184
|
}
|
|
155
185
|
async stop() {
|
|
@@ -713,8 +743,13 @@ export class PiWebServer {
|
|
|
713
743
|
this.unsubscribe = null;
|
|
714
744
|
try {
|
|
715
745
|
const result = await this.runtime.switchSession(path);
|
|
716
|
-
if (result.cancelled)
|
|
746
|
+
if (result.cancelled) {
|
|
747
|
+
// switchSession was cancelled (e.g. session_before_switch veto): the
|
|
748
|
+
// old session is still live and its events stopped - resubscribe to
|
|
749
|
+
// keep the server usable.
|
|
750
|
+
await this.bindCurrentSession(false);
|
|
717
751
|
return;
|
|
752
|
+
}
|
|
718
753
|
}
|
|
719
754
|
catch (err) {
|
|
720
755
|
// Rebind hook already ran on failure? No: on error the old session may be
|
package/dist/static/app.js
CHANGED
|
@@ -214,7 +214,6 @@ class PiWebClient {
|
|
|
214
214
|
this.streaming = {
|
|
215
215
|
active: false,
|
|
216
216
|
el: null,
|
|
217
|
-
role: 'assistant',
|
|
218
217
|
};
|
|
219
218
|
|
|
220
219
|
// Tool call rendering state: map toolCallId -> element
|
|
@@ -617,10 +616,6 @@ class PiWebClient {
|
|
|
617
616
|
}
|
|
618
617
|
}
|
|
619
618
|
|
|
620
|
-
getStats() {
|
|
621
|
-
this.send({ type: 'get_stats' });
|
|
622
|
-
}
|
|
623
|
-
|
|
624
619
|
// ------------------------------------------------------------------
|
|
625
620
|
// Markdown rendering
|
|
626
621
|
// ------------------------------------------------------------------
|
|
@@ -853,7 +848,6 @@ class PiWebClient {
|
|
|
853
848
|
toggleSpecial(div) {
|
|
854
849
|
const expanded = div.dataset.expanded === 'true';
|
|
855
850
|
div.dataset.expanded = expanded ? 'false' : 'true';
|
|
856
|
-
div.classList.toggle('expanded', !expanded);
|
|
857
851
|
this.applySpecialBlock(div);
|
|
858
852
|
}
|
|
859
853
|
|
|
@@ -864,7 +858,7 @@ class PiWebClient {
|
|
|
864
858
|
}
|
|
865
859
|
this.toolTimers.clear();
|
|
866
860
|
this.toolEls.clear();
|
|
867
|
-
this.streaming = { active: false, el: null
|
|
861
|
+
this.streaming = { active: false, el: null };
|
|
868
862
|
// Drop stale UI state from the previous session (on /resume reload)
|
|
869
863
|
const widgets = document.getElementById('widgets');
|
|
870
864
|
if (widgets) {
|
|
@@ -970,7 +964,9 @@ class PiWebClient {
|
|
|
970
964
|
default:
|
|
971
965
|
break;
|
|
972
966
|
}
|
|
973
|
-
|
|
967
|
+
// Only auto-scroll when the user is already at the bottom (reading
|
|
968
|
+
// history must not be yanked down by incoming events).
|
|
969
|
+
if (this.wasAtBottom()) this.scrollToBottom();
|
|
974
970
|
}
|
|
975
971
|
|
|
976
972
|
onMessageStart(message) {
|
|
@@ -1009,7 +1005,7 @@ class PiWebClient {
|
|
|
1009
1005
|
}
|
|
1010
1006
|
|
|
1011
1007
|
resetStreaming() {
|
|
1012
|
-
this.streaming = { active: false, el: null
|
|
1008
|
+
this.streaming = { active: false, el: null };
|
|
1013
1009
|
}
|
|
1014
1010
|
|
|
1015
1011
|
// ------------------------------------------------------------------
|
|
@@ -1095,21 +1091,35 @@ class PiWebClient {
|
|
|
1095
1091
|
}
|
|
1096
1092
|
|
|
1097
1093
|
let el = widgetsEl.querySelector(`[data-widget-key="${CSS.escape(key)}"]`);
|
|
1094
|
+
let body;
|
|
1098
1095
|
if (!el) {
|
|
1099
1096
|
el = document.createElement('div');
|
|
1100
1097
|
el.className = 'widget-block';
|
|
1101
1098
|
el.dataset.widgetKey = key;
|
|
1099
|
+
el.dataset.expanded = 'false'; // collapsible; collapsed by default
|
|
1102
1100
|
const title = document.createElement('div');
|
|
1103
1101
|
title.className = 'widget-title';
|
|
1104
|
-
|
|
1102
|
+
body = document.createElement('div');
|
|
1105
1103
|
body.className = 'widget-body';
|
|
1104
|
+
body.style.display = 'none'; // collapsed by default
|
|
1105
|
+
// Click the title to expand/collapse (keeps the widget compact).
|
|
1106
|
+
title.addEventListener('click', () => {
|
|
1107
|
+
const expanded = el.dataset.expanded === 'true';
|
|
1108
|
+
el.dataset.expanded = expanded ? 'false' : 'true';
|
|
1109
|
+
body.style.display = expanded ? 'none' : 'block';
|
|
1110
|
+
title.textContent = (expanded ? '▸ ' : '▾ ') + el.dataset.widgetKey;
|
|
1111
|
+
});
|
|
1106
1112
|
el.appendChild(title);
|
|
1107
1113
|
el.appendChild(body);
|
|
1108
1114
|
widgetsEl.appendChild(el);
|
|
1115
|
+
} else {
|
|
1116
|
+
body = el.querySelector('.widget-body');
|
|
1109
1117
|
}
|
|
1110
|
-
|
|
1118
|
+
// Header label with collapsible indicator (kept in sync).
|
|
1119
|
+
const titleEl = el.querySelector('.widget-title');
|
|
1120
|
+
titleEl.textContent = (el.dataset.expanded === 'true' ? '▾ ' : '▸ ') + key;
|
|
1111
1121
|
// ANSI escapes (extension theme) render as colored spans per line.
|
|
1112
|
-
|
|
1122
|
+
body.innerHTML = (req.widgetLines || [])
|
|
1113
1123
|
.map((line) => ansiToHtml(String(line)))
|
|
1114
1124
|
.join('<br>');
|
|
1115
1125
|
widgetsEl.style.display = 'block';
|
|
@@ -1153,7 +1163,7 @@ class PiWebClient {
|
|
|
1153
1163
|
div.appendChild(body);
|
|
1154
1164
|
this.contentEl.appendChild(div);
|
|
1155
1165
|
|
|
1156
|
-
this.streaming = { active: true, el: div, body: body
|
|
1166
|
+
this.streaming = { active: true, el: div, body: body };
|
|
1157
1167
|
this.renderAssistantContent(message, body);
|
|
1158
1168
|
}
|
|
1159
1169
|
|
|
@@ -1306,7 +1316,6 @@ class PiWebClient {
|
|
|
1306
1316
|
toggleToolExpand(div) {
|
|
1307
1317
|
const expanded = div.dataset.expanded === 'true';
|
|
1308
1318
|
div.dataset.expanded = expanded ? 'false' : 'true';
|
|
1309
|
-
div.classList.toggle('expanded', !expanded);
|
|
1310
1319
|
this.applyToolPreview(div);
|
|
1311
1320
|
}
|
|
1312
1321
|
|
|
@@ -1785,6 +1794,12 @@ class PiWebClient {
|
|
|
1785
1794
|
}
|
|
1786
1795
|
|
|
1787
1796
|
closeModal() {
|
|
1797
|
+
// If an extension dialog (select/confirm/input/editor) was open, notify
|
|
1798
|
+
// the server that it was cancelled - otherwise the extension's promise
|
|
1799
|
+
// never settles.
|
|
1800
|
+
if (this.currentExtRequest && this.currentExtRequest.id) {
|
|
1801
|
+
this.send({ type: 'extension_ui_response', id: this.currentExtRequest.id, cancelled: true });
|
|
1802
|
+
}
|
|
1788
1803
|
this.modalOverlay.style.display = 'none';
|
|
1789
1804
|
this.modalList.innerHTML = '';
|
|
1790
1805
|
this.modalSearch.value = '';
|
|
@@ -2009,20 +2024,6 @@ class PiWebClient {
|
|
|
2009
2024
|
this.scrollToBottom();
|
|
2010
2025
|
}
|
|
2011
2026
|
|
|
2012
|
-
showToast(message, type) {
|
|
2013
|
-
let container = document.getElementById('toast-container');
|
|
2014
|
-
if (!container) return;
|
|
2015
|
-
const toast = document.createElement('div');
|
|
2016
|
-
toast.className = 'toast' + (type ? ` toast-${type}` : '');
|
|
2017
|
-
// Extension notify messages may carry ANSI escapes (theme.fg); render them.
|
|
2018
|
-
toast.innerHTML = ansiToHtml(String(message));
|
|
2019
|
-
container.appendChild(toast);
|
|
2020
|
-
setTimeout(() => {
|
|
2021
|
-
toast.classList.add('toast-hide');
|
|
2022
|
-
setTimeout(() => toast.remove(), 300);
|
|
2023
|
-
}, type === 'error' ? 8000 : 5000);
|
|
2024
|
-
}
|
|
2025
|
-
|
|
2026
2027
|
// ------------------------------------------------------------------
|
|
2027
2028
|
// Usage panel (dedicated big panel for /usage, rendered from the
|
|
2028
2029
|
// server-aggregated usage_data payload)
|
|
@@ -2125,19 +2126,6 @@ class PiWebClient {
|
|
|
2125
2126
|
return '$' + c.toFixed(1);
|
|
2126
2127
|
}
|
|
2127
2128
|
|
|
2128
|
-
usageRow(stat) {
|
|
2129
|
-
const t = stat.tokens || {};
|
|
2130
|
-
return [
|
|
2131
|
-
this.fmtTokens(t.input),
|
|
2132
|
-
this.fmtTokens(t.output),
|
|
2133
|
-
this.fmtTokens((t.cacheRead || 0) + (t.cacheWrite || 0)),
|
|
2134
|
-
this.fmtTokens((t.total === undefined ? (t.input || 0) + (t.output || 0) + (t.cacheRead || 0) + (t.cacheWrite || 0) : t.total)),
|
|
2135
|
-
this.fmtCost(stat.cost),
|
|
2136
|
-
String(stat.messages),
|
|
2137
|
-
String(stat.sessions),
|
|
2138
|
-
];
|
|
2139
|
-
}
|
|
2140
|
-
|
|
2141
2129
|
renderUsageBody() {
|
|
2142
2130
|
const body = document.getElementById('usage-body');
|
|
2143
2131
|
if (!body) return;
|
|
@@ -2177,7 +2165,6 @@ class PiWebClient {
|
|
|
2177
2165
|
const key = 'p:' + p.name;
|
|
2178
2166
|
const expanded = this.usageExpanded.has(key);
|
|
2179
2167
|
const tr = rowFor(p.name, p, false, false);
|
|
2180
|
-
tr.classList.add('usage-clickable');
|
|
2181
2168
|
tr.addEventListener('click', () => {
|
|
2182
2169
|
if (this.usageExpanded.has(key)) this.usageExpanded.delete(key);
|
|
2183
2170
|
else this.usageExpanded.add(key);
|
|
@@ -2370,7 +2357,6 @@ class PiWebClient {
|
|
|
2370
2357
|
this.modalList.innerHTML = '<div class="modal-message">No other sessions available</div>';
|
|
2371
2358
|
return;
|
|
2372
2359
|
}
|
|
2373
|
-
this.resumeSessions = sessions;
|
|
2374
2360
|
this.renderModalItems(
|
|
2375
2361
|
sessions.map((s) => ({ name: s.name || s.id, desc: s.cwd, value: s.path })),
|
|
2376
2362
|
(item) => {
|
package/dist/static/index.html
CHANGED
package/dist/static/style.css
CHANGED
|
@@ -27,7 +27,6 @@
|
|
|
27
27
|
--code-inline-bg: rgba(128, 128, 128, 0.15);
|
|
28
28
|
--table-head-bg: rgba(128, 128, 128, 0.1);
|
|
29
29
|
}
|
|
30
|
-
|
|
31
30
|
/* Light theme (colors aligned with Pi's light theme) */
|
|
32
31
|
body.theme-light {
|
|
33
32
|
--bg: #f5f5f5;
|
|
@@ -674,6 +673,8 @@ body {
|
|
|
674
673
|
color: var(--accent);
|
|
675
674
|
font-weight: 600;
|
|
676
675
|
margin-bottom: 2px;
|
|
676
|
+
cursor: pointer;
|
|
677
|
+
user-select: none;
|
|
677
678
|
}
|
|
678
679
|
|
|
679
680
|
.widget-body {
|
|
@@ -890,40 +891,6 @@ body {
|
|
|
890
891
|
color: var(--text);
|
|
891
892
|
}
|
|
892
893
|
|
|
893
|
-
/* Lightweight extension notifications (TUI shows notify as transient status) */
|
|
894
|
-
#toast-container {
|
|
895
|
-
position: fixed;
|
|
896
|
-
top: 12px;
|
|
897
|
-
right: 12px;
|
|
898
|
-
z-index: 200;
|
|
899
|
-
display: flex;
|
|
900
|
-
flex-direction: column;
|
|
901
|
-
gap: 8px;
|
|
902
|
-
max-width: 420px;
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
.toast {
|
|
906
|
-
background: var(--modal-bg);
|
|
907
|
-
border: 1px solid var(--border);
|
|
908
|
-
border-left: 3px solid var(--accent);
|
|
909
|
-
border-radius: 6px;
|
|
910
|
-
padding: 8px 12px;
|
|
911
|
-
color: var(--text);
|
|
912
|
-
font-size: 13px;
|
|
913
|
-
line-height: 1.5;
|
|
914
|
-
white-space: pre-wrap;
|
|
915
|
-
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
916
|
-
opacity: 1;
|
|
917
|
-
transition: opacity 0.3s;
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
.toast-warning {
|
|
921
|
-
border-left-color: var(--warning);
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
.toast-error {
|
|
925
|
-
border-left-color: var(--error);
|
|
926
|
-
}
|
|
927
894
|
|
|
928
895
|
/* Extension notify lines appended to the chat stream (TUI showStatus
|
|
929
896
|
equivalent): persistent dim status text, colored by type. */
|
|
@@ -946,7 +913,6 @@ body {
|
|
|
946
913
|
border-left-color: var(--error);
|
|
947
914
|
}
|
|
948
915
|
|
|
949
|
-
.toast-hide {
|
|
950
916
|
opacity: 0;
|
|
951
917
|
}
|
|
952
918
|
|
|
@@ -1441,13 +1407,6 @@ body {
|
|
|
1441
1407
|
background: color-mix(in srgb, var(--muted) 12%, transparent);
|
|
1442
1408
|
}
|
|
1443
1409
|
|
|
1444
|
-
.usage-hour-bar-wrap {
|
|
1445
|
-
flex: 1;
|
|
1446
|
-
display: flex;
|
|
1447
|
-
align-items: flex-end;
|
|
1448
|
-
min-height: 0;
|
|
1449
|
-
}
|
|
1450
|
-
|
|
1451
1410
|
.usage-hour-label {
|
|
1452
1411
|
font-size: 9px;
|
|
1453
1412
|
color: var(--dim);
|
package/dist/usage-render.js
CHANGED
|
@@ -96,7 +96,6 @@ function parseSessionFile(path) {
|
|
|
96
96
|
let sessionId = "";
|
|
97
97
|
let cwd = "";
|
|
98
98
|
let parentSession = "";
|
|
99
|
-
let compactionPending = false;
|
|
100
99
|
const messages = [];
|
|
101
100
|
let content;
|
|
102
101
|
try {
|
|
@@ -130,15 +129,24 @@ function parseSessionFile(path) {
|
|
|
130
129
|
}
|
|
131
130
|
case "compaction": {
|
|
132
131
|
const usage = parseUsageAmount(entry.usage);
|
|
133
|
-
if (usage)
|
|
134
|
-
|
|
135
|
-
|
|
132
|
+
if (usage) {
|
|
133
|
+
const ts = tsOf(undefined, entry.timestamp);
|
|
134
|
+
// ts===0 means no timestamp was recoverable - skip so it can't
|
|
135
|
+
// create a 1970 spike in the hourly graph or leak out of all tabs.
|
|
136
|
+
if (ts > 0)
|
|
137
|
+
messages.push(auxMessage(usage, ts));
|
|
138
|
+
}
|
|
136
139
|
break;
|
|
137
140
|
}
|
|
138
141
|
case "branch_summary": {
|
|
139
142
|
const usage = parseUsageAmount(entry.usage);
|
|
140
|
-
if (usage)
|
|
141
|
-
|
|
143
|
+
if (usage) {
|
|
144
|
+
const ts = tsOf(undefined, entry.timestamp);
|
|
145
|
+
// ts===0 means no timestamp was recoverable - skip so it can't
|
|
146
|
+
// create a 1970 spike in the hourly graph or leak out of all tabs.
|
|
147
|
+
if (ts > 0)
|
|
148
|
+
messages.push(auxMessage(usage, ts));
|
|
149
|
+
}
|
|
142
150
|
break;
|
|
143
151
|
}
|
|
144
152
|
case "message": {
|
|
@@ -160,7 +168,6 @@ function parseSessionFile(path) {
|
|
|
160
168
|
reasoning: usage.reasoning,
|
|
161
169
|
source: "assistant",
|
|
162
170
|
});
|
|
163
|
-
compactionPending = false;
|
|
164
171
|
}
|
|
165
172
|
}
|
|
166
173
|
break;
|
|
@@ -240,6 +247,18 @@ function periodStart(key, now) {
|
|
|
240
247
|
}
|
|
241
248
|
return 0; // allTime
|
|
242
249
|
}
|
|
250
|
+
/** Exclusive upper bound of a tab window (see periodStart: lastWeek ends at
|
|
251
|
+
* this Monday 00:00; today/thisWeek/last30Days/allTime end at 'now'). */
|
|
252
|
+
function periodEnd(key, now) {
|
|
253
|
+
if (key === "lastWeek") {
|
|
254
|
+
const d = new Date(now);
|
|
255
|
+
d.setHours(0, 0, 0, 0);
|
|
256
|
+
const day = d.getDay() === 0 ? 6 : d.getDay() - 1;
|
|
257
|
+
d.setDate(d.getDate() - day);
|
|
258
|
+
return d.getTime();
|
|
259
|
+
}
|
|
260
|
+
return now;
|
|
261
|
+
}
|
|
243
262
|
/**
|
|
244
263
|
* Aggregate collected session usage into the structured payload the web
|
|
245
264
|
* panel renders. Returns null when no usage data exists. When sessionId
|
|
@@ -276,7 +295,8 @@ export function buildUsageData(sessionId) {
|
|
|
276
295
|
}
|
|
277
296
|
for (const key of TAB_KEYS) {
|
|
278
297
|
const start = periodStart(key, now);
|
|
279
|
-
|
|
298
|
+
const end = periodEnd(key, now);
|
|
299
|
+
if (ts < start || ts >= end)
|
|
280
300
|
continue;
|
|
281
301
|
const tab = tabs[key];
|
|
282
302
|
tab.totals.messages += 1;
|