@tt-a1i/openpi 0.4.0 → 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 +102 -40
- package/SETUP.md +22 -6
- package/assets/openpi-launch-card-v1.webp +0 -0
- package/bin/openpi.js +145 -0
- 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 +13 -5
- package/extensions/background-terminals/src/result-delivery.ts +4 -1
- 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/render.ts +17 -257
- package/extensions/file-search/src/binaries.ts +57 -41
- package/extensions/git-read/index.ts +1 -3
- 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 +153 -86
- 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 +123 -127
- package/extensions/shared/activity-status.ts +30 -0
- 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/capability-intent.ts +1 -1
- package/extensions/shared/child-session.ts +437 -21
- package/extensions/shared/result-delivery.ts +34 -0
- package/extensions/shared/setup-config.ts +73 -33
- package/extensions/shared/setup-episode-state.ts +1 -1
- package/extensions/shared/terminal-text.ts +110 -23
- package/extensions/shared/text-projection.ts +72 -15
- package/extensions/shared/tool-activity.ts +382 -0
- package/extensions/shared/tool-surface.ts +29 -2
- 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 +270 -59
- package/extensions/subagents/navigation.ts +34 -5
- 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/manager.ts +34 -2
- package/extensions/subagents/src/prompt.ts +32 -4
- package/extensions/subagents/src/result-artifact.ts +4 -0
- package/extensions/subagents/src/result-delivery.ts +7 -1
- package/extensions/subagents/src/runtime.ts +15 -1
- package/extensions/subagents/src/ui/takeover.ts +73 -257
- package/extensions/subagents/src/ui/transcript.ts +38 -535
- 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/ui-customization/footer.ts +0 -40
- package/extensions/ui-customization/index.ts +0 -4
- package/extensions/user-input-fold/index.ts +1 -1
- package/extensions/web/index.ts +234 -0
- package/extensions/workflows/artifacts.ts +137 -47
- package/extensions/workflows/completion-projection.ts +457 -0
- package/extensions/workflows/coordinator.ts +8 -10
- package/extensions/workflows/dashboard.ts +167 -228
- package/extensions/workflows/handoff.ts +70 -16
- package/extensions/workflows/index.ts +488 -198
- package/extensions/workflows/journal.ts +148 -13
- package/extensions/workflows/model.ts +74 -4
- package/extensions/workflows/navigation.ts +32 -8
- package/extensions/workflows/progress-projection.ts +306 -0
- package/extensions/workflows/prompt.ts +66 -6
- package/extensions/workflows/replay-safety.ts +42 -21
- package/extensions/workflows/result-delivery.ts +128 -64
- package/extensions/workflows/retention.ts +593 -0
- package/extensions/workflows/runner.ts +388 -279
- package/extensions/workflows/sandbox-child.cjs +25 -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 +1 -1
- package/skills/workflows/REFERENCE.md +4 -2
- 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 -303
|
@@ -22,7 +22,19 @@ import {
|
|
|
22
22
|
wrapTextWithAnsi,
|
|
23
23
|
} from "@earendil-works/pi-tui";
|
|
24
24
|
import { hintLine } from "../shared/screen-chrome.ts";
|
|
25
|
-
import {
|
|
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";
|
|
26
38
|
import {
|
|
27
39
|
buildPreviewError,
|
|
28
40
|
buildSessionDescription,
|
|
@@ -33,7 +45,9 @@ import {
|
|
|
33
45
|
formatRelativeTime,
|
|
34
46
|
getSessionPaneLayout,
|
|
35
47
|
type PreviewBlock,
|
|
48
|
+
type PreviewMessageLike,
|
|
36
49
|
parseLimit,
|
|
50
|
+
selectSessionStatsWindow,
|
|
37
51
|
type SessionInfoLike,
|
|
38
52
|
type SessionPreview,
|
|
39
53
|
} from "./sessions.js";
|
|
@@ -291,20 +305,16 @@ const renderPreviewBlock = (
|
|
|
291
305
|
return renderTextBlock(`◇ ${block.label}`, block.text, width, theme, "muted");
|
|
292
306
|
};
|
|
293
307
|
|
|
294
|
-
/** Cached across picker openings, while the loader bounds process-wide Git work. */
|
|
295
|
-
const statsLoader = createSessionStatsLoader();
|
|
296
|
-
const statsCache = statsLoader.cache;
|
|
297
|
-
|
|
298
308
|
const formatItemLabel = (
|
|
299
309
|
session: SessionInfoLike,
|
|
300
310
|
width: number,
|
|
301
311
|
theme: any,
|
|
302
|
-
|
|
312
|
+
statsState: SessionStatsState | undefined,
|
|
303
313
|
): string => {
|
|
304
314
|
const itemWidth = width - 4;
|
|
305
315
|
const timeStr = formatRelativeTime(session.modified);
|
|
306
316
|
|
|
307
|
-
const stats =
|
|
317
|
+
const stats = statsState?.status === "ready" ? statsState.stats : undefined;
|
|
308
318
|
let statsStr = "";
|
|
309
319
|
let statsLen = 0;
|
|
310
320
|
if (stats && (stats.add > 0 || stats.mod > 0 || stats.del > 0)) {
|
|
@@ -319,6 +329,12 @@ const formatItemLabel = (
|
|
|
319
329
|
(stats.del > 0 ? `${stats.del}`.length + 1 : 0) +
|
|
320
330
|
parts.length -
|
|
321
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;
|
|
322
338
|
}
|
|
323
339
|
|
|
324
340
|
const title = buildSessionLabel(session);
|
|
@@ -442,14 +458,28 @@ const renderPreview = (
|
|
|
442
458
|
|
|
443
459
|
const loadSessionPreview = async (
|
|
444
460
|
session: SessionInfoLike,
|
|
461
|
+
cache: ReturnType<typeof createSessionPreviewCache>,
|
|
462
|
+
signal: AbortSignal,
|
|
445
463
|
): Promise<SessionPreview> => {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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;
|
|
453
483
|
};
|
|
454
484
|
|
|
455
485
|
async function listSessions(
|
|
@@ -551,12 +581,17 @@ async function showSessionPicker(
|
|
|
551
581
|
let previewScrollOffset = 0;
|
|
552
582
|
let toolsExpanded = false;
|
|
553
583
|
let thinkingVisible = false;
|
|
554
|
-
const previewCache =
|
|
584
|
+
const previewCache = createSessionPreviewCache();
|
|
555
585
|
let activePreview: SessionPreview | undefined;
|
|
556
586
|
let previewTimer: ReturnType<typeof setTimeout> | undefined;
|
|
587
|
+
let previewController: AbortController | undefined;
|
|
557
588
|
let previewSeq = 0;
|
|
558
|
-
|
|
559
|
-
|
|
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 = "";
|
|
560
595
|
let workspaceLoadSeq = 0;
|
|
561
596
|
let disposed = false;
|
|
562
597
|
let settled = false;
|
|
@@ -565,14 +600,22 @@ async function showSessionPicker(
|
|
|
565
600
|
let showAllWorkspaces = false;
|
|
566
601
|
let isLoading = false;
|
|
567
602
|
|
|
603
|
+
const cancelPreviewLoad = () => {
|
|
604
|
+
previewSeq++;
|
|
605
|
+
if (previewTimer) clearTimeout(previewTimer);
|
|
606
|
+
previewTimer = undefined;
|
|
607
|
+
previewController?.abort();
|
|
608
|
+
previewController = undefined;
|
|
609
|
+
};
|
|
610
|
+
|
|
568
611
|
const disposePicker = () => {
|
|
569
612
|
if (disposed) return;
|
|
570
613
|
disposed = true;
|
|
571
614
|
workspaceLoadSeq++;
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
615
|
+
statsLoader.cancel();
|
|
616
|
+
cancelPreviewLoad();
|
|
617
|
+
previewCache.clear();
|
|
618
|
+
activePreview = undefined;
|
|
576
619
|
};
|
|
577
620
|
const finishPicker = (result: SessionInfoLike | null) => {
|
|
578
621
|
if (settled) return;
|
|
@@ -580,30 +623,29 @@ async function showSessionPicker(
|
|
|
580
623
|
disposePicker();
|
|
581
624
|
done(result);
|
|
582
625
|
};
|
|
583
|
-
const
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
if (disposed || signal.aborted || generation !== statsGeneration)
|
|
597
|
-
return;
|
|
598
|
-
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();
|
|
599
639
|
});
|
|
600
640
|
};
|
|
601
641
|
|
|
602
|
-
triggerStatsLoad(sorted);
|
|
603
|
-
|
|
604
642
|
const loadWorkspaceSessions = async (allWorkspaces: boolean) => {
|
|
605
643
|
const loadSeq = ++workspaceLoadSeq;
|
|
606
|
-
|
|
644
|
+
statsLoader.cancel();
|
|
645
|
+
statsWindowSignature = "";
|
|
646
|
+
cancelPreviewLoad();
|
|
647
|
+
previewCache.clear();
|
|
648
|
+
activePreview = undefined;
|
|
607
649
|
isLoading = true;
|
|
608
650
|
tui.requestRender();
|
|
609
651
|
try {
|
|
@@ -616,11 +658,11 @@ async function showSessionPicker(
|
|
|
616
658
|
if (disposed || loadSeq !== workspaceLoadSeq) return;
|
|
617
659
|
|
|
618
660
|
sorted = sortSessions(results);
|
|
661
|
+
statsUniverseVersion++;
|
|
619
662
|
for (const s of sorted) {
|
|
620
663
|
sessionByPath.set(s.path, s);
|
|
621
664
|
}
|
|
622
665
|
entries = buildSessionSearchEntries(sorted);
|
|
623
|
-
triggerStatsLoad(sorted);
|
|
624
666
|
|
|
625
667
|
filteredEntries = filterSessionEntries(entries, filter);
|
|
626
668
|
const matchedIndex = filteredEntries.findIndex(
|
|
@@ -648,6 +690,7 @@ async function showSessionPicker(
|
|
|
648
690
|
};
|
|
649
691
|
|
|
650
692
|
const schedulePreviewLoad = () => {
|
|
693
|
+
cancelPreviewLoad();
|
|
651
694
|
previewScrollOffset = 0;
|
|
652
695
|
const session = sessionByPath.get(selectedPath);
|
|
653
696
|
if (!session) {
|
|
@@ -655,28 +698,45 @@ async function showSessionPicker(
|
|
|
655
698
|
return;
|
|
656
699
|
}
|
|
657
700
|
|
|
658
|
-
const key = previewKey(session);
|
|
659
|
-
const cached = previewCache.get(key);
|
|
660
|
-
if (cached) {
|
|
661
|
-
activePreview = cached;
|
|
662
|
-
return;
|
|
663
|
-
}
|
|
664
|
-
|
|
665
701
|
activePreview = {
|
|
666
702
|
title: buildSessionLabel(session),
|
|
667
703
|
subtitle: `${session.modified.toLocaleString()} · loading preview`,
|
|
668
704
|
blocks: [{ kind: "notice", text: "Loading selected session…" }],
|
|
669
705
|
};
|
|
670
706
|
|
|
671
|
-
|
|
672
|
-
const seq = ++previewSeq;
|
|
707
|
+
const seq = previewSeq;
|
|
673
708
|
previewTimer = setTimeout(() => {
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
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
|
+
});
|
|
680
740
|
}, PREVIEW_LOAD_DEBOUNCE_MS);
|
|
681
741
|
};
|
|
682
742
|
|
|
@@ -692,7 +752,12 @@ async function showSessionPicker(
|
|
|
692
752
|
): SelectItem[] =>
|
|
693
753
|
current.map((entry) => ({
|
|
694
754
|
value: entry.session.path,
|
|
695
|
-
label: formatItemLabel(
|
|
755
|
+
label: formatItemLabel(
|
|
756
|
+
entry.session,
|
|
757
|
+
listWidth,
|
|
758
|
+
theme,
|
|
759
|
+
statsLoader.get(entry.session),
|
|
760
|
+
),
|
|
696
761
|
}));
|
|
697
762
|
|
|
698
763
|
const rebuild = () => {
|
|
@@ -728,20 +793,21 @@ async function showSessionPicker(
|
|
|
728
793
|
);
|
|
729
794
|
container.addChild(new Spacer(1));
|
|
730
795
|
} else {
|
|
731
|
-
const
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
Math.max(1, Math.min(maxVisible, Math.max(items.length, 1))),
|
|
735
|
-
{
|
|
736
|
-
selectedPrefix: (text) =>
|
|
737
|
-
theme.fg(focus === "list" ? "accent" : "muted", text),
|
|
738
|
-
selectedText: (text) =>
|
|
739
|
-
theme.fg(focus === "list" ? "accent" : "muted", text),
|
|
740
|
-
description: (text) => theme.fg("muted", text),
|
|
741
|
-
scrollInfo: (text) => theme.fg("dim", text),
|
|
742
|
-
noMatch: () => theme.fg("warning", " No matching sessions"),
|
|
743
|
-
},
|
|
796
|
+
const effectiveVisible = Math.max(
|
|
797
|
+
1,
|
|
798
|
+
Math.min(maxVisible, Math.max(filteredEntries.length, 1)),
|
|
744
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
|
+
});
|
|
745
811
|
const selectedIndex = filteredEntries.findIndex(
|
|
746
812
|
(entry) => entry.session.path === selectedPath,
|
|
747
813
|
);
|
|
@@ -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
|
);
|
|
@@ -909,8 +973,11 @@ async function showSessionPicker(
|
|
|
909
973
|
return renderSplitPane(width);
|
|
910
974
|
},
|
|
911
975
|
invalidate: () => {
|
|
976
|
+
cancelPreviewLoad();
|
|
912
977
|
previewCache.clear();
|
|
978
|
+
activePreview = undefined;
|
|
913
979
|
rebuild();
|
|
980
|
+
schedulePreviewLoad();
|
|
914
981
|
},
|
|
915
982
|
dispose: disposePicker,
|
|
916
983
|
handleInput: (data) => {
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { PreviewFileIdentity } from "./preview-loader.ts";
|
|
2
|
+
import type { SessionPreview } from "./sessions.ts";
|
|
3
|
+
|
|
4
|
+
export const PREVIEW_CACHE_MAX_ENTRIES = 16;
|
|
5
|
+
export const PREVIEW_CACHE_MAX_BYTES = 4 * 1024 * 1024;
|
|
6
|
+
|
|
7
|
+
interface PreviewCacheOptions {
|
|
8
|
+
maxEntries?: number;
|
|
9
|
+
maxBytes?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface CacheEntry {
|
|
13
|
+
preview: SessionPreview;
|
|
14
|
+
bytes: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function measureSessionPreviewBytes(value: unknown): number {
|
|
18
|
+
if (typeof value === "string") return Buffer.byteLength(value, "utf8");
|
|
19
|
+
if (Array.isArray(value)) {
|
|
20
|
+
return value.reduce(
|
|
21
|
+
(total, nested) => total + measureSessionPreviewBytes(nested),
|
|
22
|
+
0,
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
if (typeof value !== "object" || value === null) return 0;
|
|
26
|
+
return Object.entries(value).reduce(
|
|
27
|
+
(total, [key, nested]) =>
|
|
28
|
+
total +
|
|
29
|
+
Buffer.byteLength(key, "utf8") +
|
|
30
|
+
measureSessionPreviewBytes(nested),
|
|
31
|
+
0,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function previewCacheKey(identity: PreviewFileIdentity) {
|
|
36
|
+
return [
|
|
37
|
+
identity.path,
|
|
38
|
+
identity.device,
|
|
39
|
+
identity.inode,
|
|
40
|
+
identity.size,
|
|
41
|
+
identity.mtimeNs,
|
|
42
|
+
].join(":");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function createSessionPreviewCache(options: PreviewCacheOptions = {}) {
|
|
46
|
+
const maxEntries = Math.max(
|
|
47
|
+
1,
|
|
48
|
+
options.maxEntries ?? PREVIEW_CACHE_MAX_ENTRIES,
|
|
49
|
+
);
|
|
50
|
+
const maxBytes = Math.max(1, options.maxBytes ?? PREVIEW_CACHE_MAX_BYTES);
|
|
51
|
+
const cache = new Map<string, CacheEntry>();
|
|
52
|
+
let totalBytes = 0;
|
|
53
|
+
let evictionCount = 0;
|
|
54
|
+
|
|
55
|
+
const remove = (key: string) => {
|
|
56
|
+
const entry = cache.get(key);
|
|
57
|
+
if (!entry) return false;
|
|
58
|
+
cache.delete(key);
|
|
59
|
+
totalBytes -= entry.bytes;
|
|
60
|
+
return true;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const get = (key: string) => {
|
|
64
|
+
const entry = cache.get(key);
|
|
65
|
+
if (!entry) return undefined;
|
|
66
|
+
cache.delete(key);
|
|
67
|
+
cache.set(key, entry);
|
|
68
|
+
return entry.preview;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const set = (key: string, preview: SessionPreview, bytes: number) => {
|
|
72
|
+
remove(key);
|
|
73
|
+
if (!Number.isFinite(bytes) || bytes < 0 || bytes > maxBytes) return false;
|
|
74
|
+
|
|
75
|
+
cache.set(key, { preview, bytes });
|
|
76
|
+
totalBytes += bytes;
|
|
77
|
+
while (cache.size > maxEntries || totalBytes > maxBytes) {
|
|
78
|
+
const oldest = cache.keys().next().value;
|
|
79
|
+
if (oldest === undefined || !remove(oldest)) break;
|
|
80
|
+
evictionCount++;
|
|
81
|
+
}
|
|
82
|
+
return cache.has(key);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const clear = () => {
|
|
86
|
+
cache.clear();
|
|
87
|
+
totalBytes = 0;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
get,
|
|
92
|
+
set,
|
|
93
|
+
clear,
|
|
94
|
+
get entries() {
|
|
95
|
+
return cache.size;
|
|
96
|
+
},
|
|
97
|
+
get bytes() {
|
|
98
|
+
return totalBytes;
|
|
99
|
+
},
|
|
100
|
+
get evictions() {
|
|
101
|
+
return evictionCount;
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|