pi-link 0.1.1 → 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 -48
  2. package/index.ts +153 -12
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -83,7 +83,9 @@ Here's a concrete example of two terminals collaborating. Open two separate `pi
83
83
  ✓ Renamed to "builder"
84
84
 
85
85
  > /link
86
- ⚡ Link: "builder" (hub) · 2 terminals online: builder, researcher
86
+ ⚡ Link: builder (hub) · 2 online
87
+ builder: idle (5s)
88
+ researcher: idle (12s)
87
89
  ```
88
90
 
89
91
  **Terminal 2** — rename it too:
@@ -122,11 +124,11 @@ Every other terminal sees:
122
124
 
123
125
  Link is **off by default**. Without `--link`, the extension is completely silent — no status bar, no connections, no warnings.
124
126
 
125
- | Method | When | Auto-reconnect? |
126
- | ------------------- | ----------------------------------- | --- |
127
- | `pi --link` | Auto-connect on startup | Yes |
128
- | `/link-connect` | Opt-in mid-session (no flag needed) | Yes |
129
- | `/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` |
130
132
 
131
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.
132
134
 
@@ -140,11 +142,11 @@ The extension registers three tools that the LLM can invoke during agent runs.
140
142
 
141
143
  ### Which tool should I use?
142
144
 
143
- | Tool | Behavior | Returns |
144
- | -------------- | ---------------------------------------------------- | ---------------------------------------- |
145
- | `link_send` | Send a message; optionally trigger the remote LLM | Send/delivery status only |
146
- | `link_prompt` | Run a prompt on a remote terminal and wait for reply | The remote terminal's assistant response |
147
- | `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 |
148
150
 
149
151
  **If you need the other terminal's answer back, use `link_prompt`.** Use `link_send` to notify or steer without waiting.
150
152
 
@@ -182,34 +184,47 @@ Send a prompt to a remote terminal and **wait** for the LLM's response (synchron
182
184
 
183
185
  ### `link_list`
184
186
 
185
- 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.
186
198
 
187
199
  **Example output:**
188
200
 
189
201
  ```
190
- Connected terminals:
191
- • pi-1 (you)
192
- • pi-2
193
- • 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)
194
206
  ```
195
207
 
196
208
  ---
197
209
 
198
210
  ## Slash Commands
199
211
 
200
- | Command | Purpose |
201
- | ------------------------ | --------------------------------------------------------------------------------------------------------- |
202
- | `/link` | Show link status (name, role, online count) |
203
- | `/link-name [name]` | Rename this terminal. With no argument, adopts the current Pi session name if available. Collision-safe. |
204
- | `/link-broadcast <msg>` | Broadcast a chat message to all other terminals |
205
- | `/link-connect` | Connect to Pi Link (works anytime, with or without `--link`) |
206
- | `/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`) |
207
219
 
208
220
  ### Examples
209
221
 
210
222
  ```
211
223
  > /link
212
- ⚡ 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)
213
228
 
214
229
  > /link-name orchestrator
215
230
  ✓ Renamed to "orchestrator"
@@ -333,7 +348,7 @@ When the hub goes down and a client promotes itself, terminal names and in-fligh
333
348
 
334
349
  | Package | Purpose |
335
350
  | ------------------------------- | ------------------------------------------------ |
336
- | `@mariozechner/pi-coding-agent` | Pi SDK types (ExtensionAPI, ExtensionContext) |
351
+ | `@mariozechner/pi-coding-agent` | Pi SDK types (ExtensionAPI, ExtensionContext) |
337
352
  | `@mariozechner/pi-tui` | TUI Text widget for custom message rendering |
338
353
  | `@sinclair/typebox` | JSON Schema type definitions for tool parameters |
339
354
 
@@ -365,18 +380,19 @@ The `pi.extensions` field tells Pi which files to load as extensions. Here it po
365
380
 
366
381
  ### Protocol
367
382
 
368
- 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:
369
384
 
370
- | Type | Direction | Purpose |
371
- | ----------------- | ------------- | ----------------------------------------------------- |
372
- | `register` | Client → Hub | First message after connecting; requests a name |
373
- | `welcome` | Hub → Client | Confirms assigned name (deduplicated) + terminal list |
374
- | `terminal_joined` | Hub → All | Broadcast when a terminal joins |
375
- | `terminal_left` | Hub → All | Broadcast when a terminal disconnects |
376
- | `chat` | Any → Any/All | Fire-and-forget message; optionally triggers LLM turn |
377
- | `prompt_request` | Any → Any | Request a remote terminal to execute a prompt |
378
- | `prompt_response` | Any → Any | Response carrying the remote prompt result |
379
- | `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 |
380
396
 
381
397
  ### Message Flow Examples
382
398
 
@@ -388,13 +404,13 @@ Client Hub
388
404
  | register {name:"builder"} |
389
405
  |---------------------------->|
390
406
  | |
391
- | welcome {name:"builder", |
392
- | terminals:["pi-1"]} |
407
+ | welcome {name, terminals, |
408
+ | statuses} |
393
409
  |<----------------------------|
394
410
  | |
395
411
  ```
396
412
 
397
- 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).
398
414
 
399
415
  **Sending a chat message:**
400
416
 
@@ -439,13 +455,13 @@ Default names are random 4-character hex IDs: `t-a1b2`, `t-c3d4`, etc.
439
455
 
440
456
  ### State Management
441
457
 
442
- | State Field | Type | Purpose |
443
- | ------------------------ | --------------------------------------- | ----------------------------------------------------- |
444
- | `role` | `"hub" \| "client" \| "disconnected"` | Current network role |
445
- | `isAgentBusy` | `boolean` | Prevents accepting remote prompts during agent runs |
446
- | `manuallyDisconnected` | `boolean` | Set by `/link-disconnect`; suppresses auto-reconnect |
447
- | `pendingRemotePrompt` | `object \| null` | Tracks the single in-flight remote prompt execution |
448
- | `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 |
449
465
 
450
466
  ### Message Routing & Error Handling
451
467
 
@@ -467,10 +483,14 @@ The `manuallyDisconnected` flag distinguishes user-initiated disconnects (`/link
467
483
 
468
484
  The extension hooks into Pi's agent lifecycle events:
469
485
 
470
- - **`agent_start`** → Sets `isAgentBusy = true`, blocking incoming remote prompts.
471
- - **`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.
472
490
  - **`session_shutdown`** → Full cleanup via `cleanup()`: closes all sockets, resolves pending promises, and disposes the extension.
473
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
+
474
494
  ### Rendering
475
495
 
476
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.1",
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",