pi-link 0.1.7 → 0.1.9
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 +29 -1
- package/README.md +110 -51
- package/bin/pi-link.mjs +154 -0
- package/index.ts +119 -22
- package/package.json +5 -2
- package/sync.ffs_db +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,35 @@
|
|
|
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` through
|
|
5
|
+
This changelog is based on the git history from `2026-03-21` (initial commit) through the present. Versions correspond to npm publishes.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 0.1.9 — 2026-04-22
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **`--link-name <name>` flag.** Connect to link with a chosen terminal name on startup. Implies `--link` (no need for both). Persists the name and sets the Pi session name if currently unnamed. Name precedence: `--link-name` > saved `/link-name` > session name > random `t-xxxx`.
|
|
14
|
+
|
|
15
|
+
- **`pi-link start` CLI.** New bin script (`bin/pi-link.mjs`) for session-by-name resume. `pi-link start worker-1` scans `~/.pi/agent/sessions/` for a matching session name — one match resumes it, no match creates a new session, multiple matches prints candidates and exits. Extra Pi flags pass through: `pi-link start worker-1 --model sonnet --thinking high`. Local-cwd sessions prioritized.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 0.1.8 — 2026-04-16
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- **Idle-gated batched delivery for `triggerTurn:true`.** `link_send` with `triggerTurn:true` no longer calls `pi.sendMessage()` immediately. Messages queue in a local inbox, coalesce over a 200ms debounce window, and flush only when the receiver is idle (`ctx.isIdle()`). Delivered as a single `[Link: N message(s) received]` block at the start of a fresh turn. Avoids a Pi platform race where mid-run steering messages can be stranded. `triggerTurn:false` is unchanged (immediate fire-and-forget). (`82977ec`, `ca2996b`)
|
|
24
|
+
|
|
25
|
+
- **Session name as default terminal identity.** When no explicit `/link-name` is saved for a session, the terminal now adopts the Pi session name instead of a random `t-xxxx` ID. The session name is used at runtime only — it is not saved as `preferredName`, so only explicit `/link-name` calls persist across sessions.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- **Removed per-item truncation, raised batch cap.** Deleted the `ITEM_MAX_CHARS` (2 000) constant — it was silently cutting real agent work mid-word. `BATCH_MAX_CHARS` raised from 8 000 → 16 000 (~4K tokens). The batch cap is a soft limit: the first item is always included even if oversized, so one large message fills the batch alone and defers others to the next flush.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- **`flushInbox()` used `pi.isIdle()` instead of `ctx.isIdle()`.** `isIdle()` lives on `ExtensionContext`, not `ExtensionAPI`. Fixed to use the stored `ctx`.
|
|
6
34
|
|
|
7
35
|
---
|
|
8
36
|
|
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# pi-link
|
|
2
2
|
|
|
3
|
-
A WebSocket-based inter-terminal communication system that creates a local network between multiple Pi coding agent terminals. Enables terminals to discover each other, exchange messages, and orchestrate work across agents
|
|
3
|
+
A WebSocket-based inter-terminal communication system that creates a local network between multiple Pi coding agent terminals. Enables terminals to discover each other, exchange messages, and orchestrate work across agents - all automatically on `localhost`.
|
|
4
4
|
|
|
5
|
-
> Self-contained TypeScript in a single `index.ts` file. Start Pi with `--link` to enable.
|
|
5
|
+
> Self-contained TypeScript in a single `index.ts` file. Start Pi with `--link` or `--link-name <name>` to enable.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -27,10 +27,10 @@ A WebSocket-based inter-terminal communication system that creates a local netwo
|
|
|
27
27
|
|
|
28
28
|
A single Pi terminal is powerful. Multiple terminals working together unlock new patterns:
|
|
29
29
|
|
|
30
|
-
- **Research + Build**
|
|
31
|
-
- **Fan-out**
|
|
32
|
-
- **Orchestrator / Worker**
|
|
33
|
-
- **Review pipeline**
|
|
30
|
+
- **Research + Build** - one terminal investigates APIs, docs, or logs while another writes code based on the findings.
|
|
31
|
+
- **Fan-out** - split a large task across agents (e.g., "terminal A handles the backend, terminal B handles the frontend") and collect results.
|
|
32
|
+
- **Orchestrator / Worker** - designate one terminal as a coordinator that delegates subtasks to others via `link_prompt` and assembles the final output.
|
|
33
|
+
- **Review pipeline** - one terminal writes code, another reviews it, back and forth until both are satisfied.
|
|
34
34
|
|
|
35
35
|
---
|
|
36
36
|
|
|
@@ -57,16 +57,23 @@ pi uninstall npm:pi-link
|
|
|
57
57
|
|
|
58
58
|
### Usage
|
|
59
59
|
|
|
60
|
-
Link is **off by default**. Start Pi with
|
|
60
|
+
Link is **off by default**. Start Pi with `--link-name` to connect with a meaningful name:
|
|
61
61
|
|
|
62
62
|
```
|
|
63
63
|
Terminal 1 Terminal 2
|
|
64
64
|
---------- ----------
|
|
65
|
-
$ pi --link
|
|
66
|
-
✓ Link hub on :9900 as "
|
|
65
|
+
$ pi --link-name builder $ pi --link-name reviewer
|
|
66
|
+
✓ Link hub started on :9900 as "builder" ✓ Joined link as "reviewer" (2 online)
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
Or use `pi-link start` to resume an existing session by name (or create one):
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pi-link start worker-1 # resume or create session "worker-1"
|
|
73
|
+
pi-link start worker-1 --model sonnet # with extra Pi flags
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`pi --link` also works (connects with an auto-generated name). Already in a session without either flag? Connect mid-session with `/link-connect`.
|
|
70
77
|
|
|
71
78
|
Use `/link` in any terminal to check status, or let the LLM tools handle cross-terminal coordination.
|
|
72
79
|
|
|
@@ -76,7 +83,7 @@ Use `/link` in any terminal to check status, or let the LLM tools handle cross-t
|
|
|
76
83
|
|
|
77
84
|
Here's a concrete example of two terminals collaborating. Open two separate `pi --link` sessions.
|
|
78
85
|
|
|
79
|
-
**Terminal 1**
|
|
86
|
+
**Terminal 1** - rename and check status:
|
|
80
87
|
|
|
81
88
|
```
|
|
82
89
|
> /link-name builder
|
|
@@ -90,7 +97,7 @@ Here's a concrete example of two terminals collaborating. Open two separate `pi
|
|
|
90
97
|
cwd: ~/my-project
|
|
91
98
|
```
|
|
92
99
|
|
|
93
|
-
**Terminal 2**
|
|
100
|
+
**Terminal 2** - rename it too:
|
|
94
101
|
|
|
95
102
|
```
|
|
96
103
|
> /link-name researcher
|
|
@@ -124,23 +131,46 @@ Every other terminal sees:
|
|
|
124
131
|
|
|
125
132
|
## Configuration
|
|
126
133
|
|
|
127
|
-
Link is **off by default**. Without `--link`, the extension is completely silent — no status bar, no connections, no warnings.
|
|
134
|
+
Link is **off by default**. Without `--link` or `--link-name`, the extension is completely silent — no status bar, no connections, no warnings.
|
|
135
|
+
|
|
136
|
+
| Method | When | Auto-reconnect? |
|
|
137
|
+
| ----------------------- | ----------------------------------- | -------------------------------- |
|
|
138
|
+
| `pi --link-name <name>` | Connect on startup with a name | Yes |
|
|
139
|
+
| `pi --link` | Connect on startup (random name) | Yes |
|
|
140
|
+
| `pi-link start <name>` | Resume/create session, connect | Yes |
|
|
141
|
+
| `/link-connect` | Opt-in mid-session (no flag needed) | Yes |
|
|
142
|
+
| `/link-disconnect` | Opt-out mid-session | Suppressed until `/link-connect` |
|
|
143
|
+
|
|
144
|
+
`--link-name` implies `--link` — no need for both. It also persists the name and sets the Pi session name if the session is currently unnamed.
|
|
128
145
|
|
|
129
|
-
|
|
130
|
-
| ------------------ | ----------------------------------- | -------------------------------- |
|
|
131
|
-
| `pi --link` | Auto-connect on startup | Yes |
|
|
132
|
-
| `/link-connect` | Opt-in mid-session (no flag needed) | Yes |
|
|
133
|
-
| `/link-disconnect` | Opt-out mid-session | Suppressed until `/link-connect` |
|
|
146
|
+
**Name precedence:** `--link-name` flag > saved `/link-name` > Pi session name > random `t-xxxx`.
|
|
134
147
|
|
|
135
|
-
`/link-connect`
|
|
148
|
+
`/link-connect` and `/link-disconnect` save their intent to the session — resume later and the connection state is restored without needing the flag. Explicit user intent takes precedence over `--link`.
|
|
136
149
|
|
|
137
150
|
Once connected, terminals discover each other on `127.0.0.1:9900`. See [Limitations](#limitations--design-decisions) for the hardcoded port.
|
|
138
151
|
|
|
152
|
+
### `pi-link start`
|
|
153
|
+
|
|
154
|
+
The `pi-link` CLI resolves sessions by display name:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
pi-link start <name> [pi-flags...]
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
- Scans `~/.pi/agent/sessions/` for sessions with a matching name
|
|
161
|
+
- **One match** → resumes that session with `--link-name <name>`
|
|
162
|
+
- **No match** → starts a new session with `--link-name <name>`
|
|
163
|
+
- **Multiple matches** → prints candidates (cwd, modified date, path) and exits
|
|
164
|
+
- Extra Pi flags pass through: `pi-link start worker-1 --model sonnet --thinking high`
|
|
165
|
+
- `--session` and `--link-name` cannot be passed as extra flags (managed by `pi-link start`)
|
|
166
|
+
|
|
167
|
+
Sessions in the current working directory are prioritized when sorting candidates.
|
|
168
|
+
|
|
139
169
|
---
|
|
140
170
|
|
|
141
171
|
## LLM Tools
|
|
142
172
|
|
|
143
|
-
The extension registers three tools that the LLM can invoke during agent runs.
|
|
173
|
+
The extension registers three tools that the LLM can invoke during agent runs. pi-link also ships with a bundled **pi-link-coordination** skill that gives agents on-demand guidance for tool selection, delegation patterns, and avoiding common coordination mistakes.
|
|
144
174
|
|
|
145
175
|
### Which tool should I use?
|
|
146
176
|
|
|
@@ -162,9 +192,11 @@ Send a fire-and-forget chat message to a specific terminal or broadcast to all.
|
|
|
162
192
|
| `message` | `string` | Message content |
|
|
163
193
|
| `triggerTurn` | `boolean` | If `true`, the receiver's LLM responds automatically |
|
|
164
194
|
|
|
165
|
-
When `triggerTurn` is enabled, the message is
|
|
195
|
+
When `triggerTurn` is enabled, the message is queued in the receiver's local inbox. Nearby arrivals are coalesced (200ms debounce), and delivery is gated on the receiving agent being idle - ensuring it starts a clean new turn. Messages arrive as a single `[Link: N message(s) received]` block at the top of a fresh turn, not mid-run. When `triggerTurn` is `false` or omitted, delivery is immediate fire-and-forget.
|
|
196
|
+
|
|
197
|
+
Note: `triggerTurn` does **not** cause the response to come back to the caller - use `link_prompt` for that.
|
|
166
198
|
|
|
167
|
-
> **Broadcast note:** Sending to `"*"` delivers to **all other terminals**
|
|
199
|
+
> **Broadcast note:** Sending to `"*"` delivers to **all other terminals** - the sender is excluded.
|
|
168
200
|
|
|
169
201
|
Pre-validates the target name against the local terminal list before sending, catching typos early. See [Message Routing](#message-routing--error-handling) for delivery semantics.
|
|
170
202
|
|
|
@@ -177,12 +209,12 @@ Send a prompt to a remote terminal and **wait** for the LLM's response (synchron
|
|
|
177
209
|
| `to` | `string` | Target terminal name |
|
|
178
210
|
| `prompt` | `string` | Prompt text to send |
|
|
179
211
|
|
|
180
|
-
- The remote terminal processes the prompt via `pi.sendUserMessage()`
|
|
212
|
+
- The remote terminal processes the prompt via `pi.sendUserMessage()` - as if a user typed it.
|
|
181
213
|
- Returns the remote terminal's actual assistant reply text as the tool result.
|
|
182
|
-
- **Self-target rejection**
|
|
183
|
-
- **Heartbeat-based timeout**
|
|
184
|
-
- **Immediate failure on disconnect**
|
|
185
|
-
- **Early failure detection**
|
|
214
|
+
- **Self-target rejection** - prompting yourself (`to` equals your own name) returns an immediate error.
|
|
215
|
+
- **Heartbeat-based timeout** - no short fixed deadline. The target sends keepalives every 30s while working. The sender resets a 90-second inactivity timer on each keepalive. A 30-minute hard ceiling acts as a safety net against broken-but-chatty targets. A 10-minute task with regular activity never times out; a genuinely dead target times out in 90 seconds of silence.
|
|
216
|
+
- **Immediate failure on disconnect** - if the target leaves the network (`terminal_left`), pending prompts to that target fail immediately instead of waiting for the inactivity timeout.
|
|
217
|
+
- **Early failure detection** - if the message can't be delivered (e.g., target not found), the tool resolves immediately with an error instead of waiting for the timeout.
|
|
186
218
|
- Supports abort signals.
|
|
187
219
|
- Targets **one terminal at a time** (no broadcast mode).
|
|
188
220
|
- Only **one remote prompt** can execute at a time per target terminal. Concurrent requests are rejected with `"Terminal is busy"`.
|
|
@@ -193,7 +225,7 @@ Lists all connected terminals with role info, live agent status, working directo
|
|
|
193
225
|
|
|
194
226
|
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.
|
|
195
227
|
|
|
196
|
-
Each terminal's status is derived automatically from Pi lifecycle events
|
|
228
|
+
Each terminal's status is derived automatically from Pi lifecycle events - agents can't set it manually. Three states:
|
|
197
229
|
|
|
198
230
|
| Status | Meaning |
|
|
199
231
|
| ----------------- | ----------------------- |
|
|
@@ -201,7 +233,7 @@ Each terminal's status is derived automatically from Pi lifecycle events — age
|
|
|
201
233
|
| `thinking (3s)` | LLM is generating |
|
|
202
234
|
| `tool:bash (12s)` | Running a specific tool |
|
|
203
235
|
|
|
204
|
-
Durations are computed at render time from a `since` timestamp
|
|
236
|
+
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.
|
|
205
237
|
|
|
206
238
|
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
239
|
|
|
@@ -245,19 +277,20 @@ Connected terminals:
|
|
|
245
277
|
✓ Renamed to "orchestrator"
|
|
246
278
|
|
|
247
279
|
> /link-name
|
|
248
|
-
✓ Renamed to "my-session"
|
|
280
|
+
✓ Renamed to "my-session"
|
|
249
281
|
|
|
250
282
|
> /link-broadcast starting the build pipeline
|
|
251
283
|
✓ Broadcast sent
|
|
252
284
|
|
|
253
285
|
> /link-disconnect
|
|
254
|
-
✓ Disconnected from
|
|
286
|
+
✓ Disconnected from link
|
|
255
287
|
|
|
256
288
|
> /link-connect
|
|
257
|
-
✓ Joined
|
|
258
|
-
✓ Pi Link hub started on :9900 as "orchestrator" ... if no hub exists
|
|
289
|
+
✓ Joined link as "orchestrator" (3 online)
|
|
259
290
|
```
|
|
260
291
|
|
|
292
|
+
With no argument, `/link-name` adopts the Pi session name. `/link-connect` joins an existing hub if one is running; otherwise it starts the hub.
|
|
293
|
+
|
|
261
294
|
**Name persistence:** `/link-name` saves your preferred name to the session. Resume later and it's restored automatically. If the name is taken, the hub assigns a variant (e.g., `"builder-2"`), but your preferred name stays saved for the next reconnect. See [Name Uniqueness & Persistence](#name-uniqueness--persistence) for details.
|
|
262
295
|
|
|
263
296
|
See [Configuration](#configuration) for details on `--link`, `/link-connect`, and `/link-disconnect` behavior.
|
|
@@ -284,25 +317,25 @@ The network topology is **hub-spoke (star)**:
|
|
|
284
317
|
+-------+ +-------+ +-------+
|
|
285
318
|
```
|
|
286
319
|
|
|
287
|
-
- The **first terminal** to start becomes the **hub**
|
|
320
|
+
- The **first terminal** to start becomes the **hub** - it runs a `WebSocketServer` on `127.0.0.1:9900`.
|
|
288
321
|
- **Subsequent terminals** connect as **clients** via plain WebSocket.
|
|
289
322
|
- All messages route **through the hub**; clients never talk directly to each other.
|
|
290
323
|
|
|
291
324
|
### Auto-Discovery Protocol
|
|
292
325
|
|
|
293
|
-
The discovery sequence runs on startup (with `--link`) or when `/link-connect` is used. See [Configuration](#configuration) for details.
|
|
326
|
+
The discovery sequence runs on startup (with `--link` or `--link-name`) or when `/link-connect` is used. See [Configuration](#configuration) for details.
|
|
294
327
|
|
|
295
328
|
The sequence is a simple fallback:
|
|
296
329
|
|
|
297
330
|
1. Attempt to connect as a **client** to `127.0.0.1:9900`.
|
|
298
331
|
2. If connection fails → become the **hub** (start a WebSocket server on that port).
|
|
299
|
-
3. If both fail (rare race condition) → retry after a randomized 2
|
|
332
|
+
3. If both fail (rare race condition) → retry after a randomized 2-5 second backoff.
|
|
300
333
|
|
|
301
334
|
### Hub Promotion
|
|
302
335
|
|
|
303
336
|
When the hub disconnects, clients detect the WebSocket close event, enter `"disconnected"` state, and call `scheduleReconnect()`. The **first terminal to retry** becomes the new hub via the same initialize-or-fallback flow.
|
|
304
337
|
|
|
305
|
-
There is **no explicit leader election**
|
|
338
|
+
There is **no explicit leader election** - promotion is race-based.
|
|
306
339
|
|
|
307
340
|
---
|
|
308
341
|
|
|
@@ -310,7 +343,7 @@ There is **no explicit leader election** — promotion is race-based.
|
|
|
310
343
|
|
|
311
344
|
### Port 9900 is already in use
|
|
312
345
|
|
|
313
|
-
If another process occupies port 9900, the terminal can't become the hub. It will attempt to connect as a client instead (which also fails if there's no real hub), then retry after 2
|
|
346
|
+
If another process occupies port 9900, the terminal can't become the hub. It will attempt to connect as a client instead (which also fails if there's no real hub), then retry after 2-5 seconds. Free the port or modify `DEFAULT_PORT` in `index.ts` - see [Limitations](#limitations--design-decisions).
|
|
314
347
|
|
|
315
348
|
### "Terminal is busy" rejections
|
|
316
349
|
|
|
@@ -328,7 +361,7 @@ Each terminal can only execute **one remote prompt at a time**. If a `link_promp
|
|
|
328
361
|
|
|
329
362
|
### Hub promotion loses state
|
|
330
363
|
|
|
331
|
-
When the hub goes down and a client promotes itself, terminal names and in-flight prompts from the old hub session are lost. All surviving clients reconnect and re-register. This is by design
|
|
364
|
+
When the hub goes down and a client promotes itself, terminal names and in-flight prompts from the old hub session are lost. All surviving clients reconnect and re-register. This is by design - see [Limitations](#limitations--design-decisions).
|
|
332
365
|
|
|
333
366
|
---
|
|
334
367
|
|
|
@@ -339,7 +372,7 @@ When the hub goes down and a client promotes itself, terminal names and in-fligh
|
|
|
339
372
|
| 1 | **No authentication** | Any localhost process can connect to port 9900. Acceptable for local dev; don't expose the port externally. |
|
|
340
373
|
| 2 | **Hardcoded port (9900)** | Not configurable without editing `DEFAULT_PORT` in `index.ts`. Could conflict with other services on the same port. |
|
|
341
374
|
| 3 | **Race-based hub promotion** | Non-deterministic. Terminal state (names, in-flight prompts) is lost during promotion. Simple but imperfect. |
|
|
342
|
-
| 4 | **Single remote prompt per terminal** | No queuing
|
|
375
|
+
| 4 | **Single remote prompt per terminal** | No queuing - immediate rejection if busy. See [`link_prompt`](#link_prompt) and [Troubleshooting](#terminal-is-busy-rejections). |
|
|
343
376
|
| 5 | **No message persistence** | Purely ephemeral WebSocket frames. Messages are lost if the recipient is offline. |
|
|
344
377
|
| 6 | **Client rename triggers full reconnect** | Changing a client's name requires a new `register` message, so the client disconnects and reconnects. Hub renames are handled in-place with collision checks. |
|
|
345
378
|
| 7 | **Single-machine / localhost-only** | Link only binds to `127.0.0.1`; terminals on different machines cannot join. |
|
|
@@ -374,7 +407,9 @@ When the hub goes down and a client promotes itself, terminal names and in-fligh
|
|
|
374
407
|
```json
|
|
375
408
|
{
|
|
376
409
|
"name": "pi-link",
|
|
377
|
-
"
|
|
410
|
+
"bin": {
|
|
411
|
+
"pi-link": "./bin/pi-link.mjs"
|
|
412
|
+
},
|
|
378
413
|
"dependencies": {
|
|
379
414
|
"ws": "^8.20.0"
|
|
380
415
|
},
|
|
@@ -382,12 +417,13 @@ When the hub goes down and a client promotes itself, terminal names and in-fligh
|
|
|
382
417
|
"@types/ws": "^8.18.1"
|
|
383
418
|
},
|
|
384
419
|
"pi": {
|
|
385
|
-
"extensions": ["./index.ts"]
|
|
420
|
+
"extensions": ["./index.ts"],
|
|
421
|
+
"skills": ["./skills"]
|
|
386
422
|
}
|
|
387
423
|
}
|
|
388
424
|
```
|
|
389
425
|
|
|
390
|
-
|
|
426
|
+
`pi.extensions` tells Pi which files to load as extensions. `pi.skills` registers bundled skill directories. `bin` exposes the `pi-link` CLI (see [Configuration](#configuration)).
|
|
391
427
|
|
|
392
428
|
---
|
|
393
429
|
|
|
@@ -463,7 +499,7 @@ The hub enforces unique terminal names via a `uniqueName()` function. If `"build
|
|
|
463
499
|
|
|
464
500
|
Default names are random 4-character hex IDs: `t-a1b2`, `t-c3d4`, etc.
|
|
465
501
|
|
|
466
|
-
**Persistence:** `/link-name` saves the preferred name to the session via `pi.appendEntry("link-name", { name })`. On session resume, the saved name is restored and requested from the hub. Only explicit `/link-name` calls persist
|
|
502
|
+
**Persistence:** `/link-name` saves the preferred name to the session via `pi.appendEntry("link-name", { name })`. On session resume, the saved name is restored and requested from the hub. Only explicit `/link-name` calls persist - hub-assigned variants like `"builder-2"` are not saved. On reconnect, the terminal always requests the preferred name, not the last runtime name.
|
|
467
503
|
|
|
468
504
|
**Rename guards:**
|
|
469
505
|
|
|
@@ -482,6 +518,8 @@ Default names are random 4-character hex IDs: `t-a1b2`, `t-c3d4`, etc.
|
|
|
482
518
|
| `activeToolName` | `string \| null` | Name of the currently executing tool (drives `tool:<name>` status) |
|
|
483
519
|
| `stateSince` | `number` | Timestamp of last status change (used for duration display) |
|
|
484
520
|
| `currentCwd` | `string` | Current working directory reported to peers on connect |
|
|
521
|
+
| `inbox` | `array` | Queued `triggerTurn:true` messages awaiting idle-gated flush |
|
|
522
|
+
| `flushTimer` | `Timer \| null` | Pending inbox flush (debounce or busy-retry) |
|
|
485
523
|
| `manuallyDisconnected` | `boolean` | Set by `/link-disconnect`; suppresses auto-reconnect |
|
|
486
524
|
| `pendingRemotePrompt` | `object \| null` | Tracks the single in-flight remote prompt execution |
|
|
487
525
|
| `pendingPromptResponses` | `Map` | Outstanding prompt RPCs awaiting responses (includes inactivity + ceiling timers per entry) |
|
|
@@ -490,31 +528,52 @@ Default names are random 4-character hex IDs: `t-a1b2`, `t-c3d4`, etc.
|
|
|
490
528
|
|
|
491
529
|
`routeMessage()` returns a `boolean` indicating delivery status:
|
|
492
530
|
|
|
493
|
-
- **Hub**
|
|
494
|
-
- **Client**
|
|
531
|
+
- **Hub** - delivery is authoritative. If the target terminal isn't connected, the hub sends a protocol-level error back to the sender. For `prompt_request` messages to unknown targets, the hub sends a `prompt_response` with an error field so the sender's pending promise resolves immediately rather than timing out.
|
|
532
|
+
- **Client** - delivery is optimistic (`true` means "sent to hub"). The hub handles routing and errors via the protocol.
|
|
495
533
|
|
|
496
534
|
### Connection Lifecycle
|
|
497
535
|
|
|
498
536
|
Internally, teardown is split into two functions:
|
|
499
537
|
|
|
500
|
-
- **`disconnect()`**
|
|
501
|
-
- **`cleanup()`**
|
|
538
|
+
- **`disconnect()`** - closes sockets, clears connection state, resolves pending promises. Used by `/link-disconnect` and called internally by `cleanup()`.
|
|
539
|
+
- **`cleanup()`** - calls `disconnect()` then marks the extension as disposed. Used on `session_shutdown`.
|
|
502
540
|
|
|
503
|
-
The `manuallyDisconnected` flag distinguishes user-initiated disconnects (`/link-disconnect`) from connection loss. When set, `scheduleReconnect()` is suppressed
|
|
541
|
+
The `manuallyDisconnected` flag distinguishes user-initiated disconnects (`/link-disconnect`) from connection loss. When set, `scheduleReconnect()` is suppressed - the terminal stays offline until `/link-connect` is explicitly called.
|
|
504
542
|
|
|
505
543
|
### Agent Lifecycle Integration
|
|
506
544
|
|
|
507
545
|
The extension hooks into Pi's agent lifecycle events:
|
|
508
546
|
|
|
509
547
|
- **`agent_start`** → Sets `agentRunning = true`, blocking incoming remote prompts. Broadcasts `status_update` (`thinking`).
|
|
510
|
-
- **`agent_end`** → Checks if a remote prompt was running
|
|
548
|
+
- **`agent_end`** → Wakes up the inbox flush (idle-gated delivery for `triggerTurn:true` messages). 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`).
|
|
511
549
|
- **`tool_execution_start`** → Broadcasts `status_update` (`tool:<name>`).
|
|
512
550
|
- **`tool_execution_end`** → Clears tool status; broadcasts `status_update` (`thinking`) while the agent run continues.
|
|
513
551
|
- **`session_shutdown`** → Full cleanup via `cleanup()`: closes all sockets, resolves pending promises, and disposes the extension.
|
|
514
552
|
|
|
515
553
|
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.
|
|
516
554
|
|
|
517
|
-
While executing a remote prompt, the target sends a forced `status_update` every 30 seconds as a keepalive
|
|
555
|
+
While executing a remote prompt, the target sends a forced `status_update` every 30 seconds as a keepalive - reusing the existing status push mechanism. On the sender side, each incoming `status_update` from the target resets the 90-second inactivity timer. All resolution paths (response, inactivity, ceiling, abort, disconnect, delivery failure) go through a single `cleanupPending()` helper to prevent double-resolution races.
|
|
556
|
+
|
|
557
|
+
### Idle-Gated Inbox
|
|
558
|
+
|
|
559
|
+
When a `chat` message arrives with `triggerTurn:true`, it goes into a local inbox instead of calling `pi.sendMessage()` immediately. This avoids a Pi platform race where steering messages sent mid-agent-run can be stranded (see `REPORT-sendMessage-race.md`).
|
|
560
|
+
|
|
561
|
+
The flush pipeline:
|
|
562
|
+
|
|
563
|
+
1. **Debounce** - `scheduleFlush(FLUSH_DELAY_MS)` coalesces burst arrivals (200ms window).
|
|
564
|
+
2. **Idle gate** - `flushInbox()` checks `ctx.isIdle()`. If busy, retries every 500ms.
|
|
565
|
+
3. **Batch** - up to 20 messages or ~16 000 chars per delivery (soft cap - the first item is always included even if oversized).
|
|
566
|
+
4. **Deliver** - one `pi.sendMessage({ triggerTurn: true })` call with a `[Link: N message(s) received]` block.
|
|
567
|
+
5. **Drain** - if the inbox still has items, reschedule.
|
|
568
|
+
|
|
569
|
+
On `agent_end`, the inbox flush is kicked via `scheduleFlush(0)` - deferred to the next macrotask, by which time `ctx.isIdle()` returns `true`.
|
|
570
|
+
|
|
571
|
+
| Constant | Value | Purpose |
|
|
572
|
+
| ----------------- | ------ | ---------------------------------------- |
|
|
573
|
+
| `FLUSH_DELAY_MS` | 200 | Burst debounce window |
|
|
574
|
+
| `IDLE_RETRY_MS` | 500 | Busy-retry polling interval |
|
|
575
|
+
| `BATCH_MAX_ITEMS` | 20 | Max messages per batch |
|
|
576
|
+
| `BATCH_MAX_CHARS` | 16 000 | Soft cap on batch text size (~4K tokens) |
|
|
518
577
|
|
|
519
578
|
### Rendering
|
|
520
579
|
|
package/bin/pi-link.mjs
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// pi-link CLI — resolve session by name and launch Pi with --link-name
|
|
4
|
+
//
|
|
5
|
+
// Usage:
|
|
6
|
+
// pi-link start <name> [pi-flags...]
|
|
7
|
+
//
|
|
8
|
+
// If a session named <name> exists, resumes it.
|
|
9
|
+
// If not, creates a new session.
|
|
10
|
+
// Always connects to the link as <name>.
|
|
11
|
+
|
|
12
|
+
import { readdir, stat } from "fs/promises";
|
|
13
|
+
import { createReadStream } from "fs";
|
|
14
|
+
import { createInterface } from "readline";
|
|
15
|
+
import { join } from "path";
|
|
16
|
+
import { homedir } from "os";
|
|
17
|
+
import { spawn } from "child_process";
|
|
18
|
+
|
|
19
|
+
const SESSIONS_DIR = join(homedir(), ".pi", "agent", "sessions");
|
|
20
|
+
|
|
21
|
+
// ── Session scanning ───────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
async function getSessionName(filePath) {
|
|
24
|
+
let name;
|
|
25
|
+
let cwd;
|
|
26
|
+
const rl = createInterface({ input: createReadStream(filePath, "utf-8"), crlfDelay: Infinity });
|
|
27
|
+
for await (const line of rl) {
|
|
28
|
+
if (!line) continue;
|
|
29
|
+
try {
|
|
30
|
+
const entry = JSON.parse(line);
|
|
31
|
+
if (entry.type === "session" && entry.cwd) cwd = entry.cwd;
|
|
32
|
+
if (entry.type === "session_info" && entry.name !== undefined) {
|
|
33
|
+
name = entry.name?.trim() || undefined;
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
// skip malformed lines
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return { name, cwd };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function findSessionsByName(targetName) {
|
|
43
|
+
let cwdDirs;
|
|
44
|
+
try {
|
|
45
|
+
cwdDirs = await readdir(SESSIONS_DIR, { withFileTypes: true });
|
|
46
|
+
} catch {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const matches = [];
|
|
51
|
+
|
|
52
|
+
for (const dir of cwdDirs) {
|
|
53
|
+
if (!dir.isDirectory()) continue;
|
|
54
|
+
const dirPath = join(SESSIONS_DIR, dir.name);
|
|
55
|
+
|
|
56
|
+
let files;
|
|
57
|
+
try {
|
|
58
|
+
files = await readdir(dirPath);
|
|
59
|
+
} catch {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (const file of files) {
|
|
64
|
+
if (!file.endsWith(".jsonl")) continue;
|
|
65
|
+
const filePath = join(dirPath, file);
|
|
66
|
+
try {
|
|
67
|
+
const { name, cwd } = await getSessionName(filePath);
|
|
68
|
+
if (name === targetName) {
|
|
69
|
+
const stats = await stat(filePath);
|
|
70
|
+
matches.push({ path: filePath, cwd: cwd || "?", modified: stats.mtime });
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Local-first: current cwd matches before others, then by modified time
|
|
79
|
+
const localCwd = process.cwd();
|
|
80
|
+
matches.sort((a, b) => {
|
|
81
|
+
const aLocal = a.cwd === localCwd ? 1 : 0;
|
|
82
|
+
const bLocal = b.cwd === localCwd ? 1 : 0;
|
|
83
|
+
if (aLocal !== bLocal) return bLocal - aLocal;
|
|
84
|
+
return b.modified.getTime() - a.modified.getTime();
|
|
85
|
+
});
|
|
86
|
+
return matches;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── CLI ────────────────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
const args = process.argv.slice(2);
|
|
92
|
+
const command = args[0];
|
|
93
|
+
|
|
94
|
+
if (command !== "start" || args.length < 2) {
|
|
95
|
+
console.log(`Usage: pi-link start <name> [pi-flags...]
|
|
96
|
+
|
|
97
|
+
Start Pi connected to the link as <name>.
|
|
98
|
+
Resumes a session named <name> if one exists, otherwise creates a new session.
|
|
99
|
+
|
|
100
|
+
Examples:
|
|
101
|
+
pi-link start worker-1
|
|
102
|
+
pi-link start worker-1 --model sonnet
|
|
103
|
+
pi-link start worker-1 --model sonnet --thinking high`);
|
|
104
|
+
process.exit(command === "start" ? 1 : 0);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const name = args[1].trim().replace(/\s+/g, " ");
|
|
108
|
+
if (!name) {
|
|
109
|
+
console.error("Error: name cannot be empty.");
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const extraFlags = args.slice(2);
|
|
114
|
+
for (const flag of ["--session", "--link-name"]) {
|
|
115
|
+
if (extraFlags.includes(flag)) {
|
|
116
|
+
console.error(`Error: ${flag} is managed by pi-link start. Remove it.`);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
console.log(`Searching for session "${name}"...`);
|
|
122
|
+
const matches = await findSessionsByName(name);
|
|
123
|
+
|
|
124
|
+
const piArgs = [];
|
|
125
|
+
|
|
126
|
+
if (matches.length === 1) {
|
|
127
|
+
console.log(`Resuming session: ${matches[0].path}`);
|
|
128
|
+
piArgs.push("--session", matches[0].path);
|
|
129
|
+
} else if (matches.length > 1) {
|
|
130
|
+
console.error(`\nMultiple sessions named "${name}":\n`);
|
|
131
|
+
for (const m of matches) {
|
|
132
|
+
console.error(` ${m.modified.toISOString().slice(0, 19)} cwd: ${m.cwd}`);
|
|
133
|
+
console.error(` ${m.path}\n`);
|
|
134
|
+
}
|
|
135
|
+
console.error(`Use pi --session <path> --link-name ${name} to pick one.`);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
} else {
|
|
138
|
+
console.log("No existing session found. Starting new session.");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
piArgs.push("--link-name", name, ...extraFlags);
|
|
142
|
+
|
|
143
|
+
// On Windows, resolve 'pi' through the shell so .cmd/.ps1 shims work
|
|
144
|
+
const isWin = process.platform === "win32";
|
|
145
|
+
const cmd = isWin ? "cmd" : "pi";
|
|
146
|
+
const cmdArgs = isWin ? ["/c", "pi", ...piArgs] : piArgs;
|
|
147
|
+
|
|
148
|
+
const child = spawn(cmd, cmdArgs, { stdio: "inherit" });
|
|
149
|
+
|
|
150
|
+
child.on("exit", (code) => process.exit(code ?? 0));
|
|
151
|
+
child.on("error", (err) => {
|
|
152
|
+
console.error(`Failed to start pi: ${err.message}`);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
});
|
package/index.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Pi Link — WebSocket-based inter-terminal communication
|
|
3
3
|
*
|
|
4
4
|
* Connects multiple Pi terminals over a local WebSocket link.
|
|
5
|
-
* Opt-in via --link flag or /link-connect command.
|
|
5
|
+
* Opt-in via --link / --link-name flag or /link-connect command.
|
|
6
6
|
* First terminal to connect becomes the hub; others join as clients.
|
|
7
7
|
* Hub loss triggers automatic promotion of a surviving client.
|
|
8
8
|
*
|
|
@@ -28,6 +28,10 @@ const PROMPT_INACTIVITY_MS = 90_000;
|
|
|
28
28
|
const PROMPT_HARD_CEILING_MS = 1_800_000;
|
|
29
29
|
const RECONNECT_DELAY_MS = 2000;
|
|
30
30
|
const KEEPALIVE_INTERVAL_MS = 30_000;
|
|
31
|
+
const FLUSH_DELAY_MS = 200;
|
|
32
|
+
const IDLE_RETRY_MS = 500;
|
|
33
|
+
const BATCH_MAX_ITEMS = 20;
|
|
34
|
+
const BATCH_MAX_CHARS = 16_000;
|
|
31
35
|
|
|
32
36
|
// ─── Protocol ────────────────────────────────────────────────────────────────
|
|
33
37
|
|
|
@@ -111,6 +115,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
111
115
|
default: false,
|
|
112
116
|
});
|
|
113
117
|
|
|
118
|
+
pi.registerFlag("link-name", {
|
|
119
|
+
description: "Connect to link with this terminal name",
|
|
120
|
+
type: "string",
|
|
121
|
+
});
|
|
122
|
+
|
|
114
123
|
// ── State ────────────────────────────────────────────────────────────────
|
|
115
124
|
|
|
116
125
|
let role: "hub" | "client" | "disconnected" = "disconnected";
|
|
@@ -159,6 +168,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
159
168
|
let pendingRemotePrompt: { id: string; from: string } | null = null;
|
|
160
169
|
let keepaliveTimer: ReturnType<typeof setInterval> | null = null;
|
|
161
170
|
|
|
171
|
+
// Inbox: idle-gated batched delivery for triggerTurn:true messages
|
|
172
|
+
const inbox: { from: string; content: string }[] = [];
|
|
173
|
+
let flushTimer: ReturnType<typeof setTimeout> | null = null;
|
|
174
|
+
|
|
162
175
|
// ── Helpers ──────────────────────────────────────────────────────────────
|
|
163
176
|
|
|
164
177
|
function updateStatus() {
|
|
@@ -234,6 +247,54 @@ export default function (pi: ExtensionAPI) {
|
|
|
234
247
|
return normalized;
|
|
235
248
|
}
|
|
236
249
|
|
|
250
|
+
// ── Inbox: idle-gated batched delivery ───────────────────────────────────
|
|
251
|
+
|
|
252
|
+
function scheduleFlush(delay: number) {
|
|
253
|
+
if (flushTimer) clearTimeout(flushTimer);
|
|
254
|
+
flushTimer = setTimeout(flushInbox, delay);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function flushInbox() {
|
|
258
|
+
flushTimer = null;
|
|
259
|
+
if (inbox.length === 0) return;
|
|
260
|
+
if (!ctx) return;
|
|
261
|
+
|
|
262
|
+
// Only deliver when idle so triggerTurn takes the prompt-start path
|
|
263
|
+
// instead of mid-run steering, avoiding async delivery loss.
|
|
264
|
+
if (!ctx.isIdle()) {
|
|
265
|
+
scheduleFlush(IDLE_RETRY_MS);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Select batch: up to BATCH_MAX_ITEMS, ~BATCH_MAX_CHARS total (soft cap —
|
|
270
|
+
// first item always included even if oversized, others deferred to next flush)
|
|
271
|
+
const batch: string[] = [];
|
|
272
|
+
let totalChars = 0;
|
|
273
|
+
for (let i = 0; i < inbox.length && batch.length < BATCH_MAX_ITEMS; i++) {
|
|
274
|
+
const item = inbox[i];
|
|
275
|
+
const text = `From "${item.from}":\n${item.content}`;
|
|
276
|
+
if (batch.length > 0 && totalChars + text.length > BATCH_MAX_CHARS) break;
|
|
277
|
+
batch.push(text);
|
|
278
|
+
totalChars += text.length;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
pi.sendMessage(
|
|
282
|
+
{
|
|
283
|
+
customType: "link",
|
|
284
|
+
content: `[Link: ${batch.length} message(s) received]\n\n${batch.join("\n\n")}`,
|
|
285
|
+
display: true,
|
|
286
|
+
details: { batched: true, count: batch.length },
|
|
287
|
+
},
|
|
288
|
+
{ triggerTurn: true },
|
|
289
|
+
);
|
|
290
|
+
inbox.splice(0, batch.length);
|
|
291
|
+
|
|
292
|
+
// Reschedule if inbox still has items; agent_end wakeup will usually beat this
|
|
293
|
+
if (inbox.length > 0) {
|
|
294
|
+
scheduleFlush(IDLE_RETRY_MS);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
237
298
|
// ── Connection intent ──────────────────────────────────────────────────
|
|
238
299
|
|
|
239
300
|
function shouldConnect(_ctx: ExtensionContext): boolean {
|
|
@@ -449,15 +510,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
449
510
|
|
|
450
511
|
// ── Chat message ──
|
|
451
512
|
case "chat":
|
|
452
|
-
|
|
453
|
-
{
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
513
|
+
if (msg.triggerTurn) {
|
|
514
|
+
inbox.push({ from: msg.from, content: msg.content });
|
|
515
|
+
scheduleFlush(FLUSH_DELAY_MS);
|
|
516
|
+
} else {
|
|
517
|
+
pi.sendMessage(
|
|
518
|
+
{
|
|
519
|
+
customType: "link",
|
|
520
|
+
content: msg.content,
|
|
521
|
+
display: true,
|
|
522
|
+
details: { from: msg.from },
|
|
523
|
+
},
|
|
524
|
+
{ triggerTurn: false, deliverAs: "steer" },
|
|
525
|
+
);
|
|
526
|
+
}
|
|
461
527
|
break;
|
|
462
528
|
|
|
463
529
|
// ── Another terminal asks us to run a prompt ──
|
|
@@ -767,11 +833,23 @@ export default function (pi: ExtensionAPI) {
|
|
|
767
833
|
lastPushedKind = null;
|
|
768
834
|
lastPushedTool = null;
|
|
769
835
|
updateStatus();
|
|
836
|
+
|
|
837
|
+
// Inbox survives disconnect — messages are local state waiting for local delivery.
|
|
838
|
+
// Ensure pending flush still fires.
|
|
839
|
+
if (inbox.length > 0 && !flushTimer) {
|
|
840
|
+
scheduleFlush(FLUSH_DELAY_MS);
|
|
841
|
+
}
|
|
770
842
|
}
|
|
771
843
|
|
|
772
844
|
function cleanup() {
|
|
773
845
|
disposed = true;
|
|
774
846
|
disconnect();
|
|
847
|
+
// Full teardown: clear inbox and flush timer
|
|
848
|
+
inbox.length = 0;
|
|
849
|
+
if (flushTimer) {
|
|
850
|
+
clearTimeout(flushTimer);
|
|
851
|
+
flushTimer = null;
|
|
852
|
+
}
|
|
775
853
|
}
|
|
776
854
|
|
|
777
855
|
// ── Lifecycle events ─────────────────────────────────────────────────────
|
|
@@ -780,20 +858,35 @@ export default function (pi: ExtensionAPI) {
|
|
|
780
858
|
ctx = _ctx;
|
|
781
859
|
currentCwd = _ctx.cwd;
|
|
782
860
|
|
|
783
|
-
//
|
|
784
|
-
const
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
(
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
861
|
+
// Resolve terminal name: --link-name flag > saved link-name > session name > random
|
|
862
|
+
const rawLinkName = pi.getFlag("link-name");
|
|
863
|
+
const flagName =
|
|
864
|
+
typeof rawLinkName === "string"
|
|
865
|
+
? rawLinkName.trim().replace(/\s+/g, " ") || undefined
|
|
866
|
+
: undefined;
|
|
867
|
+
if (flagName) {
|
|
868
|
+
preferredName = flagName;
|
|
869
|
+
terminalName = flagName;
|
|
870
|
+
pi.appendEntry("link-name", { name: flagName });
|
|
871
|
+
if (!pi.getSessionName()) pi.setSessionName(flagName);
|
|
872
|
+
} else {
|
|
873
|
+
const saved = _ctx.sessionManager
|
|
874
|
+
.getEntries()
|
|
875
|
+
.filter(
|
|
876
|
+
(e: { type: string; customType?: string }) =>
|
|
877
|
+
e.type === "custom" && e.customType === "link-name",
|
|
878
|
+
)
|
|
879
|
+
.pop() as { data?: { name?: string } } | undefined;
|
|
880
|
+
if (saved?.data?.name) {
|
|
881
|
+
preferredName = saved.data.name;
|
|
882
|
+
terminalName = preferredName;
|
|
883
|
+
} else {
|
|
884
|
+
const sessionName = pi.getSessionName()?.trim().replace(/\s+/g, " ");
|
|
885
|
+
if (sessionName) terminalName = sessionName;
|
|
886
|
+
}
|
|
794
887
|
}
|
|
795
888
|
|
|
796
|
-
if (shouldConnect(_ctx)) await initialize();
|
|
889
|
+
if (flagName || shouldConnect(_ctx)) await initialize();
|
|
797
890
|
});
|
|
798
891
|
|
|
799
892
|
pi.on("session_shutdown", async () => {
|
|
@@ -825,6 +918,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
825
918
|
stateSince = Date.now();
|
|
826
919
|
pushStatus();
|
|
827
920
|
|
|
921
|
+
// Wake up inbox flush — agent_end fires before finishRun(), so ctx.isIdle()
|
|
922
|
+
// is still false here. scheduleFlush(0) defers to next macrotask when idle.
|
|
923
|
+
if (inbox.length > 0) scheduleFlush(0);
|
|
924
|
+
|
|
828
925
|
// If we were running a remote prompt, send the response back
|
|
829
926
|
if (pendingRemotePrompt) {
|
|
830
927
|
const { id, from } = pendingRemotePrompt;
|
package/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-link",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
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",
|
|
7
|
+
"bin": {
|
|
8
|
+
"pi-link": "./bin/pi-link.mjs"
|
|
9
|
+
},
|
|
7
10
|
"repository": {
|
|
8
11
|
"type": "git",
|
|
9
|
-
"url": "https://github.com/alvivar/pi-link"
|
|
12
|
+
"url": "git+https://github.com/alvivar/pi-link.git"
|
|
10
13
|
},
|
|
11
14
|
"keywords": [
|
|
12
15
|
"pi-package",
|
package/sync.ffs_db
DELETED
|
Binary file
|