pi2dsh 0.12.3 → 0.13.0
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/README.md +82 -22
- package/README.zh.md +67 -19
- package/dist/{analyzer-2Nx5XYOA.mjs → analyzer-CXNbeBAe.mjs} +2 -2
- package/dist/{analyzer-2Nx5XYOA.mjs.map → analyzer-CXNbeBAe.mjs.map} +1 -1
- package/dist/cli.mjs +2 -2
- package/dist/client.js +604 -31
- package/dist/compat/pi-ai.d.mts +36 -1
- package/dist/compat/pi-ai.d.mts.map +1 -1
- package/dist/compat/pi-ai.mjs +2 -2
- package/dist/compat/pi-coding-agent.d.mts +2 -1411
- package/dist/compat/pi-coding-agent.mjs +1 -1
- package/dist/compat/pi-tui.mjs +1 -1
- package/dist/{oauth-bridge-CfyImWx4.mjs → credentials-oauth-BakY-0PA.mjs} +172 -13
- package/dist/credentials-oauth-BakY-0PA.mjs.map +1 -0
- package/dist/credentials-oauth.mjs +1 -89
- package/dist/host.mjs +4 -2
- package/dist/host.mjs.map +1 -1
- package/dist/index.mjs +3 -3
- package/dist/{mcp-config-Cjae0vot.mjs → mcp-config-CZepacin.mjs} +19 -19
- package/dist/mcp-config-CZepacin.mjs.map +1 -0
- package/dist/{pi-ai-Dyg4zyLZ.mjs → pi-ai-BA3V_J0V.mjs} +40 -2
- package/dist/{pi-ai-Dyg4zyLZ.mjs.map → pi-ai-BA3V_J0V.mjs.map} +1 -1
- package/dist/pi-coding-agent-B0PT23gE.d.mts +1412 -0
- package/dist/pi-coding-agent-B0PT23gE.d.mts.map +1 -0
- package/dist/{pi-coding-agent-CYnmeJEx.mjs → pi-coding-agent-D9skobb6.mjs} +0 -0
- package/dist/pi-coding-agent-D9skobb6.mjs.map +1 -0
- package/dist/{pi-tui-5CYLcj-_.mjs → pi-tui-DruSeKmd.mjs} +3 -3
- package/dist/pi-tui-DruSeKmd.mjs.map +1 -0
- package/dist/{runtime-B_Ig28og.mjs → runtime-B-SlR8mJ.mjs} +970 -75
- package/dist/runtime-B-SlR8mJ.mjs.map +1 -0
- package/dist/runtime.d.mts +600 -0
- package/dist/runtime.d.mts.map +1 -1
- package/dist/runtime.mjs +1 -1
- package/package.json +2 -1
- package/dist/compat/pi-coding-agent.d.mts.map +0 -1
- package/dist/credentials-oauth.mjs.map +0 -1
- package/dist/mcp-config-Cjae0vot.mjs.map +0 -1
- package/dist/oauth-bridge-CfyImWx4.mjs.map +0 -1
- package/dist/pi-coding-agent-CYnmeJEx.mjs.map +0 -1
- package/dist/pi-tui-5CYLcj-_.mjs.map +0 -1
- package/dist/runtime-B_Ig28og.mjs.map +0 -1
package/dist/client.js
CHANGED
|
@@ -5,10 +5,227 @@ window.__ModuleLoader__.load({
|
|
|
5
5
|
var exports = module.exports;
|
|
6
6
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
7
|
let react = require("react");
|
|
8
|
+
//#region src/ansi.ts
|
|
9
|
+
/** The 8 base ANSI colours, in SGR order, as CSS. */
|
|
10
|
+
const BASE = [
|
|
11
|
+
"#000000",
|
|
12
|
+
"#cd3131",
|
|
13
|
+
"#0dbc79",
|
|
14
|
+
"#e5e510",
|
|
15
|
+
"#2472c8",
|
|
16
|
+
"#bc3fbc",
|
|
17
|
+
"#11a8cd",
|
|
18
|
+
"#e5e5e5"
|
|
19
|
+
];
|
|
20
|
+
/** Their bright variants (SGR 90-97 / 100-107). */
|
|
21
|
+
const BRIGHT = [
|
|
22
|
+
"#666666",
|
|
23
|
+
"#f14c4c",
|
|
24
|
+
"#23d18b",
|
|
25
|
+
"#f5f543",
|
|
26
|
+
"#3b8eea",
|
|
27
|
+
"#d670d6",
|
|
28
|
+
"#29b8db",
|
|
29
|
+
"#ffffff"
|
|
30
|
+
];
|
|
31
|
+
/**
|
|
32
|
+
* Resolve one xterm-256 index to CSS.
|
|
33
|
+
* @param index - the palette index (0-255).
|
|
34
|
+
* @returns a CSS colour.
|
|
35
|
+
*/
|
|
36
|
+
function ansi256(index) {
|
|
37
|
+
if (index < 8) return BASE[index];
|
|
38
|
+
if (index < 16) return BRIGHT[index - 8];
|
|
39
|
+
if (index < 232) {
|
|
40
|
+
const step = (value) => value === 0 ? 0 : value * 40 + 55;
|
|
41
|
+
const rest = index - 16;
|
|
42
|
+
return `rgb(${step(Math.floor(rest / 36))}, ${step(Math.floor(rest / 6) % 6)}, ${step(rest % 6)})`;
|
|
43
|
+
}
|
|
44
|
+
const grey = (index - 232) * 10 + 8;
|
|
45
|
+
return `rgb(${grey}, ${grey}, ${grey})`;
|
|
46
|
+
}
|
|
47
|
+
const SGR = String.raw`\[([0-9;]*)m`;
|
|
48
|
+
/**
|
|
49
|
+
* Split text into styled runs.
|
|
50
|
+
*
|
|
51
|
+
* Unrecognised escapes are dropped rather than printed — a code this does not
|
|
52
|
+
* model is still not something a reader should see as text. Text with no
|
|
53
|
+
* escapes comes back as a single unstyled run, so callers need no special case.
|
|
54
|
+
* @param text - possibly carrying SGR escapes.
|
|
55
|
+
* @returns the runs, in order; empty only for empty input.
|
|
56
|
+
*/
|
|
57
|
+
function parseAnsi(text) {
|
|
58
|
+
const pattern = new RegExp(SGR, "gu");
|
|
59
|
+
const runs = [];
|
|
60
|
+
let style = {};
|
|
61
|
+
let at = 0;
|
|
62
|
+
const push = (piece) => {
|
|
63
|
+
if (piece.length > 0) runs.push({
|
|
64
|
+
text: piece,
|
|
65
|
+
style: { ...style }
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
for (let match = pattern.exec(text); match !== null; match = pattern.exec(text)) {
|
|
69
|
+
push(text.slice(at, match.index));
|
|
70
|
+
at = match.index + match[0].length;
|
|
71
|
+
const codes = (match[1] ?? "").split(";").filter((part) => part !== "").map(Number);
|
|
72
|
+
if (codes.length === 0) style = {};
|
|
73
|
+
for (let index = 0; index < codes.length; index += 1) {
|
|
74
|
+
const code = codes[index];
|
|
75
|
+
if (code === 0) style = {};
|
|
76
|
+
else if (code === 1) style.fontWeight = "bold";
|
|
77
|
+
else if (code === 2) style.opacity = "0.7";
|
|
78
|
+
else if (code === 3) style.fontStyle = "italic";
|
|
79
|
+
else if (code === 4) style.textDecoration = "underline";
|
|
80
|
+
else if (code === 39) delete style.color;
|
|
81
|
+
else if (code === 49) delete style.backgroundColor;
|
|
82
|
+
else if (code >= 30 && code <= 37) style.color = BASE[code - 30];
|
|
83
|
+
else if (code >= 90 && code <= 97) style.color = BRIGHT[code - 90];
|
|
84
|
+
else if (code >= 40 && code <= 47) style.backgroundColor = BASE[code - 40];
|
|
85
|
+
else if (code >= 100 && code <= 107) style.backgroundColor = BRIGHT[code - 100];
|
|
86
|
+
else if (code === 38 || code === 48) {
|
|
87
|
+
const property = code === 38 ? "color" : "backgroundColor";
|
|
88
|
+
const kind = codes[index + 1];
|
|
89
|
+
if (kind === 5 && codes.length > index + 2) {
|
|
90
|
+
style[property] = ansi256(codes[index + 2]);
|
|
91
|
+
index += 2;
|
|
92
|
+
} else if (kind === 2 && codes.length > index + 4) {
|
|
93
|
+
style[property] = `rgb(${codes[index + 2]}, ${codes[index + 3]}, ${codes[index + 4]})`;
|
|
94
|
+
index += 4;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
push(text.slice(at));
|
|
100
|
+
return runs;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Whether text carries any SGR escape.
|
|
104
|
+
* @param text - the text to check.
|
|
105
|
+
* @returns true when at least one escape is present.
|
|
106
|
+
*/
|
|
107
|
+
function hasAnsi(text) {
|
|
108
|
+
return new RegExp(SGR, "u").test(text);
|
|
109
|
+
}
|
|
110
|
+
//#endregion
|
|
8
111
|
//#region src/client.ts
|
|
9
112
|
/** Services this half needs before it can take a seat. */
|
|
10
|
-
const inject = ["slots"];
|
|
113
|
+
const inject = ["slots", "inputTriggers"];
|
|
11
114
|
const POLL_MS = 1e3;
|
|
115
|
+
const EMPTY = {
|
|
116
|
+
threads: [],
|
|
117
|
+
surfaces: [],
|
|
118
|
+
entries: []
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* One poller per session, shared by every seat this package takes.
|
|
122
|
+
*
|
|
123
|
+
* Four components read the same payload; four independent timers would be four
|
|
124
|
+
* requests a second for one answer.
|
|
125
|
+
*/
|
|
126
|
+
const subscribers = /* @__PURE__ */ new Map();
|
|
127
|
+
const latest = /* @__PURE__ */ new Map();
|
|
128
|
+
const timers = /* @__PURE__ */ new Map();
|
|
129
|
+
/**
|
|
130
|
+
* Subscribe to one session's browser state.
|
|
131
|
+
* @param session - session id to poll for.
|
|
132
|
+
* @param notify - called with each payload, and immediately with the last one.
|
|
133
|
+
* @returns an unsubscribe function that stops the timer with the last reader.
|
|
134
|
+
*/
|
|
135
|
+
function watch(session, notify) {
|
|
136
|
+
const readers = subscribers.get(session) ?? /* @__PURE__ */ new Set();
|
|
137
|
+
readers.add(notify);
|
|
138
|
+
subscribers.set(session, readers);
|
|
139
|
+
const cached = latest.get(session);
|
|
140
|
+
if (cached !== void 0) notify(cached);
|
|
141
|
+
if (!timers.has(session)) {
|
|
142
|
+
const poll = async () => {
|
|
143
|
+
try {
|
|
144
|
+
const response = await fetch(`/pi2dsh/browser-state?session=${encodeURIComponent(session)}`);
|
|
145
|
+
if (!response.ok) return;
|
|
146
|
+
const payload = await response.json();
|
|
147
|
+
const state = {
|
|
148
|
+
threads: Array.isArray(payload.threads) ? payload.threads : [],
|
|
149
|
+
surfaces: Array.isArray(payload.surfaces) ? payload.surfaces : [],
|
|
150
|
+
entries: Array.isArray(payload.entries) ? payload.entries : [],
|
|
151
|
+
...payload.draft === void 0 ? {} : { draft: payload.draft }
|
|
152
|
+
};
|
|
153
|
+
latest.set(session, state);
|
|
154
|
+
for (const reader of subscribers.get(session) ?? []) reader(state);
|
|
155
|
+
} catch {}
|
|
156
|
+
};
|
|
157
|
+
poll();
|
|
158
|
+
timers.set(session, window.setInterval(() => {
|
|
159
|
+
poll();
|
|
160
|
+
}, POLL_MS));
|
|
161
|
+
}
|
|
162
|
+
return () => {
|
|
163
|
+
const live = subscribers.get(session);
|
|
164
|
+
if (live === void 0) return;
|
|
165
|
+
live.delete(notify);
|
|
166
|
+
if (live.size > 0) return;
|
|
167
|
+
subscribers.delete(session);
|
|
168
|
+
const timer = timers.get(session);
|
|
169
|
+
if (timer !== void 0) window.clearInterval(timer);
|
|
170
|
+
timers.delete(session);
|
|
171
|
+
latest.delete(session);
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* React binding for {@link watch}.
|
|
176
|
+
* @param session - session id, or undefined while none is selected.
|
|
177
|
+
* @returns the latest payload for that session.
|
|
178
|
+
*/
|
|
179
|
+
function useBrowserState(session) {
|
|
180
|
+
const [state, setState] = (0, react.useState)(EMPTY);
|
|
181
|
+
(0, react.useEffect)(() => {
|
|
182
|
+
if (session === void 0 || session === "") {
|
|
183
|
+
setState(EMPTY);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
return watch(session, setState);
|
|
187
|
+
}, [session]);
|
|
188
|
+
return state;
|
|
189
|
+
}
|
|
190
|
+
/** The working keys Pi's setWorkingVisible gates: hidden while hidden. */
|
|
191
|
+
const WORKING_KEYS = [
|
|
192
|
+
"workingMessage",
|
|
193
|
+
"workingIndicator",
|
|
194
|
+
"hiddenThinkingLabel"
|
|
195
|
+
];
|
|
196
|
+
/** Every value packages have set for one simple surface, in package order. */
|
|
197
|
+
function valuesFor(surfaces, key) {
|
|
198
|
+
const out = [];
|
|
199
|
+
for (const surface of surfaces) {
|
|
200
|
+
if (WORKING_KEYS.includes(key) && !surface.workingVisible) continue;
|
|
201
|
+
const text = surface.values[key];
|
|
202
|
+
if (text !== void 0) out.push({
|
|
203
|
+
owner: surface.package ?? "pi",
|
|
204
|
+
text
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
return out;
|
|
208
|
+
}
|
|
209
|
+
/** Every status entry, package by package, then key, in registration order. */
|
|
210
|
+
function statusesFor(surfaces) {
|
|
211
|
+
const out = [];
|
|
212
|
+
for (const surface of surfaces) for (const [key, text] of Object.entries(surface.statuses)) out.push({
|
|
213
|
+
owner: surface.package ?? "pi",
|
|
214
|
+
key,
|
|
215
|
+
text
|
|
216
|
+
});
|
|
217
|
+
return out;
|
|
218
|
+
}
|
|
219
|
+
/** Every widget, package by package, then key, in registration order. */
|
|
220
|
+
function widgetsFor(surfaces) {
|
|
221
|
+
const out = [];
|
|
222
|
+
for (const surface of surfaces) for (const [key, text] of Object.entries(surface.widgets)) out.push({
|
|
223
|
+
owner: surface.package ?? "pi",
|
|
224
|
+
key,
|
|
225
|
+
text
|
|
226
|
+
});
|
|
227
|
+
return out;
|
|
228
|
+
}
|
|
12
229
|
const styles = {
|
|
13
230
|
panel: {
|
|
14
231
|
position: "fixed",
|
|
@@ -67,46 +284,248 @@ window.__ModuleLoader__.load({
|
|
|
67
284
|
border: "none",
|
|
68
285
|
color: "inherit",
|
|
69
286
|
font: "inherit"
|
|
287
|
+
},
|
|
288
|
+
pillStack: {
|
|
289
|
+
position: "fixed",
|
|
290
|
+
right: "20px",
|
|
291
|
+
bottom: "20px",
|
|
292
|
+
zIndex: 39,
|
|
293
|
+
display: "flex",
|
|
294
|
+
flexDirection: "column",
|
|
295
|
+
alignItems: "flex-end",
|
|
296
|
+
gap: "6px",
|
|
297
|
+
pointerEvents: "none"
|
|
298
|
+
},
|
|
299
|
+
pill: {
|
|
300
|
+
pointerEvents: "auto",
|
|
301
|
+
padding: "5px 10px",
|
|
302
|
+
borderRadius: "999px",
|
|
303
|
+
background: "var(--dsh-color-bg-elevated, rgba(24,24,27,0.92))",
|
|
304
|
+
color: "var(--dsh-color-text, #fafafa)",
|
|
305
|
+
border: "1px solid rgba(120,120,130,0.28)",
|
|
306
|
+
font: "500 11px/1.4 system-ui, sans-serif",
|
|
307
|
+
whiteSpace: "pre-wrap"
|
|
308
|
+
},
|
|
309
|
+
inline: {
|
|
310
|
+
font: "400 12px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace",
|
|
311
|
+
whiteSpace: "pre-wrap",
|
|
312
|
+
opacity: .85
|
|
313
|
+
},
|
|
314
|
+
strip: {
|
|
315
|
+
display: "flex",
|
|
316
|
+
flexDirection: "column",
|
|
317
|
+
gap: "4px",
|
|
318
|
+
padding: "4px 2px"
|
|
319
|
+
},
|
|
320
|
+
imageTool: {
|
|
321
|
+
margin: "4px 0",
|
|
322
|
+
overflow: "hidden",
|
|
323
|
+
borderRadius: "8px",
|
|
324
|
+
border: "1px solid rgba(120,120,130,0.22)",
|
|
325
|
+
background: "var(--dsh-color-bg-subtle, rgba(120,120,130,0.06))",
|
|
326
|
+
color: "var(--dsh-color-text, inherit)",
|
|
327
|
+
font: "400 12px/1.5 system-ui, sans-serif"
|
|
328
|
+
},
|
|
329
|
+
imageToolToggle: {
|
|
330
|
+
width: "100%",
|
|
331
|
+
display: "flex",
|
|
332
|
+
alignItems: "center",
|
|
333
|
+
gap: "8px",
|
|
334
|
+
padding: "8px 10px",
|
|
335
|
+
cursor: "pointer",
|
|
336
|
+
border: "none",
|
|
337
|
+
background: "transparent",
|
|
338
|
+
color: "inherit",
|
|
339
|
+
textAlign: "left",
|
|
340
|
+
font: "500 12px/1.5 system-ui, sans-serif"
|
|
341
|
+
},
|
|
342
|
+
imageToolStatus: {
|
|
343
|
+
color: "#2FBC44",
|
|
344
|
+
fontSize: "10px"
|
|
345
|
+
},
|
|
346
|
+
imageToolSummary: {
|
|
347
|
+
opacity: .62,
|
|
348
|
+
overflow: "hidden",
|
|
349
|
+
textOverflow: "ellipsis",
|
|
350
|
+
whiteSpace: "nowrap"
|
|
351
|
+
},
|
|
352
|
+
imageToolBody: {
|
|
353
|
+
display: "flex",
|
|
354
|
+
flexDirection: "column",
|
|
355
|
+
gap: "8px",
|
|
356
|
+
padding: "0 10px 10px",
|
|
357
|
+
borderTop: "1px solid rgba(120,120,130,0.14)"
|
|
358
|
+
},
|
|
359
|
+
imageToolText: {
|
|
360
|
+
margin: "8px 0 0",
|
|
361
|
+
whiteSpace: "pre-wrap",
|
|
362
|
+
wordBreak: "break-word",
|
|
363
|
+
opacity: .82
|
|
364
|
+
},
|
|
365
|
+
imageGrid: {
|
|
366
|
+
display: "flex",
|
|
367
|
+
flexWrap: "wrap",
|
|
368
|
+
gap: "8px"
|
|
369
|
+
},
|
|
370
|
+
imageFrame: {
|
|
371
|
+
display: "block",
|
|
372
|
+
maxWidth: "min(320px, 100%)",
|
|
373
|
+
maxHeight: "320px",
|
|
374
|
+
borderRadius: "8px",
|
|
375
|
+
objectFit: "contain",
|
|
376
|
+
background: "rgba(0,0,0,0.08)"
|
|
377
|
+
},
|
|
378
|
+
imageError: {
|
|
379
|
+
padding: "12px",
|
|
380
|
+
color: "#F63218"
|
|
70
381
|
}
|
|
71
382
|
};
|
|
72
|
-
/**
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
* (`useSessions().current`), not from the address bar — the app keeps the open
|
|
77
|
-
* conversation in its state, so the URL never names it.
|
|
78
|
-
* @param props - the global standard kit every root slot component receives.
|
|
79
|
-
*/
|
|
80
|
-
function SidePanel({ useSessions }) {
|
|
81
|
-
const session = useSessions((state) => state.current);
|
|
82
|
-
const [threads, setThreads] = (0, react.useState)([]);
|
|
83
|
-
const [dismissed, setDismissed] = (0, react.useState)([]);
|
|
383
|
+
/** Pull one image through DSH's own session-authorized attachment RPC. */
|
|
384
|
+
function AuthorizedToolImage({ sessionId, attachment }) {
|
|
385
|
+
const [url, setUrl] = (0, react.useState)(void 0);
|
|
386
|
+
const [failed, setFailed] = (0, react.useState)(false);
|
|
84
387
|
(0, react.useEffect)(() => {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
388
|
+
const controller = new AbortController();
|
|
389
|
+
let objectUrl;
|
|
390
|
+
(async () => {
|
|
391
|
+
try {
|
|
392
|
+
const response = await fetch("/api/session.attachment", {
|
|
393
|
+
method: "POST",
|
|
394
|
+
headers: { "content-type": "application/json" },
|
|
395
|
+
signal: controller.signal,
|
|
396
|
+
body: JSON.stringify({
|
|
397
|
+
type: "client-request",
|
|
398
|
+
rpcId: `pi2dsh-image-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
399
|
+
method: "session.attachment",
|
|
400
|
+
payload: {
|
|
401
|
+
sessionId,
|
|
402
|
+
attachmentId: attachment.attachmentId
|
|
403
|
+
}
|
|
404
|
+
})
|
|
405
|
+
});
|
|
406
|
+
const envelope = await response.json();
|
|
407
|
+
const data = envelope.result?.ok === true ? envelope.result.value?.data : void 0;
|
|
408
|
+
if (!response.ok || typeof data !== "string") throw new Error("attachment unavailable");
|
|
409
|
+
const binary = atob(data);
|
|
410
|
+
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
411
|
+
objectUrl = URL.createObjectURL(new Blob([bytes], { type: attachment.mediaType }));
|
|
412
|
+
if (!controller.signal.aborted) setUrl(objectUrl);
|
|
413
|
+
} catch {
|
|
414
|
+
if (!controller.signal.aborted) setFailed(true);
|
|
90
415
|
}
|
|
416
|
+
})();
|
|
417
|
+
return () => {
|
|
418
|
+
controller.abort();
|
|
419
|
+
if (objectUrl !== void 0) URL.revokeObjectURL(objectUrl);
|
|
420
|
+
};
|
|
421
|
+
}, [
|
|
422
|
+
sessionId,
|
|
423
|
+
attachment.attachmentId,
|
|
424
|
+
attachment.mediaType
|
|
425
|
+
]);
|
|
426
|
+
if (failed) return (0, react.createElement)("div", { style: styles.imageError }, "Image attachment could not be loaded.");
|
|
427
|
+
if (url === void 0) return (0, react.createElement)("div", { style: styles.imageToolText }, "Loading image…");
|
|
428
|
+
return (0, react.createElement)("img", {
|
|
429
|
+
src: url,
|
|
430
|
+
alt: attachment.name ?? "Image returned by Pi tool",
|
|
431
|
+
style: styles.imageFrame,
|
|
432
|
+
"data-pi2dsh": "tool-image"
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
function firstArgumentSummary(raw) {
|
|
436
|
+
try {
|
|
437
|
+
const parsed = JSON.parse(raw);
|
|
438
|
+
const preferred = parsed.prompt ?? parsed.description;
|
|
439
|
+
if (typeof preferred === "string") return preferred;
|
|
440
|
+
const first = Object.values(parsed).find((value) => typeof value === "string");
|
|
441
|
+
return typeof first === "string" ? first : "";
|
|
442
|
+
} catch {
|
|
443
|
+
return raw;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
/** Browser row shared by the explicitly supported Pi image tools. */
|
|
447
|
+
function PiImageToolView({ toolName, block, sessionId }) {
|
|
448
|
+
const [expanded, setExpanded] = (0, react.useState)(true);
|
|
449
|
+
const settled = block.kind === "tool-result";
|
|
450
|
+
const argsRaw = settled ? block.call?.argsRaw ?? "" : block.argsRaw ?? "";
|
|
451
|
+
const content = settled && Array.isArray(block.content) ? block.content : [];
|
|
452
|
+
const images = content.flatMap((item) => item.type === "image" && item.attachment !== void 0 ? [item.attachment] : []);
|
|
453
|
+
const text = content.filter((item) => item.type === "text" && typeof item.text === "string").map((item) => item.text).join("\n");
|
|
454
|
+
const summary = firstArgumentSummary(argsRaw);
|
|
455
|
+
return (0, react.createElement)("div", {
|
|
456
|
+
style: styles.imageTool,
|
|
457
|
+
"data-pi2dsh": "image-tool-result",
|
|
458
|
+
"data-tool": toolName
|
|
459
|
+
}, (0, react.createElement)("button", {
|
|
460
|
+
type: "button",
|
|
461
|
+
style: styles.imageToolToggle,
|
|
462
|
+
"aria-expanded": expanded,
|
|
463
|
+
onClick: () => setExpanded((value) => !value)
|
|
464
|
+
}, (0, react.createElement)("span", { style: styles.imageToolStatus }, block.isError === true ? "●" : settled ? "●" : "◌"), (0, react.createElement)("span", null, toolName), summary === "" ? null : (0, react.createElement)("span", { style: styles.imageToolSummary }, `· ${summary}`)), !expanded ? null : (0, react.createElement)("div", { style: styles.imageToolBody }, text === "" ? null : (0, react.createElement)("div", { style: styles.imageToolText }, text), sessionId === void 0 || images.length === 0 ? null : (0, react.createElement)("div", { style: styles.imageGrid }, ...images.map((attachment) => (0, react.createElement)(AuthorizedToolImage, {
|
|
465
|
+
key: attachment.attachmentId,
|
|
466
|
+
sessionId,
|
|
467
|
+
attachment
|
|
468
|
+
})))));
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Register the image row under exact tool names published at package mount.
|
|
472
|
+
* Only known image tools appear in that list, so text-only and native tools
|
|
473
|
+
* retain DSH's existing cards.
|
|
474
|
+
*/
|
|
475
|
+
function installImageToolViews(scope) {
|
|
476
|
+
const installed = /* @__PURE__ */ new Set();
|
|
477
|
+
const start = () => {
|
|
478
|
+
let live = true;
|
|
479
|
+
const sync = async () => {
|
|
91
480
|
try {
|
|
92
|
-
const response = await fetch(
|
|
93
|
-
if (!response.ok) return;
|
|
481
|
+
const response = await fetch("/pi2dsh/image-tool-names");
|
|
482
|
+
if (!response.ok || !live) return;
|
|
94
483
|
const payload = await response.json();
|
|
95
|
-
if (
|
|
484
|
+
if (!Array.isArray(payload.names)) return;
|
|
485
|
+
for (const value of payload.names) {
|
|
486
|
+
if (typeof value !== "string" || value === "" || installed.has(value)) continue;
|
|
487
|
+
installed.add(value);
|
|
488
|
+
scope.slots.inject("tool.call.toolview", () => scope.slots.register({
|
|
489
|
+
name: "tool.call.toolview",
|
|
490
|
+
key: value
|
|
491
|
+
}, PiImageToolView));
|
|
492
|
+
}
|
|
96
493
|
} catch {}
|
|
97
494
|
};
|
|
98
|
-
|
|
495
|
+
sync();
|
|
99
496
|
const timer = window.setInterval(() => {
|
|
100
|
-
|
|
497
|
+
sync();
|
|
101
498
|
}, POLL_MS);
|
|
102
499
|
return () => {
|
|
103
|
-
|
|
500
|
+
live = false;
|
|
104
501
|
window.clearInterval(timer);
|
|
105
502
|
};
|
|
106
|
-
}
|
|
503
|
+
};
|
|
504
|
+
if (scope.effect !== void 0) scope.effect(start, "pi2dsh: image tool views");
|
|
505
|
+
else start();
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* The frame-wide seat: the side-conversation panel, plus whatever packages
|
|
509
|
+
* pinned frame-wide — transient title and Pi's status entries, as pills.
|
|
510
|
+
* @param props - the global standard kit every root slot component receives.
|
|
511
|
+
*/
|
|
512
|
+
function OverlaySurfaces({ useSessions }) {
|
|
513
|
+
const { threads, surfaces } = useBrowserState(useSessions((state) => state.current));
|
|
514
|
+
const [dismissed, setDismissed] = (0, react.useState)([]);
|
|
107
515
|
const shown = threads.filter((thread) => !dismissed.includes(thread.id));
|
|
108
|
-
|
|
109
|
-
|
|
516
|
+
const pills = [...valuesFor(surfaces, "title").map((entry) => ({
|
|
517
|
+
...entry,
|
|
518
|
+
key: "title"
|
|
519
|
+
})), ...statusesFor(surfaces)];
|
|
520
|
+
if (shown.length === 0 && pills.length === 0) return null;
|
|
521
|
+
return (0, react.createElement)("div", null, pills.length === 0 ? null : (0, react.createElement)("div", {
|
|
522
|
+
style: styles.pillStack,
|
|
523
|
+
"data-pi2dsh": "pills"
|
|
524
|
+
}, ...pills.map((pill, index) => (0, react.createElement)("div", {
|
|
525
|
+
key: `${pill.owner}-${pill.key}-${index}`,
|
|
526
|
+
style: styles.pill,
|
|
527
|
+
title: pill.owner
|
|
528
|
+
}, ansiText(pill.text)))), shown.length === 0 ? null : (0, react.createElement)("div", {
|
|
110
529
|
"data-pi2dsh": "side-panel",
|
|
111
530
|
style: styles.panel
|
|
112
531
|
}, ...shown.map((thread) => (0, react.createElement)("div", {
|
|
@@ -116,19 +535,173 @@ window.__ModuleLoader__.load({
|
|
|
116
535
|
style: styles.close,
|
|
117
536
|
title: "Hide",
|
|
118
537
|
onClick: () => setDismissed((list) => [...list, thread.id])
|
|
119
|
-
}, "×")), (0, react.createElement)("div", { style: styles.body }, ...thread.messages.map((message, index) => (0, react.createElement)("div", { key: index }, (0, react.createElement)("div", { style: styles.role }, message.role), (0, react.createElement)("div", { style: styles.text }, message.text)))))));
|
|
538
|
+
}, "×")), (0, react.createElement)("div", { style: styles.body }, ...thread.messages.map((message, index) => (0, react.createElement)("div", { key: index }, (0, react.createElement)("div", { style: styles.role }, message.role), (0, react.createElement)("div", { style: styles.text }, message.text))))))));
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* One session-scoped seat rendering a set of surfaces as text.
|
|
542
|
+
* @param marker - the data-pi2dsh value, so an e2e run can address the seat.
|
|
543
|
+
* @param valueKeys - which simple value surfaces this seat shows.
|
|
544
|
+
* @param opts - whether the seat also shows widgets (keyed string arrays).
|
|
545
|
+
* @returns a slot component.
|
|
546
|
+
*/
|
|
547
|
+
/**
|
|
548
|
+
* Render text that may carry ANSI colour into styled spans.
|
|
549
|
+
*
|
|
550
|
+
* Parsing lives in ./ansi.js so it can be tested without a DOM; this half
|
|
551
|
+
* only turns runs into elements. Text with no escapes returns as a plain
|
|
552
|
+
* string, so the common case adds no wrappers.
|
|
553
|
+
* @param text - the seat text, possibly with SGR escapes.
|
|
554
|
+
* @returns react children.
|
|
555
|
+
*/
|
|
556
|
+
function ansiText(text) {
|
|
557
|
+
if (!hasAnsi(text)) return text;
|
|
558
|
+
return parseAnsi(text).map((run, index) => Object.keys(run.style).length === 0 ? run.text : (0, react.createElement)("span", {
|
|
559
|
+
key: `ansi-${index}`,
|
|
560
|
+
style: run.style
|
|
561
|
+
}, run.text));
|
|
562
|
+
}
|
|
563
|
+
function textSeat(marker, valueKeys, opts = {}) {
|
|
564
|
+
return function TextSeat({ sessionId }) {
|
|
565
|
+
const { surfaces } = useBrowserState(sessionId);
|
|
566
|
+
const entries = [...valueKeys.flatMap((key) => valuesFor(surfaces, key)), ...opts.widgets === true ? widgetsFor(surfaces) : []];
|
|
567
|
+
if (entries.length === 0) return null;
|
|
568
|
+
return (0, react.createElement)("div", {
|
|
569
|
+
"data-pi2dsh": marker,
|
|
570
|
+
style: styles.strip
|
|
571
|
+
}, ...entries.map((entry, index) => (0, react.createElement)("div", {
|
|
572
|
+
key: `${entry.owner}-${index}`,
|
|
573
|
+
style: styles.inline,
|
|
574
|
+
title: entry.owner
|
|
575
|
+
}, ansiText(entry.text))));
|
|
576
|
+
};
|
|
120
577
|
}
|
|
121
578
|
/**
|
|
122
|
-
*
|
|
579
|
+
* Custom entries a package appended and renders itself.
|
|
580
|
+
*
|
|
581
|
+
* They live in pi2dsh's sidecar, not DSH's durable log — the host has no
|
|
582
|
+
* channel for event types declared outside the harness — so the host's own
|
|
583
|
+
* conversation view cannot show them. This seat is where a package's own
|
|
584
|
+
* entries become visible, drawn by the package's registered renderer.
|
|
585
|
+
* @param props - the session standard kit.
|
|
586
|
+
* @returns the entry strip, or null when the package appended none.
|
|
587
|
+
*/
|
|
588
|
+
function EntryStrip({ sessionId }) {
|
|
589
|
+
const { entries } = useBrowserState(sessionId);
|
|
590
|
+
if (entries.length === 0) return null;
|
|
591
|
+
return (0, react.createElement)("div", {
|
|
592
|
+
"data-pi2dsh": "entries",
|
|
593
|
+
style: styles.strip
|
|
594
|
+
}, ...entries.map((entry) => (0, react.createElement)("div", {
|
|
595
|
+
key: entry.id,
|
|
596
|
+
style: styles.inline,
|
|
597
|
+
title: `${entry.package ?? "pi"} · ${entry.customType}`
|
|
598
|
+
}, ansiText(entry.text))));
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* The composer half of Pi's editor calls.
|
|
602
|
+
*
|
|
603
|
+
* `inputActions` is part of the session standard kit — every session-scoped
|
|
604
|
+
* slot component receives it — so a package's `setEditorText`/`pasteToEditor`
|
|
605
|
+
* reaches the real composer instead of a buffer nobody reads. The traffic is
|
|
606
|
+
* two-way on purpose: the live draft is reported back so a package's
|
|
607
|
+
* `getEditorText` reads what the user actually has, not only its own last
|
|
608
|
+
* write.
|
|
609
|
+
* @param props - the session standard kit (state hook plus input actions).
|
|
610
|
+
* @returns nothing rendered; this seat exists for the effects.
|
|
611
|
+
*/
|
|
612
|
+
function ComposerBridge({ sessionId, useInput, inputActions }) {
|
|
613
|
+
const { draft } = useBrowserState(sessionId);
|
|
614
|
+
const live = useInput === void 0 ? "" : useInput((state) => state.draft);
|
|
615
|
+
const [appliedRev, setAppliedRev] = (0, react.useState)(0);
|
|
616
|
+
(0, react.useEffect)(() => {
|
|
617
|
+
if (draft === void 0 || inputActions === void 0) return;
|
|
618
|
+
if (draft.rev <= appliedRev) return;
|
|
619
|
+
setAppliedRev(draft.rev);
|
|
620
|
+
inputActions.setDraft(draft.text);
|
|
621
|
+
}, [
|
|
622
|
+
draft?.rev,
|
|
623
|
+
draft?.text,
|
|
624
|
+
inputActions,
|
|
625
|
+
appliedRev
|
|
626
|
+
]);
|
|
627
|
+
(0, react.useEffect)(() => {
|
|
628
|
+
if (sessionId === void 0 || sessionId === "") return;
|
|
629
|
+
fetch("/pi2dsh/editor-draft", {
|
|
630
|
+
method: "POST",
|
|
631
|
+
headers: { "content-type": "application/json" },
|
|
632
|
+
body: JSON.stringify({
|
|
633
|
+
session: sessionId,
|
|
634
|
+
draft: live
|
|
635
|
+
})
|
|
636
|
+
}).catch(() => {});
|
|
637
|
+
}, [sessionId, live]);
|
|
638
|
+
return null;
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Client plugin body: take the seats this package draws into.
|
|
123
642
|
* @param ctx - client root context.
|
|
124
643
|
*/
|
|
125
644
|
function apply(ctx) {
|
|
645
|
+
ctx.inject(["inputTriggers"], (scope) => {
|
|
646
|
+
const triggers = scope.inputTriggers;
|
|
647
|
+
if (triggers === void 0) return;
|
|
648
|
+
triggers.registerSource({
|
|
649
|
+
trigger: "@",
|
|
650
|
+
name: "pi2dsh",
|
|
651
|
+
order: 50,
|
|
652
|
+
candidates: async (_session, req) => {
|
|
653
|
+
try {
|
|
654
|
+
const response = await fetch(`/pi2dsh/completions?trigger=${encodeURIComponent("@")}&query=${encodeURIComponent(req.query)}`, { signal: req.signal });
|
|
655
|
+
if (!response.ok) return [];
|
|
656
|
+
return ((await response.json()).items ?? []).map((item) => ({
|
|
657
|
+
name: item.value,
|
|
658
|
+
...item.description === void 0 ? {} : { description: item.description }
|
|
659
|
+
}));
|
|
660
|
+
} catch {
|
|
661
|
+
return [];
|
|
662
|
+
}
|
|
663
|
+
},
|
|
664
|
+
onPick: (pick) => ({ text: pick.candidate.name })
|
|
665
|
+
});
|
|
666
|
+
});
|
|
126
667
|
ctx.inject(["slots"], (scope) => {
|
|
668
|
+
installImageToolViews(scope);
|
|
127
669
|
scope.slots.inject("shell.overlay", () => scope.slots.register({
|
|
128
670
|
name: "shell.overlay",
|
|
129
|
-
id: "pi2dsh-
|
|
671
|
+
id: "pi2dsh-overlay",
|
|
672
|
+
order: 1
|
|
673
|
+
}, OverlaySurfaces));
|
|
674
|
+
scope.slots.inject("conversation.session.header.utilities", () => scope.slots.register({
|
|
675
|
+
name: "conversation.session.header.utilities",
|
|
676
|
+
id: "pi2dsh-header",
|
|
677
|
+
order: 1
|
|
678
|
+
}, textSeat("header", ["header"])));
|
|
679
|
+
scope.slots.inject("conversation.input.dock", () => scope.slots.register({
|
|
680
|
+
name: "conversation.input.dock",
|
|
681
|
+
id: "pi2dsh-dock",
|
|
682
|
+
order: 1
|
|
683
|
+
}, textSeat("dock", [], { widgets: true })));
|
|
684
|
+
scope.slots.inject("conversation.chat.turnTail", () => scope.slots.register({
|
|
685
|
+
name: "conversation.chat.turnTail",
|
|
686
|
+
id: "pi2dsh-entries",
|
|
687
|
+
order: 1,
|
|
688
|
+
select: () => ({})
|
|
689
|
+
}, EntryStrip));
|
|
690
|
+
scope.slots.inject("conversation.input.dock", () => scope.slots.register({
|
|
691
|
+
name: "conversation.input.dock",
|
|
692
|
+
id: "pi2dsh-composer-bridge",
|
|
693
|
+
order: 2
|
|
694
|
+
}, ComposerBridge));
|
|
695
|
+
scope.slots.inject("conversation.composer.dock", () => scope.slots.register({
|
|
696
|
+
name: "conversation.composer.dock",
|
|
697
|
+
id: "pi2dsh-working",
|
|
130
698
|
order: 1
|
|
131
|
-
},
|
|
699
|
+
}, textSeat("working", [
|
|
700
|
+
"footer",
|
|
701
|
+
"workingMessage",
|
|
702
|
+
"workingIndicator",
|
|
703
|
+
"hiddenThinkingLabel"
|
|
704
|
+
])));
|
|
132
705
|
});
|
|
133
706
|
}
|
|
134
707
|
//#endregion
|