pi-link 0.1.15 → 0.1.16
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 +20 -0
- package/README.md +682 -650
- package/index.ts +448 -82
- package/package.json +1 -1
- package/skills/pi-link-coordination/SKILL.md +38 -47
package/index.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
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
|
*
|
|
9
|
-
* Tools: link_send, link_prompt, link_list
|
|
9
|
+
* Tools: link_send, link_prompt, link_list, link_compact
|
|
10
10
|
* Commands: /link, /link-name, /link-broadcast, /link-connect, /link-disconnect
|
|
11
11
|
*/
|
|
12
12
|
|
|
@@ -26,6 +26,7 @@ import { WebSocket, WebSocketServer } from "ws";
|
|
|
26
26
|
const DEFAULT_PORT = 9900;
|
|
27
27
|
const PROMPT_INACTIVITY_MS = 90_000;
|
|
28
28
|
const PROMPT_HARD_CEILING_MS = 1_800_000;
|
|
29
|
+
const COMPACT_TIMEOUT_MS = 180_000;
|
|
29
30
|
const RECONNECT_DELAY_MS = 2000;
|
|
30
31
|
const KEEPALIVE_INTERVAL_MS = 30_000;
|
|
31
32
|
const FLUSH_DELAY_MS = 200;
|
|
@@ -39,6 +40,7 @@ interface RegisterMsg {
|
|
|
39
40
|
type: "register";
|
|
40
41
|
name: string;
|
|
41
42
|
cwd?: string;
|
|
43
|
+
context?: ContextSnapshot;
|
|
42
44
|
}
|
|
43
45
|
interface WelcomeMsg {
|
|
44
46
|
type: "welcome";
|
|
@@ -46,12 +48,14 @@ interface WelcomeMsg {
|
|
|
46
48
|
terminals: string[];
|
|
47
49
|
statuses?: Record<string, LinkStatus>;
|
|
48
50
|
cwds?: Record<string, string>;
|
|
51
|
+
contexts?: Record<string, ContextSnapshot>;
|
|
49
52
|
}
|
|
50
53
|
interface TerminalJoinedMsg {
|
|
51
54
|
type: "terminal_joined";
|
|
52
55
|
name: string;
|
|
53
56
|
terminals: string[];
|
|
54
57
|
cwd?: string;
|
|
58
|
+
context?: ContextSnapshot;
|
|
55
59
|
}
|
|
56
60
|
interface TerminalLeftMsg {
|
|
57
61
|
type: "terminal_left";
|
|
@@ -84,17 +88,37 @@ interface StatusUpdateMsg {
|
|
|
84
88
|
type: "status_update";
|
|
85
89
|
name: string;
|
|
86
90
|
status: LinkStatus;
|
|
91
|
+
// Per-terminal LLM context. Absent = old terminal (ignore); null = clear
|
|
92
|
+
// stored value; object = store. Only status_update carries the null-clear.
|
|
93
|
+
context?: ContextSnapshot | null;
|
|
87
94
|
}
|
|
88
95
|
interface ErrorMsg {
|
|
89
96
|
type: "error";
|
|
90
97
|
message: string;
|
|
91
98
|
}
|
|
99
|
+
interface CompactRequestMsg {
|
|
100
|
+
type: "compact_request";
|
|
101
|
+
id: string;
|
|
102
|
+
from: string;
|
|
103
|
+
to: string;
|
|
104
|
+
instructions?: string;
|
|
105
|
+
}
|
|
106
|
+
interface CompactResponseMsg {
|
|
107
|
+
type: "compact_response";
|
|
108
|
+
id: string;
|
|
109
|
+
from: string;
|
|
110
|
+
to: string;
|
|
111
|
+
ok: boolean;
|
|
112
|
+
reason?: string; // "busy" | "not_found" | "unsupported" | error text; absent on success
|
|
113
|
+
}
|
|
92
114
|
|
|
93
115
|
type LinkStatus =
|
|
94
116
|
| { kind: "idle"; since: number }
|
|
95
117
|
| { kind: "thinking"; since: number }
|
|
96
118
|
| { kind: "tool"; toolName: string; since: number };
|
|
97
119
|
|
|
120
|
+
type ContextSnapshot = { tokens: number | null; contextWindow: number };
|
|
121
|
+
|
|
98
122
|
type LinkMessage =
|
|
99
123
|
| RegisterMsg
|
|
100
124
|
| WelcomeMsg
|
|
@@ -104,7 +128,9 @@ type LinkMessage =
|
|
|
104
128
|
| PromptRequestMsg
|
|
105
129
|
| PromptResponseMsg
|
|
106
130
|
| StatusUpdateMsg
|
|
107
|
-
| ErrorMsg
|
|
131
|
+
| ErrorMsg
|
|
132
|
+
| CompactRequestMsg
|
|
133
|
+
| CompactResponseMsg;
|
|
108
134
|
|
|
109
135
|
// ─── Extension ───────────────────────────────────────────────────────────────
|
|
110
136
|
|
|
@@ -138,11 +164,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
138
164
|
|
|
139
165
|
// Status tracking (local truth)
|
|
140
166
|
let agentRunning = false;
|
|
167
|
+
let compactRunning = false; // true while compacting for a remote request
|
|
141
168
|
let activeToolName: string | null = null;
|
|
142
169
|
let stateSince = Date.now();
|
|
143
170
|
let lastPushedKind: string | null = null;
|
|
144
171
|
let lastPushedTool: string | null = null;
|
|
145
172
|
const terminalStatuses = new Map<string, LinkStatus>(); // other terminals
|
|
173
|
+
const terminalContexts = new Map<string, ContextSnapshot>(); // other terminals' context
|
|
146
174
|
let currentCwd = "";
|
|
147
175
|
const terminalCwds = new Map<string, string>(); // other terminals' cwds
|
|
148
176
|
|
|
@@ -150,6 +178,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
150
178
|
let wss: WebSocketServer | null = null;
|
|
151
179
|
const hubClients = new Map<WebSocket, string>(); // ws → terminal name
|
|
152
180
|
const hubTerminalStatuses = new Map<string, LinkStatus>(); // hub-authoritative
|
|
181
|
+
const hubTerminalContexts = new Map<string, ContextSnapshot>(); // hub-authoritative
|
|
153
182
|
const hubTerminalCwds = new Map<string, string>(); // hub-authoritative (excludes self)
|
|
154
183
|
|
|
155
184
|
// Client state
|
|
@@ -169,6 +198,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
169
198
|
}
|
|
170
199
|
>();
|
|
171
200
|
|
|
201
|
+
// Pending compact responses (sender waiting for remote compaction to finish)
|
|
202
|
+
const pendingCompactResponses = new Map<
|
|
203
|
+
string,
|
|
204
|
+
{
|
|
205
|
+
resolve: (result: {
|
|
206
|
+
content: { type: "text"; text: string }[];
|
|
207
|
+
details: Record<string, unknown>;
|
|
208
|
+
}) => void;
|
|
209
|
+
targetName: string;
|
|
210
|
+
timeout: ReturnType<typeof setTimeout>;
|
|
211
|
+
}
|
|
212
|
+
>();
|
|
213
|
+
|
|
172
214
|
// Pending remote prompt (this terminal is executing a prompt for someone else)
|
|
173
215
|
let pendingRemotePrompt: { id: string; from: string } | null = null;
|
|
174
216
|
let keepaliveTimer: ReturnType<typeof setInterval> | null = null;
|
|
@@ -215,6 +257,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
215
257
|
return { kind: "idle", since: stateSince };
|
|
216
258
|
}
|
|
217
259
|
|
|
260
|
+
function captureContext(): ContextSnapshot | undefined {
|
|
261
|
+
if (!ctx) return undefined;
|
|
262
|
+
if (typeof ctx.getContextUsage !== "function") return undefined; // older Pi
|
|
263
|
+
const usage = ctx.getContextUsage();
|
|
264
|
+
if (!usage) return undefined;
|
|
265
|
+
if (usage.contextWindow <= 0) return undefined; // no real context to report
|
|
266
|
+
return { tokens: usage.tokens, contextWindow: usage.contextWindow };
|
|
267
|
+
}
|
|
268
|
+
|
|
218
269
|
function pushStatus(force = false) {
|
|
219
270
|
if (role === "disconnected") return;
|
|
220
271
|
const status = deriveStatus();
|
|
@@ -224,10 +275,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
224
275
|
return;
|
|
225
276
|
lastPushedKind = newKind;
|
|
226
277
|
lastPushedTool = newTool;
|
|
278
|
+
const context = captureContext(); // only when we actually push
|
|
227
279
|
const msg: StatusUpdateMsg = {
|
|
228
280
|
type: "status_update",
|
|
229
281
|
name: terminalName,
|
|
230
282
|
status,
|
|
283
|
+
context: context ?? null, // explicit null tells peers to clear
|
|
231
284
|
};
|
|
232
285
|
if (role === "hub") {
|
|
233
286
|
hubBroadcast(msg, terminalName);
|
|
@@ -236,6 +289,30 @@ export default function (pi: ExtensionAPI) {
|
|
|
236
289
|
}
|
|
237
290
|
}
|
|
238
291
|
|
|
292
|
+
// Canonicalize a link/session name: trim + collapse internal whitespace.
|
|
293
|
+
// Returns undefined for nullish/blank so callers can fall through precedence.
|
|
294
|
+
function normalizeName(name: string | undefined | null): string | undefined {
|
|
295
|
+
const n = name?.trim().replace(/\s+/g, " ");
|
|
296
|
+
return n ? n : undefined;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Latest custom session entry of a given type (last-write-wins), or undefined.
|
|
300
|
+
function latestCustomData(
|
|
301
|
+
customType: string,
|
|
302
|
+
): Record<string, unknown> | undefined {
|
|
303
|
+
if (!ctx) return undefined;
|
|
304
|
+
const entries = ctx.sessionManager.getEntries();
|
|
305
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
306
|
+
const e = entries[i] as {
|
|
307
|
+
type: string;
|
|
308
|
+
customType?: string;
|
|
309
|
+
data?: Record<string, unknown>;
|
|
310
|
+
};
|
|
311
|
+
if (e.type === "custom" && e.customType === customType) return e.data;
|
|
312
|
+
}
|
|
313
|
+
return undefined;
|
|
314
|
+
}
|
|
315
|
+
|
|
239
316
|
function formatDuration(since: number): string {
|
|
240
317
|
const sec = Math.floor((Date.now() - since) / 1000);
|
|
241
318
|
if (sec < 60) return `${sec}s`;
|
|
@@ -249,6 +326,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
249
326
|
return `${s.kind} (${dur})`;
|
|
250
327
|
}
|
|
251
328
|
|
|
329
|
+
function formatTokens(n: number): string {
|
|
330
|
+
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
|
331
|
+
if (n >= 1_000) return `${Math.round(n / 1000)}K`;
|
|
332
|
+
return `${n}`;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function formatContext(c: ContextSnapshot | null | undefined): string {
|
|
336
|
+
if (!c || c.contextWindow <= 0) return ""; // guard against bad wire data
|
|
337
|
+
const window = formatTokens(c.contextWindow);
|
|
338
|
+
if (c.tokens === null) return `?/${window}`;
|
|
339
|
+
const percent = Math.round((c.tokens / c.contextWindow) * 100);
|
|
340
|
+
return `${formatTokens(c.tokens)}/${window} (${percent}%)`;
|
|
341
|
+
}
|
|
342
|
+
|
|
252
343
|
function getStatusFor(name: string): LinkStatus | null {
|
|
253
344
|
if (name === terminalName) return deriveStatus();
|
|
254
345
|
const map = role === "hub" ? hubTerminalStatuses : terminalStatuses;
|
|
@@ -261,6 +352,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
261
352
|
return terminalCwds.get(name) ?? null;
|
|
262
353
|
}
|
|
263
354
|
|
|
355
|
+
function getContextFor(name: string): ContextSnapshot | null {
|
|
356
|
+
if (name === terminalName) return captureContext() ?? null;
|
|
357
|
+
if (role === "hub") return hubTerminalContexts.get(name) ?? null;
|
|
358
|
+
return terminalContexts.get(name) ?? null;
|
|
359
|
+
}
|
|
360
|
+
|
|
264
361
|
function shortenPath(cwd: string): string {
|
|
265
362
|
const home = os.homedir().replace(/\\/g, "/");
|
|
266
363
|
const normalized = cwd.replace(/\\/g, "/");
|
|
@@ -337,14 +434,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
337
434
|
// ── Connection intent ──────────────────────────────────────────────────
|
|
338
435
|
|
|
339
436
|
function shouldConnect(_ctx: ExtensionContext): boolean {
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
e.type === "custom" && e.customType === "link-active",
|
|
345
|
-
)
|
|
346
|
-
.pop() as { data?: { active?: boolean } } | undefined;
|
|
347
|
-
if (saved?.data?.active !== undefined) return saved.data.active;
|
|
437
|
+
const data = latestCustomData("link-active") as
|
|
438
|
+
| { active?: boolean }
|
|
439
|
+
| undefined;
|
|
440
|
+
if (data?.active !== undefined) return data.active;
|
|
348
441
|
return pi.getFlag("link") === true;
|
|
349
442
|
}
|
|
350
443
|
|
|
@@ -359,6 +452,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
359
452
|
return pending;
|
|
360
453
|
}
|
|
361
454
|
|
|
455
|
+
function cleanupPendingCompact(requestId: string) {
|
|
456
|
+
const pending = pendingCompactResponses.get(requestId);
|
|
457
|
+
if (!pending) return null;
|
|
458
|
+
clearTimeout(pending.timeout);
|
|
459
|
+
pendingCompactResponses.delete(requestId);
|
|
460
|
+
return pending;
|
|
461
|
+
}
|
|
462
|
+
|
|
362
463
|
function makeInactivityTimeout(requestId: string, targetName: string) {
|
|
363
464
|
return setTimeout(() => {
|
|
364
465
|
const pending = cleanupPending(requestId);
|
|
@@ -436,7 +537,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
436
537
|
* still reject via protocol-level error responses).
|
|
437
538
|
*/
|
|
438
539
|
function routeMessage(
|
|
439
|
-
msg:
|
|
540
|
+
msg:
|
|
541
|
+
| ChatMsg
|
|
542
|
+
| PromptRequestMsg
|
|
543
|
+
| PromptResponseMsg
|
|
544
|
+
| CompactRequestMsg
|
|
545
|
+
| CompactResponseMsg,
|
|
440
546
|
): boolean {
|
|
441
547
|
if (role === "hub") {
|
|
442
548
|
if (msg.to === "*") {
|
|
@@ -464,13 +570,26 @@ export default function (pi: ExtensionAPI) {
|
|
|
464
570
|
response: "",
|
|
465
571
|
error: errText,
|
|
466
572
|
}
|
|
467
|
-
:
|
|
573
|
+
: msg.type === "compact_request"
|
|
574
|
+
? {
|
|
575
|
+
type: "compact_response",
|
|
576
|
+
id: msg.id,
|
|
577
|
+
from: terminalName,
|
|
578
|
+
to: msg.from,
|
|
579
|
+
ok: false,
|
|
580
|
+
reason: "not_found",
|
|
581
|
+
}
|
|
582
|
+
: { type: "error", message: errText };
|
|
468
583
|
|
|
469
584
|
if (msg.from === terminalName) {
|
|
470
|
-
// For prompt_request, deliver the error response
|
|
471
|
-
//
|
|
472
|
-
// result (via return false) is sufficient; no extra UI toast.
|
|
473
|
-
if (
|
|
585
|
+
// For prompt_request/compact_request, deliver the error response
|
|
586
|
+
// locally so the matching pending map resolves. For chat, skip — the
|
|
587
|
+
// tool result (via return false) is sufficient; no extra UI toast.
|
|
588
|
+
if (
|
|
589
|
+
errorMsg.type === "prompt_response" ||
|
|
590
|
+
errorMsg.type === "compact_response"
|
|
591
|
+
)
|
|
592
|
+
handleIncoming(errorMsg);
|
|
474
593
|
} else {
|
|
475
594
|
hubClientByName(msg.from)?.send(JSON.stringify(errorMsg));
|
|
476
595
|
}
|
|
@@ -494,6 +613,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
494
613
|
connectedTerminals = msg.terminals;
|
|
495
614
|
terminalStatuses.clear();
|
|
496
615
|
terminalCwds.clear();
|
|
616
|
+
terminalContexts.clear();
|
|
497
617
|
if (msg.statuses) {
|
|
498
618
|
for (const [name, status] of Object.entries(msg.statuses)) {
|
|
499
619
|
terminalStatuses.set(name, status);
|
|
@@ -504,6 +624,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
504
624
|
terminalCwds.set(name, cwd);
|
|
505
625
|
}
|
|
506
626
|
}
|
|
627
|
+
if (msg.contexts) {
|
|
628
|
+
for (const [name, c] of Object.entries(msg.contexts)) {
|
|
629
|
+
terminalContexts.set(name, c);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
507
632
|
updateStatus();
|
|
508
633
|
notify(
|
|
509
634
|
`Joined link as "${terminalName}" (${connectedTerminals.length} online)`,
|
|
@@ -516,6 +641,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
516
641
|
case "terminal_joined":
|
|
517
642
|
connectedTerminals = msg.terminals;
|
|
518
643
|
if (role !== "hub" && msg.cwd) terminalCwds.set(msg.name, msg.cwd);
|
|
644
|
+
if (role !== "hub" && msg.context)
|
|
645
|
+
terminalContexts.set(msg.name, msg.context);
|
|
519
646
|
updateStatus();
|
|
520
647
|
notify(`"${msg.name}" joined the link`, "info");
|
|
521
648
|
break;
|
|
@@ -523,8 +650,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
523
650
|
case "terminal_left":
|
|
524
651
|
connectedTerminals = msg.terminals;
|
|
525
652
|
terminalStatuses.delete(msg.name);
|
|
526
|
-
if (role !== "hub")
|
|
527
|
-
|
|
653
|
+
if (role !== "hub") {
|
|
654
|
+
terminalCwds.delete(msg.name);
|
|
655
|
+
terminalContexts.delete(msg.name);
|
|
656
|
+
}
|
|
657
|
+
// Fail any pending prompts/compacts to the departed terminal
|
|
528
658
|
for (const [id, pending] of pendingPromptResponses) {
|
|
529
659
|
if (pending.targetName === msg.name) {
|
|
530
660
|
const p = cleanupPending(id);
|
|
@@ -538,6 +668,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
538
668
|
}
|
|
539
669
|
}
|
|
540
670
|
}
|
|
671
|
+
for (const [id, pending] of pendingCompactResponses) {
|
|
672
|
+
if (pending.targetName === msg.name) {
|
|
673
|
+
const p = cleanupPendingCompact(id);
|
|
674
|
+
if (p) {
|
|
675
|
+
p.resolve(
|
|
676
|
+
textResult(`Terminal "${msg.name}" disconnected`, {
|
|
677
|
+
to: msg.name,
|
|
678
|
+
error: "disconnected",
|
|
679
|
+
}),
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
541
684
|
updateStatus();
|
|
542
685
|
notify(`"${msg.name}" left the link`, "info");
|
|
543
686
|
break;
|
|
@@ -545,6 +688,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
545
688
|
// ── Status update from another terminal ──
|
|
546
689
|
case "status_update":
|
|
547
690
|
terminalStatuses.set(msg.name, msg.status);
|
|
691
|
+
if (msg.context) terminalContexts.set(msg.name, msg.context);
|
|
692
|
+
else if (msg.context === null) terminalContexts.delete(msg.name);
|
|
548
693
|
resetInactivityFor(msg.name);
|
|
549
694
|
break;
|
|
550
695
|
|
|
@@ -566,9 +711,60 @@ export default function (pi: ExtensionAPI) {
|
|
|
566
711
|
}
|
|
567
712
|
break;
|
|
568
713
|
|
|
714
|
+
// ── Another terminal asks us to compact our context ──
|
|
715
|
+
case "compact_request": {
|
|
716
|
+
if (agentRunning || pendingRemotePrompt || compactRunning) {
|
|
717
|
+
routeMessage({
|
|
718
|
+
type: "compact_response",
|
|
719
|
+
id: msg.id,
|
|
720
|
+
from: terminalName,
|
|
721
|
+
to: msg.from,
|
|
722
|
+
ok: false,
|
|
723
|
+
reason: "busy",
|
|
724
|
+
});
|
|
725
|
+
break;
|
|
726
|
+
}
|
|
727
|
+
const { id, from } = msg;
|
|
728
|
+
let finished = false;
|
|
729
|
+
const finish = (ok: boolean, reason?: string) => {
|
|
730
|
+
if (finished) return;
|
|
731
|
+
finished = true;
|
|
732
|
+
compactRunning = false;
|
|
733
|
+
routeMessage({
|
|
734
|
+
type: "compact_response",
|
|
735
|
+
id,
|
|
736
|
+
from: terminalName,
|
|
737
|
+
to: from,
|
|
738
|
+
ok,
|
|
739
|
+
reason,
|
|
740
|
+
});
|
|
741
|
+
};
|
|
742
|
+
if (!ctx?.compact) {
|
|
743
|
+
finish(false, "unsupported");
|
|
744
|
+
break;
|
|
745
|
+
}
|
|
746
|
+
compactRunning = true;
|
|
747
|
+
notify(`"${from}" requested compact`, "info");
|
|
748
|
+
// compact() aborts the current turn first, so the busy guard above
|
|
749
|
+
// keeps us from interrupting active work. The runtime guarantees
|
|
750
|
+
// exactly one of onComplete/onError fires, so compactRunning can't
|
|
751
|
+
// get stuck and the sender won't hang.
|
|
752
|
+
try {
|
|
753
|
+
ctx.compact({
|
|
754
|
+
customInstructions: msg.instructions,
|
|
755
|
+
onComplete: () => finish(true),
|
|
756
|
+
onError: (e) =>
|
|
757
|
+
finish(false, e instanceof Error ? e.message : String(e)),
|
|
758
|
+
});
|
|
759
|
+
} catch (e) {
|
|
760
|
+
finish(false, e instanceof Error ? e.message : String(e));
|
|
761
|
+
}
|
|
762
|
+
break;
|
|
763
|
+
}
|
|
764
|
+
|
|
569
765
|
// ── Another terminal asks us to run a prompt ──
|
|
570
766
|
case "prompt_request":
|
|
571
|
-
if (agentRunning || pendingRemotePrompt) {
|
|
767
|
+
if (agentRunning || pendingRemotePrompt || compactRunning) {
|
|
572
768
|
routeMessage({
|
|
573
769
|
type: "prompt_response",
|
|
574
770
|
id: msg.id,
|
|
@@ -610,6 +806,30 @@ export default function (pi: ExtensionAPI) {
|
|
|
610
806
|
break;
|
|
611
807
|
}
|
|
612
808
|
|
|
809
|
+
// ── Response to a compact we requested ──
|
|
810
|
+
case "compact_response": {
|
|
811
|
+
const pending = cleanupPendingCompact(msg.id);
|
|
812
|
+
if (pending) {
|
|
813
|
+
// Use the requested target, not msg.from: a hub-synthesized
|
|
814
|
+
// not_found response comes from the hub, not the worker.
|
|
815
|
+
const target = pending.targetName;
|
|
816
|
+
if (msg.ok) {
|
|
817
|
+
pending.resolve(
|
|
818
|
+
textResult(`Compacted "${target}"`, { to: target }),
|
|
819
|
+
);
|
|
820
|
+
} else {
|
|
821
|
+
const reason = msg.reason ?? "failed";
|
|
822
|
+
pending.resolve(
|
|
823
|
+
textResult(`Compact on "${target}" not done: ${reason}`, {
|
|
824
|
+
to: target,
|
|
825
|
+
error: reason,
|
|
826
|
+
}),
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
break;
|
|
831
|
+
}
|
|
832
|
+
|
|
613
833
|
case "error":
|
|
614
834
|
notify(`Link: ${msg.message}`, "error");
|
|
615
835
|
break;
|
|
@@ -631,6 +851,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
631
851
|
clientName = uniqueName(msg.name);
|
|
632
852
|
hubClients.set(clientWs, clientName);
|
|
633
853
|
if (msg.cwd) hubTerminalCwds.set(clientName, msg.cwd);
|
|
854
|
+
if (msg.context) hubTerminalContexts.set(clientName, msg.context);
|
|
634
855
|
const list = terminalList();
|
|
635
856
|
connectedTerminals = list;
|
|
636
857
|
updateStatus();
|
|
@@ -646,6 +867,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
646
867
|
for (const [name, cwd] of hubTerminalCwds) {
|
|
647
868
|
if (name !== clientName) cwds[name] = cwd;
|
|
648
869
|
}
|
|
870
|
+
const contexts: Record<string, ContextSnapshot> = {};
|
|
871
|
+
const hubContext = captureContext();
|
|
872
|
+
if (hubContext) contexts[terminalName] = hubContext; // hub's own context
|
|
873
|
+
for (const [name, c] of hubTerminalContexts) {
|
|
874
|
+
if (name !== clientName) contexts[name] = c;
|
|
875
|
+
}
|
|
649
876
|
clientWs.send(
|
|
650
877
|
JSON.stringify({
|
|
651
878
|
type: "welcome",
|
|
@@ -653,15 +880,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
653
880
|
terminals: list,
|
|
654
881
|
statuses,
|
|
655
882
|
cwds,
|
|
883
|
+
contexts,
|
|
656
884
|
} satisfies WelcomeMsg),
|
|
657
885
|
);
|
|
658
886
|
|
|
659
|
-
// Notify everyone else (include joiner's cwd)
|
|
887
|
+
// Notify everyone else (include joiner's cwd + context)
|
|
660
888
|
const joined: TerminalJoinedMsg = {
|
|
661
889
|
type: "terminal_joined",
|
|
662
890
|
name: clientName,
|
|
663
891
|
terminals: list,
|
|
664
892
|
cwd: msg.cwd,
|
|
893
|
+
context: msg.context,
|
|
665
894
|
};
|
|
666
895
|
hubBroadcast(joined, clientName);
|
|
667
896
|
return;
|
|
@@ -673,11 +902,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
673
902
|
// Status update — store and fan out to other clients only (not back to hub)
|
|
674
903
|
if (msg.type === "status_update") {
|
|
675
904
|
hubTerminalStatuses.set(clientName, msg.status);
|
|
905
|
+
if (msg.context) hubTerminalContexts.set(clientName, msg.context);
|
|
906
|
+
else if (msg.context === null) hubTerminalContexts.delete(clientName);
|
|
676
907
|
resetInactivityFor(clientName);
|
|
677
908
|
const normalized: StatusUpdateMsg = {
|
|
678
909
|
type: "status_update",
|
|
679
910
|
name: clientName,
|
|
680
911
|
status: msg.status,
|
|
912
|
+
context: msg.context, // undefined omitted by JSON; null forwarded to clear
|
|
681
913
|
};
|
|
682
914
|
const json = JSON.stringify(normalized);
|
|
683
915
|
for (const [otherWs, name] of hubClients) {
|
|
@@ -692,7 +924,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
692
924
|
if (
|
|
693
925
|
msg.type === "chat" ||
|
|
694
926
|
msg.type === "prompt_request" ||
|
|
695
|
-
msg.type === "prompt_response"
|
|
927
|
+
msg.type === "prompt_response" ||
|
|
928
|
+
msg.type === "compact_request" ||
|
|
929
|
+
msg.type === "compact_response"
|
|
696
930
|
) {
|
|
697
931
|
routeMessage({ ...msg, from: clientName });
|
|
698
932
|
}
|
|
@@ -700,20 +934,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
700
934
|
|
|
701
935
|
clientWs.on("close", () => {
|
|
702
936
|
if (disposed) return;
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
}
|
|
937
|
+
const name = hubClients.get(clientWs);
|
|
938
|
+
if (!name) return; // already removed (e.g. via disconnect) — ignore stale event
|
|
939
|
+
hubClients.delete(clientWs);
|
|
940
|
+
hubTerminalStatuses.delete(name);
|
|
941
|
+
hubTerminalContexts.delete(name);
|
|
942
|
+
hubTerminalCwds.delete(name);
|
|
943
|
+
const list = terminalList();
|
|
944
|
+
connectedTerminals = list;
|
|
945
|
+
updateStatus();
|
|
946
|
+
const left: TerminalLeftMsg = {
|
|
947
|
+
type: "terminal_left",
|
|
948
|
+
name,
|
|
949
|
+
terminals: list,
|
|
950
|
+
};
|
|
951
|
+
hubBroadcast(left, name);
|
|
717
952
|
});
|
|
718
953
|
|
|
719
954
|
clientWs.on("error", () => {
|
|
@@ -793,6 +1028,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
793
1028
|
type: "register",
|
|
794
1029
|
name: preferredName ?? terminalName,
|
|
795
1030
|
cwd: currentCwd || undefined,
|
|
1031
|
+
context: captureContext(),
|
|
796
1032
|
} satisfies RegisterMsg),
|
|
797
1033
|
);
|
|
798
1034
|
resolve(true);
|
|
@@ -869,8 +1105,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
869
1105
|
keepaliveTimer = null;
|
|
870
1106
|
}
|
|
871
1107
|
pendingRemotePrompt = null;
|
|
1108
|
+
compactRunning = false;
|
|
872
1109
|
|
|
873
|
-
// Clean up pending prompts
|
|
1110
|
+
// Clean up pending prompts and compacts
|
|
874
1111
|
for (const id of [...pendingPromptResponses.keys()]) {
|
|
875
1112
|
const pending = cleanupPending(id);
|
|
876
1113
|
if (pending) {
|
|
@@ -879,6 +1116,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
879
1116
|
);
|
|
880
1117
|
}
|
|
881
1118
|
}
|
|
1119
|
+
for (const id of [...pendingCompactResponses.keys()]) {
|
|
1120
|
+
const pending = cleanupPendingCompact(id);
|
|
1121
|
+
if (pending) {
|
|
1122
|
+
pending.resolve(
|
|
1123
|
+
textResult("Link disconnected", { error: "disconnected" }),
|
|
1124
|
+
);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
882
1127
|
|
|
883
1128
|
// Close client connection
|
|
884
1129
|
if (ws) {
|
|
@@ -898,6 +1143,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
898
1143
|
connectedTerminals = [];
|
|
899
1144
|
terminalStatuses.clear();
|
|
900
1145
|
hubTerminalStatuses.clear();
|
|
1146
|
+
terminalContexts.clear();
|
|
1147
|
+
hubTerminalContexts.clear();
|
|
901
1148
|
terminalCwds.clear();
|
|
902
1149
|
hubTerminalCwds.clear();
|
|
903
1150
|
lastPushedKind = null;
|
|
@@ -943,7 +1190,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
943
1190
|
const cliRaw = pi.getFlag("link-name");
|
|
944
1191
|
let cliFlagName: string | undefined;
|
|
945
1192
|
if (typeof cliRaw === "string") {
|
|
946
|
-
cliFlagName = cliRaw
|
|
1193
|
+
cliFlagName = normalizeName(cliRaw);
|
|
947
1194
|
if (!cliFlagName) {
|
|
948
1195
|
console.error("Error: --link-name requires a non-empty value.");
|
|
949
1196
|
process.exit(1);
|
|
@@ -952,7 +1199,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
952
1199
|
|
|
953
1200
|
const envRaw = process.env.PI_LINK_NAME;
|
|
954
1201
|
delete process.env.PI_LINK_NAME;
|
|
955
|
-
const envFlagName = envRaw
|
|
1202
|
+
const envFlagName = normalizeName(envRaw);
|
|
956
1203
|
|
|
957
1204
|
const flagName = cliFlagName ?? envFlagName;
|
|
958
1205
|
const fromEnv = !cliFlagName && !!envFlagName;
|
|
@@ -964,19 +1211,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
964
1211
|
// Skip append if the saved name already matches; persistence is needed
|
|
965
1212
|
// only for first-time set or actual change. Reduces session-file growth
|
|
966
1213
|
// on repeated startups (common in automation).
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
data?: { name?: unknown };
|
|
974
|
-
};
|
|
975
|
-
if (e.type !== "custom" || e.customType !== "link-name") continue;
|
|
976
|
-
if (typeof e.data?.name === "string") latestSaved = e.data.name;
|
|
977
|
-
break;
|
|
978
|
-
}
|
|
979
|
-
if (latestSaved?.trim().replace(/\s+/g, " ") !== flagName) {
|
|
1214
|
+
const latest = latestCustomData("link-name") as
|
|
1215
|
+
| { name?: unknown }
|
|
1216
|
+
| undefined;
|
|
1217
|
+
const latestSaved =
|
|
1218
|
+
typeof latest?.name === "string" ? latest.name : undefined;
|
|
1219
|
+
if (normalizeName(latestSaved) !== flagName) {
|
|
980
1220
|
pi.appendEntry("link-name", { name: flagName });
|
|
981
1221
|
}
|
|
982
1222
|
|
|
@@ -984,18 +1224,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
984
1224
|
// Public --link-name is link-only.
|
|
985
1225
|
if (fromEnv && !pi.getSessionName()) pi.setSessionName(flagName);
|
|
986
1226
|
} else {
|
|
987
|
-
const saved =
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
.pop() as { data?: { name?: string } } | undefined;
|
|
994
|
-
if (saved?.data?.name) {
|
|
995
|
-
preferredName = saved.data.name;
|
|
1227
|
+
const saved = latestCustomData("link-name") as
|
|
1228
|
+
| { name?: string }
|
|
1229
|
+
| undefined;
|
|
1230
|
+
const savedName = normalizeName(saved?.name);
|
|
1231
|
+
if (savedName) {
|
|
1232
|
+
preferredName = savedName;
|
|
996
1233
|
terminalName = preferredName;
|
|
997
1234
|
} else {
|
|
998
|
-
const sessionName = pi.getSessionName()
|
|
1235
|
+
const sessionName = normalizeName(pi.getSessionName());
|
|
999
1236
|
if (sessionName) terminalName = sessionName;
|
|
1000
1237
|
}
|
|
1001
1238
|
}
|
|
@@ -1014,6 +1251,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
1014
1251
|
pushStatus();
|
|
1015
1252
|
});
|
|
1016
1253
|
|
|
1254
|
+
pi.on("session_compact", async () => {
|
|
1255
|
+
// Tokens just dropped sharply — force a push so peers see the new context.
|
|
1256
|
+
pushStatus(true);
|
|
1257
|
+
});
|
|
1258
|
+
|
|
1017
1259
|
pi.on("tool_execution_start", async (event) => {
|
|
1018
1260
|
activeToolName = event.toolName;
|
|
1019
1261
|
stateSince = Date.now();
|
|
@@ -1082,6 +1324,30 @@ export default function (pi: ExtensionAPI) {
|
|
|
1082
1324
|
return text.length > max ? text.slice(0, max) + "..." : text;
|
|
1083
1325
|
}
|
|
1084
1326
|
|
|
1327
|
+
// Shared "target not found" result for the send/prompt/compact tools.
|
|
1328
|
+
// Returns null when the target is present, so callers can `if (miss) return miss;`.
|
|
1329
|
+
function targetNotFound(to: string) {
|
|
1330
|
+
return connectedTerminals.includes(to)
|
|
1331
|
+
? null
|
|
1332
|
+
: textResult(
|
|
1333
|
+
`Terminal "${to}" not found. Connected: ${connectedTerminals.join(", ")}`,
|
|
1334
|
+
{ to, error: "not_found" },
|
|
1335
|
+
);
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
// Shared ✓/✗ result renderer for link_send and link_compact.
|
|
1339
|
+
function renderIconResult(
|
|
1340
|
+
result: { content: { type: string; text?: string }[]; details?: unknown },
|
|
1341
|
+
theme: { fg(role: string, text: string): string },
|
|
1342
|
+
) {
|
|
1343
|
+
const txt = result.content[0];
|
|
1344
|
+
const details = result.details as Record<string, unknown> | undefined;
|
|
1345
|
+
const icon = details?.error
|
|
1346
|
+
? theme.fg("error", "✗ ")
|
|
1347
|
+
: theme.fg("success", "✓ ");
|
|
1348
|
+
return new Text(icon + (txt?.type === "text" ? txt.text : ""), 0, 0);
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1085
1351
|
// ── Tools ────────────────────────────────────────────────────────────────
|
|
1086
1352
|
|
|
1087
1353
|
pi.registerTool({
|
|
@@ -1110,11 +1376,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
1110
1376
|
if (role === "disconnected") return notConnectedResult();
|
|
1111
1377
|
|
|
1112
1378
|
// Pre-validate target exists locally (best-effort, catches typos and definitely-absent names)
|
|
1113
|
-
if (params.to !== "*"
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
{ to: params.to, error: "not_found" },
|
|
1117
|
-
);
|
|
1379
|
+
if (params.to !== "*") {
|
|
1380
|
+
const miss = targetNotFound(params.to);
|
|
1381
|
+
if (miss) return miss;
|
|
1118
1382
|
}
|
|
1119
1383
|
|
|
1120
1384
|
const delivered = routeMessage({
|
|
@@ -1153,14 +1417,108 @@ export default function (pi: ExtensionAPI) {
|
|
|
1153
1417
|
return new Text(text, 0, 0);
|
|
1154
1418
|
},
|
|
1155
1419
|
|
|
1156
|
-
renderResult(result, _options, theme)
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1420
|
+
renderResult: (result, _options, theme) => renderIconResult(result, theme),
|
|
1421
|
+
});
|
|
1422
|
+
|
|
1423
|
+
pi.registerTool({
|
|
1424
|
+
name: "link_compact",
|
|
1425
|
+
label: "Link Compact",
|
|
1426
|
+
description: [
|
|
1427
|
+
"Ask another Pi terminal to compact its context window and wait until it finishes.",
|
|
1428
|
+
"Returns once the target has compacted, so you can immediately send it new work.",
|
|
1429
|
+
"Busy targets (mid-turn or already compacting) decline; retry when idle.",
|
|
1430
|
+
].join(" "),
|
|
1431
|
+
promptSnippet: "Ask another Pi terminal to compact its context window",
|
|
1432
|
+
parameters: Type.Object({
|
|
1433
|
+
to: Type.String({ description: "Target terminal name" }),
|
|
1434
|
+
instructions: Type.Optional(
|
|
1435
|
+
Type.String({
|
|
1436
|
+
description: "Optional custom compaction instructions for the target",
|
|
1437
|
+
}),
|
|
1438
|
+
),
|
|
1439
|
+
}),
|
|
1440
|
+
|
|
1441
|
+
async execute(_toolCallId, params, signal) {
|
|
1442
|
+
if (role === "disconnected") return notConnectedResult();
|
|
1443
|
+
|
|
1444
|
+
if (params.to === terminalName) {
|
|
1445
|
+
return textResult("Cannot compact yourself - use /compact.", {
|
|
1446
|
+
to: params.to,
|
|
1447
|
+
error: "self_target",
|
|
1448
|
+
});
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
const miss = targetNotFound(params.to);
|
|
1452
|
+
if (miss) return miss;
|
|
1453
|
+
|
|
1454
|
+
const requestId = crypto.randomUUID();
|
|
1455
|
+
|
|
1456
|
+
return new Promise((resolve) => {
|
|
1457
|
+
const timeout = setTimeout(() => {
|
|
1458
|
+
const pending = cleanupPendingCompact(requestId);
|
|
1459
|
+
if (pending) {
|
|
1460
|
+
pending.resolve(
|
|
1461
|
+
textResult(
|
|
1462
|
+
`Compact request to "${params.to}" timed out (${COMPACT_TIMEOUT_MS / 1000}s)`,
|
|
1463
|
+
{ to: params.to, error: "timeout" },
|
|
1464
|
+
),
|
|
1465
|
+
);
|
|
1466
|
+
}
|
|
1467
|
+
}, COMPACT_TIMEOUT_MS);
|
|
1468
|
+
|
|
1469
|
+
pendingCompactResponses.set(requestId, {
|
|
1470
|
+
resolve,
|
|
1471
|
+
targetName: params.to,
|
|
1472
|
+
timeout,
|
|
1473
|
+
});
|
|
1474
|
+
|
|
1475
|
+
signal?.addEventListener(
|
|
1476
|
+
"abort",
|
|
1477
|
+
() => {
|
|
1478
|
+
const pending = cleanupPendingCompact(requestId);
|
|
1479
|
+
if (pending) {
|
|
1480
|
+
pending.resolve(
|
|
1481
|
+
textResult("Compact request aborted", {
|
|
1482
|
+
to: params.to,
|
|
1483
|
+
error: "aborted",
|
|
1484
|
+
}),
|
|
1485
|
+
);
|
|
1486
|
+
}
|
|
1487
|
+
},
|
|
1488
|
+
{ once: true },
|
|
1489
|
+
);
|
|
1490
|
+
|
|
1491
|
+
const delivered = routeMessage({
|
|
1492
|
+
type: "compact_request",
|
|
1493
|
+
id: requestId,
|
|
1494
|
+
from: terminalName,
|
|
1495
|
+
to: params.to,
|
|
1496
|
+
instructions: params.instructions,
|
|
1497
|
+
});
|
|
1498
|
+
|
|
1499
|
+
if (!delivered && pendingCompactResponses.has(requestId)) {
|
|
1500
|
+
const pending = cleanupPendingCompact(requestId);
|
|
1501
|
+
if (pending) {
|
|
1502
|
+
pending.resolve(
|
|
1503
|
+
textResult(`Failed to request compact on "${params.to}"`, {
|
|
1504
|
+
to: params.to,
|
|
1505
|
+
error: "not_delivered",
|
|
1506
|
+
}),
|
|
1507
|
+
);
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
});
|
|
1163
1511
|
},
|
|
1512
|
+
|
|
1513
|
+
renderCall(args, theme) {
|
|
1514
|
+
let text = theme.fg("toolTitle", theme.bold("link_compact "));
|
|
1515
|
+
text += theme.fg("accent", String(args.to));
|
|
1516
|
+
if (typeof args.instructions === "string")
|
|
1517
|
+
text += "\n " + theme.fg("dim", truncatePreview(args.instructions));
|
|
1518
|
+
return new Text(text, 0, 0);
|
|
1519
|
+
},
|
|
1520
|
+
|
|
1521
|
+
renderResult: (result, _options, theme) => renderIconResult(result, theme),
|
|
1164
1522
|
});
|
|
1165
1523
|
|
|
1166
1524
|
pi.registerTool({
|
|
@@ -1188,12 +1546,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
1188
1546
|
});
|
|
1189
1547
|
}
|
|
1190
1548
|
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
`Terminal "${params.to}" not found. Connected: ${connectedTerminals.join(", ")}`,
|
|
1194
|
-
{ to: params.to, error: "not_found" },
|
|
1195
|
-
);
|
|
1196
|
-
}
|
|
1549
|
+
const miss = targetNotFound(params.to);
|
|
1550
|
+
if (miss) return miss;
|
|
1197
1551
|
|
|
1198
1552
|
const requestId = crypto.randomUUID();
|
|
1199
1553
|
|
|
@@ -1302,6 +1656,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1302
1656
|
|
|
1303
1657
|
const statuses: Record<string, string> = {};
|
|
1304
1658
|
const cwds: Record<string, string> = {};
|
|
1659
|
+
const contexts: Record<string, ContextSnapshot> = {};
|
|
1305
1660
|
const list = connectedTerminals
|
|
1306
1661
|
.map((name) => {
|
|
1307
1662
|
const status = getStatusFor(name);
|
|
@@ -1309,8 +1664,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
1309
1664
|
if (statusStr) statuses[name] = statusStr;
|
|
1310
1665
|
const cwd = getCwdFor(name);
|
|
1311
1666
|
if (cwd) cwds[name] = cwd;
|
|
1667
|
+
const context = getContextFor(name);
|
|
1668
|
+
if (context) contexts[name] = context;
|
|
1669
|
+
const ctxStr = formatContext(context);
|
|
1312
1670
|
const marker = name === terminalName ? " (you)" : "";
|
|
1313
1671
|
let line = ` \u2022 ${name}${marker}${statusStr ? " " + statusStr : ""}`;
|
|
1672
|
+
if (ctxStr) line += ` \u00b7 ${ctxStr}`;
|
|
1314
1673
|
if (cwd) line += `\n cwd: ${cwd}`;
|
|
1315
1674
|
return line;
|
|
1316
1675
|
})
|
|
@@ -1320,6 +1679,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1320
1679
|
terminals: connectedTerminals,
|
|
1321
1680
|
statuses,
|
|
1322
1681
|
cwds,
|
|
1682
|
+
contexts,
|
|
1323
1683
|
self: terminalName,
|
|
1324
1684
|
role,
|
|
1325
1685
|
});
|
|
@@ -1331,6 +1691,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1331
1691
|
terminals?: string[];
|
|
1332
1692
|
statuses?: Record<string, string>;
|
|
1333
1693
|
cwds?: Record<string, string>;
|
|
1694
|
+
contexts?: Record<string, ContextSnapshot>;
|
|
1334
1695
|
self?: string;
|
|
1335
1696
|
role?: string;
|
|
1336
1697
|
}
|
|
@@ -1347,11 +1708,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
1347
1708
|
const isSelf = name === details.self;
|
|
1348
1709
|
const status = details.statuses?.[name] ?? "";
|
|
1349
1710
|
const cwd = details.cwds?.[name];
|
|
1711
|
+
const ctxStr = formatContext(details.contexts?.[name]);
|
|
1350
1712
|
const nameStr = isSelf ? `\u2022 ${name} (you)` : `\u2022 ${name}`;
|
|
1351
1713
|
text +=
|
|
1352
1714
|
"\n " +
|
|
1353
1715
|
(isSelf ? theme.fg("accent", nameStr) : theme.fg("text", nameStr)) +
|
|
1354
|
-
(status ? " " + theme.fg("dim", status) : "")
|
|
1716
|
+
(status ? " " + theme.fg("dim", status) : "") +
|
|
1717
|
+
(ctxStr ? theme.fg("dim", " \u00b7 " + ctxStr) : "");
|
|
1355
1718
|
if (cwd) text += "\n " + theme.fg("dim", `cwd: ${shortenPath(cwd)}`);
|
|
1356
1719
|
}
|
|
1357
1720
|
return new Text(text, 0, 0);
|
|
@@ -1371,8 +1734,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
1371
1734
|
const status = getStatusFor(name);
|
|
1372
1735
|
const statusStr = status ? formatStatus(status) : "";
|
|
1373
1736
|
const cwd = getCwdFor(name);
|
|
1737
|
+
const ctxStr = formatContext(getContextFor(name));
|
|
1374
1738
|
const marker = name === terminalName ? " (you)" : "";
|
|
1375
1739
|
let line = `${name}${marker}${statusStr ? ": " + statusStr : ""}`;
|
|
1740
|
+
if (ctxStr) line += ` \u00b7 ${ctxStr}`;
|
|
1376
1741
|
if (cwd) line += `\n cwd: ${shortenPath(cwd)}`;
|
|
1377
1742
|
return line;
|
|
1378
1743
|
});
|
|
@@ -1386,10 +1751,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
1386
1751
|
pi.registerCommand("link-name", {
|
|
1387
1752
|
description: "Change link name. No arg = use session name",
|
|
1388
1753
|
handler: async (args, _ctx) => {
|
|
1389
|
-
let newName = args
|
|
1754
|
+
let newName = normalizeName(args) ?? "";
|
|
1390
1755
|
if (!newName) {
|
|
1391
1756
|
// No argument: use session name if available
|
|
1392
|
-
const sessionName = pi.getSessionName()
|
|
1757
|
+
const sessionName = normalizeName(pi.getSessionName());
|
|
1393
1758
|
if (sessionName) {
|
|
1394
1759
|
newName = sessionName;
|
|
1395
1760
|
} else {
|
|
@@ -1444,6 +1809,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1444
1809
|
name: newName,
|
|
1445
1810
|
terminals: list,
|
|
1446
1811
|
cwd: currentCwd,
|
|
1812
|
+
context: captureContext(),
|
|
1447
1813
|
},
|
|
1448
1814
|
terminalName,
|
|
1449
1815
|
);
|