@tt-a1i/openpi 0.3.1 → 0.5.0
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 +184 -59
- package/SETUP.md +23 -7
- package/assets/openpi-launch-card-v1.webp +0 -0
- package/bin/openpi.js +145 -0
- package/extensions/ask-user/index.ts +30 -14
- package/extensions/background-terminals/index.ts +30 -2
- package/extensions/background-terminals/src/domain.ts +2 -0
- package/extensions/background-terminals/src/manager.ts +486 -106
- package/extensions/background-terminals/src/output.ts +33 -0
- package/extensions/background-terminals/src/prompt.ts +14 -6
- package/extensions/background-terminals/src/result-delivery.ts +4 -1
- package/extensions/background-terminals/src/ui/ps.ts +132 -129
- package/extensions/capabilities/index.ts +30 -42
- package/extensions/capabilities/src/ui.ts +93 -0
- package/extensions/clear-context/index.ts +83 -0
- package/extensions/context-pivot/index.ts +16 -6
- package/extensions/cron/schedule.ts +7 -1
- package/extensions/file-mutation-display/index.ts +34 -76
- package/extensions/file-mutation-display/render.ts +146 -87
- package/extensions/file-search/index.ts +8 -7
- package/extensions/file-search/src/binaries.ts +75 -59
- package/extensions/git-info/src/changed-files-view.ts +47 -14
- package/extensions/git-read/index.ts +328 -0
- package/extensions/git-read/src/args.ts +171 -0
- package/extensions/git-read/src/process.ts +81 -0
- package/extensions/git-read/src/prompt.ts +56 -0
- package/extensions/model-info/index.ts +21 -33
- package/extensions/model-info/session-metrics.ts +96 -0
- package/extensions/plan-mode/bash-policy.ts +54 -9
- package/extensions/plan-mode/index.ts +7 -2
- package/extensions/post-edit/index.ts +16 -6
- package/extensions/sessions/git-stats.ts +258 -72
- package/extensions/sessions/index.ts +222 -140
- package/extensions/sessions/preview-cache.ts +104 -0
- package/extensions/sessions/preview-loader.ts +856 -0
- package/extensions/sessions/sessions.ts +43 -4
- package/extensions/setup/index.ts +127 -131
- package/extensions/shared/activity-status.ts +36 -5
- package/extensions/shared/agent-session-page.ts +319 -0
- package/extensions/shared/agent-tool-renderer.ts +218 -0
- package/extensions/shared/agent-transcript.ts +524 -0
- package/extensions/shared/below-editor-navigation.ts +26 -0
- package/extensions/shared/capability-intent.ts +53 -0
- package/extensions/shared/child-session.ts +444 -22
- package/extensions/shared/result-budget.ts +134 -0
- package/extensions/shared/result-delivery.ts +34 -0
- package/extensions/shared/screen-chrome.ts +133 -0
- package/extensions/shared/setup-config.ts +97 -38
- package/extensions/shared/setup-episode-state.ts +1 -1
- package/extensions/shared/spinner.ts +28 -0
- package/extensions/shared/terminal-text.ts +110 -23
- package/extensions/shared/text-projection.ts +113 -0
- package/extensions/shared/tool-activity.ts +382 -0
- package/extensions/shared/tool-surface.ts +42 -8
- package/extensions/shared/transcript-viewport.ts +46 -0
- package/extensions/shared/web-observer-registry.ts +390 -0
- package/extensions/shared/worktree.ts +11 -0
- package/extensions/subagents/index.ts +461 -186
- package/extensions/subagents/navigation.ts +86 -28
- package/extensions/subagents/src/agent-types.ts +37 -15
- package/extensions/subagents/src/backend.ts +12 -1
- package/extensions/subagents/src/backends/pi.ts +375 -66
- package/extensions/subagents/src/domain.ts +5 -0
- package/extensions/subagents/src/id-sequence.ts +84 -0
- package/extensions/subagents/src/manager.ts +651 -536
- package/extensions/subagents/src/prompt.ts +185 -42
- package/extensions/subagents/src/result-artifact.ts +146 -0
- package/extensions/subagents/src/result-delivery.ts +7 -1
- package/extensions/subagents/src/runtime.ts +23 -6
- package/extensions/subagents/src/ui/takeover.ts +128 -337
- package/extensions/subagents/src/ui/transcript.ts +38 -501
- package/extensions/subagents/src/ui/wait-result.ts +103 -15
- package/extensions/suggestions/src/ui.ts +10 -4
- package/extensions/tasks/index.ts +0 -3
- package/extensions/tasks/ui.ts +79 -62
- package/extensions/ui-customization/footer.ts +7 -44
- package/extensions/ui-customization/index.ts +0 -4
- package/extensions/user-input-fold/index.ts +185 -0
- package/extensions/web/index.ts +234 -0
- package/extensions/workflows/artifacts.ts +147 -22
- package/extensions/workflows/completion-projection.ts +457 -0
- package/extensions/workflows/controller.ts +14 -2
- package/extensions/workflows/coordinator.ts +62 -0
- package/extensions/workflows/dashboard.ts +458 -339
- package/extensions/workflows/handoff.ts +121 -25
- package/extensions/workflows/index.ts +1042 -492
- package/extensions/workflows/journal.ts +148 -13
- package/extensions/workflows/model.ts +131 -19
- package/extensions/workflows/navigation.ts +61 -18
- package/extensions/workflows/progress-projection.ts +306 -0
- package/extensions/workflows/prompt.ts +166 -10
- package/extensions/workflows/replay-safety.ts +58 -27
- package/extensions/workflows/result-delivery.ts +253 -0
- package/extensions/workflows/retention.ts +593 -0
- package/extensions/workflows/runner.ts +388 -279
- package/extensions/workflows/sandbox-child.cjs +36 -3
- package/extensions/workflows/sandbox.ts +62 -8
- package/extensions/workflows/serialization.ts +325 -17
- package/extensions/workflows/tool-renderer.ts +22 -0
- package/extensions/workflows/transcript.ts +149 -0
- package/extensions/workspace-cleanup-guard/index.ts +54 -0
- package/extensions/workspace-cleanup-guard/workspace-provenance.ts +563 -0
- package/package.json +28 -8
- package/skills/subagents/REFERENCE.md +189 -0
- package/skills/subagents/SKILL.md +2 -2
- package/skills/workflows/REFERENCE.md +10 -5
- package/skills/workflows/SKILL.md +53 -10
- package/web/adapter/pi-adapter.ts +661 -0
- package/web/host/browser-launcher.ts +20 -0
- package/web/host/static-assets.ts +4 -0
- package/web/host/terminal-status.ts +38 -0
- package/web/host/web-host.ts +789 -0
- package/web/http-dispatcher.ts +125 -0
- package/web/protocol/types.ts +462 -0
- package/web/runtime/pi-runtime.ts +991 -0
- package/web/runtime/types.ts +71 -0
- package/web/runtime/web-host-lease.ts +497 -0
- package/web/trace.ts +18 -0
- package/web/ui/app.js +1398 -0
- package/web/ui/index.html +139 -0
- package/web/ui/styles.css +598 -0
- package/web/vite.config.mjs +34 -0
- package/extensions/execution-convergence/active-evidence.ts +0 -129
- package/extensions/execution-convergence/index.ts +0 -442
- package/extensions/execution-convergence/workspace-provenance.ts +0 -338
- package/extensions/setup/intercom-fs-helper.cjs +0 -130
- package/extensions/setup/intercom.ts +0 -603
- package/extensions/subagents/src/backends/stub.ts +0 -296
- package/extensions/subagents/src/format.ts +0 -48
|
@@ -5,22 +5,36 @@ import type {
|
|
|
5
5
|
} from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import {
|
|
7
7
|
DynamicBorder,
|
|
8
|
-
SessionManager,
|
|
9
8
|
keyHint,
|
|
9
|
+
SessionManager,
|
|
10
10
|
} from "@earendil-works/pi-coding-agent";
|
|
11
11
|
import {
|
|
12
12
|
CancellableLoader,
|
|
13
13
|
Container,
|
|
14
14
|
Key,
|
|
15
|
-
|
|
15
|
+
matchesKey,
|
|
16
16
|
type SelectItem,
|
|
17
|
+
SelectList,
|
|
17
18
|
Spacer,
|
|
18
19
|
Text,
|
|
19
|
-
matchesKey,
|
|
20
20
|
truncateToWidth,
|
|
21
21
|
visibleWidth,
|
|
22
22
|
wrapTextWithAnsi,
|
|
23
23
|
} from "@earendil-works/pi-tui";
|
|
24
|
+
import { hintLine } from "../shared/screen-chrome.ts";
|
|
25
|
+
import {
|
|
26
|
+
createSessionStatsLoader,
|
|
27
|
+
type SessionStatsState,
|
|
28
|
+
} from "./git-stats.js";
|
|
29
|
+
import {
|
|
30
|
+
createSessionPreviewCache,
|
|
31
|
+
measureSessionPreviewBytes,
|
|
32
|
+
previewCacheKey,
|
|
33
|
+
} from "./preview-cache.js";
|
|
34
|
+
import {
|
|
35
|
+
loadSessionPreviewData,
|
|
36
|
+
readPreviewFileIdentity,
|
|
37
|
+
} from "./preview-loader.js";
|
|
24
38
|
import {
|
|
25
39
|
buildPreviewError,
|
|
26
40
|
buildSessionDescription,
|
|
@@ -28,14 +42,15 @@ import {
|
|
|
28
42
|
buildSessionPreview,
|
|
29
43
|
buildSessionSearchEntries,
|
|
30
44
|
filterSessionEntries,
|
|
45
|
+
formatRelativeTime,
|
|
31
46
|
getSessionPaneLayout,
|
|
32
|
-
parseLimit,
|
|
33
47
|
type PreviewBlock,
|
|
48
|
+
type PreviewMessageLike,
|
|
49
|
+
parseLimit,
|
|
50
|
+
selectSessionStatsWindow,
|
|
34
51
|
type SessionInfoLike,
|
|
35
52
|
type SessionPreview,
|
|
36
|
-
formatRelativeTime,
|
|
37
53
|
} from "./sessions.js";
|
|
38
|
-
import { createSessionStatsLoader, type SessionStats } from "./git-stats.js";
|
|
39
54
|
|
|
40
55
|
const DEFAULT_VISIBLE = 12;
|
|
41
56
|
const SNIPPET_MAX = 60;
|
|
@@ -82,7 +97,7 @@ const themeText = (
|
|
|
82
97
|
): string => {
|
|
83
98
|
if (kind === "title") return theme.fg("accent", theme.bold(text));
|
|
84
99
|
if (kind === "subtitle") return theme.fg("dim", text);
|
|
85
|
-
if (kind === "rule") return theme.fg("
|
|
100
|
+
if (kind === "rule") return theme.fg("borderMuted", text);
|
|
86
101
|
if (kind === "user") return theme.fg("accent", theme.bold(text));
|
|
87
102
|
if (kind === "assistant") return theme.fg("warning", theme.bold(text));
|
|
88
103
|
if (kind === "tool") return theme.fg("muted", theme.bold(text));
|
|
@@ -290,20 +305,16 @@ const renderPreviewBlock = (
|
|
|
290
305
|
return renderTextBlock(`◇ ${block.label}`, block.text, width, theme, "muted");
|
|
291
306
|
};
|
|
292
307
|
|
|
293
|
-
/** Cached across picker openings, while the loader bounds process-wide Git work. */
|
|
294
|
-
const statsLoader = createSessionStatsLoader();
|
|
295
|
-
const statsCache = statsLoader.cache;
|
|
296
|
-
|
|
297
308
|
const formatItemLabel = (
|
|
298
309
|
session: SessionInfoLike,
|
|
299
310
|
width: number,
|
|
300
311
|
theme: any,
|
|
301
|
-
|
|
312
|
+
statsState: SessionStatsState | undefined,
|
|
302
313
|
): string => {
|
|
303
314
|
const itemWidth = width - 4;
|
|
304
315
|
const timeStr = formatRelativeTime(session.modified);
|
|
305
316
|
|
|
306
|
-
const stats =
|
|
317
|
+
const stats = statsState?.status === "ready" ? statsState.stats : undefined;
|
|
307
318
|
let statsStr = "";
|
|
308
319
|
let statsLen = 0;
|
|
309
320
|
if (stats && (stats.add > 0 || stats.mod > 0 || stats.del > 0)) {
|
|
@@ -318,6 +329,12 @@ const formatItemLabel = (
|
|
|
318
329
|
(stats.del > 0 ? `${stats.del}`.length + 1 : 0) +
|
|
319
330
|
parts.length -
|
|
320
331
|
1;
|
|
332
|
+
} else if (statsState?.status === "pending") {
|
|
333
|
+
statsStr = theme.fg("dim", "…");
|
|
334
|
+
statsLen = 1;
|
|
335
|
+
} else if (statsState?.status === "unavailable") {
|
|
336
|
+
statsStr = theme.fg("warning", "?");
|
|
337
|
+
statsLen = 1;
|
|
321
338
|
}
|
|
322
339
|
|
|
323
340
|
const title = buildSessionLabel(session);
|
|
@@ -357,31 +374,20 @@ const renderPreview = (
|
|
|
357
374
|
): { lines: string[]; totalLines: number; maxScroll: number } => {
|
|
358
375
|
const raw: string[] = [];
|
|
359
376
|
|
|
377
|
+
// Left-aligned, like every other OpenPI heading: centred titles read as a
|
|
378
|
+
// different application than the panel they sit inside, and they wander as
|
|
379
|
+
// the pane resizes.
|
|
360
380
|
if (!preview) {
|
|
361
|
-
raw.push(
|
|
362
|
-
|
|
363
|
-
themeText(theme, "title", "Preview"),
|
|
364
|
-
);
|
|
365
|
-
raw.push(
|
|
366
|
-
" ".repeat(Math.max(0, Math.floor((width - 25) / 2))) +
|
|
367
|
-
themeText(theme, "subtitle", "Loading selected session…"),
|
|
368
|
-
);
|
|
381
|
+
raw.push(themeText(theme, "title", "Preview"));
|
|
382
|
+
raw.push(themeText(theme, "subtitle", "Loading selected session…"));
|
|
369
383
|
} else {
|
|
370
|
-
const titleStr = "Thread
|
|
371
|
-
|
|
384
|
+
const titleStr = "Thread preview";
|
|
385
|
+
raw.push(
|
|
372
386
|
focus === "preview"
|
|
373
387
|
? themeText(theme, "title", titleStr)
|
|
374
|
-
: themeText(theme, "subtitle", titleStr)
|
|
375
|
-
const titlePadding = " ".repeat(
|
|
376
|
-
Math.max(0, Math.floor((width - titleStr.length) / 2)),
|
|
388
|
+
: themeText(theme, "subtitle", titleStr),
|
|
377
389
|
);
|
|
378
|
-
raw.push(
|
|
379
|
-
|
|
380
|
-
const subStr = preview.subtitle;
|
|
381
|
-
const subPadding = " ".repeat(
|
|
382
|
-
Math.max(0, Math.floor((width - visibleWidth(subStr)) / 2)),
|
|
383
|
-
);
|
|
384
|
-
raw.push(`${subPadding}${themeText(theme, "subtitle", subStr)}`);
|
|
390
|
+
raw.push(themeText(theme, "subtitle", preview.subtitle));
|
|
385
391
|
|
|
386
392
|
raw.push(themeText(theme, "rule", "─".repeat(Math.max(0, width))));
|
|
387
393
|
|
|
@@ -438,10 +444,10 @@ const renderPreview = (
|
|
|
438
444
|
if (i >= thumbStart && i < thumbStart + thumbSize) {
|
|
439
445
|
scrollChar = theme.fg("text", "█");
|
|
440
446
|
} else {
|
|
441
|
-
scrollChar = theme.fg("
|
|
447
|
+
scrollChar = theme.fg("borderMuted", "│");
|
|
442
448
|
}
|
|
443
449
|
} else {
|
|
444
|
-
scrollChar = theme.fg("
|
|
450
|
+
scrollChar = theme.fg("borderMuted", "│");
|
|
445
451
|
}
|
|
446
452
|
|
|
447
453
|
visible.push(padAnsiRight(line, width - 1) + scrollChar);
|
|
@@ -452,14 +458,28 @@ const renderPreview = (
|
|
|
452
458
|
|
|
453
459
|
const loadSessionPreview = async (
|
|
454
460
|
session: SessionInfoLike,
|
|
461
|
+
cache: ReturnType<typeof createSessionPreviewCache>,
|
|
462
|
+
signal: AbortSignal,
|
|
455
463
|
): Promise<SessionPreview> => {
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
464
|
+
const initialIdentity = await readPreviewFileIdentity(session.path);
|
|
465
|
+
const cached = cache.get(previewCacheKey(initialIdentity));
|
|
466
|
+
if (cached) return cached;
|
|
467
|
+
|
|
468
|
+
const data = await loadSessionPreviewData(session.path, { signal });
|
|
469
|
+
const preview = buildSessionPreview(
|
|
470
|
+
session,
|
|
471
|
+
data.messages as PreviewMessageLike[],
|
|
472
|
+
{
|
|
473
|
+
totalMessages: data.totalMessages,
|
|
474
|
+
truncatedBytes: data.truncatedBytes,
|
|
475
|
+
},
|
|
476
|
+
);
|
|
477
|
+
cache.set(
|
|
478
|
+
previewCacheKey(data.identity),
|
|
479
|
+
preview,
|
|
480
|
+
measureSessionPreviewBytes(preview),
|
|
481
|
+
);
|
|
482
|
+
return preview;
|
|
463
483
|
};
|
|
464
484
|
|
|
465
485
|
async function listSessions(
|
|
@@ -474,7 +494,9 @@ async function listSessions(
|
|
|
474
494
|
const sessions = await ctx.ui.custom<SessionInfoLike[] | null>(
|
|
475
495
|
(tui, theme, _kb, done) => {
|
|
476
496
|
const container = new Container();
|
|
477
|
-
|
|
497
|
+
// Same tone as the split-pane frame and every other OpenPI panel; this
|
|
498
|
+
// was the last `border` call left in the package.
|
|
499
|
+
const borderColor = (text: string) => theme.fg("borderMuted", text);
|
|
478
500
|
|
|
479
501
|
const loader = new CancellableLoader(
|
|
480
502
|
tui,
|
|
@@ -559,12 +581,17 @@ async function showSessionPicker(
|
|
|
559
581
|
let previewScrollOffset = 0;
|
|
560
582
|
let toolsExpanded = false;
|
|
561
583
|
let thinkingVisible = false;
|
|
562
|
-
const previewCache =
|
|
584
|
+
const previewCache = createSessionPreviewCache();
|
|
563
585
|
let activePreview: SessionPreview | undefined;
|
|
564
586
|
let previewTimer: ReturnType<typeof setTimeout> | undefined;
|
|
587
|
+
let previewController: AbortController | undefined;
|
|
565
588
|
let previewSeq = 0;
|
|
566
|
-
|
|
567
|
-
|
|
589
|
+
// Stats use a picker-scoped, per-query first-observation cache. Rows that
|
|
590
|
+
// become visible later may observe later repository state; reopening the
|
|
591
|
+
// picker refreshes every row without polling repositories while idle.
|
|
592
|
+
const statsLoader = createSessionStatsLoader();
|
|
593
|
+
let statsUniverseVersion = 0;
|
|
594
|
+
let statsWindowSignature = "";
|
|
568
595
|
let workspaceLoadSeq = 0;
|
|
569
596
|
let disposed = false;
|
|
570
597
|
let settled = false;
|
|
@@ -573,14 +600,22 @@ async function showSessionPicker(
|
|
|
573
600
|
let showAllWorkspaces = false;
|
|
574
601
|
let isLoading = false;
|
|
575
602
|
|
|
603
|
+
const cancelPreviewLoad = () => {
|
|
604
|
+
previewSeq++;
|
|
605
|
+
if (previewTimer) clearTimeout(previewTimer);
|
|
606
|
+
previewTimer = undefined;
|
|
607
|
+
previewController?.abort();
|
|
608
|
+
previewController = undefined;
|
|
609
|
+
};
|
|
610
|
+
|
|
576
611
|
const disposePicker = () => {
|
|
577
612
|
if (disposed) return;
|
|
578
613
|
disposed = true;
|
|
579
614
|
workspaceLoadSeq++;
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
615
|
+
statsLoader.cancel();
|
|
616
|
+
cancelPreviewLoad();
|
|
617
|
+
previewCache.clear();
|
|
618
|
+
activePreview = undefined;
|
|
584
619
|
};
|
|
585
620
|
const finishPicker = (result: SessionInfoLike | null) => {
|
|
586
621
|
if (settled) return;
|
|
@@ -588,30 +623,29 @@ async function showSessionPicker(
|
|
|
588
623
|
disposePicker();
|
|
589
624
|
done(result);
|
|
590
625
|
};
|
|
591
|
-
const
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
if (disposed || signal.aborted || generation !== statsGeneration)
|
|
605
|
-
return;
|
|
606
|
-
tui.requestRender();
|
|
626
|
+
const ensureStatsWindowLoad = (visibleCount: number) => {
|
|
627
|
+
const targets = selectSessionStatsWindow(
|
|
628
|
+
filteredEntries.map((entry) => entry.session),
|
|
629
|
+
selectedPath,
|
|
630
|
+
visibleCount,
|
|
631
|
+
);
|
|
632
|
+
const signature = `${statsUniverseVersion}:${targets
|
|
633
|
+
.map((session) => session.path)
|
|
634
|
+
.join("\0")}`;
|
|
635
|
+
if (signature === statsWindowSignature) return;
|
|
636
|
+
statsWindowSignature = signature;
|
|
637
|
+
void statsLoader.reconcile(targets, sorted, () => {
|
|
638
|
+
if (!disposed) tui.requestRender();
|
|
607
639
|
});
|
|
608
640
|
};
|
|
609
641
|
|
|
610
|
-
triggerStatsLoad(sorted);
|
|
611
|
-
|
|
612
642
|
const loadWorkspaceSessions = async (allWorkspaces: boolean) => {
|
|
613
643
|
const loadSeq = ++workspaceLoadSeq;
|
|
614
|
-
|
|
644
|
+
statsLoader.cancel();
|
|
645
|
+
statsWindowSignature = "";
|
|
646
|
+
cancelPreviewLoad();
|
|
647
|
+
previewCache.clear();
|
|
648
|
+
activePreview = undefined;
|
|
615
649
|
isLoading = true;
|
|
616
650
|
tui.requestRender();
|
|
617
651
|
try {
|
|
@@ -624,11 +658,11 @@ async function showSessionPicker(
|
|
|
624
658
|
if (disposed || loadSeq !== workspaceLoadSeq) return;
|
|
625
659
|
|
|
626
660
|
sorted = sortSessions(results);
|
|
661
|
+
statsUniverseVersion++;
|
|
627
662
|
for (const s of sorted) {
|
|
628
663
|
sessionByPath.set(s.path, s);
|
|
629
664
|
}
|
|
630
665
|
entries = buildSessionSearchEntries(sorted);
|
|
631
|
-
triggerStatsLoad(sorted);
|
|
632
666
|
|
|
633
667
|
filteredEntries = filterSessionEntries(entries, filter);
|
|
634
668
|
const matchedIndex = filteredEntries.findIndex(
|
|
@@ -656,6 +690,7 @@ async function showSessionPicker(
|
|
|
656
690
|
};
|
|
657
691
|
|
|
658
692
|
const schedulePreviewLoad = () => {
|
|
693
|
+
cancelPreviewLoad();
|
|
659
694
|
previewScrollOffset = 0;
|
|
660
695
|
const session = sessionByPath.get(selectedPath);
|
|
661
696
|
if (!session) {
|
|
@@ -663,28 +698,45 @@ async function showSessionPicker(
|
|
|
663
698
|
return;
|
|
664
699
|
}
|
|
665
700
|
|
|
666
|
-
const key = previewKey(session);
|
|
667
|
-
const cached = previewCache.get(key);
|
|
668
|
-
if (cached) {
|
|
669
|
-
activePreview = cached;
|
|
670
|
-
return;
|
|
671
|
-
}
|
|
672
|
-
|
|
673
701
|
activePreview = {
|
|
674
702
|
title: buildSessionLabel(session),
|
|
675
703
|
subtitle: `${session.modified.toLocaleString()} · loading preview`,
|
|
676
704
|
blocks: [{ kind: "notice", text: "Loading selected session…" }],
|
|
677
705
|
};
|
|
678
706
|
|
|
679
|
-
|
|
680
|
-
const seq = ++previewSeq;
|
|
707
|
+
const seq = previewSeq;
|
|
681
708
|
previewTimer = setTimeout(() => {
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
709
|
+
previewTimer = undefined;
|
|
710
|
+
const controller = new AbortController();
|
|
711
|
+
previewController = controller;
|
|
712
|
+
void loadSessionPreview(session, previewCache, controller.signal)
|
|
713
|
+
.then((preview) => {
|
|
714
|
+
if (
|
|
715
|
+
controller.signal.aborted ||
|
|
716
|
+
seq !== previewSeq ||
|
|
717
|
+
selectedPath !== session.path
|
|
718
|
+
) {
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
activePreview = preview;
|
|
722
|
+
tui.requestRender();
|
|
723
|
+
})
|
|
724
|
+
.catch((error) => {
|
|
725
|
+
if (
|
|
726
|
+
controller.signal.aborted ||
|
|
727
|
+
seq !== previewSeq ||
|
|
728
|
+
selectedPath !== session.path
|
|
729
|
+
) {
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
activePreview = buildPreviewError(session, error);
|
|
733
|
+
tui.requestRender();
|
|
734
|
+
})
|
|
735
|
+
.finally(() => {
|
|
736
|
+
if (previewController === controller) {
|
|
737
|
+
previewController = undefined;
|
|
738
|
+
}
|
|
739
|
+
});
|
|
688
740
|
}, PREVIEW_LOAD_DEBOUNCE_MS);
|
|
689
741
|
};
|
|
690
742
|
|
|
@@ -700,7 +752,12 @@ async function showSessionPicker(
|
|
|
700
752
|
): SelectItem[] =>
|
|
701
753
|
current.map((entry) => ({
|
|
702
754
|
value: entry.session.path,
|
|
703
|
-
label: formatItemLabel(
|
|
755
|
+
label: formatItemLabel(
|
|
756
|
+
entry.session,
|
|
757
|
+
listWidth,
|
|
758
|
+
theme,
|
|
759
|
+
statsLoader.get(entry.session),
|
|
760
|
+
),
|
|
704
761
|
}));
|
|
705
762
|
|
|
706
763
|
const rebuild = () => {
|
|
@@ -732,24 +789,25 @@ async function showSessionPicker(
|
|
|
732
789
|
if (isLoading) {
|
|
733
790
|
container.addChild(new Spacer(1));
|
|
734
791
|
container.addChild(
|
|
735
|
-
new Text(theme.fg("muted", " Loading sessions
|
|
792
|
+
new Text(theme.fg("muted", " Loading sessions…"), 1, 0),
|
|
736
793
|
);
|
|
737
794
|
container.addChild(new Spacer(1));
|
|
738
795
|
} else {
|
|
739
|
-
const
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
Math.max(1, Math.min(maxVisible, Math.max(items.length, 1))),
|
|
743
|
-
{
|
|
744
|
-
selectedPrefix: (text) =>
|
|
745
|
-
theme.fg(focus === "list" ? "accent" : "muted", text),
|
|
746
|
-
selectedText: (text) =>
|
|
747
|
-
theme.fg(focus === "list" ? "accent" : "muted", text),
|
|
748
|
-
description: (text) => theme.fg("muted", text),
|
|
749
|
-
scrollInfo: (text) => theme.fg("dim", text),
|
|
750
|
-
noMatch: () => theme.fg("warning", " No matching sessions"),
|
|
751
|
-
},
|
|
796
|
+
const effectiveVisible = Math.max(
|
|
797
|
+
1,
|
|
798
|
+
Math.min(maxVisible, Math.max(filteredEntries.length, 1)),
|
|
752
799
|
);
|
|
800
|
+
ensureStatsWindowLoad(effectiveVisible);
|
|
801
|
+
const items = buildItems(filteredEntries, width);
|
|
802
|
+
selectList = new SelectList(items, effectiveVisible, {
|
|
803
|
+
selectedPrefix: (text) =>
|
|
804
|
+
theme.fg(focus === "list" ? "accent" : "muted", text),
|
|
805
|
+
selectedText: (text) =>
|
|
806
|
+
theme.fg(focus === "list" ? "accent" : "muted", text),
|
|
807
|
+
description: (text) => theme.fg("muted", text),
|
|
808
|
+
scrollInfo: (text) => theme.fg("dim", text),
|
|
809
|
+
noMatch: () => theme.fg("warning", " No matching sessions"),
|
|
810
|
+
});
|
|
753
811
|
const selectedIndex = filteredEntries.findIndex(
|
|
754
812
|
(entry) => entry.session.path === selectedPath,
|
|
755
813
|
);
|
|
@@ -762,7 +820,15 @@ async function showSessionPicker(
|
|
|
762
820
|
}
|
|
763
821
|
container.addChild(
|
|
764
822
|
new Text(
|
|
765
|
-
|
|
823
|
+
hintLine(
|
|
824
|
+
theme,
|
|
825
|
+
[
|
|
826
|
+
["↑↓", "navigate"],
|
|
827
|
+
["enter", "open"],
|
|
828
|
+
["esc", "cancel"],
|
|
829
|
+
],
|
|
830
|
+
Math.max(1, width - 2),
|
|
831
|
+
),
|
|
766
832
|
1,
|
|
767
833
|
0,
|
|
768
834
|
),
|
|
@@ -777,29 +843,29 @@ async function showSessionPicker(
|
|
|
777
843
|
listWidth: number,
|
|
778
844
|
previewWidth: number,
|
|
779
845
|
): string => {
|
|
780
|
-
const leftTitle = "
|
|
846
|
+
const leftTitle = " sessions ";
|
|
847
|
+
const rightTitle = " preview ";
|
|
781
848
|
const left = `┌─${leftTitle}${"─".repeat(Math.max(0, listWidth - leftTitle.length - 2))}`;
|
|
782
|
-
const right =
|
|
783
|
-
|
|
849
|
+
const right =
|
|
850
|
+
`─${rightTitle}${"─".repeat(Math.max(0, previewWidth - rightTitle.length - 2))}` +
|
|
851
|
+
"┐";
|
|
852
|
+
return `${theme.fg("borderMuted", left)}${theme.fg("borderMuted", "─┬─")}${theme.fg("borderMuted", right)}`;
|
|
784
853
|
};
|
|
785
854
|
|
|
786
855
|
const buildBottomBorder = (
|
|
787
856
|
listWidth: number,
|
|
788
857
|
previewWidth: number,
|
|
789
|
-
previewStats: string,
|
|
790
858
|
): string => {
|
|
791
|
-
const help = showAllWorkspaces
|
|
792
|
-
? " Opt+W/Ctrl+T current workspace · Esc close "
|
|
793
|
-
: " Opt+W/Ctrl+T all workspaces · Esc close ";
|
|
794
859
|
const left = `└${"─".repeat(Math.max(0, listWidth - 1))}`;
|
|
795
|
-
const right = `${"─".repeat(Math.max(0, previewWidth -
|
|
796
|
-
return `${theme.fg("
|
|
860
|
+
const right = `${"─".repeat(Math.max(0, previewWidth - 1))}┘`;
|
|
861
|
+
return `${theme.fg("borderMuted", left)}${theme.fg("borderMuted", "─┴─")}${theme.fg("borderMuted", right)}`;
|
|
797
862
|
};
|
|
798
863
|
|
|
799
864
|
const renderSplitPane = (width: number): string[] => {
|
|
800
865
|
const layout = getSessionPaneLayout(width);
|
|
801
866
|
const termRows = Math.max(12, tui.terminal?.rows ?? 24);
|
|
802
|
-
|
|
867
|
+
// Frame (2) + hint line (1).
|
|
868
|
+
const contentHeight = Math.max(8, termRows - 3);
|
|
803
869
|
const filterLine = filter.length
|
|
804
870
|
? `${theme.fg("muted", "Filter: ")}${theme.fg("text", filter)}`
|
|
805
871
|
: `${theme.fg("muted", "Filter: ")}${theme.fg("dim", "type to filter")}`;
|
|
@@ -811,23 +877,21 @@ async function showSessionPicker(
|
|
|
811
877
|
leftLines.push("");
|
|
812
878
|
leftLines.push(theme.fg("muted", " Loading sessions..."));
|
|
813
879
|
} else {
|
|
814
|
-
const
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
Math.max(
|
|
818
|
-
listHeight,
|
|
819
|
-
Math.min(maxVisible, Math.max(items.length, 1)),
|
|
820
|
-
),
|
|
821
|
-
{
|
|
822
|
-
selectedPrefix: (text) =>
|
|
823
|
-
theme.fg(focus === "list" ? "accent" : "muted", text),
|
|
824
|
-
selectedText: (text) =>
|
|
825
|
-
theme.fg(focus === "list" ? "accent" : "muted", text),
|
|
826
|
-
description: (text) => theme.fg("muted", text),
|
|
827
|
-
scrollInfo: (text) => theme.fg("dim", text),
|
|
828
|
-
noMatch: () => theme.fg("warning", " No matching sessions"),
|
|
829
|
-
},
|
|
880
|
+
const effectiveVisible = Math.max(
|
|
881
|
+
listHeight,
|
|
882
|
+
Math.min(maxVisible, Math.max(filteredEntries.length, 1)),
|
|
830
883
|
);
|
|
884
|
+
ensureStatsWindowLoad(effectiveVisible);
|
|
885
|
+
const items = buildItems(filteredEntries, layout.listWidth);
|
|
886
|
+
selectList = new SelectList(items, effectiveVisible, {
|
|
887
|
+
selectedPrefix: (text) =>
|
|
888
|
+
theme.fg(focus === "list" ? "accent" : "muted", text),
|
|
889
|
+
selectedText: (text) =>
|
|
890
|
+
theme.fg(focus === "list" ? "accent" : "muted", text),
|
|
891
|
+
description: (text) => theme.fg("muted", text),
|
|
892
|
+
scrollInfo: (text) => theme.fg("dim", text),
|
|
893
|
+
noMatch: () => theme.fg("warning", " No matching sessions"),
|
|
894
|
+
});
|
|
831
895
|
const selectedIndex = filteredEntries.findIndex(
|
|
832
896
|
(entry) => entry.session.path === selectedPath,
|
|
833
897
|
);
|
|
@@ -861,26 +925,41 @@ async function showSessionPicker(
|
|
|
861
925
|
previewScrollOffset,
|
|
862
926
|
renderedPreview.maxScroll,
|
|
863
927
|
);
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
928
|
+
// Hints live on their own line under the frame instead of being packed
|
|
929
|
+
// into the bottom border, where they had to compete with the border for
|
|
930
|
+
// the same row and lost the keys in a wall of dim text.
|
|
931
|
+
const hints = hintLine(
|
|
932
|
+
theme,
|
|
933
|
+
[
|
|
934
|
+
renderedPreview.maxScroll > 0
|
|
935
|
+
? ([
|
|
936
|
+
"",
|
|
937
|
+
`${previewScrollOffset + 1}-${Math.min(previewScrollOffset + contentHeight, renderedPreview.totalLines)}/${renderedPreview.totalLines}`,
|
|
938
|
+
] as const)
|
|
939
|
+
: undefined,
|
|
940
|
+
renderedPreview.maxScroll > 0
|
|
941
|
+
? (["pgup/pgdn", "scroll"] as const)
|
|
942
|
+
: undefined,
|
|
943
|
+
["t", toolsExpanded ? "compact" : "tools"],
|
|
944
|
+
["h", thinkingVisible ? "hide thinking" : "thinking"],
|
|
945
|
+
[
|
|
946
|
+
"opt+w/ctrl+t",
|
|
947
|
+
showAllWorkspaces ? "current workspace" : "all workspaces",
|
|
948
|
+
],
|
|
949
|
+
["esc", "close"],
|
|
950
|
+
],
|
|
951
|
+
width,
|
|
952
|
+
);
|
|
869
953
|
|
|
870
954
|
const lines = [buildTopBorder(layout.listWidth, layout.previewWidth)];
|
|
871
955
|
for (let i = 0; i < contentHeight; i++) {
|
|
872
956
|
const left = padAnsiRight(leftLines[i] ?? "", layout.listWidth);
|
|
873
957
|
const right =
|
|
874
958
|
renderedPreview.lines[i] ?? " ".repeat(layout.previewWidth);
|
|
875
|
-
lines.push(`${left}${theme.fg("
|
|
959
|
+
lines.push(`${left}${theme.fg("borderMuted", " │ ")}${right}`);
|
|
876
960
|
}
|
|
877
|
-
lines.push(
|
|
878
|
-
|
|
879
|
-
layout.listWidth,
|
|
880
|
-
layout.previewWidth,
|
|
881
|
-
previewStats,
|
|
882
|
-
),
|
|
883
|
-
);
|
|
961
|
+
lines.push(buildBottomBorder(layout.listWidth, layout.previewWidth));
|
|
962
|
+
lines.push(hints);
|
|
884
963
|
return lines.map((line) => truncateToWidth(line, width, "", true));
|
|
885
964
|
};
|
|
886
965
|
|
|
@@ -894,8 +973,11 @@ async function showSessionPicker(
|
|
|
894
973
|
return renderSplitPane(width);
|
|
895
974
|
},
|
|
896
975
|
invalidate: () => {
|
|
976
|
+
cancelPreviewLoad();
|
|
897
977
|
previewCache.clear();
|
|
978
|
+
activePreview = undefined;
|
|
898
979
|
rebuild();
|
|
980
|
+
schedulePreviewLoad();
|
|
899
981
|
},
|
|
900
982
|
dispose: disposePicker,
|
|
901
983
|
handleInput: (data) => {
|