pi-powerline-footer 0.2.6 → 0.2.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 +17 -0
- package/index.ts +124 -40
- package/package.json +1 -1
- package/welcome.ts +250 -231
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.2.9] - 2026-01-17
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- Welcome overlay/header now dismisses when agent starts streaming (fixes `p "command"` case where welcome would briefly flash)
|
|
7
|
+
- Race condition where dismissal request could be lost due to 100ms setup delay in overlay
|
|
8
|
+
|
|
9
|
+
## [0.2.8] - 2026-01-16
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- `quietStartup: true` → shows welcome as header (dismisses on first input)
|
|
13
|
+
- `quietStartup: false` or not set → shows welcome as centered overlay (dismisses on key/timeout)
|
|
14
|
+
- Both modes use same two-column layout: logo, model info, tips, loaded counts, recent sessions
|
|
15
|
+
- Refactored welcome.ts to share rendering logic between header and overlay
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- `/powerline` toggle off now clears all custom UI (editor, footer, header)
|
|
19
|
+
|
|
3
20
|
## [0.2.6] - 2026-01-16
|
|
4
21
|
|
|
5
22
|
### Fixed
|
package/index.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { ExtensionAPI, ReadonlyFooterDataProvider } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import type { AssistantMessage } from "@mariozechner/pi-ai";
|
|
3
3
|
import { visibleWidth } from "@mariozechner/pi-tui";
|
|
4
|
-
import { readFileSync } from "node:fs";
|
|
4
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
5
6
|
|
|
6
7
|
import type { SegmentContext, StatusLinePreset } from "./types.js";
|
|
7
8
|
import { getPreset, PRESETS } from "./presets.js";
|
|
@@ -9,7 +10,7 @@ import { getSeparator } from "./separators.js";
|
|
|
9
10
|
import { renderSegment } from "./segments.js";
|
|
10
11
|
import { getGitStatus, invalidateGitStatus, invalidateGitBranch } from "./git-status.js";
|
|
11
12
|
import { ansi, getFgAnsiCode } from "./colors.js";
|
|
12
|
-
import { WelcomeComponent, discoverLoadedCounts, getRecentSessions } from "./welcome.js";
|
|
13
|
+
import { WelcomeComponent, WelcomeHeader, discoverLoadedCounts, getRecentSessions } from "./welcome.js";
|
|
13
14
|
|
|
14
15
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
15
16
|
// Configuration
|
|
@@ -23,6 +24,21 @@ let config: PowerlineConfig = {
|
|
|
23
24
|
preset: "default",
|
|
24
25
|
};
|
|
25
26
|
|
|
27
|
+
// Check if quietStartup is enabled in settings
|
|
28
|
+
function isQuietStartup(): boolean {
|
|
29
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || "";
|
|
30
|
+
const settingsPath = join(homeDir, ".pi", "agent", "settings.json");
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
if (existsSync(settingsPath)) {
|
|
34
|
+
const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
35
|
+
return settings.quietStartup === true;
|
|
36
|
+
}
|
|
37
|
+
} catch {}
|
|
38
|
+
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
26
42
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
27
43
|
// Status Line Builder (for top border)
|
|
28
44
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -71,6 +87,9 @@ export default function powerlineFooter(pi: ExtensionAPI) {
|
|
|
71
87
|
let getThinkingLevelFn: (() => string) | null = null;
|
|
72
88
|
let isStreaming = false;
|
|
73
89
|
let tuiRef: any = null; // Store TUI reference for forcing re-renders
|
|
90
|
+
let dismissWelcomeOverlay: (() => void) | null = null; // Callback to dismiss welcome overlay
|
|
91
|
+
let welcomeHeaderActive = false; // Track if welcome header should be cleared on first input
|
|
92
|
+
let welcomeOverlayShouldDismiss = false; // Track early dismissal request (before overlay setup completes)
|
|
74
93
|
|
|
75
94
|
// Track session start
|
|
76
95
|
pi.on("session_start", async (_event, ctx) => {
|
|
@@ -84,7 +103,12 @@ export default function powerlineFooter(pi: ExtensionAPI) {
|
|
|
84
103
|
|
|
85
104
|
if (enabled && ctx.hasUI) {
|
|
86
105
|
setupCustomEditor(ctx);
|
|
87
|
-
|
|
106
|
+
// quietStartup: true → compact header, otherwise → full overlay
|
|
107
|
+
if (isQuietStartup()) {
|
|
108
|
+
setupWelcomeHeader(ctx);
|
|
109
|
+
} else {
|
|
110
|
+
setupWelcomeOverlay(ctx);
|
|
111
|
+
}
|
|
88
112
|
}
|
|
89
113
|
});
|
|
90
114
|
|
|
@@ -131,14 +155,41 @@ export default function powerlineFooter(pi: ExtensionAPI) {
|
|
|
131
155
|
});
|
|
132
156
|
|
|
133
157
|
// Track streaming state (footer only shows status during streaming)
|
|
134
|
-
|
|
158
|
+
// Also dismiss welcome when agent starts responding (handles `p "command"` case)
|
|
159
|
+
pi.on("stream_start", async (_event, ctx) => {
|
|
135
160
|
isStreaming = true;
|
|
161
|
+
if (dismissWelcomeOverlay) {
|
|
162
|
+
dismissWelcomeOverlay();
|
|
163
|
+
dismissWelcomeOverlay = null;
|
|
164
|
+
} else {
|
|
165
|
+
// Overlay not set up yet (100ms delay) - mark for immediate dismissal when it does
|
|
166
|
+
welcomeOverlayShouldDismiss = true;
|
|
167
|
+
}
|
|
168
|
+
if (welcomeHeaderActive) {
|
|
169
|
+
welcomeHeaderActive = false;
|
|
170
|
+
ctx.ui.setHeader(undefined);
|
|
171
|
+
}
|
|
136
172
|
});
|
|
137
173
|
|
|
138
174
|
pi.on("stream_end", async () => {
|
|
139
175
|
isStreaming = false;
|
|
140
176
|
});
|
|
141
177
|
|
|
178
|
+
// Dismiss welcome overlay/header on first user message
|
|
179
|
+
pi.on("user_message", async (_event, ctx) => {
|
|
180
|
+
if (dismissWelcomeOverlay) {
|
|
181
|
+
dismissWelcomeOverlay();
|
|
182
|
+
dismissWelcomeOverlay = null;
|
|
183
|
+
} else {
|
|
184
|
+
// Overlay not set up yet (100ms delay) - mark for immediate dismissal when it does
|
|
185
|
+
welcomeOverlayShouldDismiss = true;
|
|
186
|
+
}
|
|
187
|
+
if (welcomeHeaderActive) {
|
|
188
|
+
welcomeHeaderActive = false;
|
|
189
|
+
ctx.ui.setHeader(undefined);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
142
193
|
// Command to toggle/configure
|
|
143
194
|
pi.registerCommand("powerline", {
|
|
144
195
|
description: "Configure powerline status (toggle, preset)",
|
|
@@ -153,9 +204,10 @@ export default function powerlineFooter(pi: ExtensionAPI) {
|
|
|
153
204
|
setupCustomEditor(ctx);
|
|
154
205
|
ctx.ui.notify("Powerline enabled", "info");
|
|
155
206
|
} else {
|
|
156
|
-
//
|
|
207
|
+
// Clear all custom UI components
|
|
157
208
|
ctx.ui.setEditorComponent(undefined);
|
|
158
209
|
ctx.ui.setFooter(undefined);
|
|
210
|
+
ctx.ui.setHeader(undefined);
|
|
159
211
|
footerDataRef = null;
|
|
160
212
|
tuiRef = null;
|
|
161
213
|
ctx.ui.notify("Defaults restored", "info");
|
|
@@ -249,6 +301,25 @@ export default function powerlineFooter(pi: ExtensionAPI) {
|
|
|
249
301
|
// Create custom editor that overrides render for status in top border
|
|
250
302
|
const editor = new CustomEditor(tui, theme, keybindings);
|
|
251
303
|
|
|
304
|
+
// Override handleInput to dismiss welcome on first keypress
|
|
305
|
+
const originalHandleInput = editor.handleInput.bind(editor);
|
|
306
|
+
editor.handleInput = (data: string) => {
|
|
307
|
+
// Dismiss welcome overlay/header on first keypress
|
|
308
|
+
if (dismissWelcomeOverlay) {
|
|
309
|
+
const dismiss = dismissWelcomeOverlay;
|
|
310
|
+
dismissWelcomeOverlay = null;
|
|
311
|
+
setTimeout(dismiss, 0);
|
|
312
|
+
} else {
|
|
313
|
+
// Overlay not set up yet (100ms delay) - mark for immediate dismissal when it does
|
|
314
|
+
welcomeOverlayShouldDismiss = true;
|
|
315
|
+
}
|
|
316
|
+
if (welcomeHeaderActive) {
|
|
317
|
+
welcomeHeaderActive = false;
|
|
318
|
+
ctx.ui.setHeader(undefined);
|
|
319
|
+
}
|
|
320
|
+
originalHandleInput(data);
|
|
321
|
+
};
|
|
322
|
+
|
|
252
323
|
// Store original render
|
|
253
324
|
const originalRender = editor.render.bind(editor);
|
|
254
325
|
|
|
@@ -299,10 +370,9 @@ export default function powerlineFooter(pi: ExtensionAPI) {
|
|
|
299
370
|
} else {
|
|
300
371
|
// Status too wide - truncate by removing segments from the end
|
|
301
372
|
// Build progressively shorter content until it fits
|
|
302
|
-
const allSegments = [...presetDef.leftSegments, ...presetDef.rightSegments];
|
|
303
373
|
let truncatedContent = "";
|
|
304
374
|
|
|
305
|
-
for (let numSegments =
|
|
375
|
+
for (let numSegments = presetDef.leftSegments.length - 1; numSegments >= 1; numSegments--) {
|
|
306
376
|
const limitedPreset = {
|
|
307
377
|
...presetDef,
|
|
308
378
|
leftSegments: presetDef.leftSegments.slice(0, numSegments),
|
|
@@ -368,8 +438,7 @@ export default function powerlineFooter(pi: ExtensionAPI) {
|
|
|
368
438
|
return {
|
|
369
439
|
dispose: unsub,
|
|
370
440
|
invalidate() {
|
|
371
|
-
//
|
|
372
|
-
tui.requestRender();
|
|
441
|
+
// No cache to clear - render is always fresh
|
|
373
442
|
},
|
|
374
443
|
render(width: number): string[] {
|
|
375
444
|
// Only show status in footer during streaming (editor hidden)
|
|
@@ -388,10 +457,9 @@ export default function powerlineFooter(pi: ExtensionAPI) {
|
|
|
388
457
|
return [statusContent + " ".repeat(width - statusWidth)];
|
|
389
458
|
} else {
|
|
390
459
|
// Truncate by removing segments (same logic as editor)
|
|
391
|
-
const allSegments = [...presetDef.leftSegments, ...presetDef.rightSegments];
|
|
392
460
|
let truncatedContent = "";
|
|
393
461
|
|
|
394
|
-
for (let numSegments =
|
|
462
|
+
for (let numSegments = presetDef.leftSegments.length - 1; numSegments >= 1; numSegments--) {
|
|
395
463
|
const limitedPreset = {
|
|
396
464
|
...presetDef,
|
|
397
465
|
leftSegments: presetDef.leftSegments.slice(0, numSegments),
|
|
@@ -413,30 +481,45 @@ export default function powerlineFooter(pi: ExtensionAPI) {
|
|
|
413
481
|
});
|
|
414
482
|
}
|
|
415
483
|
|
|
416
|
-
function
|
|
417
|
-
// Get version from package.json
|
|
418
|
-
let version = "0.0.0";
|
|
419
|
-
try {
|
|
420
|
-
const pkgPath = new URL("./package.json", import.meta.url).pathname;
|
|
421
|
-
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
422
|
-
version = pkg.version || version;
|
|
423
|
-
} catch {}
|
|
424
|
-
|
|
425
|
-
// Get model info
|
|
484
|
+
function setupWelcomeHeader(ctx: any) {
|
|
426
485
|
const modelName = ctx.model?.name || ctx.model?.id || "No model";
|
|
427
486
|
const providerName = ctx.model?.provider || "Unknown";
|
|
487
|
+
const loadedCounts = discoverLoadedCounts();
|
|
488
|
+
const recentSessions = getRecentSessions(3);
|
|
428
489
|
|
|
429
|
-
|
|
490
|
+
const header = new WelcomeHeader(modelName, providerName, recentSessions, loadedCounts);
|
|
491
|
+
welcomeHeaderActive = true; // Will be cleared on first user input
|
|
492
|
+
|
|
493
|
+
ctx.ui.setHeader((_tui: any, _theme: any) => {
|
|
494
|
+
return {
|
|
495
|
+
render(width: number): string[] {
|
|
496
|
+
return header.render(width);
|
|
497
|
+
},
|
|
498
|
+
invalidate() {
|
|
499
|
+
header.invalidate();
|
|
500
|
+
},
|
|
501
|
+
};
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function setupWelcomeOverlay(ctx: any) {
|
|
506
|
+
const modelName = ctx.model?.name || ctx.model?.id || "No model";
|
|
507
|
+
const providerName = ctx.model?.provider || "Unknown";
|
|
430
508
|
const loadedCounts = discoverLoadedCounts();
|
|
431
509
|
const recentSessions = getRecentSessions(3);
|
|
432
510
|
|
|
433
|
-
// Small delay to let pi-mono finish initialization
|
|
511
|
+
// Small delay to let pi-mono finish initialization
|
|
434
512
|
setTimeout(() => {
|
|
435
|
-
//
|
|
513
|
+
// Skip overlay entirely if dismissal was requested during the delay
|
|
514
|
+
// (e.g., `p "command"` triggers stream_start before this runs)
|
|
515
|
+
if (welcomeOverlayShouldDismiss) {
|
|
516
|
+
welcomeOverlayShouldDismiss = false;
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
|
|
436
520
|
ctx.ui.custom(
|
|
437
521
|
(tui: any, _theme: any, _keybindings: any, done: (result: void) => void) => {
|
|
438
522
|
const welcome = new WelcomeComponent(
|
|
439
|
-
version,
|
|
440
523
|
modelName,
|
|
441
524
|
providerName,
|
|
442
525
|
recentSessions,
|
|
@@ -450,30 +533,33 @@ export default function powerlineFooter(pi: ExtensionAPI) {
|
|
|
450
533
|
if (dismissed) return;
|
|
451
534
|
dismissed = true;
|
|
452
535
|
clearInterval(interval);
|
|
536
|
+
dismissWelcomeOverlay = null;
|
|
453
537
|
done();
|
|
454
538
|
};
|
|
455
539
|
|
|
456
|
-
//
|
|
540
|
+
// Store dismiss callback so user_message/keypress can trigger it
|
|
541
|
+
dismissWelcomeOverlay = dismiss;
|
|
542
|
+
|
|
543
|
+
// Double-check: dismissal might have been requested between the outer check
|
|
544
|
+
// and this callback running
|
|
545
|
+
if (welcomeOverlayShouldDismiss) {
|
|
546
|
+
welcomeOverlayShouldDismiss = false;
|
|
547
|
+
dismiss();
|
|
548
|
+
}
|
|
549
|
+
|
|
457
550
|
const interval = setInterval(() => {
|
|
458
551
|
if (dismissed) return;
|
|
459
552
|
countdown--;
|
|
460
553
|
welcome.setCountdown(countdown);
|
|
461
554
|
tui.requestRender();
|
|
462
|
-
|
|
463
|
-
if (countdown <= 0) {
|
|
464
|
-
dismiss();
|
|
465
|
-
}
|
|
555
|
+
if (countdown <= 0) dismiss();
|
|
466
556
|
}, 1000);
|
|
467
557
|
|
|
468
|
-
// Create a focusable wrapper component
|
|
469
|
-
// Must have 'focused' property for TUI to recognize it as focusable
|
|
470
558
|
return {
|
|
471
|
-
focused: false,
|
|
559
|
+
focused: false,
|
|
472
560
|
invalidate: () => welcome.invalidate(),
|
|
473
561
|
render: (width: number) => welcome.render(width),
|
|
474
|
-
handleInput: (_data: string) =>
|
|
475
|
-
dismiss();
|
|
476
|
-
},
|
|
562
|
+
handleInput: (_data: string) => dismiss(),
|
|
477
563
|
dispose: () => {
|
|
478
564
|
dismissed = true;
|
|
479
565
|
clearInterval(interval);
|
|
@@ -487,9 +573,7 @@ export default function powerlineFooter(pi: ExtensionAPI) {
|
|
|
487
573
|
horizontalAlign: "center",
|
|
488
574
|
}),
|
|
489
575
|
},
|
|
490
|
-
).catch(() => {
|
|
491
|
-
|
|
492
|
-
});
|
|
493
|
-
}, 100); // Small delay to let init complete
|
|
576
|
+
).catch(() => {});
|
|
577
|
+
}, 100);
|
|
494
578
|
}
|
|
495
579
|
}
|
package/package.json
CHANGED
package/welcome.ts
CHANGED
|
@@ -16,31 +16,232 @@ export interface LoadedCounts {
|
|
|
16
16
|
promptTemplates: number;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
20
|
+
// Shared rendering utilities
|
|
21
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
22
|
+
|
|
23
|
+
const PI_LOGO = [
|
|
24
|
+
"▀████████████▀",
|
|
25
|
+
" ╘███ ███ ",
|
|
26
|
+
" ███ ███ ",
|
|
27
|
+
" ███ ███ ",
|
|
28
|
+
" ▄███▄ ▄███▄ ",
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const GRADIENT_COLORS = [
|
|
32
|
+
"\x1b[38;5;199m",
|
|
33
|
+
"\x1b[38;5;171m",
|
|
34
|
+
"\x1b[38;5;135m",
|
|
35
|
+
"\x1b[38;5;99m",
|
|
36
|
+
"\x1b[38;5;75m",
|
|
37
|
+
"\x1b[38;5;51m",
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
function bold(text: string): string {
|
|
41
|
+
return `\x1b[1m${text}\x1b[22m`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function dim(text: string): string {
|
|
45
|
+
return getFgAnsiCode("sep") + text + ansi.reset;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function checkmark(): string {
|
|
49
|
+
return fgOnly("gitClean", "✓");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function gradientLine(line: string): string {
|
|
53
|
+
const reset = ansi.reset;
|
|
54
|
+
let result = "";
|
|
55
|
+
let colorIdx = 0;
|
|
56
|
+
const step = Math.max(1, Math.floor(line.length / GRADIENT_COLORS.length));
|
|
57
|
+
|
|
58
|
+
for (let i = 0; i < line.length; i++) {
|
|
59
|
+
if (i > 0 && i % step === 0 && colorIdx < GRADIENT_COLORS.length - 1) colorIdx++;
|
|
60
|
+
const char = line[i];
|
|
61
|
+
if (char !== " ") {
|
|
62
|
+
result += GRADIENT_COLORS[colorIdx] + char + reset;
|
|
63
|
+
} else {
|
|
64
|
+
result += char;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function centerText(text: string, width: number): string {
|
|
71
|
+
const visLen = visibleWidth(text);
|
|
72
|
+
if (visLen > width) return truncateToWidth(text, width);
|
|
73
|
+
if (visLen === width) return text;
|
|
74
|
+
const leftPad = Math.floor((width - visLen) / 2);
|
|
75
|
+
const rightPad = width - visLen - leftPad;
|
|
76
|
+
return " ".repeat(leftPad) + text + " ".repeat(rightPad);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function fitToWidth(str: string, width: number): string {
|
|
80
|
+
const visLen = visibleWidth(str);
|
|
81
|
+
if (visLen > width) return truncateToWidth(str, width);
|
|
82
|
+
return str + " ".repeat(width - visLen);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function truncateToWidth(str: string, width: number): string {
|
|
86
|
+
const ellipsis = "…";
|
|
87
|
+
const maxWidth = Math.max(0, width - 1);
|
|
88
|
+
let truncated = "";
|
|
89
|
+
let currentWidth = 0;
|
|
90
|
+
let inEscape = false;
|
|
91
|
+
|
|
92
|
+
for (const char of str) {
|
|
93
|
+
if (char === "\x1b") inEscape = true;
|
|
94
|
+
if (inEscape) {
|
|
95
|
+
truncated += char;
|
|
96
|
+
if (char === "m") inEscape = false;
|
|
97
|
+
} else if (currentWidth < maxWidth) {
|
|
98
|
+
truncated += char;
|
|
99
|
+
currentWidth++;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (visibleWidth(str) > width) return truncated + ellipsis;
|
|
104
|
+
return truncated;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
interface WelcomeData {
|
|
108
|
+
modelName: string;
|
|
109
|
+
providerName: string;
|
|
110
|
+
recentSessions: RecentSession[];
|
|
111
|
+
loadedCounts: LoadedCounts;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function buildLeftColumn(data: WelcomeData, colWidth: number): string[] {
|
|
115
|
+
const logoColored = PI_LOGO.map((line) => gradientLine(line));
|
|
116
|
+
|
|
117
|
+
return [
|
|
118
|
+
"",
|
|
119
|
+
centerText(bold("Welcome back!"), colWidth),
|
|
120
|
+
"",
|
|
121
|
+
...logoColored.map((l) => centerText(l, colWidth)),
|
|
122
|
+
"",
|
|
123
|
+
centerText(fgOnly("model", data.modelName), colWidth),
|
|
124
|
+
centerText(dim(data.providerName), colWidth),
|
|
125
|
+
];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function buildRightColumn(data: WelcomeData, colWidth: number): string[] {
|
|
129
|
+
const hChar = "─";
|
|
130
|
+
const separator = ` ${dim(hChar.repeat(colWidth - 2))}`;
|
|
131
|
+
|
|
132
|
+
// Session lines
|
|
133
|
+
const sessionLines: string[] = [];
|
|
134
|
+
if (data.recentSessions.length === 0) {
|
|
135
|
+
sessionLines.push(` ${dim("No recent sessions")}`);
|
|
136
|
+
} else {
|
|
137
|
+
for (const session of data.recentSessions.slice(0, 3)) {
|
|
138
|
+
sessionLines.push(
|
|
139
|
+
` ${dim("• ")}${fgOnly("path", session.name)}${dim(` (${session.timeAgo})`)}`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Loaded counts lines
|
|
145
|
+
const countLines: string[] = [];
|
|
146
|
+
const { contextFiles, extensions, skills, promptTemplates } = data.loadedCounts;
|
|
147
|
+
|
|
148
|
+
if (contextFiles > 0 || extensions > 0 || skills > 0 || promptTemplates > 0) {
|
|
149
|
+
if (contextFiles > 0) {
|
|
150
|
+
countLines.push(` ${checkmark()} ${fgOnly("gitClean", `${contextFiles}`)} context file${contextFiles !== 1 ? "s" : ""}`);
|
|
151
|
+
}
|
|
152
|
+
if (extensions > 0) {
|
|
153
|
+
countLines.push(` ${checkmark()} ${fgOnly("gitClean", `${extensions}`)} extension${extensions !== 1 ? "s" : ""}`);
|
|
154
|
+
}
|
|
155
|
+
if (skills > 0) {
|
|
156
|
+
countLines.push(` ${checkmark()} ${fgOnly("gitClean", `${skills}`)} skill${skills !== 1 ? "s" : ""}`);
|
|
157
|
+
}
|
|
158
|
+
if (promptTemplates > 0) {
|
|
159
|
+
countLines.push(` ${checkmark()} ${fgOnly("gitClean", `${promptTemplates}`)} prompt template${promptTemplates !== 1 ? "s" : ""}`);
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
countLines.push(` ${dim("No extensions loaded")}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return [
|
|
166
|
+
` ${bold(fgOnly("accent", "Tips"))}`,
|
|
167
|
+
` ${dim("/")} for commands`,
|
|
168
|
+
` ${dim("!")} to run bash`,
|
|
169
|
+
` ${dim("Shift+Tab")} cycle thinking`,
|
|
170
|
+
separator,
|
|
171
|
+
` ${bold(fgOnly("accent", "Loaded"))}`,
|
|
172
|
+
...countLines,
|
|
173
|
+
separator,
|
|
174
|
+
` ${bold(fgOnly("accent", "Recent sessions"))}`,
|
|
175
|
+
...sessionLines,
|
|
176
|
+
"",
|
|
177
|
+
];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function renderWelcomeBox(
|
|
181
|
+
data: WelcomeData,
|
|
182
|
+
termWidth: number,
|
|
183
|
+
bottomLine: string,
|
|
184
|
+
): string[] {
|
|
185
|
+
const minWidth = 76;
|
|
186
|
+
const maxWidth = 96;
|
|
187
|
+
const boxWidth = Math.max(minWidth, Math.min(termWidth - 2, maxWidth));
|
|
188
|
+
const leftCol = 26;
|
|
189
|
+
const rightCol = boxWidth - leftCol - 3;
|
|
190
|
+
|
|
191
|
+
const hChar = "─";
|
|
192
|
+
const v = dim("│");
|
|
193
|
+
const tl = dim("╭");
|
|
194
|
+
const tr = dim("╮");
|
|
195
|
+
const bl = dim("╰");
|
|
196
|
+
const br = dim("╯");
|
|
197
|
+
|
|
198
|
+
const leftLines = buildLeftColumn(data, leftCol);
|
|
199
|
+
const rightLines = buildRightColumn(data, rightCol);
|
|
200
|
+
|
|
201
|
+
const lines: string[] = [];
|
|
202
|
+
|
|
203
|
+
// Top border with title
|
|
204
|
+
const title = " pi agent ";
|
|
205
|
+
const titlePrefix = dim(hChar.repeat(3));
|
|
206
|
+
const titleStyled = titlePrefix + fgOnly("model", title);
|
|
207
|
+
const titleVisLen = 3 + visibleWidth(title);
|
|
208
|
+
const afterTitle = boxWidth - 2 - titleVisLen;
|
|
209
|
+
const afterTitleText = afterTitle > 0 ? dim(hChar.repeat(afterTitle)) : "";
|
|
210
|
+
lines.push(tl + titleStyled + afterTitleText + tr);
|
|
211
|
+
|
|
212
|
+
// Content rows
|
|
213
|
+
const maxRows = Math.max(leftLines.length, rightLines.length);
|
|
214
|
+
for (let i = 0; i < maxRows; i++) {
|
|
215
|
+
const left = fitToWidth(leftLines[i] ?? "", leftCol);
|
|
216
|
+
const right = fitToWidth(rightLines[i] ?? "", rightCol);
|
|
217
|
+
lines.push(v + left + v + right + v);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Bottom border
|
|
221
|
+
lines.push(bl + bottomLine + br);
|
|
222
|
+
|
|
223
|
+
return lines;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
227
|
+
// Welcome Components
|
|
228
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
229
|
+
|
|
19
230
|
/**
|
|
20
231
|
* Welcome overlay component for pi agent.
|
|
21
232
|
* Displays a branded splash screen with logo, tips, and loaded counts.
|
|
22
|
-
* Includes a countdown timer for auto-dismiss.
|
|
23
233
|
*/
|
|
24
234
|
export class WelcomeComponent implements Component {
|
|
25
|
-
private
|
|
26
|
-
private modelName: string;
|
|
27
|
-
private providerName: string;
|
|
28
|
-
private recentSessions: RecentSession[];
|
|
29
|
-
private loadedCounts: LoadedCounts;
|
|
235
|
+
private data: WelcomeData;
|
|
30
236
|
private countdown: number = 30;
|
|
31
237
|
|
|
32
238
|
constructor(
|
|
33
|
-
version: string,
|
|
34
239
|
modelName: string,
|
|
35
240
|
providerName: string,
|
|
36
241
|
recentSessions: RecentSession[] = [],
|
|
37
242
|
loadedCounts: LoadedCounts = { contextFiles: 0, extensions: 0, skills: 0, promptTemplates: 0 },
|
|
38
243
|
) {
|
|
39
|
-
this.
|
|
40
|
-
this.modelName = modelName;
|
|
41
|
-
this.providerName = providerName;
|
|
42
|
-
this.recentSessions = recentSessions;
|
|
43
|
-
this.loadedCounts = loadedCounts;
|
|
244
|
+
this.data = { modelName, providerName, recentSessions, loadedCounts };
|
|
44
245
|
}
|
|
45
246
|
|
|
46
247
|
setCountdown(seconds: number): void {
|
|
@@ -50,227 +251,63 @@ export class WelcomeComponent implements Component {
|
|
|
50
251
|
invalidate(): void {}
|
|
51
252
|
|
|
52
253
|
render(termWidth: number): string[] {
|
|
53
|
-
// Box dimensions - responsive with min/max
|
|
54
254
|
const minWidth = 76;
|
|
55
255
|
const maxWidth = 96;
|
|
56
256
|
const boxWidth = Math.max(minWidth, Math.min(termWidth - 2, maxWidth));
|
|
57
|
-
const leftCol = 26;
|
|
58
|
-
const rightCol = boxWidth - leftCol - 3; // 3 = │ + │ + │
|
|
59
|
-
|
|
60
|
-
// Block-based PI logo (gradient: magenta → cyan)
|
|
61
|
-
const piLogo = [
|
|
62
|
-
"▀████████████▀",
|
|
63
|
-
" ╘███ ███ ",
|
|
64
|
-
" ███ ███ ",
|
|
65
|
-
" ███ ███ ",
|
|
66
|
-
" ▄███▄ ▄███▄ ",
|
|
67
|
-
];
|
|
68
|
-
|
|
69
|
-
// Apply gradient to logo
|
|
70
|
-
const logoColored = piLogo.map((line) => this.gradientLine(line));
|
|
71
|
-
|
|
72
|
-
// Left column - centered content
|
|
73
|
-
const leftLines = [
|
|
74
|
-
"",
|
|
75
|
-
this.centerText(this.bold("Welcome back!"), leftCol),
|
|
76
|
-
"",
|
|
77
|
-
...logoColored.map((l) => this.centerText(l, leftCol)),
|
|
78
|
-
"",
|
|
79
|
-
this.centerText(fgOnly("model", this.modelName), leftCol),
|
|
80
|
-
this.centerText(this.dim(this.providerName), leftCol),
|
|
81
|
-
];
|
|
82
|
-
|
|
83
|
-
// Right column separator
|
|
84
|
-
const separatorWidth = rightCol - 2;
|
|
85
|
-
const hChar = "─";
|
|
86
|
-
const separator = ` ${this.dim(hChar.repeat(separatorWidth))}`;
|
|
87
|
-
|
|
88
|
-
// Recent sessions content
|
|
89
|
-
const sessionLines: string[] = [];
|
|
90
|
-
if (this.recentSessions.length === 0) {
|
|
91
|
-
sessionLines.push(` ${this.dim("No recent sessions")}`);
|
|
92
|
-
} else {
|
|
93
|
-
for (const session of this.recentSessions.slice(0, 3)) {
|
|
94
|
-
sessionLines.push(
|
|
95
|
-
` ${this.dim("• ")}${fgOnly("path", session.name)}${this.dim(` (${session.timeAgo})`)}`,
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Loaded counts content
|
|
101
|
-
const countLines: string[] = [];
|
|
102
|
-
const { contextFiles, extensions, skills, promptTemplates } = this.loadedCounts;
|
|
103
257
|
|
|
104
|
-
|
|
105
|
-
if (contextFiles > 0) {
|
|
106
|
-
countLines.push(` ${this.checkmark()} ${fgOnly("gitClean", `${contextFiles}`)} context file${contextFiles !== 1 ? "s" : ""}`);
|
|
107
|
-
}
|
|
108
|
-
if (extensions > 0) {
|
|
109
|
-
countLines.push(` ${this.checkmark()} ${fgOnly("gitClean", `${extensions}`)} extension${extensions !== 1 ? "s" : ""}`);
|
|
110
|
-
}
|
|
111
|
-
if (skills > 0) {
|
|
112
|
-
countLines.push(` ${this.checkmark()} ${fgOnly("gitClean", `${skills}`)} skill${skills !== 1 ? "s" : ""}`);
|
|
113
|
-
}
|
|
114
|
-
if (promptTemplates > 0) {
|
|
115
|
-
countLines.push(` ${this.checkmark()} ${fgOnly("gitClean", `${promptTemplates}`)} prompt template${promptTemplates !== 1 ? "s" : ""}`);
|
|
116
|
-
}
|
|
117
|
-
} else {
|
|
118
|
-
countLines.push(` ${this.dim("No extensions loaded")}`);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Right column
|
|
122
|
-
const rightLines = [
|
|
123
|
-
` ${this.bold(fgOnly("accent", "Tips"))}`,
|
|
124
|
-
` ${this.dim("/")} for commands`,
|
|
125
|
-
` ${this.dim("!")} to run bash`,
|
|
126
|
-
` ${this.dim("Shift+Tab")} cycle thinking`,
|
|
127
|
-
separator,
|
|
128
|
-
` ${this.bold(fgOnly("accent", "Loaded"))}`,
|
|
129
|
-
...countLines,
|
|
130
|
-
separator,
|
|
131
|
-
` ${this.bold(fgOnly("accent", "Recent sessions"))}`,
|
|
132
|
-
...sessionLines,
|
|
133
|
-
"",
|
|
134
|
-
];
|
|
135
|
-
|
|
136
|
-
// Border characters (dim)
|
|
137
|
-
const v = this.dim("│");
|
|
138
|
-
const tl = this.dim("╭");
|
|
139
|
-
const tr = this.dim("╮");
|
|
140
|
-
const bl = this.dim("╰");
|
|
141
|
-
const br = this.dim("╯");
|
|
142
|
-
|
|
143
|
-
const lines: string[] = [];
|
|
144
|
-
|
|
145
|
-
// Top border with embedded title
|
|
146
|
-
const title = ` pi agent v${this.version} `;
|
|
147
|
-
const titlePrefix = this.dim(hChar.repeat(3));
|
|
148
|
-
const titleStyled = titlePrefix + fgOnly("model", title);
|
|
149
|
-
const titleVisLen = 3 + visibleWidth(title);
|
|
150
|
-
const afterTitle = boxWidth - 2 - titleVisLen;
|
|
151
|
-
const afterTitleText = afterTitle > 0 ? this.dim(hChar.repeat(afterTitle)) : "";
|
|
152
|
-
lines.push(tl + titleStyled + afterTitleText + tr);
|
|
153
|
-
|
|
154
|
-
// Content rows
|
|
155
|
-
const maxRows = Math.max(leftLines.length, rightLines.length);
|
|
156
|
-
for (let i = 0; i < maxRows; i++) {
|
|
157
|
-
const left = this.fitToWidth(leftLines[i] ?? "", leftCol);
|
|
158
|
-
const right = this.fitToWidth(rightLines[i] ?? "", rightCol);
|
|
159
|
-
lines.push(v + left + v + right + v);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Bottom border with countdown
|
|
258
|
+
// Bottom line with countdown
|
|
163
259
|
const countdownText = ` Press any key to continue (${this.countdown}s) `;
|
|
164
|
-
const countdownStyled =
|
|
165
|
-
const bottomContentWidth = boxWidth - 2;
|
|
260
|
+
const countdownStyled = dim(countdownText);
|
|
261
|
+
const bottomContentWidth = boxWidth - 2;
|
|
166
262
|
const countdownVisLen = visibleWidth(countdownText);
|
|
167
263
|
const leftPad = Math.floor((bottomContentWidth - countdownVisLen) / 2);
|
|
168
264
|
const rightPad = bottomContentWidth - countdownVisLen - leftPad;
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
bl +
|
|
172
|
-
this.dim(hChar.repeat(Math.max(0, leftPad))) +
|
|
265
|
+
const hChar = "─";
|
|
266
|
+
const bottomLine = dim(hChar.repeat(Math.max(0, leftPad))) +
|
|
173
267
|
countdownStyled +
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
);
|
|
177
|
-
|
|
178
|
-
return lines;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
private bold(text: string): string {
|
|
182
|
-
return `\x1b[1m${text}\x1b[22m`;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
private dim(text: string): string {
|
|
186
|
-
return getFgAnsiCode("sep") + text + ansi.reset;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
private checkmark(): string {
|
|
190
|
-
return fgOnly("gitClean", "✓");
|
|
268
|
+
dim(hChar.repeat(Math.max(0, rightPad)));
|
|
269
|
+
|
|
270
|
+
return renderWelcomeBox(this.data, termWidth, bottomLine);
|
|
191
271
|
}
|
|
272
|
+
}
|
|
192
273
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if (visLen === width) {
|
|
200
|
-
return text; // Exact fit, no centering needed
|
|
201
|
-
}
|
|
202
|
-
const leftPad = Math.floor((width - visLen) / 2);
|
|
203
|
-
const rightPad = width - visLen - leftPad;
|
|
204
|
-
return " ".repeat(leftPad) + text + " ".repeat(rightPad);
|
|
205
|
-
}
|
|
274
|
+
/**
|
|
275
|
+
* Welcome header - same layout as overlay but persistent (no countdown).
|
|
276
|
+
* Used when quietStartup: true.
|
|
277
|
+
*/
|
|
278
|
+
export class WelcomeHeader implements Component {
|
|
279
|
+
private data: WelcomeData;
|
|
206
280
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
"\x1b[38;5;75m", // cyan-blue
|
|
215
|
-
"\x1b[38;5;51m", // bright cyan
|
|
216
|
-
];
|
|
217
|
-
const reset = ansi.reset;
|
|
218
|
-
|
|
219
|
-
let result = "";
|
|
220
|
-
let colorIdx = 0;
|
|
221
|
-
const step = Math.max(1, Math.floor(line.length / colors.length));
|
|
222
|
-
|
|
223
|
-
for (let i = 0; i < line.length; i++) {
|
|
224
|
-
if (i > 0 && i % step === 0 && colorIdx < colors.length - 1) {
|
|
225
|
-
colorIdx++;
|
|
226
|
-
}
|
|
227
|
-
const char = line[i];
|
|
228
|
-
if (char !== " ") {
|
|
229
|
-
result += colors[colorIdx] + char + reset;
|
|
230
|
-
} else {
|
|
231
|
-
result += char;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
return result;
|
|
281
|
+
constructor(
|
|
282
|
+
modelName: string,
|
|
283
|
+
providerName: string,
|
|
284
|
+
recentSessions: RecentSession[] = [],
|
|
285
|
+
loadedCounts: LoadedCounts = { contextFiles: 0, extensions: 0, skills: 0, promptTemplates: 0 },
|
|
286
|
+
) {
|
|
287
|
+
this.data = { modelName, providerName, recentSessions, loadedCounts };
|
|
235
288
|
}
|
|
236
289
|
|
|
237
|
-
|
|
238
|
-
private fitToWidth(str: string, width: number): string {
|
|
239
|
-
const visLen = visibleWidth(str);
|
|
240
|
-
if (visLen > width) {
|
|
241
|
-
return this.truncateToWidth(str, width);
|
|
242
|
-
}
|
|
243
|
-
return str + " ".repeat(width - visLen);
|
|
244
|
-
}
|
|
290
|
+
invalidate(): void {}
|
|
245
291
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
const
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
let currentWidth = 0;
|
|
252
|
-
let inEscape = false;
|
|
292
|
+
render(termWidth: number): string[] {
|
|
293
|
+
const minWidth = 76;
|
|
294
|
+
const maxWidth = 96;
|
|
295
|
+
const boxWidth = Math.max(minWidth, Math.min(termWidth - 2, maxWidth));
|
|
296
|
+
const hChar = "─";
|
|
253
297
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
if (char === "m") inEscape = false;
|
|
259
|
-
} else if (currentWidth < maxWidth) {
|
|
260
|
-
truncated += char;
|
|
261
|
-
currentWidth++;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
298
|
+
// Bottom line with column separator (leftCol=26, rightCol=boxWidth-29)
|
|
299
|
+
const leftCol = 26;
|
|
300
|
+
const rightCol = boxWidth - leftCol - 3;
|
|
301
|
+
const bottomLine = dim(hChar.repeat(leftCol)) + dim("┴") + dim(hChar.repeat(rightCol));
|
|
264
302
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
return truncated;
|
|
303
|
+
const lines = renderWelcomeBox(this.data, termWidth, bottomLine);
|
|
304
|
+
lines.push(""); // Add empty line for spacing
|
|
305
|
+
return lines;
|
|
269
306
|
}
|
|
270
307
|
}
|
|
271
308
|
|
|
272
309
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
273
|
-
// Discovery
|
|
310
|
+
// Discovery functions
|
|
274
311
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
275
312
|
|
|
276
313
|
/**
|
|
@@ -285,7 +322,6 @@ export function discoverLoadedCounts(): LoadedCounts {
|
|
|
285
322
|
let skills = 0;
|
|
286
323
|
let promptTemplates = 0;
|
|
287
324
|
|
|
288
|
-
// Count AGENTS.md context files (check all locations pi-mono supports)
|
|
289
325
|
const agentsMdPaths = [
|
|
290
326
|
join(homeDir, ".pi", "agent", "AGENTS.md"),
|
|
291
327
|
join(homeDir, ".claude", "AGENTS.md"),
|
|
@@ -295,12 +331,9 @@ export function discoverLoadedCounts(): LoadedCounts {
|
|
|
295
331
|
];
|
|
296
332
|
|
|
297
333
|
for (const path of agentsMdPaths) {
|
|
298
|
-
if (existsSync(path))
|
|
299
|
-
contextFiles++;
|
|
300
|
-
}
|
|
334
|
+
if (existsSync(path)) contextFiles++;
|
|
301
335
|
}
|
|
302
336
|
|
|
303
|
-
// Count extensions - both standalone .ts files and directories with index.ts
|
|
304
337
|
const extensionDirs = [
|
|
305
338
|
join(homeDir, ".pi", "agent", "extensions"),
|
|
306
339
|
join(cwd, "extensions"),
|
|
@@ -318,7 +351,6 @@ export function discoverLoadedCounts(): LoadedCounts {
|
|
|
318
351
|
const stats = statSync(entryPath);
|
|
319
352
|
|
|
320
353
|
if (stats.isDirectory()) {
|
|
321
|
-
// Directory extension - check for index.ts or package.json
|
|
322
354
|
if (existsSync(join(entryPath, "index.ts")) || existsSync(join(entryPath, "package.json"))) {
|
|
323
355
|
if (!countedExtensions.has(entry)) {
|
|
324
356
|
countedExtensions.add(entry);
|
|
@@ -326,7 +358,6 @@ export function discoverLoadedCounts(): LoadedCounts {
|
|
|
326
358
|
}
|
|
327
359
|
}
|
|
328
360
|
} else if (entry.endsWith(".ts") && !entry.startsWith(".")) {
|
|
329
|
-
// Standalone .ts file extension
|
|
330
361
|
const name = basename(entry, ".ts");
|
|
331
362
|
if (!countedExtensions.has(name)) {
|
|
332
363
|
countedExtensions.add(name);
|
|
@@ -338,7 +369,6 @@ export function discoverLoadedCounts(): LoadedCounts {
|
|
|
338
369
|
}
|
|
339
370
|
}
|
|
340
371
|
|
|
341
|
-
// Count skills
|
|
342
372
|
const skillDirs = [
|
|
343
373
|
join(homeDir, ".pi", "agent", "skills"),
|
|
344
374
|
join(cwd, ".pi", "skills"),
|
|
@@ -355,7 +385,6 @@ export function discoverLoadedCounts(): LoadedCounts {
|
|
|
355
385
|
const entryPath = join(dir, entry);
|
|
356
386
|
try {
|
|
357
387
|
if (statSync(entryPath).isDirectory()) {
|
|
358
|
-
// Check for SKILL.md
|
|
359
388
|
if (existsSync(join(entryPath, "SKILL.md"))) {
|
|
360
389
|
if (!countedSkills.has(entry)) {
|
|
361
390
|
countedSkills.add(entry);
|
|
@@ -369,7 +398,6 @@ export function discoverLoadedCounts(): LoadedCounts {
|
|
|
369
398
|
}
|
|
370
399
|
}
|
|
371
400
|
|
|
372
|
-
// Count prompt templates (slash commands) - recursively find .md files
|
|
373
401
|
const templateDirs = [
|
|
374
402
|
join(homeDir, ".pi", "agent", "commands"),
|
|
375
403
|
join(homeDir, ".claude", "commands"),
|
|
@@ -388,7 +416,6 @@ export function discoverLoadedCounts(): LoadedCounts {
|
|
|
388
416
|
try {
|
|
389
417
|
const stats = statSync(entryPath);
|
|
390
418
|
if (stats.isDirectory()) {
|
|
391
|
-
// Recurse into subdirectories
|
|
392
419
|
countTemplatesInDir(entryPath);
|
|
393
420
|
} else if (entry.endsWith(".md")) {
|
|
394
421
|
const name = basename(entry, ".md");
|
|
@@ -411,12 +438,10 @@ export function discoverLoadedCounts(): LoadedCounts {
|
|
|
411
438
|
|
|
412
439
|
/**
|
|
413
440
|
* Get recent sessions from the sessions directory.
|
|
414
|
-
* pi-mono stores sessions in subdirectories: ~/.pi/agent/sessions/<project-path>/*.jsonl
|
|
415
441
|
*/
|
|
416
442
|
export function getRecentSessions(maxCount: number = 3): RecentSession[] {
|
|
417
443
|
const homeDir = process.env.HOME || process.env.USERPROFILE || "";
|
|
418
444
|
|
|
419
|
-
// Try multiple possible session directories (pi-mono uses ~/.pi/agent/sessions/)
|
|
420
445
|
const sessionsDirs = [
|
|
421
446
|
join(homeDir, ".pi", "agent", "sessions"),
|
|
422
447
|
join(homeDir, ".pi", "sessions"),
|
|
@@ -433,15 +458,11 @@ export function getRecentSessions(maxCount: number = 3): RecentSession[] {
|
|
|
433
458
|
try {
|
|
434
459
|
const stats = statSync(entryPath);
|
|
435
460
|
if (stats.isDirectory()) {
|
|
436
|
-
// Recurse into subdirectories (project folders)
|
|
437
461
|
scanDir(entryPath);
|
|
438
462
|
} else if (entry.endsWith(".jsonl")) {
|
|
439
|
-
// Extract project name from parent directory
|
|
440
463
|
const parentName = basename(dir);
|
|
441
|
-
// Clean up the directory name (it's URL-encoded path like --Users-nicobailon-...)
|
|
442
464
|
let projectName = parentName;
|
|
443
465
|
if (parentName.startsWith("--")) {
|
|
444
|
-
// Extract last path segment
|
|
445
466
|
const parts = parentName.split("-").filter(p => p);
|
|
446
467
|
projectName = parts[parts.length - 1] || parentName;
|
|
447
468
|
}
|
|
@@ -458,7 +479,6 @@ export function getRecentSessions(maxCount: number = 3): RecentSession[] {
|
|
|
458
479
|
|
|
459
480
|
if (sessions.length === 0) return [];
|
|
460
481
|
|
|
461
|
-
// Sort by modification time (newest first) and deduplicate by name
|
|
462
482
|
sessions.sort((a, b) => b.mtime - a.mtime);
|
|
463
483
|
|
|
464
484
|
const seen = new Set<string>();
|
|
@@ -470,7 +490,6 @@ export function getRecentSessions(maxCount: number = 3): RecentSession[] {
|
|
|
470
490
|
}
|
|
471
491
|
}
|
|
472
492
|
|
|
473
|
-
// Format time ago
|
|
474
493
|
const now = Date.now();
|
|
475
494
|
return uniqueSessions.slice(0, maxCount).map(s => ({
|
|
476
495
|
name: s.name.length > 20 ? s.name.slice(0, 17) + "…" : s.name,
|