pi-link 0.4.0 → 0.4.1

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/CHANGELOG.md CHANGED
@@ -6,6 +6,20 @@ This changelog is based on the git history from `2026-03-21` (initial commit) th
6
6
 
7
7
  ---
8
8
 
9
+ ## 0.4.1 — 2026-09-07
10
+
11
+ ### Added
12
+
13
+ - **Received link messages now sit in Pi's own message panel.** A delivery is drawn in the same background panel Pi gives extension messages, indented by your configured output padding, so link traffic lines up with the rest of the transcript instead of standing outside it.
14
+
15
+ - **A long delivery is collapsed to a preview.** It shows the first six content rows as they wrap at the available content width inside the panel, then `... (N more lines, ctrl+o to expand)`; a message that already fits shows whole, with no hint. The six counts content rows only — the panel's padding and the hint itself sit outside it, so the panel has no fixed height. Expanding is Pi's existing global toggle, `Ctrl+O` unless you have rebound `app.tools.expand`, and the hint names your actual binding when one is assigned. It expands every custom message at once, including ones restored from an earlier session; pi-link adds no toggle of its own. When the expansion shortcut is unbound, incoming and outgoing previews show `bind app.tools.expand to expand`. Only the display changes — the full text is still what reaches the model, what was delivered, and what the session file keeps.
16
+
17
+ ### Changed
18
+
19
+ - **Outgoing `link_send` and `link_compact` calls now follow Pi's global expansion toggle.** Collapsed, the message — or the compaction instructions — has runs of whitespace collapsed to single spaces and keeps the first 60 characters of that result; on a narrow terminal that preview may still wrap onto more than one row. Expanded, the uncollapsed text is shown with its line breaks and spacing as rendered by Pi. Indentation of the surrounding call is unchanged, and the tool's own result line still reports send or compaction status — never the other terminal's reply.
20
+
21
+ ---
22
+
9
23
  ## 0.4.0 — 2026-09-04
10
24
 
11
25
  ### Added
package/README.md CHANGED
@@ -717,3 +717,9 @@ Because a gated flush does not reschedule, the release path is load-bearing. `re
717
717
  ### Rendering
718
718
 
719
719
  Delivered link batches render with a styled `⚡ [link]` prefix using the theme's accent color; sender attribution lives in the `From "name":` blocks inside the message body, not in the prefix. The link status text in Pi's footer uses `theme.fg("dim", ...)` to match Pi's standard footer styling.
720
+
721
+ Deliveries sit in the same background panel Pi draws around extension messages by default, indented by your configured output padding, so link messages line up with the rest of the transcript instead of standing outside it.
722
+
723
+ A long delivery is previewed rather than printed whole. Collapsed, it shows the first six rows as they wrap at the current window width, followed by `... (N more lines, ctrl+o to expand)`; a message that already fits in six rows shows in full with no hint. Expanding is Pi's own global toggle — `Ctrl+O` unless you have rebound `app.tools.expand`, and the hint always names your actual binding — so it expands every custom message at once, including messages restored from an earlier session. If you have unbound `app.tools.expand` entirely there is no key to name, so both hints read `bind app.tools.expand to expand` instead. Only the display changes: the full text is what reaches the model, what `link_send` delivered, and what the session file keeps.
724
+
725
+ The same toggle also expands outgoing calls: `link_send` and `link_compact` collapse the message or instructions into a one-line preview — runs of whitespace become single spaces — and keep its first 60 characters, which a narrow terminal may still wrap onto more than one row. Expanding reveals the uncollapsed text with its line breaks and spacing as rendered by Pi. Expanding a call says nothing about delivery — the tool's own result line reports that.
package/index.ts CHANGED
@@ -12,10 +12,12 @@
12
12
 
13
13
  import {
14
14
  VERSION as PI_VERSION,
15
+ keyHint,
16
+ keyText,
15
17
  type ExtensionAPI,
16
18
  type ExtensionContext,
17
19
  } from "@earendil-works/pi-coding-agent";
18
- import { Text } from "@earendil-works/pi-tui";
20
+ import { Box, Text, type Component } from "@earendil-works/pi-tui";
19
21
  import { Type } from "typebox";
20
22
  import * as crypto from "node:crypto";
21
23
  import { createServer, type Server as HttpServer } from "node:http";
@@ -40,6 +42,8 @@ const CONNECT_HANDSHAKE_TIMEOUT_MS = 5_000;
40
42
  const FLUSH_DELAY_MS = 200;
41
43
  const BATCH_MAX_ITEMS = 20;
42
44
  const BATCH_MAX_CHARS = 16_000;
45
+ // Visual rows of an incoming message kept while Pi's global expansion is off.
46
+ const PREVIEW_ROWS = 6;
43
47
 
44
48
  // ─── Protocol ────────────────────────────────────────────────────────────────
45
49
 
@@ -151,6 +155,49 @@ function piVersionSupported(version: string): boolean {
151
155
  return prerelease === undefined; // exactly the floor: only the release qualifies
152
156
  }
153
157
 
158
+ // keyText is empty when app.tools.expand has no binding, and the native hint would then
159
+ // read " to expand" with no key in it, naming nothing the reader can press.
160
+ function expansionHint(dim: (text: string) => string): string {
161
+ return keyText("app.tools.expand")
162
+ ? keyHint("app.tools.expand", "to expand")
163
+ : dim("bind app.tools.expand to expand");
164
+ }
165
+
166
+ /**
167
+ * The first PREVIEW_ROWS rows of `content`, plus a hint when rows were dropped.
168
+ *
169
+ * The rows come from rendering `content` at the real width, not from splitting the
170
+ * raw string on newlines: one long logical line wraps into many visual rows, and
171
+ * the same message yields different rows in a narrow window. Rendering is Text's
172
+ * job here, cached by (text, width), so the preview stays a slice of the identical
173
+ * output the expanded view shows.
174
+ */
175
+ function messagePreview(
176
+ content: Text,
177
+ dim: (text: string) => string,
178
+ ): Component {
179
+ const hint = new Text("", 0, 0);
180
+ return {
181
+ render(width: number): string[] {
182
+ const rows = content.render(width);
183
+ if (rows.length <= PREVIEW_ROWS) return rows;
184
+ // Rebuilt every render so a rebound key is never shown stale; it is one short
185
+ // line, and Text still caches the content rows, which are the expensive part.
186
+ hint.setText(
187
+ dim(`... (${rows.length - PREVIEW_ROWS} more lines, `) +
188
+ expansionHint(dim) +
189
+ dim(")"),
190
+ );
191
+ // The budget covers content only, so the hint may wrap rather than be cut off.
192
+ return [...rows.slice(0, PREVIEW_ROWS), ...hint.render(width)];
193
+ },
194
+ invalidate(): void {
195
+ content.invalidate();
196
+ hint.invalidate();
197
+ },
198
+ };
199
+ }
200
+
154
201
  // ─── Extension ───────────────────────────────────────────────────────────────
155
202
 
156
203
  export default function (pi: ExtensionAPI) {
@@ -1490,8 +1537,19 @@ export default function (pi: ExtensionAPI) {
1490
1537
  return textResult("Not connected to link", { error: "not_connected" });
1491
1538
  }
1492
1539
 
1493
- function truncatePreview(text: string) {
1494
- return text.length > 60 ? text.slice(0, 60) + "..." : text;
1540
+ // Collapsed one-line preview for the two outgoing tools. Whitespace is normalized so a
1541
+ // multiline message stays on the tool's single preview line, then clipped to 60
1542
+ // characters. The hint appears only when characters are actually hidden: expanding shows
1543
+ // the original text, so collapsing whitespace alone is not something to advertise.
1544
+ // Styling is applied per segment, like messagePreview above: the hint ends with a
1545
+ // foreground reset, so punctuation appended after it would render undimmed.
1546
+ function truncatePreview(text: string, dim: (text: string) => string) {
1547
+ const preview = text.replace(/\s+/g, " ");
1548
+ return preview.length > 60
1549
+ ? dim(preview.slice(0, 60) + "... (") +
1550
+ expansionHint(dim) +
1551
+ dim(")")
1552
+ : dim(preview);
1495
1553
  }
1496
1554
 
1497
1555
  // Shared "target not found" result for the send/compact tools.
@@ -1566,16 +1624,19 @@ export default function (pi: ExtensionAPI) {
1566
1624
  return textResult(`${verb} ${target}`, { to: params.to });
1567
1625
  },
1568
1626
 
1569
- renderCall(args, theme) {
1627
+ renderCall(args, theme, context) {
1628
+ const dim = (text: string) => theme.fg("dim", text);
1570
1629
  const preview =
1571
1630
  typeof args.message === "string"
1572
- ? truncatePreview(args.message)
1573
- : "...";
1631
+ ? context.expanded
1632
+ ? dim(args.message)
1633
+ : truncatePreview(args.message, dim)
1634
+ : dim("...");
1574
1635
  const text =
1575
1636
  theme.fg("toolTitle", theme.bold("link_send ")) +
1576
1637
  theme.fg("accent", args.to) +
1577
1638
  "\n " +
1578
- theme.fg("dim", preview);
1639
+ preview;
1579
1640
  return new Text(text, 0, 0);
1580
1641
  },
1581
1642
 
@@ -1679,11 +1740,16 @@ export default function (pi: ExtensionAPI) {
1679
1740
  });
1680
1741
  },
1681
1742
 
1682
- renderCall(args, theme) {
1743
+ renderCall(args, theme, context) {
1683
1744
  let text = theme.fg("toolTitle", theme.bold("link_compact "));
1684
1745
  text += theme.fg("accent", String(args.to));
1746
+ const dim = (t: string) => theme.fg("dim", t);
1685
1747
  if (typeof args.instructions === "string")
1686
- text += "\n " + theme.fg("dim", truncatePreview(args.instructions));
1748
+ text +=
1749
+ "\n " +
1750
+ (context.expanded
1751
+ ? dim(args.instructions)
1752
+ : truncatePreview(args.instructions, dim));
1687
1753
  return new Text(text, 0, 0);
1688
1754
  },
1689
1755
 
@@ -1915,12 +1981,22 @@ export default function (pi: ExtensionAPI) {
1915
1981
 
1916
1982
  // ── Message renderer ─────────────────────────────────────────────────────
1917
1983
 
1918
- pi.registerMessageRenderer("link", (message, _options, theme) => {
1984
+ pi.registerMessageRenderer("link", (message, options, theme) => {
1919
1985
  const from =
1920
1986
  (message.details as Record<string, unknown> | undefined)?.from ?? "link";
1921
1987
  const text =
1922
1988
  theme.fg("accent", `⚡ [${from}] `) +
1923
1989
  theme.fg("text", String(message.content));
1924
- return new Text(text, 0, 0);
1990
+ const content = new Text(text, 0, 0);
1991
+ // The same panel Pi draws around extension messages by default: returning our own
1992
+ // component bypasses the host's box, so a link message would otherwise sit unframed
1993
+ // among framed ones. `outputPad` is the user's configured output padding.
1994
+ const box = new Box(options.outputPad, 1, (t) => theme.bg("customMessageBg", t));
1995
+ box.addChild(
1996
+ options.expanded
1997
+ ? content
1998
+ : messagePreview(content, (s) => theme.fg("muted", s)),
1999
+ );
2000
+ return box;
1925
2001
  });
1926
2002
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-link",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "WebSocket-based inter-terminal communication for Pi. Connect multiple Pi terminals over a local link network.",
5
5
  "author": "alvivar",
6
6
  "license": "MIT",