pi-link 0.1.0 → 0.1.2

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 (3) hide show
  1. package/README.md +68 -54
  2. package/index.ts +153 -12
  3. package/package.json +4 -2
package/README.md CHANGED
@@ -49,12 +49,6 @@ A single Pi terminal is powerful. Multiple terminals working together unlock new
49
49
  pi install npm:pi-link
50
50
  ```
51
51
 
52
- or from source:
53
-
54
- ```bash
55
- pi install https://github.com/alvivar/pi-link
56
- ```
57
-
58
52
  ### Uninstall
59
53
 
60
54
  ```bash
@@ -89,7 +83,9 @@ Here's a concrete example of two terminals collaborating. Open two separate `pi
89
83
  ✓ Renamed to "builder"
90
84
 
91
85
  > /link
92
- ⚡ Link: "builder" (hub) · 2 terminals online: builder, researcher
86
+ ⚡ Link: builder (hub) · 2 online
87
+ builder: idle (5s)
88
+ researcher: idle (12s)
93
89
  ```
94
90
 
95
91
  **Terminal 2** — rename it too:
@@ -128,11 +124,11 @@ Every other terminal sees:
128
124
 
129
125
  Link is **off by default**. Without `--link`, the extension is completely silent — no status bar, no connections, no warnings.
130
126
 
131
- | Method | When | Auto-reconnect? |
132
- | ------------------- | ----------------------------------- | --- |
133
- | `pi --link` | Auto-connect on startup | Yes |
134
- | `/link-connect` | Opt-in mid-session (no flag needed) | Yes |
135
- | `/link-disconnect` | Opt-out mid-session | Suppressed until `/link-connect` |
127
+ | Method | When | Auto-reconnect? |
128
+ | ------------------ | ----------------------------------- | -------------------------------- |
129
+ | `pi --link` | Auto-connect on startup | Yes |
130
+ | `/link-connect` | Opt-in mid-session (no flag needed) | Yes |
131
+ | `/link-disconnect` | Opt-out mid-session | Suppressed until `/link-connect` |
136
132
 
137
133
  `/link-connect` enables full participation in Pi Link regardless of whether `--link` was passed. `/link-disconnect` always wins — even over `--link` — until you explicitly `/link-connect` again.
138
134
 
@@ -146,11 +142,11 @@ The extension registers three tools that the LLM can invoke during agent runs.
146
142
 
147
143
  ### Which tool should I use?
148
144
 
149
- | Tool | Behavior | Returns |
150
- | -------------- | ---------------------------------------------------- | ---------------------------------------- |
151
- | `link_send` | Send a message; optionally trigger the remote LLM | Send/delivery status only |
152
- | `link_prompt` | Run a prompt on a remote terminal and wait for reply | The remote terminal's assistant response |
153
- | `link_list` | List currently connected terminals | Terminal directory with roles |
145
+ | Tool | Behavior | Returns |
146
+ | ------------- | ---------------------------------------------------- | ---------------------------------------- |
147
+ | `link_send` | Send a message; optionally trigger the remote LLM | Send/delivery status only |
148
+ | `link_prompt` | Run a prompt on a remote terminal and wait for reply | The remote terminal's assistant response |
149
+ | `link_list` | List currently connected terminals | Terminal directory with roles and status |
154
150
 
155
151
  **If you need the other terminal's answer back, use `link_prompt`.** Use `link_send` to notify or steer without waiting.
156
152
 
@@ -188,34 +184,47 @@ Send a prompt to a remote terminal and **wait** for the LLM's response (synchron
188
184
 
189
185
  ### `link_list`
190
186
 
191
- Lists all connected terminals with role info and self-identification. Takes no parameters.
187
+ Lists all connected terminals with role info, live agent status, and self-identification. Takes no parameters.
188
+
189
+ Each terminal's status is derived automatically from Pi lifecycle events — agents can't set it manually. Three states:
190
+
191
+ | Status | Meaning |
192
+ | ----------------- | ----------------------- |
193
+ | `idle (2m)` | Waiting for user input |
194
+ | `thinking (3s)` | LLM is generating |
195
+ | `tool:bash (12s)` | Running a specific tool |
196
+
197
+ Durations are computed at render time from a `since` timestamp — no timer traffic over the wire. Terminals that just joined with no status data yet render as blank, not fake idle.
192
198
 
193
199
  **Example output:**
194
200
 
195
201
  ```
196
- Connected terminals:
197
- • pi-1 (you)
198
- • pi-2
199
- • pi-3
202
+ link (hub) 3 terminal(s)
203
+ builder@pi-link (you) thinking (3s)
204
+ reviewer@pi-link idle (45s)
205
+ docs@pi-link tool:read (2s)
200
206
  ```
201
207
 
202
208
  ---
203
209
 
204
210
  ## Slash Commands
205
211
 
206
- | Command | Purpose |
207
- | ------------------------ | --------------------------------------------------------------------------------------------------------- |
208
- | `/link` | Show link status (name, role, online count) |
209
- | `/link-name [name]` | Rename this terminal. With no argument, adopts the current Pi session name if available. Collision-safe. |
210
- | `/link-broadcast <msg>` | Broadcast a chat message to all other terminals |
211
- | `/link-connect` | Connect to Pi Link (works anytime, with or without `--link`) |
212
- | `/link-disconnect` | Disconnect from Pi Link and suppress auto-reconnect (overrides `--link`) |
212
+ | Command | Purpose |
213
+ | ----------------------- | -------------------------------------------------------------------------------------------------------- |
214
+ | `/link` | Show link status (name, role, online count, agent status per terminal) |
215
+ | `/link-name [name]` | Rename this terminal. With no argument, adopts the current Pi session name if available. Collision-safe. |
216
+ | `/link-broadcast <msg>` | Broadcast a chat message to all other terminals |
217
+ | `/link-connect` | Connect to Pi Link (works anytime, with or without `--link`) |
218
+ | `/link-disconnect` | Disconnect from Pi Link and suppress auto-reconnect (overrides `--link`) |
213
219
 
214
220
  ### Examples
215
221
 
216
222
  ```
217
223
  > /link
218
- ⚡ Link: "builder" (hub) · 3 online: builder, worker-1, worker-2
224
+ ⚡ Link: builder (hub) · 3 online
225
+ builder: idle (12s)
226
+ worker-1: thinking (3s)
227
+ worker-2: tool:bash (5s)
219
228
 
220
229
  > /link-name orchestrator
221
230
  ✓ Renamed to "orchestrator"
@@ -339,7 +348,7 @@ When the hub goes down and a client promotes itself, terminal names and in-fligh
339
348
 
340
349
  | Package | Purpose |
341
350
  | ------------------------------- | ------------------------------------------------ |
342
- | `@mariozechner/pi-coding-agent` | Pi SDK types (ExtensionAPI, ExtensionContext) |
351
+ | `@mariozechner/pi-coding-agent` | Pi SDK types (ExtensionAPI, ExtensionContext) |
343
352
  | `@mariozechner/pi-tui` | TUI Text widget for custom message rendering |
344
353
  | `@sinclair/typebox` | JSON Schema type definitions for tool parameters |
345
354
 
@@ -371,18 +380,19 @@ The `pi.extensions` field tells Pi which files to load as extensions. Here it po
371
380
 
372
381
  ### Protocol
373
382
 
374
- The wire protocol consists of **8 message types**, all serialized as JSON over WebSocket frames:
383
+ The wire protocol consists of **9 message types**, all serialized as JSON over WebSocket frames:
375
384
 
376
- | Type | Direction | Purpose |
377
- | ----------------- | ------------- | ----------------------------------------------------- |
378
- | `register` | Client → Hub | First message after connecting; requests a name |
379
- | `welcome` | Hub → Client | Confirms assigned name (deduplicated) + terminal list |
380
- | `terminal_joined` | Hub → All | Broadcast when a terminal joins |
381
- | `terminal_left` | Hub → All | Broadcast when a terminal disconnects |
382
- | `chat` | Any → Any/All | Fire-and-forget message; optionally triggers LLM turn |
383
- | `prompt_request` | Any → Any | Request a remote terminal to execute a prompt |
384
- | `prompt_response` | Any → Any | Response carrying the remote prompt result |
385
- | `error` | Hub → Client | Error notification |
385
+ | Type | Direction | Purpose |
386
+ | ----------------- | --------------- | ------------------------------------------------------- |
387
+ | `register` | Client → Hub | First message after connecting; requests a name |
388
+ | `welcome` | Hub → Client | Confirms assigned name, terminal list + status snapshot |
389
+ | `terminal_joined` | Hub → All | Broadcast when a terminal joins |
390
+ | `terminal_left` | Hub → All | Broadcast when a terminal disconnects |
391
+ | `chat` | Any → Any/All | Fire-and-forget message; optionally triggers LLM turn |
392
+ | `prompt_request` | Any → Any | Request a remote terminal to execute a prompt |
393
+ | `prompt_response` | Any → Any | Response carrying the remote prompt result |
394
+ | `status_update` | Any → Hub → All | Terminal broadcasts its agent status change |
395
+ | `error` | Hub → Client | Error notification |
386
396
 
387
397
  ### Message Flow Examples
388
398
 
@@ -394,13 +404,13 @@ Client Hub
394
404
  | register {name:"builder"} |
395
405
  |---------------------------->|
396
406
  | |
397
- | welcome {name:"builder", |
398
- | terminals:["pi-1"]} |
407
+ | welcome {name, terminals, |
408
+ | statuses} |
399
409
  |<----------------------------|
400
410
  | |
401
411
  ```
402
412
 
403
- Hub then broadcasts `terminal_joined` to the other connected terminals.
413
+ Hub then broadcasts `terminal_joined` to the other connected terminals. The `welcome` message includes a status snapshot for all connected terminals (fields omitted above for brevity).
404
414
 
405
415
  **Sending a chat message:**
406
416
 
@@ -445,13 +455,13 @@ Default names are random 4-character hex IDs: `t-a1b2`, `t-c3d4`, etc.
445
455
 
446
456
  ### State Management
447
457
 
448
- | State Field | Type | Purpose |
449
- | ------------------------ | --------------------------------------- | ----------------------------------------------------- |
450
- | `role` | `"hub" \| "client" \| "disconnected"` | Current network role |
451
- | `isAgentBusy` | `boolean` | Prevents accepting remote prompts during agent runs |
452
- | `manuallyDisconnected` | `boolean` | Set by `/link-disconnect`; suppresses auto-reconnect |
453
- | `pendingRemotePrompt` | `object \| null` | Tracks the single in-flight remote prompt execution |
454
- | `pendingPromptResponses` | `Map` | Outstanding prompt RPCs awaiting responses |
458
+ | State Field | Type | Purpose |
459
+ | ------------------------ | ------------------------------------- | ---------------------------------------------------- |
460
+ | `role` | `"hub" \| "client" \| "disconnected"` | Current network role |
461
+ | `isAgentBusy` | `boolean` | Prevents accepting remote prompts during agent runs |
462
+ | `manuallyDisconnected` | `boolean` | Set by `/link-disconnect`; suppresses auto-reconnect |
463
+ | `pendingRemotePrompt` | `object \| null` | Tracks the single in-flight remote prompt execution |
464
+ | `pendingPromptResponses` | `Map` | Outstanding prompt RPCs awaiting responses |
455
465
 
456
466
  ### Message Routing & Error Handling
457
467
 
@@ -473,10 +483,14 @@ The `manuallyDisconnected` flag distinguishes user-initiated disconnects (`/link
473
483
 
474
484
  The extension hooks into Pi's agent lifecycle events:
475
485
 
476
- - **`agent_start`** → Sets `isAgentBusy = true`, blocking incoming remote prompts.
477
- - **`agent_end`** → Checks if a remote prompt was running. If so, extracts the last assistant response from `event.messages` and sends back a `prompt_response`.
486
+ - **`agent_start`** → Sets `isAgentBusy = true`, blocking incoming remote prompts. Broadcasts `status_update` (`thinking`).
487
+ - **`agent_end`** → Checks if a remote prompt was running. If so, extracts the last assistant response from `event.messages` and sends back a `prompt_response`. Broadcasts `status_update` (`idle`).
488
+ - **`tool_execution_start`** → Broadcasts `status_update` (`tool:<name>`).
489
+ - **`tool_execution_end`** → Clears tool status; broadcasts `status_update` (`thinking`) while the agent run continues.
478
490
  - **`session_shutdown`** → Full cleanup via `cleanup()`: closes all sockets, resolves pending promises, and disposes the extension.
479
491
 
492
+ Status updates are push-based: each terminal broadcasts changes to the hub, which fans them out. New joiners receive a status snapshot for all terminals in the `welcome` message. Durations are computed at render time from a `since` timestamp — no polling or timer traffic over the wire.
493
+
480
494
  ### Rendering
481
495
 
482
496
  Incoming link chat messages render with a styled `⚡ [sender]` prefix using the theme's accent color. The link status text in Pi's footer uses `theme.fg("dim", ...)` to match Pi's standard footer styling.
package/index.ts CHANGED
@@ -45,6 +45,7 @@ interface WelcomeMsg {
45
45
  type: "welcome";
46
46
  name: string;
47
47
  terminals: string[];
48
+ statuses?: Record<string, LinkStatus>;
48
49
  }
49
50
  interface TerminalJoinedMsg {
50
51
  type: "terminal_joined";
@@ -78,11 +79,21 @@ interface PromptResponseMsg {
78
79
  response: string;
79
80
  error?: string;
80
81
  }
82
+ interface StatusUpdateMsg {
83
+ type: "status_update";
84
+ name: string;
85
+ status: LinkStatus;
86
+ }
81
87
  interface ErrorMsg {
82
88
  type: "error";
83
89
  message: string;
84
90
  }
85
91
 
92
+ type LinkStatus =
93
+ | { kind: "idle"; since: number }
94
+ | { kind: "thinking"; since: number }
95
+ | { kind: "tool"; toolName: string; since: number };
96
+
86
97
  type LinkMessage =
87
98
  | RegisterMsg
88
99
  | WelcomeMsg
@@ -91,6 +102,7 @@ type LinkMessage =
91
102
  | ChatMsg
92
103
  | PromptRequestMsg
93
104
  | PromptResponseMsg
105
+ | StatusUpdateMsg
94
106
  | ErrorMsg;
95
107
 
96
108
  // ─── Extension ───────────────────────────────────────────────────────────────
@@ -108,14 +120,22 @@ export default function (pi: ExtensionAPI) {
108
120
  let terminalName = `t-${crypto.randomUUID().slice(0, 4)}`;
109
121
  let connectedTerminals: string[] = [];
110
122
  let ctx: ExtensionContext | undefined;
111
- let isAgentBusy = false;
112
123
  let disposed = false;
113
124
  let manuallyDisconnected = false;
114
125
  let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
115
126
 
127
+ // Status tracking (local truth)
128
+ let agentRunning = false;
129
+ let activeToolName: string | null = null;
130
+ let stateSince = Date.now();
131
+ let lastPushedKind: string | null = null;
132
+ let lastPushedTool: string | null = null;
133
+ const terminalStatuses = new Map<string, LinkStatus>(); // other terminals
134
+
116
135
  // Hub state
117
136
  let wss: WebSocketServer | null = null;
118
137
  const hubClients = new Map<WebSocket, string>(); // ws → terminal name
138
+ const hubTerminalStatuses = new Map<string, LinkStatus>(); // hub-authoritative
119
139
 
120
140
  // Client state
121
141
  let ws: WebSocket | null = null;
@@ -148,6 +168,53 @@ export default function (pi: ExtensionAPI) {
148
168
  ctx.ui.setStatus("link", theme.fg("dim", info));
149
169
  }
150
170
 
171
+ function deriveStatus(): LinkStatus {
172
+ if (activeToolName)
173
+ return { kind: "tool", toolName: activeToolName, since: stateSince };
174
+ if (agentRunning) return { kind: "thinking", since: stateSince };
175
+ return { kind: "idle", since: stateSince };
176
+ }
177
+
178
+ function pushStatus(force = false) {
179
+ if (role === "disconnected") return;
180
+ const status = deriveStatus();
181
+ const newKind = status.kind;
182
+ const newTool = status.kind === "tool" ? status.toolName : null;
183
+ if (!force && newKind === lastPushedKind && newTool === lastPushedTool)
184
+ return;
185
+ lastPushedKind = newKind;
186
+ lastPushedTool = newTool;
187
+ const msg: StatusUpdateMsg = {
188
+ type: "status_update",
189
+ name: terminalName,
190
+ status,
191
+ };
192
+ if (role === "hub") {
193
+ hubBroadcast(msg, terminalName);
194
+ } else if (ws?.readyState === WebSocket.OPEN) {
195
+ ws.send(JSON.stringify(msg));
196
+ }
197
+ }
198
+
199
+ function formatDuration(since: number): string {
200
+ const sec = Math.floor((Date.now() - since) / 1000);
201
+ if (sec < 60) return `${sec}s`;
202
+ if (sec < 3600) return `${Math.floor(sec / 60)}m`;
203
+ return `${Math.floor(sec / 3600)}h`;
204
+ }
205
+
206
+ function formatStatus(s: LinkStatus): string {
207
+ const dur = formatDuration(s.since);
208
+ if (s.kind === "tool") return `tool:${s.toolName} (${dur})`;
209
+ return `${s.kind} (${dur})`;
210
+ }
211
+
212
+ function getStatusFor(name: string): LinkStatus | null {
213
+ if (name === terminalName) return deriveStatus();
214
+ const map = role === "hub" ? hubTerminalStatuses : terminalStatuses;
215
+ return map.get(name) ?? null;
216
+ }
217
+
151
218
  function allTerminalNames(): Set<string> {
152
219
  const names = new Set<string>();
153
220
  names.add(terminalName); // hub's own name
@@ -257,11 +324,18 @@ export default function (pi: ExtensionAPI) {
257
324
  case "welcome":
258
325
  terminalName = msg.name;
259
326
  connectedTerminals = msg.terminals;
327
+ terminalStatuses.clear();
328
+ if (msg.statuses) {
329
+ for (const [name, status] of Object.entries(msg.statuses)) {
330
+ terminalStatuses.set(name, status);
331
+ }
332
+ }
260
333
  updateStatus();
261
334
  ctx?.ui.notify(
262
335
  `Joined link as "${terminalName}" (${connectedTerminals.length} online)`,
263
336
  "info",
264
337
  );
338
+ pushStatus(true);
265
339
  break;
266
340
 
267
341
  // ── Directory updates ──
@@ -273,10 +347,16 @@ export default function (pi: ExtensionAPI) {
273
347
 
274
348
  case "terminal_left":
275
349
  connectedTerminals = msg.terminals;
350
+ terminalStatuses.delete(msg.name);
276
351
  updateStatus();
277
352
  ctx?.ui.notify(`"${msg.name}" left the link`, "info");
278
353
  break;
279
354
 
355
+ // ── Status update from another terminal ──
356
+ case "status_update":
357
+ terminalStatuses.set(msg.name, msg.status);
358
+ break;
359
+
280
360
  // ── Chat message ──
281
361
  case "chat":
282
362
  pi.sendMessage(
@@ -292,7 +372,7 @@ export default function (pi: ExtensionAPI) {
292
372
 
293
373
  // ── Another terminal asks us to run a prompt ──
294
374
  case "prompt_request":
295
- if (isAgentBusy || pendingRemotePrompt) {
375
+ if (agentRunning || pendingRemotePrompt) {
296
376
  routeMessage({
297
377
  type: "prompt_response",
298
378
  id: msg.id,
@@ -353,12 +433,18 @@ export default function (pi: ExtensionAPI) {
353
433
  connectedTerminals = list;
354
434
  updateStatus();
355
435
 
356
- // Confirm to the new client
436
+ // Confirm to the new client (include status snapshot)
437
+ const statuses: Record<string, LinkStatus> = {};
438
+ statuses[terminalName] = deriveStatus(); // hub's own status
439
+ for (const [name, status] of hubTerminalStatuses) {
440
+ if (name !== clientName) statuses[name] = status;
441
+ }
357
442
  clientWs.send(
358
443
  JSON.stringify({
359
444
  type: "welcome",
360
445
  name: clientName,
361
446
  terminals: list,
447
+ statuses,
362
448
  } satisfies WelcomeMsg),
363
449
  );
364
450
 
@@ -375,6 +461,21 @@ export default function (pi: ExtensionAPI) {
375
461
  // Ignore messages from unregistered clients
376
462
  if (!clientName) return;
377
463
 
464
+ // Status update — store and fan out to other clients only (not back to hub)
465
+ if (msg.type === "status_update") {
466
+ hubTerminalStatuses.set(clientName, msg.status);
467
+ const normalized: StatusUpdateMsg = {
468
+ type: "status_update",
469
+ name: clientName,
470
+ status: msg.status,
471
+ };
472
+ const json = JSON.stringify(normalized);
473
+ for (const [otherWs, name] of hubClients) {
474
+ if (name !== clientName) otherWs.send(json);
475
+ }
476
+ return;
477
+ }
478
+
378
479
  // Route chat / prompt messages
379
480
  if (
380
481
  msg.type === "chat" ||
@@ -388,6 +489,7 @@ export default function (pi: ExtensionAPI) {
388
489
  clientWs.on("close", () => {
389
490
  if (clientName) {
390
491
  hubClients.delete(clientWs);
492
+ hubTerminalStatuses.delete(clientName);
391
493
  const list = terminalList();
392
494
  connectedTerminals = list;
393
495
  updateStatus();
@@ -545,6 +647,10 @@ export default function (pi: ExtensionAPI) {
545
647
 
546
648
  role = "disconnected";
547
649
  connectedTerminals = [];
650
+ terminalStatuses.clear();
651
+ hubTerminalStatuses.clear();
652
+ lastPushedKind = null;
653
+ lastPushedTool = null;
548
654
  updateStatus();
549
655
  }
550
656
 
@@ -565,11 +671,29 @@ export default function (pi: ExtensionAPI) {
565
671
  });
566
672
 
567
673
  pi.on("agent_start", async () => {
568
- isAgentBusy = true;
674
+ agentRunning = true;
675
+ activeToolName = null;
676
+ stateSince = Date.now();
677
+ pushStatus();
678
+ });
679
+
680
+ pi.on("tool_execution_start", async (event) => {
681
+ activeToolName = event.toolName;
682
+ stateSince = Date.now();
683
+ pushStatus();
684
+ });
685
+
686
+ pi.on("tool_execution_end", async () => {
687
+ activeToolName = null;
688
+ if (agentRunning) stateSince = Date.now();
689
+ pushStatus();
569
690
  });
570
691
 
571
692
  pi.on("agent_end", async (event) => {
572
- isAgentBusy = false;
693
+ agentRunning = false;
694
+ activeToolName = null;
695
+ stateSince = Date.now();
696
+ pushStatus();
573
697
 
574
698
  // If we were running a remote prompt, send the response back
575
699
  if (pendingRemotePrompt) {
@@ -806,15 +930,20 @@ export default function (pi: ExtensionAPI) {
806
930
  async execute() {
807
931
  if (role === "disconnected") return notConnectedResult();
808
932
 
933
+ const statuses: Record<string, string> = {};
809
934
  const list = connectedTerminals
810
935
  .map((name) => {
936
+ const status = getStatusFor(name);
937
+ const statusStr = status ? formatStatus(status) : "";
938
+ if (statusStr) statuses[name] = statusStr;
811
939
  const marker = name === terminalName ? " (you)" : "";
812
- return ` • ${name}${marker}`;
940
+ return ` • ${name}${marker}${statusStr ? " " + statusStr : ""}`;
813
941
  })
814
942
  .join("\n");
815
943
 
816
944
  return textResult(`Connected terminals:\n${list}`, {
817
945
  terminals: connectedTerminals,
946
+ statuses,
818
947
  self: terminalName,
819
948
  role,
820
949
  });
@@ -822,7 +951,12 @@ export default function (pi: ExtensionAPI) {
822
951
 
823
952
  renderResult(result, _options, theme) {
824
953
  const details = result.details as
825
- | { terminals?: string[]; self?: string; role?: string }
954
+ | {
955
+ terminals?: string[];
956
+ statuses?: Record<string, string>;
957
+ self?: string;
958
+ role?: string;
959
+ }
826
960
  | undefined;
827
961
  if (!details?.terminals) {
828
962
  const txt = result.content[0];
@@ -834,11 +968,12 @@ export default function (pi: ExtensionAPI) {
834
968
  text += theme.fg("accent", `${details.terminals.length} terminal(s)`);
835
969
  for (const name of details.terminals) {
836
970
  const isSelf = name === details.self;
971
+ const status = details.statuses?.[name] ?? "";
972
+ const nameStr = isSelf ? `• ${name} (you)` : `• ${name}`;
837
973
  text +=
838
974
  "\n " +
839
- (isSelf
840
- ? theme.fg("accent", `• ${name} (you)`)
841
- : theme.fg("text", `• ${name}`));
975
+ (isSelf ? theme.fg("accent", nameStr) : theme.fg("text", nameStr)) +
976
+ (status ? " " + theme.fg("dim", status) : "");
842
977
  }
843
978
  return new Text(text, 0, 0);
844
979
  },
@@ -853,9 +988,14 @@ export default function (pi: ExtensionAPI) {
853
988
  _ctx.ui.notify("Link: not connected", "warning");
854
989
  return;
855
990
  }
856
- const names = connectedTerminals.join(", ");
991
+ const lines = connectedTerminals.map((name) => {
992
+ const status = getStatusFor(name);
993
+ const statusStr = status ? formatStatus(status) : "";
994
+ const marker = name === terminalName ? " (you)" : "";
995
+ return `${name}${marker}${statusStr ? ": " + statusStr : ""}`;
996
+ });
857
997
  _ctx.ui.notify(
858
- `Link: ${terminalName} (${role}) · ${connectedTerminals.length} online: ${names}`,
998
+ `Link: ${terminalName} (${role}) · ${connectedTerminals.length} online\n${lines.join("\n")}`,
859
999
  "info",
860
1000
  );
861
1001
  },
@@ -905,6 +1045,7 @@ export default function (pi: ExtensionAPI) {
905
1045
  { type: "terminal_joined", name: newName, terminals: list },
906
1046
  newName,
907
1047
  );
1048
+ pushStatus(true);
908
1049
  _ctx.ui.notify(`Renamed to "${newName}"`, "info");
909
1050
  } else if (role === "client") {
910
1051
  // Reconnect with new name — hub will enforce uniqueness via register
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-link",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
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",
@@ -23,6 +23,8 @@
23
23
  "@types/ws": "^8.18.1"
24
24
  },
25
25
  "pi": {
26
- "extensions": ["./index.ts"]
26
+ "extensions": [
27
+ "./index.ts"
28
+ ]
27
29
  }
28
30
  }