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.
Files changed (41) hide show
  1. package/README.md +82 -22
  2. package/README.zh.md +67 -19
  3. package/dist/{analyzer-2Nx5XYOA.mjs → analyzer-CXNbeBAe.mjs} +2 -2
  4. package/dist/{analyzer-2Nx5XYOA.mjs.map → analyzer-CXNbeBAe.mjs.map} +1 -1
  5. package/dist/cli.mjs +2 -2
  6. package/dist/client.js +604 -31
  7. package/dist/compat/pi-ai.d.mts +36 -1
  8. package/dist/compat/pi-ai.d.mts.map +1 -1
  9. package/dist/compat/pi-ai.mjs +2 -2
  10. package/dist/compat/pi-coding-agent.d.mts +2 -1411
  11. package/dist/compat/pi-coding-agent.mjs +1 -1
  12. package/dist/compat/pi-tui.mjs +1 -1
  13. package/dist/{oauth-bridge-CfyImWx4.mjs → credentials-oauth-BakY-0PA.mjs} +172 -13
  14. package/dist/credentials-oauth-BakY-0PA.mjs.map +1 -0
  15. package/dist/credentials-oauth.mjs +1 -89
  16. package/dist/host.mjs +4 -2
  17. package/dist/host.mjs.map +1 -1
  18. package/dist/index.mjs +3 -3
  19. package/dist/{mcp-config-Cjae0vot.mjs → mcp-config-CZepacin.mjs} +19 -19
  20. package/dist/mcp-config-CZepacin.mjs.map +1 -0
  21. package/dist/{pi-ai-Dyg4zyLZ.mjs → pi-ai-BA3V_J0V.mjs} +40 -2
  22. package/dist/{pi-ai-Dyg4zyLZ.mjs.map → pi-ai-BA3V_J0V.mjs.map} +1 -1
  23. package/dist/pi-coding-agent-B0PT23gE.d.mts +1412 -0
  24. package/dist/pi-coding-agent-B0PT23gE.d.mts.map +1 -0
  25. package/dist/{pi-coding-agent-CYnmeJEx.mjs → pi-coding-agent-D9skobb6.mjs} +0 -0
  26. package/dist/pi-coding-agent-D9skobb6.mjs.map +1 -0
  27. package/dist/{pi-tui-5CYLcj-_.mjs → pi-tui-DruSeKmd.mjs} +3 -3
  28. package/dist/pi-tui-DruSeKmd.mjs.map +1 -0
  29. package/dist/{runtime-B_Ig28og.mjs → runtime-B-SlR8mJ.mjs} +970 -75
  30. package/dist/runtime-B-SlR8mJ.mjs.map +1 -0
  31. package/dist/runtime.d.mts +600 -0
  32. package/dist/runtime.d.mts.map +1 -1
  33. package/dist/runtime.mjs +1 -1
  34. package/package.json +2 -1
  35. package/dist/compat/pi-coding-agent.d.mts.map +0 -1
  36. package/dist/credentials-oauth.mjs.map +0 -1
  37. package/dist/mcp-config-Cjae0vot.mjs.map +0 -1
  38. package/dist/oauth-bridge-CfyImWx4.mjs.map +0 -1
  39. package/dist/pi-coding-agent-CYnmeJEx.mjs.map +0 -1
  40. package/dist/pi-tui-5CYLcj-_.mjs.map +0 -1
  41. 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
- * The panel: one card per live side thread of the session on screen.
74
- *
75
- * The current session comes from the framework's own global seat
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
- let alive = true;
86
- const poll = async () => {
87
- if (session === void 0 || session === "") {
88
- if (alive) setThreads([]);
89
- return;
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(`/pi2dsh/side-panel?session=${encodeURIComponent(session)}`);
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 (alive) setThreads(Array.isArray(payload.threads) ? payload.threads : []);
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
- poll();
495
+ sync();
99
496
  const timer = window.setInterval(() => {
100
- poll();
497
+ sync();
101
498
  }, POLL_MS);
102
499
  return () => {
103
- alive = false;
500
+ live = false;
104
501
  window.clearInterval(timer);
105
502
  };
106
- }, [session]);
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
- if (shown.length === 0) return null;
109
- return (0, react.createElement)("div", {
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
- * Client plugin body: take the frame-wide overlay seat.
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-side-panel",
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
- }, SidePanel));
699
+ }, textSeat("working", [
700
+ "footer",
701
+ "workingMessage",
702
+ "workingIndicator",
703
+ "hiddenThinkingLabel"
704
+ ])));
132
705
  });
133
706
  }
134
707
  //#endregion