pi-subagent-in-memory 0.1.2 → 0.1.3

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 CHANGED
@@ -32,11 +32,20 @@ If `provider` and `model` are omitted, the subagent inherits the main agent's mo
32
32
 
33
33
  Each running subagent is displayed as a colored card widget above the editor:
34
34
 
35
- - Cards show **title**, **model**, **live output preview**, **elapsed time**, and **status icon** ( running, completed, error)
35
+ - Cards show **title**, **model**, **prompt preview**, **elapsed time**, and **status indicator** ( started, working…, ✅ finished, error)
36
+ - The prompt passed to the subagent is displayed as card content, so you can see at a glance what each subagent is doing
36
37
  - Cards auto-layout into a responsive grid (1–3 columns based on `columnWidthPercent`)
37
- - Output preview shows the last 4 lines of subagent activity, including tool execution indicators (⏳ running, ✅ done, ❌ failed)
38
+ - Subagent number badge (`#1`, `#2`, …) shown on the top-right corner of each card
38
39
  - Six rotating color themes for visual distinction between cards
39
40
 
41
+ ### 🔍 Subagent Detail Overlay (`Ctrl+N`)
42
+
43
+ Press **Ctrl+1** through **Ctrl+9** to open a detail popup for the corresponding subagent:
44
+
45
+ - **Prompt** — Full prompt text with word wrapping (up to 5 lines)
46
+ - **Messages** — Live-updating stream of the subagent's activity (text output, tool calls, status changes), always showing the latest 5 lines
47
+ - Press the same **Ctrl+N** shortcut or **Escape** to close the overlay
48
+
40
49
  ### 📝 JSONL Session Logging
41
50
 
42
51
  Every subagent session is logged to disk for debugging and auditing:
@@ -15,6 +15,7 @@
15
15
  * - Nested subagent support (subagents can spawn subagents)
16
16
  * - /in-memory-clear-widgets slash command to remove widget cards
17
17
  * - Multi-provider support (Anthropic, OpenAI, Google, etc.)
18
+ * - Ctrl+Alt+<N> to inspect subagent prompt & live messages
18
19
  *
19
20
  * Results are written to ./.pi/subagent-in-memory/<mainSessionId>/subagent_<N>/result.md
20
21
  * (or error.md on failure) so the calling agent gets a short pointer instead of
@@ -38,7 +39,8 @@ import type { AgentToolResult, AgentToolUpdateCallback } from "@mariozechner/pi-
38
39
  import { resolveModel } from "./model.ts";
39
40
  import { renderCard, type CardTheme } from "./tui-draw.ts";
40
41
 
41
- import { visibleWidth, truncateToWidth } from "@mariozechner/pi-tui";
42
+ import { visibleWidth, truncateToWidth, wrapTextWithAnsi, matchesKey, Key } from "@mariozechner/pi-tui";
43
+ import type { Focusable } from "@mariozechner/pi-tui";
42
44
  import { mkdirSync, writeFileSync, appendFileSync } from "node:fs";
43
45
  import { join } from "node:path";
44
46
  import { randomUUID } from "node:crypto";
@@ -50,11 +52,13 @@ function jsonlAppend(filePath: string, data: Record<string, any>) {
50
52
 
51
53
  // ── Subagent card state ─────────────────────────────────────────
52
54
  interface SubagentCard {
55
+ num: number;
53
56
  sessionId: string;
54
57
  title: string;
55
58
  modelLabel: string;
56
59
  status: "created" | "running" | "completed" | "error";
57
- textPreview: string;
60
+ prompt: string;
61
+ messages: string;
58
62
  columnWidthPercent: number;
59
63
  startedAt: number;
60
64
  endedAt?: number;
@@ -81,76 +85,232 @@ const subagents: SubagentCard[] = [];
81
85
  let currentCtx: { ui: any } | null = null;
82
86
  let mainSessionId = "unknown";
83
87
  let subagentCount = 0;
88
+ let flashTimer: ReturnType<typeof setInterval> | null = null;
89
+
90
+ // Track open detail overlay so we can trigger re-renders
91
+ let activeDetailTui: any = null;
84
92
 
85
93
  function updateSubagentWidget() {
86
94
  if (!currentCtx) return;
87
95
  const ctx = currentCtx;
88
96
 
97
+ // Start flash timer for "working..." dot animation if not already running
98
+ if (!flashTimer) {
99
+ flashTimer = setInterval(() => {
100
+ const hasActive = subagents.some((sa) => sa.status === "running" || sa.status === "created");
101
+ if (currentCtx && (hasActive || activeDetailTui)) {
102
+ currentCtx.ui.setWidget(
103
+ "in-memory-subagent-cards",
104
+ buildWidgetFactory(),
105
+ { placement: "aboveEditor" }
106
+ );
107
+ // Also re-render the detail overlay if open
108
+ if (activeDetailTui) {
109
+ try { activeDetailTui.requestRender(); } catch {}
110
+ }
111
+ }
112
+ }, 500);
113
+ }
114
+
89
115
  ctx.ui.setWidget(
90
116
  "in-memory-subagent-cards",
91
- (_tui: any, theme: any) => ({
92
- render(width: number): string[] {
93
- if (subagents.length === 0) return [];
94
-
95
- // Derive cols from columnWidthPercent (all cards share the same value).
96
- const pct = subagents[subagents.length - 1].columnWidthPercent;
97
- const cols = Math.min(3, Math.max(1, Math.round(100 / pct)));
98
- const gap = 1;
99
- const colWidth = Math.floor((width - gap * (cols - 1)) / cols);
100
- const maxContentLines = 4;
101
- const lines: string[] = [""];
102
-
103
- for (let i = 0; i < subagents.length; i += cols) {
104
- const rowCards = subagents.slice(i, i + cols).map((sa, idx) => {
105
- const cardTheme = CARD_THEMES[(i + idx) % CARD_THEMES.length];
106
-
107
- const titleText = `${sa.title} [${sa.modelLabel}]`;
108
- const innerW = colWidth - 4;
109
-
110
- const allText = sa.textPreview || "…";
111
- const contentLines = allText.split("\n");
112
- const trimmedLines = contentLines.map((l) =>
113
- visibleWidth(l) > innerW ? "…" + truncateToWidth(l, innerW - 1) : l
114
- );
115
- const visible = trimmedLines.slice(-maxContentLines);
116
- const content = (contentLines.length > maxContentLines ? "…\n" : "") + visible.join("\n");
117
-
118
- const statusIcon = sa.status === "completed" ? "✓" : sa.status === "error" ? "✗" : "●";
119
- const footer = `${statusIcon} ${formatElapsed(sa.startedAt, sa.endedAt)}`;
120
-
121
- return renderCard({
122
- title: titleText,
123
- content,
124
- footer,
125
- colWidth,
126
- theme,
127
- cardTheme,
128
- });
129
- });
117
+ buildWidgetFactory(),
118
+ { placement: "aboveEditor" }
119
+ );
120
+ }
130
121
 
131
- // Pad incomplete rows
132
- while (rowCards.length < cols) {
133
- rowCards.push(Array(rowCards[0].length).fill(" ".repeat(colWidth)));
122
+ function buildWidgetFactory() {
123
+ return (_tui: any, theme: any) => ({
124
+ render(width: number): string[] {
125
+ if (subagents.length === 0) return [];
126
+
127
+ // Derive cols from columnWidthPercent (all cards share the same value).
128
+ const pct = subagents[subagents.length - 1].columnWidthPercent;
129
+ const cols = Math.min(3, Math.max(1, Math.round(100 / pct)));
130
+ const gap = 1;
131
+ const colWidth = Math.floor((width - gap * (cols - 1)) / cols);
132
+ const maxContentLines = 4;
133
+ const lines: string[] = [""];
134
+
135
+ for (let i = 0; i < subagents.length; i += cols) {
136
+ const rowCards = subagents.slice(i, i + cols).map((sa, idx) => {
137
+ const cardTheme = CARD_THEMES[(i + idx) % CARD_THEMES.length];
138
+
139
+ const titleText = `${sa.title} [${sa.modelLabel}]`;
140
+ const innerW = colWidth - 4;
141
+
142
+ // Show the prompt as content
143
+ const allText = sa.prompt || "…";
144
+ const contentLines = allText.split("\n");
145
+ const trimmedLines = contentLines.map((l) =>
146
+ visibleWidth(l) > innerW ? truncateToWidth(l, innerW - 1) + "…" : l
147
+ );
148
+ const visible = trimmedLines.slice(0, maxContentLines);
149
+ const content = visible.join("\n") + (contentLines.length > maxContentLines ? "\n…" : "");
150
+
151
+ // Status indicator with animated dots for "working"
152
+ // All status strings are padded to the same visible width so the
153
+ // elapsed-time counter stays in a fixed column.
154
+ let statusRaw: string;
155
+ if (sa.status === "created") {
156
+ statusRaw = "⏳ started";
157
+ } else if (sa.status === "running") {
158
+ const dotPhase = Math.floor(Date.now() / 2000) % 3;
159
+ statusRaw = "⚡ working" + ".".repeat(dotPhase + 1);
160
+ } else if (sa.status === "completed") {
161
+ statusRaw = "✅ finished";
162
+ } else {
163
+ statusRaw = "❌ error";
134
164
  }
165
+ // Pad to fixed visible width (longest is "⚡ working..." or "✅ finished")
166
+ const STATUS_WIDTH = 14;
167
+ const visPad = Math.max(0, STATUS_WIDTH - visibleWidth(statusRaw));
168
+ const elapsed = formatElapsed(sa.startedAt, sa.endedAt);
169
+ const footer = `${statusRaw}${" ".repeat(visPad)} ${elapsed}`;
170
+
171
+ return renderCard({
172
+ title: titleText,
173
+ badge: `#${sa.num}`,
174
+ content,
175
+ footer,
176
+ footerRight: `Ctrl+${sa.num}`,
177
+ colWidth,
178
+ theme,
179
+ cardTheme,
180
+ });
181
+ });
135
182
 
136
- const cardHeight = Math.max(...rowCards.map((c) => c.length));
137
- for (const card of rowCards) {
138
- while (card.length < cardHeight) {
139
- card.push(" ".repeat(colWidth));
140
- }
141
- }
183
+ // Pad incomplete rows
184
+ while (rowCards.length < cols) {
185
+ rowCards.push(Array(rowCards[0].length).fill(" ".repeat(colWidth)));
186
+ }
142
187
 
143
- for (let row = 0; row < cardHeight; row++) {
144
- lines.push(rowCards.map((card) => card[row]).join(" ".repeat(gap)));
188
+ const cardHeight = Math.max(...rowCards.map((c) => c.length));
189
+ for (const card of rowCards) {
190
+ while (card.length < cardHeight) {
191
+ card.push(" ".repeat(colWidth));
145
192
  }
146
193
  }
147
194
 
148
- return lines;
149
- },
150
- invalidate() {},
151
- }),
152
- { placement: "aboveEditor" }
153
- );
195
+ for (let row = 0; row < cardHeight; row++) {
196
+ lines.push(rowCards.map((card) => card[row]).join(" ".repeat(gap)));
197
+ }
198
+ }
199
+
200
+ return lines;
201
+ },
202
+ invalidate() {},
203
+ });
204
+ }
205
+
206
+ // ── Detail overlay component ────────────────────────────────────
207
+ class SubagentDetailOverlay implements Focusable {
208
+ focused = false;
209
+
210
+ constructor(
211
+ private card: SubagentCard,
212
+ private cardNum: number,
213
+ private theme: any,
214
+ private done: (result: void) => void,
215
+ ) {}
216
+
217
+ handleInput(data: string): void {
218
+ // Close on Escape, Enter, or the same Ctrl+N that opened it
219
+ if (
220
+ matchesKey(data, "escape") ||
221
+ matchesKey(data, "return") ||
222
+ matchesKey(data, Key.ctrl(`${this.cardNum}` as any))
223
+ ) {
224
+ this.done();
225
+ return;
226
+ }
227
+ }
228
+
229
+ render(width: number): string[] {
230
+ const th = this.theme;
231
+ const sa = this.card;
232
+ const innerW = width - 2; // borders are 2 chars total
233
+
234
+ const pad = (s: string, len: number) => {
235
+ const vis = visibleWidth(s);
236
+ return s + " ".repeat(Math.max(0, len - vis));
237
+ };
238
+ const row = (content: string) =>
239
+ th.fg("border", "│") + pad(content, innerW) + th.fg("border", "│");
240
+ const divider = () =>
241
+ th.fg("border", "├" + "─".repeat(innerW) + "┤");
242
+
243
+ const lines: string[] = [];
244
+
245
+ // Top border
246
+ lines.push(th.fg("border", "╭" + "─".repeat(innerW) + "╮"));
247
+
248
+ // Header
249
+ const statusIcon = sa.status === "created" ? "⏳"
250
+ : sa.status === "running" ? "⚡"
251
+ : sa.status === "completed" ? "✅"
252
+ : "❌";
253
+ const headerText = ` ${statusIcon} Subagent #${sa.num}: ${sa.title} [${sa.modelLabel}]`;
254
+ lines.push(row(th.fg("accent", th.bold(truncateToWidth(headerText, innerW)))));
255
+ lines.push(row(th.fg("dim", ` ${formatElapsed(sa.startedAt, sa.endedAt)} elapsed`)));
256
+
257
+ // Prompt section — word-wrap to show at least 3 lines, max 5
258
+ lines.push(divider());
259
+ lines.push(row(th.fg("accent", " PROMPT")));
260
+ const promptWrapWidth = innerW - 2; // 1 char padding each side
261
+ const promptLines = wrapTextWithAnsi(sa.prompt, promptWrapWidth);
262
+ const PROMPT_MIN = 3;
263
+ const PROMPT_MAX = 5;
264
+ const promptDisplay = promptLines.slice(0, PROMPT_MAX);
265
+ for (const pl of promptDisplay) {
266
+ lines.push(row(" " + th.fg("text", truncateToWidth(pl, innerW - 1))));
267
+ }
268
+ // Pad to minimum rows so prompt section is always visible
269
+ for (let r = promptDisplay.length; r < PROMPT_MIN; r++) {
270
+ lines.push(row(""));
271
+ }
272
+ if (promptLines.length > PROMPT_MAX) {
273
+ lines.push(row(th.fg("dim", ` … (${promptLines.length - PROMPT_MAX} more lines)`)));
274
+ }
275
+
276
+ // Messages section — always show latest 5 lines
277
+ lines.push(divider());
278
+ lines.push(row(th.fg("accent", " MESSAGES")));
279
+
280
+ const MSG_VISIBLE = 5;
281
+ const msgText = sa.messages || "(no messages yet)";
282
+ const allMsgLines = msgText.split("\n");
283
+ // Always auto-scroll to the latest lines
284
+ const msgStart = Math.max(0, allMsgLines.length - MSG_VISIBLE);
285
+ const visibleMsgLines = allMsgLines.slice(msgStart);
286
+ for (const ml of visibleMsgLines) {
287
+ lines.push(row(" " + th.fg("muted", truncateToWidth(ml, innerW - 1))));
288
+ }
289
+ // Pad to minimum rows
290
+ for (let r = visibleMsgLines.length; r < MSG_VISIBLE; r++) {
291
+ lines.push(row(""));
292
+ }
293
+ if (allMsgLines.length > MSG_VISIBLE) {
294
+ lines.push(row(th.fg("dim", ` … ${allMsgLines.length - MSG_VISIBLE} earlier lines hidden`)));
295
+ }
296
+
297
+ // Bottom border with right-aligned hint
298
+ const hint = ` Esc / Ctrl+${sa.num} `;
299
+ const hintLen = hint.length;
300
+ const dashBefore = Math.max(0, innerW - hintLen);
301
+ lines.push(
302
+ th.fg("border", "╰" + "─".repeat(dashBefore)) +
303
+ th.fg("dim", hint) +
304
+ th.fg("border", "╯")
305
+ );
306
+
307
+ return lines;
308
+ }
309
+
310
+ invalidate(): void {}
311
+ dispose(): void {
312
+ activeDetailTui = null;
313
+ }
154
314
  }
155
315
 
156
316
  // ── Parameter schema ────────────────────────────────────────────
@@ -189,6 +349,11 @@ const SubagentParams = Type.Object({
189
349
 
190
350
  type SubagentParamsType = Static<typeof SubagentParams>;
191
351
 
352
+ // ── Helper: append to card messages ─────────────────────────────
353
+ function appendMessage(card: SubagentCard, msg: string) {
354
+ card.messages += (card.messages ? "\n" : "") + msg;
355
+ }
356
+
192
357
  // ── Core execution logic ────────────────────────────────────────
193
358
  async function executeSubagent(
194
359
  toolCallId: string,
@@ -267,11 +432,13 @@ async function executeSubagent(
267
432
 
268
433
  // Track card
269
434
  const card: SubagentCard = {
435
+ num: subagentNum,
270
436
  sessionId: session.sessionId,
271
437
  title: params.title ?? params.task.slice(0, 30),
272
438
  modelLabel: modelId,
273
439
  status: "created",
274
- textPreview: "",
440
+ prompt: params.task,
441
+ messages: "",
275
442
  columnWidthPercent: params.columnWidthPercent ?? 50,
276
443
  startedAt: Date.now(),
277
444
  };
@@ -296,6 +463,7 @@ async function executeSubagent(
296
463
  session.abort();
297
464
  card.status = "error";
298
465
  card.endedAt = Date.now();
466
+ appendMessage(card, "[aborted]");
299
467
  updateSubagentWidget();
300
468
  };
301
469
 
@@ -318,6 +486,7 @@ async function executeSubagent(
318
486
  switch (event.type) {
319
487
  case "agent_start":
320
488
  card.status = "running";
489
+ appendMessage(card, "[agent started]");
321
490
  updateSubagentWidget();
322
491
  jsonlAppend(jsonlPath, baseLog);
323
492
  lastEventId = eventId;
@@ -332,8 +501,8 @@ async function executeSubagent(
332
501
  if (ame.type === "text_delta") {
333
502
  textDeltaBuffer += ame.delta;
334
503
  finalText += ame.delta;
335
- card.textPreview = finalText;
336
- updateSubagentWidget();
504
+ // Append delta text to messages for live view
505
+ card.messages += ame.delta;
337
506
  onUpdate?.({
338
507
  content: [{ type: "text", text: finalText }],
339
508
  details: {
@@ -369,8 +538,7 @@ async function executeSubagent(
369
538
  }
370
539
 
371
540
  case "tool_execution_start":
372
- card.textPreview = finalText + `\n[${event.toolName} ⏳]`;
373
- updateSubagentWidget();
541
+ appendMessage(card, `\n[🔧 ${event.toolName} ⏳]`);
374
542
  jsonlAppend(jsonlPath, { ...baseLog, toolName: event.toolName, args: event.args });
375
543
  lastEventId = eventId;
376
544
  onUpdate?.({
@@ -385,8 +553,7 @@ async function executeSubagent(
385
553
  break;
386
554
 
387
555
  case "tool_execution_end":
388
- card.textPreview = finalText + `\n[${event.toolName} ${event.isError ? "❌" : "✅"}]`;
389
- updateSubagentWidget();
556
+ appendMessage(card, `[🔧 ${event.toolName} ${event.isError ? "❌" : "✅"}]`);
390
557
  jsonlAppend(jsonlPath, { ...baseLog, toolName: event.toolName, isError: event.isError, result: event.result });
391
558
  lastEventId = eventId;
392
559
  onUpdate?.({
@@ -406,6 +573,7 @@ async function executeSubagent(
406
573
  case "agent_end":
407
574
  card.status = "completed";
408
575
  card.endedAt = Date.now();
576
+ appendMessage(card, "\n[agent completed]");
409
577
  updateSubagentWidget();
410
578
  jsonlAppend(jsonlPath, { ...baseLog, finalTextLength: finalText.length });
411
579
  resolve(finalText || "Subagent completed with no text output.");
@@ -444,6 +612,7 @@ async function executeSubagent(
444
612
  session.prompt(params.task).catch((err) => {
445
613
  card.status = "error";
446
614
  card.endedAt = Date.now();
615
+ appendMessage(card, `[error: ${err?.message ?? String(err)}]`);
447
616
  updateSubagentWidget();
448
617
  reject(err);
449
618
  });
@@ -519,11 +688,44 @@ export default function (pi: ExtensionAPI) {
519
688
  description: "Clear all in-memory subagent card widgets",
520
689
  handler: async (_args, ctx) => {
521
690
  subagents.length = 0;
691
+ if (flashTimer) { clearInterval(flashTimer); flashTimer = null; }
522
692
  ctx.ui.setWidget("in-memory-subagent-cards", undefined);
523
693
  ctx.ui.notify("In-memory subagent widgets cleared", "info");
524
694
  },
525
695
  });
526
696
 
697
+ // Register Ctrl+1 through Ctrl+9 to open subagent detail overlay
698
+ for (let n = 1; n <= 9; n++) {
699
+ pi.registerShortcut(Key.ctrl(`${n}` as any), {
700
+ description: `Inspect subagent #${n}`,
701
+ handler: async (ctx) => {
702
+ const card = subagents.find((sa) => sa.num === n);
703
+ if (!card) {
704
+ ctx.ui.notify(`No subagent #${n}`, "warning");
705
+ return;
706
+ }
707
+
708
+ await ctx.ui.custom<void>(
709
+ (tui: any, theme: any, _keybindings: any, done: (result: void) => void) => {
710
+ activeDetailTui = tui;
711
+ return new SubagentDetailOverlay(card, n, theme, done);
712
+ },
713
+ {
714
+ overlay: true,
715
+ overlayOptions: {
716
+ anchor: "center",
717
+ width: "80%",
718
+ maxHeight: "80%",
719
+ minWidth: 60,
720
+ },
721
+ }
722
+ );
723
+
724
+ activeDetailTui = null;
725
+ },
726
+ });
727
+ }
728
+
527
729
  pi.registerTool<typeof SubagentParams>({
528
730
  name: "subagent_create",
529
731
  label: "Subagent",
@@ -14,8 +14,10 @@ export interface CardTheme {
14
14
 
15
15
  export interface RenderCardOptions {
16
16
  title: string;
17
+ badge?: string;
17
18
  content?: string;
18
19
  footer?: string;
20
+ footerRight?: string;
19
21
  colWidth: number;
20
22
  theme: {
21
23
  fg: (style: string, text: string) => string;
@@ -31,7 +33,7 @@ export interface RenderCardOptions {
31
33
  * Each line is exactly `colWidth` visible characters wide (including borders).
32
34
  */
33
35
  export function renderCard(opts: RenderCardOptions): string[] {
34
- const { title, content, footer, colWidth, theme, cardTheme } = opts;
36
+ const { title, badge, content, footer, colWidth, theme, cardTheme } = opts;
35
37
  const w = colWidth - 2; // inner width (minus left+right border)
36
38
  const { bg, br } = cardTheme;
37
39
 
@@ -43,15 +45,28 @@ export function renderCard(opts: RenderCardOptions): string[] {
43
45
  return bord("│") + bg + text + bg + pad + BG_RESET + bord("│");
44
46
  };
45
47
 
48
+ /** Like borderLine but places a right-aligned badge before the right border */
49
+ const borderLineWithBadge = (text: string, badgeText: string) => {
50
+ const styledBadge = theme.fg("accent", theme.bold(badgeText));
51
+ const badgeVisLen = visibleWidth(badgeText);
52
+ const textVisLen = visibleWidth(text);
53
+ const gap = Math.max(1, w - textVisLen - badgeVisLen);
54
+ return bord("│") + bg + text + bg + " ".repeat(gap) + styledBadge + BG_RESET + bord("│");
55
+ };
56
+
46
57
  const top = "┌" + "─".repeat(w) + "┐";
47
58
  const bot = "└" + "─".repeat(w) + "┘";
48
59
 
49
60
  const lines: string[] = [bord(top)];
50
61
 
51
- // Title — truncate raw text, then style
52
- const truncTitle = truncateToWidth(title, w - 1);
62
+ // Title line with optional badge on the right
63
+ const truncTitle = truncateToWidth(title, badge ? w - visibleWidth(badge) - 2 : w - 1);
53
64
  const styledTitle = theme.fg("accent", theme.bold(truncTitle));
54
- lines.push(borderLine(" " + styledTitle));
65
+ if (badge) {
66
+ lines.push(borderLineWithBadge(" " + styledTitle, badge));
67
+ } else {
68
+ lines.push(borderLine(" " + styledTitle));
69
+ }
55
70
 
56
71
  // Content (defaults to "ready" muted) — supports multi-line
57
72
  const contentText = content ?? "ready";
@@ -61,11 +76,24 @@ export function renderCard(opts: RenderCardOptions): string[] {
61
76
  lines.push(borderLine(" " + styledContent));
62
77
  }
63
78
 
64
- // Optional footer — truncate then style
79
+ // Optional footer — with optional right-aligned text
65
80
  if (footer !== undefined) {
66
- const truncFooter = truncateToWidth(footer, w - 1);
67
- const styledFooter = theme.fg("muted", truncFooter);
68
- lines.push(borderLine(" " + styledFooter));
81
+ const { footerRight } = opts;
82
+ if (footerRight) {
83
+ const rightVis = visibleWidth(footerRight);
84
+ const maxLeft = w - rightVis - 2; // 1 pad each side
85
+ const truncFooter = truncateToWidth(footer, maxLeft);
86
+ const styledLeft = theme.fg("muted", truncFooter);
87
+ const styledRight = theme.fg("dim", footerRight);
88
+ const leftVis = visibleWidth(truncFooter);
89
+ const gap = Math.max(1, w - 1 - leftVis - rightVis);
90
+ const combined = " " + styledLeft + " ".repeat(gap) + styledRight;
91
+ lines.push(borderLine(combined));
92
+ } else {
93
+ const truncFooter = truncateToWidth(footer, w - 1);
94
+ const styledFooter = theme.fg("muted", truncFooter);
95
+ lines.push(borderLine(" " + styledFooter));
96
+ }
69
97
  }
70
98
 
71
99
  lines.push(bord(bot));
Binary file
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "pi-subagent-in-memory",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "In-process subagent tool for pi with live TUI card widgets, JSONL session logging, and zero system-prompt overhead.",
5
5
  "repository": {
6
6
  "type": "git",
7
- "url": "git@github.com:ross-jill-ws/pi-subagent-in-memory.git"
7
+ "url": "git+https://github.com/ross-jill-ws/pi-subagent-in-memory.git"
8
8
  },
9
9
  "author": "Ross Z",
10
10
  "license": "MIT",