pi-sdk-web 0.5.2 → 0.5.3
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 +18 -21
- package/dist/static/app.js +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) {
|
|
@@ -792,31 +805,15 @@ export class PiWebServer {
|
|
|
792
805
|
// (appendEntry -> modal), same as before.
|
|
793
806
|
const cmdPath = (cmd.sourceInfo && cmd.sourceInfo.path) || "";
|
|
794
807
|
const customOnly = /pi-magic-context|magic-context/.test(cmdPath);
|
|
795
|
-
const ui = this.uiContext;
|
|
796
|
-
const originalSink = ui.sink;
|
|
797
808
|
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
|
-
};
|
|
809
|
+
// No sink wrapping: notify() broadcasts go through as-is (no title),
|
|
810
|
+
// so the frontend renders them as persistent status lines - the same
|
|
811
|
+
// as TUI's notify -> showStatus (a dim line appended to the chat),
|
|
812
|
+
// whether or not they come from a command. Command *results* (custom
|
|
813
|
+
// entries) still become titled modals via the captured entries below.
|
|
812
814
|
await cmd.handler(args, this.buildCommandContext(customOnly ? false : true));
|
|
813
815
|
}
|
|
814
|
-
catch (e) {
|
|
815
|
-
ui.sink = originalSink;
|
|
816
|
-
throw e;
|
|
817
|
-
}
|
|
818
816
|
finally {
|
|
819
|
-
ui.sink = originalSink;
|
|
820
817
|
unsubscribe();
|
|
821
818
|
}
|
|
822
819
|
// 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();
|
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)
|