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.
- package/README.md +68 -48
- package/index.ts +153 -12
- 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:
|
|
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
|
|
126
|
-
|
|
|
127
|
-
| `pi --link`
|
|
128
|
-
| `/link-connect`
|
|
129
|
-
| `/link-disconnect`
|
|
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
|
|
144
|
-
|
|
|
145
|
-
| `link_send`
|
|
146
|
-
| `link_prompt`
|
|
147
|
-
| `link_list`
|
|
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
|
-
|
|
191
|
-
• pi-
|
|
192
|
-
• pi-
|
|
193
|
-
• pi-
|
|
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
|
|
201
|
-
|
|
|
202
|
-
| `/link`
|
|
203
|
-
| `/link-name [name]`
|
|
204
|
-
| `/link-broadcast <msg>`
|
|
205
|
-
| `/link-connect`
|
|
206
|
-
| `/link-disconnect`
|
|
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:
|
|
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 **
|
|
383
|
+
The wire protocol consists of **9 message types**, all serialized as JSON over WebSocket frames:
|
|
369
384
|
|
|
370
|
-
| Type | Direction
|
|
371
|
-
| ----------------- |
|
|
372
|
-
| `register` | Client → Hub
|
|
373
|
-
| `welcome` | Hub → Client
|
|
374
|
-
| `terminal_joined` | Hub → All
|
|
375
|
-
| `terminal_left` | Hub → All
|
|
376
|
-
| `chat` | Any → Any/All
|
|
377
|
-
| `prompt_request` | Any → Any
|
|
378
|
-
| `prompt_response` | Any → Any
|
|
379
|
-
| `
|
|
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
|
|
392
|
-
|
|
|
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
|
|
443
|
-
| ------------------------ |
|
|
444
|
-
| `role` | `"hub" \| "client" \| "disconnected"`
|
|
445
|
-
| `isAgentBusy` | `boolean`
|
|
446
|
-
| `manuallyDisconnected` | `boolean`
|
|
447
|
-
| `pendingRemotePrompt` | `object \| null`
|
|
448
|
-
| `pendingPromptResponses` | `Map`
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
| {
|
|
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
|
-
|
|
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
|
|
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
|
|
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