pi-link 0.1.4 → 0.1.6
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 +25 -1
- package/README.md +40 -26
- package/index.ts +96 -82
- package/package.json +1 -1
- package/sync.ffs_db +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to pi-link are documented here.
|
|
4
4
|
|
|
5
|
-
This changelog is based on the git history from `2026-03-21`
|
|
5
|
+
This changelog is based on the git history from `2026-03-21` through `2026-04-03` (current). Versions correspond to npm publishes.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 0.1.6 — 2026-04-03
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Persistent connection intent.** `/link-connect` and `/link-disconnect` now save their state to the session via `pi.appendEntry("link-active", ...)`. On `session_start`, the saved preference is checked before falling back to `--link`. Connect once and it stays connected across session resumes without needing the flag.
|
|
14
|
+
|
|
15
|
+
### Removed
|
|
16
|
+
|
|
17
|
+
- **`cwd_update` message type.** Working directories are now only reported on connect (via `register`/`welcome`), not mid-session. Protocol returns to 9 message types.
|
|
18
|
+
|
|
19
|
+
- **`session_switch` handler.** Removed the session-switch logic that handled name and cwd changes on `/resume`. Simplifies the lifecycle.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 0.1.5 — 2026-04-02
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- **Working directory sharing.** Each terminal reports its `cwd` on connect and on session switch. New `cwd_update` protocol message (10th message type) broadcasts mid-session directory changes. `link_list` and `/link` now show per-terminal working directories — full absolute paths in tool output, `~/…` shortened in the TUI. Agents can use this to choose the right target, use explicit paths when terminals differ, and catch wrong-project mistakes early.
|
|
28
|
+
|
|
29
|
+
- **Header comment cleanup.** Simplified the top-of-file doc comment — removed feature bullet list and install instructions in favor of a concise summary.
|
|
6
30
|
|
|
7
31
|
---
|
|
8
32
|
|
package/README.md
CHANGED
|
@@ -85,7 +85,9 @@ Here's a concrete example of two terminals collaborating. Open two separate `pi
|
|
|
85
85
|
> /link
|
|
86
86
|
⚡ Link: builder (hub) · 2 online
|
|
87
87
|
builder: idle (5s)
|
|
88
|
+
cwd: ~/my-project
|
|
88
89
|
researcher: idle (12s)
|
|
90
|
+
cwd: ~/my-project
|
|
89
91
|
```
|
|
90
92
|
|
|
91
93
|
**Terminal 2** — rename it too:
|
|
@@ -142,11 +144,11 @@ The extension registers three tools that the LLM can invoke during agent runs.
|
|
|
142
144
|
|
|
143
145
|
### Which tool should I use?
|
|
144
146
|
|
|
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
|
|
147
|
+
| Tool | Behavior | Returns |
|
|
148
|
+
| ------------- | ---------------------------------------------------- | ----------------------------------------- |
|
|
149
|
+
| `link_send` | Send a message; optionally trigger the remote LLM | Send/delivery status only |
|
|
150
|
+
| `link_prompt` | Run a prompt on a remote terminal and wait for reply | The remote terminal's assistant response |
|
|
151
|
+
| `link_list` | List currently connected terminals | Terminal list with roles, status, and cwd |
|
|
150
152
|
|
|
151
153
|
**If you need the other terminal's answer back, use `link_prompt`.** Use `link_send` to notify or steer without waiting.
|
|
152
154
|
|
|
@@ -187,7 +189,9 @@ Send a prompt to a remote terminal and **wait** for the LLM's response (synchron
|
|
|
187
189
|
|
|
188
190
|
### `link_list`
|
|
189
191
|
|
|
190
|
-
Lists all connected terminals with role info, live agent status, and self-identification. Takes no parameters.
|
|
192
|
+
Lists all connected terminals with role info, live agent status, working directory, and self-identification. Takes no parameters.
|
|
193
|
+
|
|
194
|
+
Each terminal reports its current working directory on connect. `link_list` shows the full absolute path so agents can choose the right target, use explicit paths when terminals differ, and catch wrong-project mistakes early.
|
|
191
195
|
|
|
192
196
|
Each terminal's status is derived automatically from Pi lifecycle events — agents can't set it manually. Three states:
|
|
193
197
|
|
|
@@ -199,13 +203,18 @@ Each terminal's status is derived automatically from Pi lifecycle events — age
|
|
|
199
203
|
|
|
200
204
|
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.
|
|
201
205
|
|
|
206
|
+
Working directories use full absolute paths in tool output. In the TUI (`/link`), paths are shortened to `~/...` when possible to keep the display compact.
|
|
207
|
+
|
|
202
208
|
**Example output:**
|
|
203
209
|
|
|
204
210
|
```
|
|
205
|
-
|
|
206
|
-
•
|
|
207
|
-
|
|
208
|
-
•
|
|
211
|
+
Connected terminals:
|
|
212
|
+
• opus@pi-link (you) idle (12s)
|
|
213
|
+
cwd: C:\Users\andre\.pi
|
|
214
|
+
• gpt@pi-link thinking (3s)
|
|
215
|
+
cwd: C:\Users\andre\.pi
|
|
216
|
+
• docs@pi-link idle (1m)
|
|
217
|
+
cwd: C:\Users\andre\.pi
|
|
209
218
|
```
|
|
210
219
|
|
|
211
220
|
---
|
|
@@ -214,7 +223,7 @@ link (hub) 3 terminal(s)
|
|
|
214
223
|
|
|
215
224
|
| Command | Purpose |
|
|
216
225
|
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
|
217
|
-
| `/link` | Show link status (name, role, online count, agent status per terminal)
|
|
226
|
+
| `/link` | Show link status (name, role, online count, agent status, and cwd per terminal) |
|
|
218
227
|
| `/link-name [name]` | Rename and save as this session's preferred link name. With no argument, adopts the Pi session name. Restored on resume. |
|
|
219
228
|
| `/link-broadcast <msg>` | Broadcast a chat message to all other terminals |
|
|
220
229
|
| `/link-connect` | Connect to Pi Link (works anytime, with or without `--link`) |
|
|
@@ -226,8 +235,11 @@ link (hub) 3 terminal(s)
|
|
|
226
235
|
> /link
|
|
227
236
|
⚡ Link: builder (hub) · 3 online
|
|
228
237
|
builder: idle (12s)
|
|
238
|
+
cwd: ~/my-project
|
|
229
239
|
worker-1: thinking (3s)
|
|
240
|
+
cwd: ~/my-project
|
|
230
241
|
worker-2: tool:bash (5s)
|
|
242
|
+
cwd: ~/other-project
|
|
231
243
|
|
|
232
244
|
> /link-name orchestrator
|
|
233
245
|
✓ Renamed to "orchestrator"
|
|
@@ -385,19 +397,19 @@ The `pi.extensions` field tells Pi which files to load as extensions. Here it po
|
|
|
385
397
|
|
|
386
398
|
### Protocol
|
|
387
399
|
|
|
388
|
-
The wire protocol consists of **9 message types**, all serialized as JSON over WebSocket frames
|
|
400
|
+
The wire protocol consists of **9 message types**, all serialized as JSON over WebSocket frames. Cwd-related fields are optional for backward compatibility.
|
|
389
401
|
|
|
390
|
-
| Type | Direction | Purpose
|
|
391
|
-
| ----------------- | --------------- |
|
|
392
|
-
| `register` | Client → Hub | First message after connecting; requests a name
|
|
393
|
-
| `welcome` | Hub → Client | Confirms assigned name, terminal list + status
|
|
394
|
-
| `terminal_joined` | Hub → All | Broadcast when a terminal joins
|
|
395
|
-
| `terminal_left` | Hub → All | Broadcast when a terminal disconnects
|
|
396
|
-
| `chat` | Any → Any/All | Fire-and-forget message; optionally triggers LLM turn
|
|
397
|
-
| `prompt_request` | Any → Any | Request a remote terminal to execute a prompt
|
|
398
|
-
| `prompt_response` | Any → Any | Response carrying the remote prompt result
|
|
399
|
-
| `status_update` | Any → Hub → All | Terminal broadcasts its agent status change
|
|
400
|
-
| `error` | Hub → Client | Error notification
|
|
402
|
+
| Type | Direction | Purpose |
|
|
403
|
+
| ----------------- | --------------- | ----------------------------------------------------------------------- |
|
|
404
|
+
| `register` | Client → Hub | First message after connecting; requests a name, optionally reports cwd |
|
|
405
|
+
| `welcome` | Hub → Client | Confirms assigned name, terminal list + status/cwd snapshots |
|
|
406
|
+
| `terminal_joined` | Hub → All | Broadcast when a terminal joins; may include cwd |
|
|
407
|
+
| `terminal_left` | Hub → All | Broadcast when a terminal disconnects |
|
|
408
|
+
| `chat` | Any → Any/All | Fire-and-forget message; optionally triggers LLM turn |
|
|
409
|
+
| `prompt_request` | Any → Any | Request a remote terminal to execute a prompt |
|
|
410
|
+
| `prompt_response` | Any → Any | Response carrying the remote prompt result |
|
|
411
|
+
| `status_update` | Any → Hub → All | Terminal broadcasts its agent status change |
|
|
412
|
+
| `error` | Hub → Client | Error notification |
|
|
401
413
|
|
|
402
414
|
### Message Flow Examples
|
|
403
415
|
|
|
@@ -406,16 +418,17 @@ The wire protocol consists of **9 message types**, all serialized as JSON over W
|
|
|
406
418
|
```
|
|
407
419
|
Client Hub
|
|
408
420
|
| |
|
|
409
|
-
| register {name:"builder"
|
|
421
|
+
| register {name:"builder", |
|
|
422
|
+
| cwd:"C:\\Users\\..."} |
|
|
410
423
|
|---------------------------->|
|
|
411
424
|
| |
|
|
412
425
|
| welcome {name, terminals, |
|
|
413
|
-
| statuses}
|
|
426
|
+
| statuses, cwds} |
|
|
414
427
|
|<----------------------------|
|
|
415
428
|
| |
|
|
416
429
|
```
|
|
417
430
|
|
|
418
|
-
Hub then broadcasts `terminal_joined` to the other connected terminals. The `welcome` message includes
|
|
431
|
+
Hub then broadcasts `terminal_joined` to the other connected terminals. The `welcome` message includes status and cwd snapshots for all connected terminals (fields omitted above for brevity). `terminal_joined` also includes the new terminal's optional cwd.
|
|
419
432
|
|
|
420
433
|
**Sending a chat message:**
|
|
421
434
|
|
|
@@ -468,6 +481,7 @@ Default names are random 4-character hex IDs: `t-a1b2`, `t-c3d4`, etc.
|
|
|
468
481
|
| `agentRunning` | `boolean` | Whether an agent run is active; blocks incoming remote prompts |
|
|
469
482
|
| `activeToolName` | `string \| null` | Name of the currently executing tool (drives `tool:<name>` status) |
|
|
470
483
|
| `stateSince` | `number` | Timestamp of last status change (used for duration display) |
|
|
484
|
+
| `currentCwd` | `string` | Current working directory reported to peers on connect |
|
|
471
485
|
| `manuallyDisconnected` | `boolean` | Set by `/link-disconnect`; suppresses auto-reconnect |
|
|
472
486
|
| `pendingRemotePrompt` | `object \| null` | Tracks the single in-flight remote prompt execution |
|
|
473
487
|
| `pendingPromptResponses` | `Map` | Outstanding prompt RPCs awaiting responses (includes inactivity + ceiling timers per entry) |
|
package/index.ts
CHANGED
|
@@ -2,21 +2,12 @@
|
|
|
2
2
|
* Pi Link — WebSocket-based inter-terminal communication
|
|
3
3
|
*
|
|
4
4
|
* Connects multiple Pi terminals over a local WebSocket link.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Opt-in via --link flag or /link-connect command.
|
|
6
|
+
* First terminal to connect becomes the hub; others join as clients.
|
|
7
|
+
* Hub loss triggers automatic promotion of a surviving client.
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* - Named terminals with uniqueness enforcement
|
|
11
|
-
* - LLM tools: link_send (chat), link_prompt (remote prompt + response), link_list
|
|
12
|
-
* - Commands: /link, /link-name, /link-broadcast, /link-connect, /link-disconnect
|
|
13
|
-
* - Custom message renderer for incoming link messages
|
|
14
|
-
* - Auto-reconnect with hub promotion on disconnect
|
|
15
|
-
*
|
|
16
|
-
* Install:
|
|
17
|
-
* cd ~/.pi/agent/extensions/pi-link && npm install
|
|
18
|
-
*
|
|
19
|
-
* Then just start two or more `pi` terminals — they discover each other.
|
|
9
|
+
* Tools: link_send, link_prompt, link_list
|
|
10
|
+
* Commands: /link, /link-name, /link-broadcast, /link-connect, /link-disconnect
|
|
20
11
|
*/
|
|
21
12
|
|
|
22
13
|
import type {
|
|
@@ -26,6 +17,7 @@ import type {
|
|
|
26
17
|
import { Text } from "@mariozechner/pi-tui";
|
|
27
18
|
import { Type } from "@sinclair/typebox";
|
|
28
19
|
import * as crypto from "node:crypto";
|
|
20
|
+
import * as os from "node:os";
|
|
29
21
|
|
|
30
22
|
import { WebSocket, WebSocketServer } from "ws";
|
|
31
23
|
|
|
@@ -42,17 +34,20 @@ const KEEPALIVE_INTERVAL_MS = 30_000;
|
|
|
42
34
|
interface RegisterMsg {
|
|
43
35
|
type: "register";
|
|
44
36
|
name: string;
|
|
37
|
+
cwd?: string;
|
|
45
38
|
}
|
|
46
39
|
interface WelcomeMsg {
|
|
47
40
|
type: "welcome";
|
|
48
41
|
name: string;
|
|
49
42
|
terminals: string[];
|
|
50
43
|
statuses?: Record<string, LinkStatus>;
|
|
44
|
+
cwds?: Record<string, string>;
|
|
51
45
|
}
|
|
52
46
|
interface TerminalJoinedMsg {
|
|
53
47
|
type: "terminal_joined";
|
|
54
48
|
name: string;
|
|
55
49
|
terminals: string[];
|
|
50
|
+
cwd?: string;
|
|
56
51
|
}
|
|
57
52
|
interface TerminalLeftMsg {
|
|
58
53
|
type: "terminal_left";
|
|
@@ -134,11 +129,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
134
129
|
let lastPushedKind: string | null = null;
|
|
135
130
|
let lastPushedTool: string | null = null;
|
|
136
131
|
const terminalStatuses = new Map<string, LinkStatus>(); // other terminals
|
|
132
|
+
let currentCwd = "";
|
|
133
|
+
const terminalCwds = new Map<string, string>(); // other terminals' cwds
|
|
137
134
|
|
|
138
135
|
// Hub state
|
|
139
136
|
let wss: WebSocketServer | null = null;
|
|
140
137
|
const hubClients = new Map<WebSocket, string>(); // ws → terminal name
|
|
141
138
|
const hubTerminalStatuses = new Map<string, LinkStatus>(); // hub-authoritative
|
|
139
|
+
const hubTerminalCwds = new Map<string, string>(); // hub-authoritative (excludes self)
|
|
142
140
|
|
|
143
141
|
// Client state
|
|
144
142
|
let ws: WebSocket | null = null;
|
|
@@ -221,6 +219,35 @@ export default function (pi: ExtensionAPI) {
|
|
|
221
219
|
return map.get(name) ?? null;
|
|
222
220
|
}
|
|
223
221
|
|
|
222
|
+
function getCwdFor(name: string): string | null {
|
|
223
|
+
if (name === terminalName) return currentCwd || null;
|
|
224
|
+
if (role === "hub") return hubTerminalCwds.get(name) ?? null;
|
|
225
|
+
return terminalCwds.get(name) ?? null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function shortenPath(cwd: string): string {
|
|
229
|
+
const home = os.homedir().replace(/\\/g, "/");
|
|
230
|
+
const normalized = cwd.replace(/\\/g, "/");
|
|
231
|
+
if (normalized === home) return "~";
|
|
232
|
+
if (normalized.startsWith(home + "/"))
|
|
233
|
+
return "~" + normalized.slice(home.length);
|
|
234
|
+
return normalized;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ── Connection intent ──────────────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
function shouldConnect(_ctx: ExtensionContext): boolean {
|
|
240
|
+
const saved = _ctx.sessionManager
|
|
241
|
+
.getEntries()
|
|
242
|
+
.filter(
|
|
243
|
+
(e: { type: string; customType?: string }) =>
|
|
244
|
+
e.type === "custom" && e.customType === "link-active",
|
|
245
|
+
)
|
|
246
|
+
.pop() as { data?: { active?: boolean } } | undefined;
|
|
247
|
+
if (saved?.data?.active !== undefined) return saved.data.active;
|
|
248
|
+
return pi.getFlag("link") === true;
|
|
249
|
+
}
|
|
250
|
+
|
|
224
251
|
// ── Pending prompt helpers ───────────────────────────────────────────────
|
|
225
252
|
|
|
226
253
|
function cleanupPending(requestId: string) {
|
|
@@ -365,11 +392,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
365
392
|
terminalName = msg.name;
|
|
366
393
|
connectedTerminals = msg.terminals;
|
|
367
394
|
terminalStatuses.clear();
|
|
395
|
+
terminalCwds.clear();
|
|
368
396
|
if (msg.statuses) {
|
|
369
397
|
for (const [name, status] of Object.entries(msg.statuses)) {
|
|
370
398
|
terminalStatuses.set(name, status);
|
|
371
399
|
}
|
|
372
400
|
}
|
|
401
|
+
if (msg.cwds) {
|
|
402
|
+
for (const [name, cwd] of Object.entries(msg.cwds)) {
|
|
403
|
+
terminalCwds.set(name, cwd);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
373
406
|
updateStatus();
|
|
374
407
|
ctx?.ui.notify(
|
|
375
408
|
`Joined link as "${terminalName}" (${connectedTerminals.length} online)`,
|
|
@@ -378,9 +411,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
378
411
|
pushStatus(true);
|
|
379
412
|
break;
|
|
380
413
|
|
|
381
|
-
// ──
|
|
414
|
+
// ── Membership updates ──
|
|
382
415
|
case "terminal_joined":
|
|
383
416
|
connectedTerminals = msg.terminals;
|
|
417
|
+
if (role !== "hub" && msg.cwd) terminalCwds.set(msg.name, msg.cwd);
|
|
384
418
|
updateStatus();
|
|
385
419
|
ctx?.ui.notify(`"${msg.name}" joined the link`, "info");
|
|
386
420
|
break;
|
|
@@ -388,6 +422,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
388
422
|
case "terminal_left":
|
|
389
423
|
connectedTerminals = msg.terminals;
|
|
390
424
|
terminalStatuses.delete(msg.name);
|
|
425
|
+
if (role !== "hub") terminalCwds.delete(msg.name);
|
|
391
426
|
// Fail any pending prompts to the departed terminal immediately
|
|
392
427
|
for (const [id, pending] of pendingPromptResponses) {
|
|
393
428
|
if (pending.targetName === msg.name) {
|
|
@@ -488,30 +523,38 @@ export default function (pi: ExtensionAPI) {
|
|
|
488
523
|
if (msg.type === "register") {
|
|
489
524
|
clientName = uniqueName(msg.name);
|
|
490
525
|
hubClients.set(clientWs, clientName);
|
|
526
|
+
if (msg.cwd) hubTerminalCwds.set(clientName, msg.cwd);
|
|
491
527
|
const list = terminalList();
|
|
492
528
|
connectedTerminals = list;
|
|
493
529
|
updateStatus();
|
|
494
530
|
|
|
495
|
-
// Confirm to the new client (include status
|
|
531
|
+
// Confirm to the new client (include status + cwd snapshots)
|
|
496
532
|
const statuses: Record<string, LinkStatus> = {};
|
|
497
533
|
statuses[terminalName] = deriveStatus(); // hub's own status
|
|
498
534
|
for (const [name, status] of hubTerminalStatuses) {
|
|
499
535
|
if (name !== clientName) statuses[name] = status;
|
|
500
536
|
}
|
|
537
|
+
const cwds: Record<string, string> = {};
|
|
538
|
+
if (currentCwd) cwds[terminalName] = currentCwd; // hub's own cwd
|
|
539
|
+
for (const [name, cwd] of hubTerminalCwds) {
|
|
540
|
+
if (name !== clientName) cwds[name] = cwd;
|
|
541
|
+
}
|
|
501
542
|
clientWs.send(
|
|
502
543
|
JSON.stringify({
|
|
503
544
|
type: "welcome",
|
|
504
545
|
name: clientName,
|
|
505
546
|
terminals: list,
|
|
506
547
|
statuses,
|
|
548
|
+
cwds,
|
|
507
549
|
} satisfies WelcomeMsg),
|
|
508
550
|
);
|
|
509
551
|
|
|
510
|
-
// Notify everyone else
|
|
552
|
+
// Notify everyone else (include joiner's cwd)
|
|
511
553
|
const joined: TerminalJoinedMsg = {
|
|
512
554
|
type: "terminal_joined",
|
|
513
555
|
name: clientName,
|
|
514
556
|
terminals: list,
|
|
557
|
+
cwd: msg.cwd,
|
|
515
558
|
};
|
|
516
559
|
hubBroadcast(joined, clientName);
|
|
517
560
|
return;
|
|
@@ -550,6 +593,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
550
593
|
if (clientName) {
|
|
551
594
|
hubClients.delete(clientWs);
|
|
552
595
|
hubTerminalStatuses.delete(clientName);
|
|
596
|
+
hubTerminalCwds.delete(clientName);
|
|
553
597
|
const list = terminalList();
|
|
554
598
|
connectedTerminals = list;
|
|
555
599
|
updateStatus();
|
|
@@ -614,6 +658,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
614
658
|
JSON.stringify({
|
|
615
659
|
type: "register",
|
|
616
660
|
name: preferredName ?? terminalName,
|
|
661
|
+
cwd: currentCwd || undefined,
|
|
617
662
|
} satisfies RegisterMsg),
|
|
618
663
|
);
|
|
619
664
|
resolve(true);
|
|
@@ -717,6 +762,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
717
762
|
connectedTerminals = [];
|
|
718
763
|
terminalStatuses.clear();
|
|
719
764
|
hubTerminalStatuses.clear();
|
|
765
|
+
terminalCwds.clear();
|
|
766
|
+
hubTerminalCwds.clear();
|
|
720
767
|
lastPushedKind = null;
|
|
721
768
|
lastPushedTool = null;
|
|
722
769
|
updateStatus();
|
|
@@ -731,6 +778,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
731
778
|
|
|
732
779
|
pi.on("session_start", async (_event, _ctx) => {
|
|
733
780
|
ctx = _ctx;
|
|
781
|
+
currentCwd = _ctx.cwd;
|
|
734
782
|
|
|
735
783
|
// Restore preferred link name from session
|
|
736
784
|
const saved = _ctx.sessionManager
|
|
@@ -745,70 +793,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
745
793
|
terminalName = preferredName;
|
|
746
794
|
}
|
|
747
795
|
|
|
748
|
-
if (
|
|
796
|
+
if (shouldConnect(_ctx)) await initialize();
|
|
749
797
|
});
|
|
750
798
|
|
|
751
799
|
pi.on("session_shutdown", async () => {
|
|
752
800
|
cleanup();
|
|
753
801
|
});
|
|
754
802
|
|
|
755
|
-
pi.on("session_switch", async (_event, _ctx) => {
|
|
756
|
-
ctx = _ctx;
|
|
757
|
-
|
|
758
|
-
// Restore preferred name from the new session
|
|
759
|
-
const saved = _ctx.sessionManager
|
|
760
|
-
.getEntries()
|
|
761
|
-
.filter(
|
|
762
|
-
(e: { type: string; customType?: string }) =>
|
|
763
|
-
e.type === "custom" && e.customType === "link-name",
|
|
764
|
-
)
|
|
765
|
-
.pop() as { data?: { name?: string } } | undefined;
|
|
766
|
-
|
|
767
|
-
preferredName = saved?.data?.name ?? null;
|
|
768
|
-
const desiredName = preferredName ?? `t-${crypto.randomUUID().slice(0, 4)}`;
|
|
769
|
-
|
|
770
|
-
if (desiredName === terminalName) return; // no identity change needed
|
|
771
|
-
|
|
772
|
-
if (role === "hub") {
|
|
773
|
-
// Hub rename in-place — avoid tearing down the server
|
|
774
|
-
const takenByOther = Array.from(hubClients.values()).includes(
|
|
775
|
-
desiredName,
|
|
776
|
-
);
|
|
777
|
-
if (takenByOther) {
|
|
778
|
-
// Can't use preferred name — keep current identity
|
|
779
|
-
ctx?.ui.notify(
|
|
780
|
-
`Session preferred name "${desiredName}" is taken, keeping "${terminalName}"`,
|
|
781
|
-
"warning",
|
|
782
|
-
);
|
|
783
|
-
return;
|
|
784
|
-
}
|
|
785
|
-
const old = terminalName;
|
|
786
|
-
terminalName = desiredName;
|
|
787
|
-
const list = terminalList();
|
|
788
|
-
connectedTerminals = list;
|
|
789
|
-
updateStatus();
|
|
790
|
-
// Notify clients only — hub already updated local state
|
|
791
|
-
hubBroadcast(
|
|
792
|
-
{ type: "terminal_left", name: old, terminals: list },
|
|
793
|
-
terminalName,
|
|
794
|
-
);
|
|
795
|
-
hubBroadcast(
|
|
796
|
-
{ type: "terminal_joined", name: desiredName, terminals: list },
|
|
797
|
-
terminalName,
|
|
798
|
-
);
|
|
799
|
-
pushStatus(true);
|
|
800
|
-
} else if (role === "client") {
|
|
801
|
-
// Client — disconnect and reconnect with new name
|
|
802
|
-
terminalName = desiredName;
|
|
803
|
-
disconnect();
|
|
804
|
-
manuallyDisconnected = false;
|
|
805
|
-
await initialize();
|
|
806
|
-
} else {
|
|
807
|
-
// Disconnected — just update local name
|
|
808
|
-
terminalName = desiredName;
|
|
809
|
-
}
|
|
810
|
-
});
|
|
811
|
-
|
|
812
803
|
pi.on("agent_start", async () => {
|
|
813
804
|
agentRunning = true;
|
|
814
805
|
activeToolName = null;
|
|
@@ -1099,19 +1090,25 @@ export default function (pi: ExtensionAPI) {
|
|
|
1099
1090
|
if (role === "disconnected") return notConnectedResult();
|
|
1100
1091
|
|
|
1101
1092
|
const statuses: Record<string, string> = {};
|
|
1093
|
+
const cwds: Record<string, string> = {};
|
|
1102
1094
|
const list = connectedTerminals
|
|
1103
1095
|
.map((name) => {
|
|
1104
1096
|
const status = getStatusFor(name);
|
|
1105
1097
|
const statusStr = status ? formatStatus(status) : "";
|
|
1106
1098
|
if (statusStr) statuses[name] = statusStr;
|
|
1099
|
+
const cwd = getCwdFor(name);
|
|
1100
|
+
if (cwd) cwds[name] = cwd;
|
|
1107
1101
|
const marker = name === terminalName ? " (you)" : "";
|
|
1108
|
-
|
|
1102
|
+
let line = ` \u2022 ${name}${marker}${statusStr ? " " + statusStr : ""}`;
|
|
1103
|
+
if (cwd) line += `\n cwd: ${cwd}`;
|
|
1104
|
+
return line;
|
|
1109
1105
|
})
|
|
1110
1106
|
.join("\n");
|
|
1111
1107
|
|
|
1112
1108
|
return textResult(`Connected terminals:\n${list}`, {
|
|
1113
1109
|
terminals: connectedTerminals,
|
|
1114
1110
|
statuses,
|
|
1111
|
+
cwds,
|
|
1115
1112
|
self: terminalName,
|
|
1116
1113
|
role,
|
|
1117
1114
|
});
|
|
@@ -1122,6 +1119,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1122
1119
|
| {
|
|
1123
1120
|
terminals?: string[];
|
|
1124
1121
|
statuses?: Record<string, string>;
|
|
1122
|
+
cwds?: Record<string, string>;
|
|
1125
1123
|
self?: string;
|
|
1126
1124
|
role?: string;
|
|
1127
1125
|
}
|
|
@@ -1137,11 +1135,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
1137
1135
|
for (const name of details.terminals) {
|
|
1138
1136
|
const isSelf = name === details.self;
|
|
1139
1137
|
const status = details.statuses?.[name] ?? "";
|
|
1140
|
-
const
|
|
1138
|
+
const cwd = details.cwds?.[name];
|
|
1139
|
+
const nameStr = isSelf ? `\u2022 ${name} (you)` : `\u2022 ${name}`;
|
|
1141
1140
|
text +=
|
|
1142
1141
|
"\n " +
|
|
1143
1142
|
(isSelf ? theme.fg("accent", nameStr) : theme.fg("text", nameStr)) +
|
|
1144
1143
|
(status ? " " + theme.fg("dim", status) : "");
|
|
1144
|
+
if (cwd) text += "\n " + theme.fg("dim", `cwd: ${shortenPath(cwd)}`);
|
|
1145
1145
|
}
|
|
1146
1146
|
return new Text(text, 0, 0);
|
|
1147
1147
|
},
|
|
@@ -1159,8 +1159,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
1159
1159
|
const lines = connectedTerminals.map((name) => {
|
|
1160
1160
|
const status = getStatusFor(name);
|
|
1161
1161
|
const statusStr = status ? formatStatus(status) : "";
|
|
1162
|
+
const cwd = getCwdFor(name);
|
|
1162
1163
|
const marker = name === terminalName ? " (you)" : "";
|
|
1163
|
-
|
|
1164
|
+
let line = `${name}${marker}${statusStr ? ": " + statusStr : ""}`;
|
|
1165
|
+
if (cwd) line += `\n cwd: ${shortenPath(cwd)}`;
|
|
1166
|
+
return line;
|
|
1164
1167
|
});
|
|
1165
1168
|
_ctx.ui.notify(
|
|
1166
1169
|
`Link: ${terminalName} (${role}) · ${connectedTerminals.length} online\n${lines.join("\n")}`,
|
|
@@ -1225,7 +1228,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
1225
1228
|
terminalName,
|
|
1226
1229
|
);
|
|
1227
1230
|
hubBroadcast(
|
|
1228
|
-
{
|
|
1231
|
+
{
|
|
1232
|
+
type: "terminal_joined",
|
|
1233
|
+
name: newName,
|
|
1234
|
+
terminals: list,
|
|
1235
|
+
cwd: currentCwd,
|
|
1236
|
+
},
|
|
1229
1237
|
terminalName,
|
|
1230
1238
|
);
|
|
1231
1239
|
pushStatus(true);
|
|
@@ -1275,18 +1283,23 @@ export default function (pi: ExtensionAPI) {
|
|
|
1275
1283
|
pi.registerCommand("link-disconnect", {
|
|
1276
1284
|
description: "Disconnect from the link",
|
|
1277
1285
|
handler: async (_args, _ctx) => {
|
|
1286
|
+
pi.appendEntry("link-active", { active: false });
|
|
1287
|
+
manuallyDisconnected = true;
|
|
1278
1288
|
if (role === "disconnected") {
|
|
1279
|
-
|
|
1289
|
+
if (reconnectTimer) {
|
|
1290
|
+
clearTimeout(reconnectTimer);
|
|
1291
|
+
reconnectTimer = null;
|
|
1292
|
+
}
|
|
1293
|
+
_ctx.ui.notify("Link disconnected", "info");
|
|
1280
1294
|
return;
|
|
1281
1295
|
}
|
|
1282
|
-
manuallyDisconnected = true;
|
|
1283
1296
|
disconnect();
|
|
1284
1297
|
_ctx.ui.notify("Disconnected from link", "info");
|
|
1285
1298
|
},
|
|
1286
1299
|
});
|
|
1287
1300
|
|
|
1288
1301
|
pi.registerCommand("link-connect", {
|
|
1289
|
-
description: "Connect to the link
|
|
1302
|
+
description: "Connect to the link",
|
|
1290
1303
|
handler: async (_args, _ctx) => {
|
|
1291
1304
|
if (role !== "disconnected") {
|
|
1292
1305
|
_ctx.ui.notify(
|
|
@@ -1295,6 +1308,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1295
1308
|
);
|
|
1296
1309
|
return;
|
|
1297
1310
|
}
|
|
1311
|
+
pi.appendEntry("link-active", { active: true });
|
|
1298
1312
|
manuallyDisconnected = false;
|
|
1299
1313
|
await initialize();
|
|
1300
1314
|
},
|
package/package.json
CHANGED
package/sync.ffs_db
ADDED
|
Binary file
|