pi-sdk-web 0.5.2 → 0.5.4
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 +28 -21
- package/dist/static/app.js +23 -2
- package/dist/static/style.css +10 -0
- package/dist/ui-context.js +36 -1
- package/dist/usage-render.js +5 -12
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -270,6 +270,19 @@ export class PiWebServer {
|
|
|
270
270
|
if (Object.keys(extStatus).length > 0) {
|
|
271
271
|
this.sendJson(ws, { type: "ext_status", data: extStatus });
|
|
272
272
|
}
|
|
273
|
+
// Current widgets (setWidget may have fired before this client connected,
|
|
274
|
+
// e.g. magic-context's todos overlay) - replay as individual requests so
|
|
275
|
+
// the browser's extension_ui_request handler renders them.
|
|
276
|
+
for (const [key, w] of Object.entries(this.uiContext.getWidgetSnapshot())) {
|
|
277
|
+
this.sendJson(ws, {
|
|
278
|
+
type: "extension_ui_request",
|
|
279
|
+
id: crypto.randomUUID(),
|
|
280
|
+
method: "setWidget",
|
|
281
|
+
widgetKey: key,
|
|
282
|
+
widgetLines: w.lines,
|
|
283
|
+
widgetPlacement: w.placement,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
273
286
|
}
|
|
274
287
|
sendJson(ws, obj) {
|
|
275
288
|
if (ws.readyState === WebSocket.OPEN) {
|
|
@@ -568,6 +581,11 @@ export class PiWebServer {
|
|
|
568
581
|
break;
|
|
569
582
|
}
|
|
570
583
|
case "get_scoped_models":
|
|
584
|
+
// Refresh model catalogs (e.g. pi-deepinfra's lazy catalog swap)
|
|
585
|
+
// before broadcasting, so late-swapped models (zai-org/GLM-5.3-
|
|
586
|
+
// Flash etc.) appear in the list like they do in the TUI. Timeout
|
|
587
|
+
// is bounded by the refresh implementation (network wait).
|
|
588
|
+
await this.session.modelRuntime.refresh().catch(() => { });
|
|
571
589
|
this.broadcast({
|
|
572
590
|
type: "scoped_models",
|
|
573
591
|
data: {
|
|
@@ -577,6 +595,11 @@ export class PiWebServer {
|
|
|
577
595
|
thinkingLevel: s.thinkingLevel,
|
|
578
596
|
})),
|
|
579
597
|
available: this.session.modelRuntime.getAvailableSnapshot(),
|
|
598
|
+
// Settings' enabled-model patterns. TUI's scoped-models selector
|
|
599
|
+
// also shows configured patterns that have no matching provider
|
|
600
|
+
// model (diagnostic no-match) - mirror that so configured models
|
|
601
|
+
// (e.g. deepinfra/zai-org/GLM-5.3-Flash) aren't missing here.
|
|
602
|
+
configured: this.session.settingsManager.getEnabledModels() ?? [],
|
|
580
603
|
},
|
|
581
604
|
});
|
|
582
605
|
break;
|
|
@@ -792,31 +815,15 @@ export class PiWebServer {
|
|
|
792
815
|
// (appendEntry -> modal), same as before.
|
|
793
816
|
const cmdPath = (cmd.sourceInfo && cmd.sourceInfo.path) || "";
|
|
794
817
|
const customOnly = /pi-magic-context|magic-context/.test(cmdPath);
|
|
795
|
-
const ui = this.uiContext;
|
|
796
|
-
const originalSink = ui.sink;
|
|
797
818
|
try {
|
|
798
|
-
//
|
|
799
|
-
//
|
|
800
|
-
//
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
if (req && req.method === "notify") {
|
|
804
|
-
// Tag command-time notifies with the command name as title so the
|
|
805
|
-
// browser renders them as a titled modal (colored, Markdown).
|
|
806
|
-
originalSink({ ...obj, title: `/${name}` });
|
|
807
|
-
}
|
|
808
|
-
else {
|
|
809
|
-
originalSink(obj);
|
|
810
|
-
}
|
|
811
|
-
};
|
|
819
|
+
// No sink wrapping: notify() broadcasts go through as-is (no title),
|
|
820
|
+
// so the frontend renders them as persistent status lines - the same
|
|
821
|
+
// as TUI's notify -> showStatus (a dim line appended to the chat),
|
|
822
|
+
// whether or not they come from a command. Command *results* (custom
|
|
823
|
+
// entries) still become titled modals via the captured entries below.
|
|
812
824
|
await cmd.handler(args, this.buildCommandContext(customOnly ? false : true));
|
|
813
825
|
}
|
|
814
|
-
catch (e) {
|
|
815
|
-
ui.sink = originalSink;
|
|
816
|
-
throw e;
|
|
817
|
-
}
|
|
818
826
|
finally {
|
|
819
|
-
ui.sink = originalSink;
|
|
820
827
|
unsubscribe();
|
|
821
828
|
}
|
|
822
829
|
// Show command output as a modal (TUI-like). The session entry remains
|
package/dist/static/app.js
CHANGED
|
@@ -1944,8 +1944,18 @@ class PiWebClient {
|
|
|
1944
1944
|
}
|
|
1945
1945
|
|
|
1946
1946
|
appendNotifyLine(message, type) {
|
|
1947
|
+
// TUI's showStatus reuses the LAST status line: consecutive notifies
|
|
1948
|
+
// update it in place (a sticky status); a new line starts only when
|
|
1949
|
+
// something else (a message) was inserted in between. Mirror that here.
|
|
1950
|
+
const last = this.contentEl.lastElementChild;
|
|
1951
|
+
if (last && last.classList.contains('notify-line') && last.dataset.notifyType === (type || '')) {
|
|
1952
|
+
last.innerHTML = ansiToHtml(String(message));
|
|
1953
|
+
this.scrollToBottom();
|
|
1954
|
+
return;
|
|
1955
|
+
}
|
|
1947
1956
|
const div = document.createElement('div');
|
|
1948
1957
|
div.className = 'notify-line' + (type ? ` notify-${type}` : '');
|
|
1958
|
+
div.dataset.notifyType = type || '';
|
|
1949
1959
|
div.innerHTML = ansiToHtml(String(message));
|
|
1950
1960
|
this.contentEl.appendChild(div);
|
|
1951
1961
|
this.scrollToBottom();
|
|
@@ -2359,7 +2369,18 @@ class PiWebClient {
|
|
|
2359
2369
|
|
|
2360
2370
|
handleScopedModels(data) {
|
|
2361
2371
|
if (this.modalMode !== 'scoped-models') return;
|
|
2362
|
-
|
|
2372
|
+
const available = data.available || [];
|
|
2373
|
+
// Merge settings' configured model patterns (e.g. deepinfra/zai-org/
|
|
2374
|
+
// GLM-5.3-Flash) that have no matching provider model - TUI's selector
|
|
2375
|
+
// shows these as configured entries (no-match diagnostics) too.
|
|
2376
|
+
const known = new Set(available.map((m) => `${m.provider}/${m.id}`));
|
|
2377
|
+
const configured = (data.configured || [])
|
|
2378
|
+
.filter((p) => !known.has(p))
|
|
2379
|
+
.map((p) => {
|
|
2380
|
+
const slash = p.indexOf('/');
|
|
2381
|
+
return { provider: p.slice(0, slash), id: p.slice(slash + 1), configured: true };
|
|
2382
|
+
});
|
|
2383
|
+
this.scopedModelsAll = [...available, ...configured];
|
|
2363
2384
|
this.scopedModelsData = data;
|
|
2364
2385
|
this.scopedModelsSelected = new Set(
|
|
2365
2386
|
(data.scoped || []).map((s) => `${s.provider}/${s.id}`),
|
|
@@ -2385,7 +2406,7 @@ class PiWebClient {
|
|
|
2385
2406
|
return (
|
|
2386
2407
|
`<div class="modal-item scoped-model ${checked ? 'selected' : ''}" data-key="${key}">` +
|
|
2387
2408
|
`<span class="modal-check">${checked ? '☑' : '☐'}</span>` +
|
|
2388
|
-
`<span class="modal-item-name">${this.escapeHtml(key)}</span>` +
|
|
2409
|
+
`<span class="modal-item-name">${this.escapeHtml(key)}${m.configured ? ' <span class="configured-badge">[configured]</span>' : ''}</span>` +
|
|
2389
2410
|
`</div>`
|
|
2390
2411
|
);
|
|
2391
2412
|
})
|
package/dist/static/style.css
CHANGED
|
@@ -1475,3 +1475,13 @@ body {
|
|
|
1475
1475
|
#usage-close:hover {
|
|
1476
1476
|
color: var(--text);
|
|
1477
1477
|
}
|
|
1478
|
+
|
|
1479
|
+
.configured-badge {
|
|
1480
|
+
font-size: 9px;
|
|
1481
|
+
color: var(--dim);
|
|
1482
|
+
background: var(--tool-pending-bg);
|
|
1483
|
+
padding: 1px 4px;
|
|
1484
|
+
border-radius: 3px;
|
|
1485
|
+
margin-left: 4px;
|
|
1486
|
+
vertical-align: middle;
|
|
1487
|
+
}
|
package/dist/ui-context.js
CHANGED
|
@@ -66,6 +66,13 @@ export class WebUIContext {
|
|
|
66
66
|
webTheme = createWebTheme();
|
|
67
67
|
/** Latest setStatus values per key, so late-connecting browsers get current state */
|
|
68
68
|
statusMap = new Map();
|
|
69
|
+
/** Latest setWidget lines per key (persistent widgets, e.g. TodoOverlay):
|
|
70
|
+
* replayed to late-connecting browsers like setStatus snapshots. */
|
|
71
|
+
widgetMap = new Map();
|
|
72
|
+
/** Current widget snapshot (key -> lines) for new connections. */
|
|
73
|
+
getWidgetSnapshot() {
|
|
74
|
+
return Object.fromEntries(this.widgetMap);
|
|
75
|
+
}
|
|
69
76
|
constructor(sink) {
|
|
70
77
|
this.sink = sink;
|
|
71
78
|
// Pi's ExtensionRunner wraps the ui context with `{...ui}` (a shallow
|
|
@@ -106,6 +113,7 @@ export class WebUIContext {
|
|
|
106
113
|
}
|
|
107
114
|
this.pending.clear();
|
|
108
115
|
this.statusMap.clear();
|
|
116
|
+
this.widgetMap.clear();
|
|
109
117
|
}
|
|
110
118
|
/** Handle a browser `extension_ui_response` message. */
|
|
111
119
|
respond(id, response) {
|
|
@@ -177,14 +185,41 @@ export class WebUIContext {
|
|
|
177
185
|
this.sink({ type: "extension_ui_request", id: crypto.randomUUID(), method: "setTitle", title });
|
|
178
186
|
}
|
|
179
187
|
setWidget(key, content, options) {
|
|
188
|
+
let lines;
|
|
189
|
+
if (Array.isArray(content)) {
|
|
190
|
+
lines = content.map((l) => String(l));
|
|
191
|
+
}
|
|
192
|
+
else if (typeof content === "function") {
|
|
193
|
+
// Some extensions (e.g. magic-context's TodoOverlay) pass a TUI
|
|
194
|
+
// component factory instead of string lines. Web has no TUI renderer,
|
|
195
|
+
// but the factory yields a render(width) -> string[] that we can
|
|
196
|
+
// evaluate with a no-op tui stub and broadcast as text lines - the
|
|
197
|
+
// browser renders them (with ANSI colors) in the widgets panel.
|
|
198
|
+
try {
|
|
199
|
+
const stubTui = { requestRender: () => { }, invalidate: () => { } };
|
|
200
|
+
const component = content(stubTui, this.webTheme);
|
|
201
|
+
const render = component && component.render;
|
|
202
|
+
if (typeof render === "function") {
|
|
203
|
+
const width = 78;
|
|
204
|
+
const out = render(width);
|
|
205
|
+
if (Array.isArray(out))
|
|
206
|
+
lines = out.map((l) => String(l));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
// fall through with no lines - widget renders empty
|
|
211
|
+
}
|
|
212
|
+
}
|
|
180
213
|
this.sink({
|
|
181
214
|
type: "extension_ui_request",
|
|
182
215
|
id: crypto.randomUUID(),
|
|
183
216
|
method: "setWidget",
|
|
184
217
|
widgetKey: key,
|
|
185
|
-
widgetLines:
|
|
218
|
+
widgetLines: lines,
|
|
186
219
|
widgetPlacement: options?.placement,
|
|
187
220
|
});
|
|
221
|
+
// Store for late-connecting browsers (replayed on WS connect).
|
|
222
|
+
this.widgetMap.set(key, { lines, placement: options?.placement });
|
|
188
223
|
}
|
|
189
224
|
// ------------------------------------------------------------------
|
|
190
225
|
// Terminal-specific features: no-op in web mode
|
package/dist/usage-render.js
CHANGED
|
@@ -73,11 +73,10 @@ function tsOf(messageTimestamp, entryTimestamp) {
|
|
|
73
73
|
}
|
|
74
74
|
return 0;
|
|
75
75
|
}
|
|
76
|
-
function auxMessage(usage, ts
|
|
76
|
+
function auxMessage(usage, ts) {
|
|
77
77
|
return {
|
|
78
78
|
provider: "aux",
|
|
79
79
|
model: "auxiliary",
|
|
80
|
-
thinkingLevel: undefined,
|
|
81
80
|
cost: usage.cost,
|
|
82
81
|
input: usage.input,
|
|
83
82
|
output: usage.output,
|
|
@@ -85,7 +84,6 @@ function auxMessage(usage, ts, sourceId) {
|
|
|
85
84
|
cacheWrite: usage.cacheWrite,
|
|
86
85
|
timestamp: ts,
|
|
87
86
|
reasoning: usage.reasoning,
|
|
88
|
-
afterCompaction: false,
|
|
89
87
|
source: "auxiliary",
|
|
90
88
|
};
|
|
91
89
|
}
|
|
@@ -98,7 +96,6 @@ function parseSessionFile(path) {
|
|
|
98
96
|
let sessionId = "";
|
|
99
97
|
let cwd = "";
|
|
100
98
|
let parentSession = "";
|
|
101
|
-
let thinkingLevel;
|
|
102
99
|
let compactionPending = false;
|
|
103
100
|
const messages = [];
|
|
104
101
|
let content;
|
|
@@ -129,21 +126,19 @@ function parseSessionFile(path) {
|
|
|
129
126
|
break;
|
|
130
127
|
}
|
|
131
128
|
case "thinking_level_change": {
|
|
132
|
-
if (typeof entry.thinkingLevel === "string")
|
|
133
|
-
thinkingLevel = entry.thinkingLevel;
|
|
134
129
|
break;
|
|
135
130
|
}
|
|
136
131
|
case "compaction": {
|
|
137
132
|
const usage = parseUsageAmount(entry.usage);
|
|
138
133
|
if (usage)
|
|
139
|
-
messages.push(auxMessage(usage, tsOf(undefined, entry.timestamp)
|
|
134
|
+
messages.push(auxMessage(usage, tsOf(undefined, entry.timestamp)));
|
|
140
135
|
compactionPending = true;
|
|
141
136
|
break;
|
|
142
137
|
}
|
|
143
138
|
case "branch_summary": {
|
|
144
139
|
const usage = parseUsageAmount(entry.usage);
|
|
145
140
|
if (usage)
|
|
146
|
-
messages.push(auxMessage(usage, tsOf(undefined, entry.timestamp)
|
|
141
|
+
messages.push(auxMessage(usage, tsOf(undefined, entry.timestamp)));
|
|
147
142
|
break;
|
|
148
143
|
}
|
|
149
144
|
case "message": {
|
|
@@ -156,7 +151,6 @@ function parseSessionFile(path) {
|
|
|
156
151
|
messages.push({
|
|
157
152
|
provider: msg.provider,
|
|
158
153
|
model: msg.model,
|
|
159
|
-
thinkingLevel,
|
|
160
154
|
cost: usage.cost,
|
|
161
155
|
input: usage.input,
|
|
162
156
|
output: usage.output,
|
|
@@ -164,7 +158,6 @@ function parseSessionFile(path) {
|
|
|
164
158
|
cacheWrite: usage.cacheWrite,
|
|
165
159
|
timestamp: tsOf(msg.timestamp, entry.timestamp),
|
|
166
160
|
reasoning: usage.reasoning,
|
|
167
|
-
afterCompaction: compactionPending,
|
|
168
161
|
source: "assistant",
|
|
169
162
|
});
|
|
170
163
|
compactionPending = false;
|
|
@@ -205,7 +198,7 @@ function addTokens(a, b) {
|
|
|
205
198
|
a.cacheWrite += b.cacheWrite;
|
|
206
199
|
}
|
|
207
200
|
/** Format a cost value the way the extension does (0 -> "-"). */
|
|
208
|
-
|
|
201
|
+
function formatUsageCost(cost) {
|
|
209
202
|
if (cost === 0)
|
|
210
203
|
return "-";
|
|
211
204
|
if (cost < 0.01)
|
|
@@ -219,7 +212,7 @@ export function formatUsageCost(cost) {
|
|
|
219
212
|
return `$${Math.round(cost)}`;
|
|
220
213
|
}
|
|
221
214
|
/** Format a token count compactly (12.3K, 4.5M, ...). */
|
|
222
|
-
|
|
215
|
+
function formatUsageTokens(n) {
|
|
223
216
|
if (n >= 1e9)
|
|
224
217
|
return `${(n / 1e9).toFixed(2)}B`;
|
|
225
218
|
if (n >= 1e6)
|