pi-fast-resume 1.1.0 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -68,6 +68,12 @@ Tested with **1,771 sessions, 1.46 GB** of JSONL data on disk.
68
68
 
69
69
  **With `pi install`** (recommended):
70
70
 
71
+ ```bash
72
+ pi install npm:pi-fast-resume
73
+ ```
74
+
75
+ Or install from GitHub:
76
+
71
77
  ```bash
72
78
  pi install https://github.com/monotykamary/pi-fast-resume
73
79
  ```
package/fast-resume.ts CHANGED
@@ -73,6 +73,7 @@ import {
73
73
  loadSessionHeaders,
74
74
  sortByModified,
75
75
  sortByModifiedDesc,
76
+ filterByCwd,
76
77
  canonicalizePath,
77
78
  type SessionHeader,
78
79
  type SessionFileMeta,
@@ -88,8 +89,8 @@ import {
88
89
  type FlatSessionNode,
89
90
  type SortMode,
90
91
  type NameFilter,
92
+ type PickerScope,
91
93
  } from "./src/search.js";
92
- import type { PickerScope } from "./src/picker-state.js";
93
94
 
94
95
  const HOME = homedir();
95
96
 
@@ -121,6 +122,43 @@ export interface FastResumeResult {
121
122
 
122
123
  type StatusMessage = { type: "info" | "error"; message: string };
123
124
 
125
+ // Session loading helpers — mirror SessionManager.list / listAll behavior
126
+ // while keeping partial reads.
127
+
128
+ function loadCurrentSessionsImmediate(
129
+ cwd: string,
130
+ sessionDir: string | undefined,
131
+ usesDefaultSessionDir: boolean,
132
+ ): SessionHeader[] {
133
+ if (!sessionDir) return [];
134
+ const metas = sortByModifiedDesc(scanSessionDir(sessionDir));
135
+ let headers = loadSessionHeaders(metas);
136
+ if (!usesDefaultSessionDir) {
137
+ // Custom session dirs may contain sessions from multiple cwds; filter to
138
+ // the current one, matching SessionManager.list behavior.
139
+ headers = filterByCwd(headers, cwd);
140
+ }
141
+ return sortByModified(headers);
142
+ }
143
+
144
+ function loadAllSessionMetas(
145
+ sessionDir: string | undefined,
146
+ usesDefaultSessionDir: boolean,
147
+ ): SessionFileMeta[] {
148
+ if (usesDefaultSessionDir) {
149
+ return scanAllSessionDirs();
150
+ }
151
+ if (!sessionDir) return [];
152
+ return sortByModifiedDesc(scanSessionDir(sessionDir));
153
+ }
154
+
155
+ // ReadonlySessionManager doesn't declare usesDefaultSessionDir, but the
156
+ // runtime SessionManager has it. Default to true so old pi versions behave
157
+ // like the original default-dir-only fast-resume.
158
+ function getUsesDefaultSessionDir(sessionManager: ExtensionCommandContext["sessionManager"]): boolean {
159
+ return (sessionManager as any).usesDefaultSessionDir?.() ?? true;
160
+ }
161
+
124
162
  // Helpers
125
163
 
126
164
  function shortenPath(path: string): string {
@@ -639,6 +677,10 @@ class FastResumePicker extends Container {
639
677
  private mode: "list" | "rename" = "list";
640
678
  private renameTargetPath: string | null = null;
641
679
 
680
+ private cwd: string;
681
+ private sessionDir: string | undefined;
682
+ private usesDefaultSessionDir: boolean;
683
+
642
684
  // Focusable — propagate to sessionList or renameInput
643
685
  private _focused = false;
644
686
  get focused() { return this._focused; }
@@ -668,18 +710,24 @@ class FastResumePicker extends Container {
668
710
  constructor(
669
711
  theme: Theme,
670
712
  currentCwd: string,
713
+ sessionDir: string | undefined,
714
+ usesDefaultSessionDir: boolean,
671
715
  currentSessionPath: string | undefined,
672
716
  initialCurrentSessions: SessionHeader[],
673
717
  allMetas: SessionFileMeta[],
674
718
  allSessions: SessionHeader[] | null,
675
719
  done: (result: FastResumeResult) => void,
676
720
  tuiRequestRender: () => void,
721
+ initialQuery?: string,
677
722
  ) {
678
723
  super();
679
724
  this.theme = theme;
680
725
  this.done = done;
681
726
  this.tuiRequestRender = tuiRequestRender;
682
727
  this.allMetas = allMetas;
728
+ this.cwd = currentCwd;
729
+ this.sessionDir = sessionDir;
730
+ this.usesDefaultSessionDir = usesDefaultSessionDir;
683
731
 
684
732
  // Create header
685
733
  this.header = new FastResumeHeader(theme, tuiRequestRender);
@@ -698,6 +746,12 @@ class FastResumePicker extends Container {
698
746
  // Set initial data into the list
699
747
  this.sessionList.setSessions(initialCurrentSessions, false);
700
748
 
749
+ // Seed an optional initial query from /fast-resume <query>
750
+ if (initialQuery !== undefined && initialQuery !== "") {
751
+ this.sessionList.searchInput.setValue(initialQuery);
752
+ this.sessionList.filterSessions(initialQuery);
753
+ }
754
+
701
755
  // Wire session list events
702
756
  this.sessionList.onSelect = (sessionPath) => {
703
757
  this.header.clearStatusTimeout();
@@ -819,12 +873,54 @@ class FastResumePicker extends Container {
819
873
  }
820
874
  }
821
875
 
876
+ private rescanCurrentScope(): SessionHeader[] {
877
+ if (!this.sessionDir) return [];
878
+ const metas = sortByModifiedDesc(scanSessionDir(this.sessionDir));
879
+ let headers = loadSessionHeaders(metas);
880
+ if (!this.usesDefaultSessionDir) {
881
+ headers = filterByCwd(headers, this.cwd);
882
+ }
883
+ return sortByModified(headers);
884
+ }
885
+
886
+ private rescanAllScope(): SessionHeader[] {
887
+ if (this.usesDefaultSessionDir) {
888
+ const metas = scanAllSessionDirs();
889
+ const headers = loadSessionHeaders(metas);
890
+ return sortByModified(headers);
891
+ }
892
+ if (!this.sessionDir) return [];
893
+ const metas = sortByModifiedDesc(scanSessionDir(this.sessionDir));
894
+ const headers = loadSessionHeaders(metas);
895
+ return sortByModified(headers);
896
+ }
897
+
822
898
  private async refreshSessionsAfterMutation(): Promise<void> {
823
- // After delete/rename, the in-memory arrays are already updated (filtered above).
824
- // Just re-apply them to the session list.
825
- const sessions = this.scope === "all" ? (this.allSessions ?? []) : (this.currentSessions ?? []);
826
- const showCwd = this.scope === "all";
827
- this.sessionList.setSessions(sessions, showCwd);
899
+ // Rescan from disk so renames, deletes, and newly created sessions are
900
+ // reflected in the list. This mirrors upstream's loadScope(scope, "refresh").
901
+ // Bump the sequence number first so any in-progress background all-load
902
+ // stops before it can overwrite the rescanned data.
903
+ this.allLoadSeq++;
904
+ try {
905
+ if (this.scope === "current") {
906
+ this.currentSessions = this.rescanCurrentScope();
907
+ this.sessionList.setSessions(this.currentSessions, false);
908
+ } else {
909
+ this.allLoading = true;
910
+ this.allSessions = this.rescanAllScope();
911
+ this.allLoading = false;
912
+ this.sessionList.setSessions(this.allSessions, true);
913
+ }
914
+ this.header.loading = false;
915
+ this.header.loadProgress = null;
916
+ } catch (err) {
917
+ this.currentLoading = false;
918
+ this.allLoading = false;
919
+ this.header.loading = false;
920
+ this.header.loadProgress = null;
921
+ const message = err instanceof Error ? err.message : String(err);
922
+ this.header.setStatusMessage({ type: "error", message: `Failed to refresh: ${message}` }, 4000);
923
+ }
828
924
  }
829
925
 
830
926
  private startAllLoadBackground(): void {
@@ -951,19 +1047,14 @@ async function showFastResumePicker(
951
1047
  ): Promise<void> {
952
1048
  const cwd = ctx.cwd;
953
1049
  const sessionDir = ctx.sessionManager.getSessionDir();
1050
+ const usesDefaultSessionDir = getUsesDefaultSessionDir(ctx.sessionManager);
954
1051
 
955
1052
  const t0 = Date.now();
956
1053
 
957
- // Phase 1: stat all session files
958
- const currentMetas = sessionDir ? scanSessionDir(sessionDir) : [];
959
- const allMetas = scanAllSessionDirs();
960
-
961
- // Phase 2: quickly parse the first 30 for instant display
962
- const INITIAL_BATCH = 30;
963
- const sortedCurrent = sortByModifiedDesc(currentMetas);
964
- const quickMetas = sortedCurrent.slice(0, INITIAL_BATCH);
965
- const quickHeaders = loadSessionHeaders(quickMetas);
966
- const currentSessions = sortByModified(quickHeaders);
1054
+ // Load the current-scope sessions immediately, and collect metadata for the
1055
+ // incremental "all" scope load that happens in the background.
1056
+ const currentSessions = loadCurrentSessionsImmediate(cwd, sessionDir, usesDefaultSessionDir);
1057
+ const allMetas = loadAllSessionMetas(sessionDir, usesDefaultSessionDir);
967
1058
 
968
1059
  const loadTime = Date.now() - t0;
969
1060
 
@@ -977,12 +1068,15 @@ async function showFastResumePicker(
977
1068
  const picker = new FastResumePicker(
978
1069
  theme,
979
1070
  cwd,
1071
+ sessionDir,
1072
+ usesDefaultSessionDir,
980
1073
  ctx.sessionManager.getSessionFile(),
981
1074
  currentSessions,
982
1075
  allMetas,
983
1076
  null, // allSessions not yet loaded — will load in background
984
1077
  (result) => done(result),
985
1078
  () => _tui.requestRender(),
1079
+ initialQuery,
986
1080
  );
987
1081
 
988
1082
  return picker;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-fast-resume",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "Fast session picker for pi — reads headers + first messages from 16KB partial reads instead of full-file parsing",
5
5
  "type": "module",
6
6
  "author": "Tom X Nguyen",
@@ -37,8 +37,8 @@
37
37
  "lint:dead": "knip --no-gitignore"
38
38
  },
39
39
  "devDependencies": {
40
- "@earendil-works/pi-coding-agent": "0.75.4",
41
- "@earendil-works/pi-tui": "0.75.4",
40
+ "@earendil-works/pi-coding-agent": "0.79.4",
41
+ "@earendil-works/pi-tui": "0.79.4",
42
42
  "@types/node": "25.9.1",
43
43
  "@vitest/coverage-v8": "4.1.7",
44
44
  "knip": "6.14.1",
package/src/index.ts CHANGED
@@ -1,7 +1,5 @@
1
1
  export { parseSessionFromBuffer, loadSessionHeader, loadSessionHeaders, scanAllSessionDirs, scanSessionDir, sortByModified, sortByModifiedDesc, filterByCwd, matchQuery, canonicalizePath } from "./scanner.js";
2
2
  export type { SessionHeader, SessionFileMeta } from "./scanner.js";
3
- export { createInitialState, setScope, setSessions, setFilter, moveSelection } from "./picker-state.js";
4
- export type { PickerScope, PickerState } from "./picker-state.js";
5
3
  export {
6
4
  parseSearchQuery,
7
5
  matchSession,
@@ -18,6 +16,7 @@ export type {
18
16
  MatchResult,
19
17
  SortMode,
20
18
  NameFilter,
19
+ PickerScope,
21
20
  SessionTreeNode,
22
21
  FlatSessionNode,
23
22
  } from "./search.js";
package/src/scanner.ts CHANGED
@@ -46,7 +46,7 @@ export function parseSessionFromBuffer(
46
46
 
47
47
  let header: { id: string; timestamp: string; cwd?: string; parentSession?: string } | null = null;
48
48
  let firstUserMsg = "";
49
- let name = "";
49
+ let name: string | undefined;
50
50
  let msgCount = 0;
51
51
  let lastActivityTime: number | undefined;
52
52
 
@@ -60,8 +60,7 @@ export function parseSessionFromBuffer(
60
60
  continue;
61
61
  }
62
62
  if (entry.type === "session_info") {
63
- const n = entry.name?.trim();
64
- if (n) name = n;
63
+ name = entry.name?.trim() || undefined;
65
64
  }
66
65
  if (entry.type === "message") {
67
66
  msgCount++;
@@ -126,8 +125,8 @@ export function parseSessionFromBuffer(
126
125
  created: new Date(header.timestamp),
127
126
  modified,
128
127
  messageCount: msgCount,
129
- firstMessage: firstUserMsg || "",
130
- name: name || undefined,
128
+ firstMessage: firstUserMsg || "(no messages)",
129
+ name,
131
130
  };
132
131
  }
133
132
 
package/src/search.ts CHANGED
@@ -12,6 +12,8 @@
12
12
  import { fuzzyMatch } from "@earendil-works/pi-tui";
13
13
  import { canonicalizePath, type SessionHeader } from "./scanner.js";
14
14
 
15
+ export type PickerScope = "current" | "all";
16
+
15
17
  // Search query parsing
16
18
 
17
19
  export type SearchTokenKind = "fuzzy" | "phrase";
@@ -1,95 +0,0 @@
1
- import type { SessionHeader } from "./scanner.js";
2
-
3
- export type PickerScope = "current" | "all";
4
-
5
- export interface PickerState {
6
- scope: PickerScope;
7
- sessions: SessionHeader[];
8
- filteredSessions: SessionHeader[];
9
- selectedIndex: number;
10
- filterQuery: string;
11
- loadingDone: boolean;
12
- loadedCount: number;
13
- totalCount: number;
14
- }
15
-
16
- export function createInitialState(
17
- initialSessions: SessionHeader[],
18
- totalCount: number,
19
- loadingDone: boolean,
20
- ): PickerState {
21
- return {
22
- scope: "current",
23
- sessions: initialSessions,
24
- filteredSessions: initialSessions,
25
- selectedIndex: 0,
26
- filterQuery: "",
27
- loadingDone,
28
- loadedCount: initialSessions.length,
29
- totalCount,
30
- };
31
- }
32
-
33
- export function setScope(state: PickerState, scope: PickerScope): PickerState {
34
- return {
35
- ...state,
36
- scope,
37
- filterQuery: "",
38
- selectedIndex: 0,
39
- };
40
- }
41
-
42
- export function setSessions(
43
- state: PickerState,
44
- sessions: SessionHeader[],
45
- loadingDone: boolean,
46
- ): PickerState {
47
- const filtered = applyFilter(sessions, state.filterQuery);
48
- return {
49
- ...state,
50
- sessions,
51
- filteredSessions: filtered,
52
- loadingDone,
53
- loadedCount: sessions.length,
54
- selectedIndex: Math.min(state.selectedIndex, Math.max(0, filtered.length - 1)),
55
- };
56
- }
57
-
58
- export function setFilter(
59
- state: PickerState,
60
- query: string,
61
- ): PickerState {
62
- const filtered = applyFilter(state.sessions, query);
63
- return {
64
- ...state,
65
- filterQuery: query,
66
- filteredSessions: filtered,
67
- selectedIndex: Math.min(state.selectedIndex, Math.max(0, filtered.length - 1)),
68
- };
69
- }
70
-
71
- export function moveSelection(
72
- state: PickerState,
73
- delta: number,
74
- ): PickerState {
75
- const max = Math.max(0, state.filteredSessions.length - 1);
76
- return {
77
- ...state,
78
- selectedIndex: Math.max(0, Math.min(max, state.selectedIndex + delta)),
79
- };
80
- }
81
-
82
- function applyFilter(
83
- sessions: SessionHeader[],
84
- query: string,
85
- ): SessionHeader[] {
86
- if (!query) return sessions;
87
- const q = query.toLowerCase();
88
- return sessions.filter(
89
- (s) =>
90
- s.firstMessage.toLowerCase().includes(q) ||
91
- (s.name?.toLowerCase().includes(q) ?? false) ||
92
- s.cwd.toLowerCase().includes(q) ||
93
- s.id.toLowerCase().includes(q),
94
- );
95
- }