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 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
- // While the command runs, notify() broadcasts are tagged with the
799
- // command name as title, so the browser shows them as a titled modal
800
- // (colored, Markdown-rendered) instead of a plain stream line.
801
- ui.sink = (obj) => {
802
- const req = obj;
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
@@ -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
- this.scopedModelsAll = data.available || [];
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
  })
@@ -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
+ }
@@ -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: Array.isArray(content) ? content : undefined,
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
@@ -73,11 +73,10 @@ function tsOf(messageTimestamp, entryTimestamp) {
73
73
  }
74
74
  return 0;
75
75
  }
76
- function auxMessage(usage, ts, sourceId) {
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), typeof entry.id === "string" ? entry.id : ""));
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), typeof entry.id === "string" ? entry.id : ""));
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
- export function formatUsageCost(cost) {
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
- export function formatUsageTokens(n) {
215
+ function formatUsageTokens(n) {
223
216
  if (n >= 1e9)
224
217
  return `${(n / 1e9).toFixed(2)}B`;
225
218
  if (n >= 1e6)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-sdk-web",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "description": "Browser Web access for Pi (AI coding agent) via the Pi SDK - standalone module, zero modification to Pi itself",
5
5
  "type": "module",
6
6
  "bin": {