pisesh 0.1.11 → 0.1.13

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 CHANGED
@@ -2,45 +2,64 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.1.13] - 2026-08-01
6
+
7
+ ### Added
8
+ - Press `o` to resume with the model and thinking recorded in the session while `Enter` uses current defaults.
9
+ - Show the current-default and original-session resume modes in the session details view.
10
+ - Support `PI_AGENT_DIR` and `PI_SESSION_DIR`, including flat custom session directories.
11
+ - Add `pisesh --version` and `pisesh --clean-favorites`; press `x` in the TUI to remove stale favorites.
12
+ - Report successful orphan-call repairs and stop with a visible error when a session cannot be repaired safely.
13
+
14
+ ### Fixed
15
+ - Resume favorite sessions with the current default provider, model, and thinking level instead of restoring stale values recorded in the session.
16
+ - Fall back to pi's native session restore when the settings file is unavailable or invalid.
17
+
18
+ ## [0.1.12] - 2026-07-20
19
+
20
+ ### Fixed
21
+ - Skip interrupted tool calls from assistant turns that ended with `error` or `aborted`, preventing OpenAI Responses from receiving an unmatched `function_call_output` after resume or fork.
22
+ - Remove incompatible synthetic results previously written by pisesh and repair their child links when affected sessions are resumed.
23
+
5
24
  ## [0.1.11] - 2026-07-14
6
25
 
7
26
  ### Fixed
8
27
  - `/sesh` now launches the CLI bundled with the pi package instead of requiring `pisesh` on the user's `PATH`.
9
28
  - Clarified that the standalone `pisesh` shell command requires `npm install -g pisesh`.
10
29
 
11
- ## [0.1.10] 2026-06-09
30
+ ## [0.1.10] - 2026-06-09
12
31
 
13
32
  ### Fixed
14
- - **Resume no longer crashes on sessions interrupted mid-tool-call.** If a session ended while a tool call was still in flight (e.g. an image generation cut off by a usage limit or a closed window), its transcript held a tool call with no recorded result. On resume, pi would re-fire that dead tool and the spawned process could exit non-zero surfacing as `pisesh exited with code 1`, repeatedly. `resumeSession` now heals the target session first: every orphaned tool call gets a synthetic `[interrupted]` result injected before pi is spawned, so the resumed history is always well-formed and no dead tool is re-run. Idempotent, never throws, and writes a one-time `.bak-orphanheal` backup of the session before modifying it.
33
+ - **Resume no longer crashes on sessions interrupted mid-tool-call.** If a session ended while a tool call was still in flight (e.g. an image generation cut off by a usage limit or a closed window), its transcript held a tool call with no recorded result. On resume, pi would re-fire that dead tool and the spawned process could exit non-zero, surfacing as `pisesh exited with code 1`, repeatedly. `resumeSession` now heals the target session first: every orphaned tool call gets a synthetic `[interrupted]` result injected before pi is spawned, so the resumed history is always well-formed and no dead tool is re-run. Idempotent, never throws, and writes a one-time `.bak-orphanheal` backup of the session before modifying it.
15
34
 
16
- ## [0.1.9] 2026-06-03
35
+ ## [0.1.9] - 2026-06-03
17
36
 
18
37
  ### Fixed
19
38
  - Name / cwd editor (`e`) now has a real caret. Arrow keys (and `Home` / `End`) move inside the text you already typed, so you can insert or delete in the middle instead of only at the end. Backspace, forward `Delete`, and inserts all act at the caret. Cursor moves by whole code points, so CJK and emoji never get split.
20
39
 
21
- ## [0.1.4] 2026-06-03
40
+ ## [0.1.4] - 2026-06-03
22
41
 
23
42
  ### Added
24
- - **`Here` tab** filters the list to sessions whose effective cwd matches the directory pisesh was launched from. Tab order is now **★ Favorites → Today → Here → All**.
25
- - **Inline rename (`e`)** set a custom display title that overrides the first-prompt label; renamed sessions are marked with a cyan `✎` in the list.
26
- - **Edit cwd (`p`)** arrow-key directory browser to re-point the working directory pi resumes into (also drives the `Here` filter).
43
+ - **`Here` tab** filters the list to sessions whose effective cwd matches the directory pisesh was launched from. Tab order is now **★ Favorites → Today → Here → All**.
44
+ - **Inline rename (`e`)** sets a custom display title that overrides the first-prompt label; renamed sessions are marked with a cyan `✎` in the list.
45
+ - **Edit cwd (`p`)** provides an arrow-key directory browser to re-point the working directory pi resumes into (also drives the `Here` filter).
27
46
  - Per-session overrides (custom title / cwd) persist to `~/.pi/agent/pisesh-meta.json`, keyed by session id. Session jsonl files remain read-only.
28
47
  - README terminal screenshots for the list view, rename panel, and cwd browser.
29
48
 
30
- ## [0.1.0] 2026-05-31
49
+ ## [0.1.0] - 2026-05-31
31
50
 
32
51
  Initial release.
33
52
 
34
53
  ### Added
35
- - `pisesh` CLI binary keyboard-driven TUI that lists every pi session under `~/.pi/agent/sessions/`
54
+ - `pisesh` CLI binary with a keyboard-driven TUI that lists every pi session under `~/.pi/agent/sessions/`
36
55
  - Tabs: **★ Favorites**, **Today**, **All**
37
56
  - Star / unstar with `f` or Space; favorites persist to `~/.pi/agent/favorites.json`
38
57
  - Search across id / project / first user prompt with `/`
39
58
  - Session details view (`d`): full prompt, file path, byte size, timestamps
40
59
  - `Enter` resumes the selected session via `pi --session <id> --session-dir <dir>` in the original cwd
41
60
  - `[NOW]` badge marks the session belonging to the pi instance that spawned pisesh (set via `PISESH_CURRENT_SESSION` env var)
42
- - Alternate screen buffer (`\x1b[?1049h`) exit restores terminal byte-for-byte; no scrollback pollution
61
+ - Alternate screen buffer (`\x1b[?1049h`) so exit restores terminal byte-for-byte; no scrollback pollution
43
62
  - CJK-aware truncation and padding (Hangul / CJK ideographs / emoji counted as 2 cells)
44
63
  - Signal handlers (`SIGINT`, `SIGTERM`, `exit`) restore cursor + main buffer on unexpected exit
45
64
  - Non-TUI CLI: `--list`, `--json`, `--star <id>`, `--unstar <id>`, `--help`
46
- - Pi extension at `extensions/sesh.ts` registers `/sesh` slash command which spawns pisesh inside pi via `ui.custom` + `tui.stop()`
65
+ - Pi extension at `extensions/sesh.ts` registers `/sesh` slash command which spawns pisesh inside pi via `ui.custom` + `tui.stop()`
package/README.md CHANGED
@@ -45,7 +45,7 @@ Pi accumulates sessions across many working directories: your home, several proj
45
45
  - You re-open the wrong session and pollute it with unrelated context
46
46
  - You waste time searching by timestamp guessing
47
47
 
48
- pisesh is a **single-file Node script** (no dependencies, ~900 LoC) that gives you everything `pi --resume` doesn't.
48
+ pisesh is a **single-file Node script** (no dependencies, ~1,100 LoC) that gives you everything `pi --resume` doesn't.
49
49
 
50
50
  ### Value at a glance
51
51
 
@@ -61,7 +61,7 @@ pisesh is a **single-file Node script** (no dependencies, ~900 LoC) that gives y
61
61
  | Read Korean / Chinese / Japanese prompts | Display-width-aware truncation; columns never blow up on CJK |
62
62
  | Open from anywhere | Run as standalone `pisesh` shell command, or `/sesh` inside pi |
63
63
  | Zero install pain | No build step, no native deps, runs on Node 18+ everywhere |
64
- | Trust it with your history | pisesh writes only two small JSON files (favorites + overrides); session jsonl files are read-only |
64
+ | Trust it with your history | favorites and overrides stay in sidecar JSON; session history changes only for orphan-call repair |
65
65
 
66
66
  ## Getting started
67
67
 
@@ -100,7 +100,9 @@ For local pi testing, run `pi install .` from the cloned repository so the exten
100
100
  | `↑` `↓` / `j` `k` | move cursor |
101
101
  | `Tab` / `h` / `l` | switch tab (`★ Favorites` → `Today` → `Here` → `All`) |
102
102
  | `f` / `Space` | star / unstar the selected session |
103
- | `Enter` | resume the session; runs `pi --session <id>` in its (or the overridden) cwd |
103
+ | `x` | remove favorites whose session files no longer exist |
104
+ | `Enter` | resume using the current default model and thinking settings |
105
+ | `o` | resume using the model and thinking recorded in the session |
104
106
  | `e` | edit name: set a custom display title, shown with `✎` in the list |
105
107
  | `p` | edit cwd with an arrow-key directory browser; sets the resume / `Here` dir |
106
108
  | `d` | session details (full prompt, file, byte size, timestamps) |
@@ -120,6 +122,8 @@ pisesh --list # print starred session IDs (one per line)
120
122
  pisesh --json # full favorites file as JSON
121
123
  pisesh --star <partial-uuid> # star a session from a script
122
124
  pisesh --unstar <partial-uuid> # unstar
125
+ pisesh --clean-favorites # remove favorites whose sessions are gone
126
+ pisesh --version # print installed version
123
127
  pisesh --help
124
128
  ```
125
129
 
@@ -138,6 +142,8 @@ pisesh --help
138
142
  | Storage | Two JSON files: `~/.pi/agent/favorites.json` (starred ids) + `~/.pi/agent/pisesh-meta.json` (per-session title / cwd overrides) |
139
143
  | Session discovery | Direct filesystem scan of `~/.pi/agent/sessions/<projectSlug>/*.jsonl`; first 96 KB parsed |
140
144
  | Process model | Slash command pauses pi's TUI, spawns pisesh with inherited stdio, restarts pi on exit |
145
+ | Resume settings | `Enter` uses current defaults; `o` preserves the model and thinking recorded in the session |
146
+ | Custom paths | Honors `PI_AGENT_DIR` and `PI_SESSION_DIR`, including a flat custom session directory |
141
147
 
142
148
  ### What it explicitly does **not** depend on
143
149
 
@@ -150,9 +156,9 @@ pisesh --help
150
156
 
151
157
  | What | Where |
152
158
  | ---------- | ----------------------------------------------------------- |
153
- | Favorites | `~/.pi/agent/favorites.json` |
154
- | Overrides | `~/.pi/agent/pisesh-meta.json` (per-session custom title / cwd, keyed by session id) |
155
- | Sessions | `~/.pi/agent/sessions/<projectSlug>/<timestamp>_<uuid>.jsonl` (pi's native layout; pisesh never writes here) |
159
+ | Favorites | `$PI_AGENT_DIR/favorites.json` (defaults to `~/.pi/agent/favorites.json`) |
160
+ | Overrides | `$PI_AGENT_DIR/pisesh-meta.json` (per-session custom title / cwd, keyed by session id) |
161
+ | Sessions | `$PI_SESSION_DIR`, or `$PI_AGENT_DIR/sessions` by default (repaired only when an orphaned tool call would break resume) |
156
162
 
157
163
  Favorites file shape:
158
164
 
package/bin/pisesh CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  // pisesh — pi session bookmark & resume TUI
3
3
  // Tabs: ★ Favorites / Today / Here / All
4
- // Storage: ~/.pi/agent/favorites.json (starred session ids)
5
- // ~/.pi/agent/pisesh-meta.json (per-session title / cwd overrides)
6
- // Sessions: ~/.pi/agent/sessions/<projectSlug>/<ts>_<uuid>.jsonl
4
+ // Storage: $PI_AGENT_DIR/favorites.json (starred session ids)
5
+ // $PI_AGENT_DIR/pisesh-meta.json (per-session title / cwd overrides)
6
+ // Sessions: $PI_SESSION_DIR or $PI_AGENT_DIR/sessions
7
7
 
8
8
  'use strict';
9
9
 
@@ -15,9 +15,20 @@ const readline = require('readline');
15
15
 
16
16
  // ── Paths ─────────────────────────────────────────────
17
17
  const HOME = os.homedir();
18
- const SESSIONS_ROOT = path.join(HOME, '.pi/agent/sessions');
19
- const FAV_FILE = path.join(HOME, '.pi/agent/favorites.json');
20
- const META_FILE = path.join(HOME, '.pi/agent/pisesh-meta.json');
18
+ const AGENT_DIR = path.resolve(process.env.PI_AGENT_DIR || path.join(HOME, '.pi/agent'));
19
+ const SESSIONS_ROOT = path.resolve(process.env.PI_SESSION_DIR || path.join(AGENT_DIR, 'sessions'));
20
+ const FAV_FILE = path.join(AGENT_DIR, 'favorites.json');
21
+ const META_FILE = path.join(AGENT_DIR, 'pisesh-meta.json');
22
+ const SETTINGS_FILE = path.join(AGENT_DIR, 'settings.json');
23
+ const VERSION = (() => {
24
+ for (const file of [
25
+ path.resolve(__dirname, '../package.json'),
26
+ path.join(AGENT_DIR, 'npm/node_modules/pisesh/package.json'),
27
+ ]) {
28
+ try { return JSON.parse(fs.readFileSync(file, 'utf8')).version; } catch {}
29
+ }
30
+ return 'unknown';
31
+ })();
21
32
 
22
33
  // Current session id (set by the /sesh extension before spawning). Lets us
23
34
  // flag the currently-attached session in the list with a [NOW] badge.
@@ -177,16 +188,29 @@ function readSessionMeta(file) {
177
188
  }
178
189
 
179
190
  function scanSessions() {
180
- let projectDirs;
181
- try {
182
- projectDirs = fs.readdirSync(SESSIONS_ROOT, { withFileTypes: true })
183
- .filter(d => d.isDirectory());
184
- } catch { return []; }
191
+ let rootEntries;
192
+ try { rootEntries = fs.readdirSync(SESSIONS_ROOT, { withFileTypes: true }); }
193
+ catch { return []; }
194
+
195
+ const projectDirs = rootEntries
196
+ .filter(entry => entry.isDirectory())
197
+ .map(entry => ({
198
+ name: entry.name,
199
+ path: path.join(SESSIONS_ROOT, entry.name),
200
+ label: decodeProjectSlug(entry.name),
201
+ }));
202
+ if (rootEntries.some(entry => entry.isFile() && entry.name.endsWith('.jsonl'))) {
203
+ projectDirs.unshift({
204
+ name: path.basename(SESSIONS_ROOT),
205
+ path: SESSIONS_ROOT,
206
+ label: CURRENT_CWD || SESSIONS_ROOT,
207
+ });
208
+ }
185
209
 
186
210
  const results = [];
187
211
  for (const d of projectDirs) {
188
- const projectPath = path.join(SESSIONS_ROOT, d.name);
189
- const projectLabel = decodeProjectSlug(d.name);
212
+ const projectPath = d.path;
213
+ const projectLabel = d.label;
190
214
  let files;
191
215
  try { files = fs.readdirSync(projectPath).filter(f => f.endsWith('.jsonl')); }
192
216
  catch { continue; }
@@ -225,12 +249,23 @@ function scanSessions() {
225
249
  return results;
226
250
  }
227
251
 
252
+ function cleanStaleFavorites(knownSessions = sessions) {
253
+ const knownIds = new Set(knownSessions.map(session => session.id));
254
+ const stale = [...favorites].filter(id => !knownIds.has(id));
255
+ if (stale.length > 0) {
256
+ for (const id of stale) favorites.delete(id);
257
+ saveFavorites(favorites);
258
+ }
259
+ return stale;
260
+ }
261
+
228
262
  // ── State ─────────────────────────────────────────────
229
263
  let sessions = [];
230
264
  let tabs = ['★ Favorites', 'Today', 'Here', 'All'];
231
265
  let tabIdx = 0;
232
266
  let cursor = 0;
233
267
  let filter = '';
268
+ let notice = '';
234
269
  let mode = 'list'; // list | filter | details | edit | browse
235
270
  // edit mode state (title text editor)
236
271
  let editField = ''; // 'title' | 'cwd'
@@ -450,12 +485,15 @@ function renderList() {
450
485
  out += ' ' + A.D + '↑↓' + A.R + ' move '
451
486
  + A.D + 'Tab' + A.R + ' tab '
452
487
  + A.D + 'f' + A.R + ' star '
453
- + A.D + 'Enter' + A.R + ' resume '
488
+ + A.D + 'x' + A.R + ' clean '
489
+ + A.D + 'Enter' + A.R + ' current '
490
+ + A.D + 'o' + A.R + ' original '
454
491
  + A.D + 'e' + A.R + ' edit name '
455
492
  + A.D + 'p' + A.R + ' edit cwd '
456
493
  + A.D + 'd' + A.R + ' details '
457
494
  + A.D + '/' + A.R + ' search '
458
495
  + A.D + 'q' + A.R + ' quit' + A.R + '\n';
496
+ if (notice) out += ' ' + A.cyn + notice + A.R + '\n';
459
497
 
460
498
  process.stdout.write(out);
461
499
  }
@@ -524,6 +562,11 @@ function renderDetails() {
524
562
  out += row('cwd', s.effectiveCwd + (s.cwdOverride ? A.cyn + ' (custom)' + A.R : ''), A.mag);
525
563
  if (s.cwdOverride) out += row('recorded', s.cwd, A.gry);
526
564
  out += row('project', s.project);
565
+ const defaults = loadResumeDefaults();
566
+ out += row('Enter uses', defaults
567
+ ? `${defaults.model || 'session model'} / thinking ${defaults.thinking || 'session value'}`
568
+ : 'session model and thinking', A.grn);
569
+ out += row('o uses', 'session-recorded model and thinking', A.gry);
527
570
  out += row('size', fmtSize(s.size));
528
571
  out += row('file', s.file, A.gry);
529
572
  out += '\n ' + A.D + 'first prompt:' + A.R + '\n';
@@ -532,7 +575,8 @@ function renderDetails() {
532
575
  for (const line of wrapped) out += ' ' + line + '\n';
533
576
 
534
577
  out += '\n' + A.gry + '─'.repeat(Math.max(1, W - 1)) + A.R + '\n';
535
- out += ' ' + A.D + 'Enter' + A.R + ' resume '
578
+ out += ' ' + A.D + 'Enter' + A.R + ' current defaults '
579
+ + A.D + 'o' + A.R + ' original settings '
536
580
  + A.D + 'f' + A.R + ' star '
537
581
  + A.D + 'e' + A.R + ' edit name '
538
582
  + A.D + 'p' + A.R + ' edit cwd '
@@ -618,7 +662,12 @@ function handleList(str, key) {
618
662
  saveFavorites(favorites);
619
663
  }
620
664
  }
621
- else if (k === 'return') resumeSession(list[cursor]);
665
+ else if (k === 'return') resumeSession(list[cursor], true);
666
+ else if (k === 'o') resumeSession(list[cursor], false);
667
+ else if (k === 'x') {
668
+ const removed = cleanStaleFavorites(sessions).length;
669
+ notice = removed ? `removed ${removed} stale favorite${removed === 1 ? '' : 's'}` : 'no stale favorites';
670
+ }
622
671
  else if (k === 'd') { if (list[cursor]) mode = 'details'; }
623
672
  else if (k === 'e') startEdit(list[cursor], 'title');
624
673
  else if (k === 'p') startBrowse(list[cursor]);
@@ -644,7 +693,8 @@ function handleDetails(str, key) {
644
693
  const s = visibleSessions()[cursor];
645
694
  if (k === 'escape' || k === 'q' || k === 'd') { mode = 'list'; return; }
646
695
  if (key.ctrl && k === 'c') return quit();
647
- if (k === 'return') return resumeSession(s);
696
+ if (k === 'return') return resumeSession(s, true);
697
+ if (k === 'o') return resumeSession(s, false);
648
698
  if (k === 'f' && s) {
649
699
  if (s.favored) favorites.delete(s.id); else favorites.add(s.id);
650
700
  saveFavorites(favorites);
@@ -843,25 +893,54 @@ function tryCopy(text) {
843
893
  // interrupted when the prior session hit a usage limit or was closed mid-tool.
844
894
  // Left unhealed, the spawned pi crashes (exit 1), which surfaces to the parent
845
895
  // as "pisesh exited with code N". We inject a synthetic "[interrupted]"
846
- // toolResult for every orphaned toolCall so the resumed history is always
847
- // well-formed and pi never re-fires a dead tool. Idempotent; never throws.
896
+ // toolResult for unfinished valid turns, but not errored/aborted assistant turns
897
+ // that pi omits during replay. Old incompatible synthetic results are removed.
898
+ // Idempotent. Throws when the session cannot be read, backed up, or written.
848
899
  function healOrphanedToolCalls(file) {
849
900
  try {
850
901
  const lines = fs.readFileSync(file, 'utf8').split('\n').filter(Boolean);
902
+ const entries = lines.map(raw => {
903
+ try { return { raw, value: JSON.parse(raw) }; } catch { return { raw, value: null }; }
904
+ });
905
+ const droppedToolCallIds = new Set();
906
+ for (const { value: o } of entries) {
907
+ const m = o && o.message;
908
+ if (m && m.role === 'assistant' && (m.stopReason === 'error' || m.stopReason === 'aborted')) {
909
+ for (const b of Array.isArray(m.content) ? m.content : []) {
910
+ if (b && b.type === 'toolCall' && b.id) droppedToolCallIds.add(b.id);
911
+ }
912
+ }
913
+ }
914
+
915
+ const removedParents = new Map();
916
+ const kept = entries.filter(({ value: o }) => {
917
+ const m = o && o.message;
918
+ const text = m && Array.isArray(m.content) && m.content.find(b => b && b.type === 'text');
919
+ const isBadSynthetic = m && m.role === 'toolResult'
920
+ && droppedToolCallIds.has(m.toolCallId)
921
+ && text && text.text.startsWith('[tool call interrupted');
922
+ if (isBadSynthetic) removedParents.set(o.id, o.parentId);
923
+ return !isBadSynthetic;
924
+ });
925
+ for (const { value: o } of kept) {
926
+ if (!o || typeof o.parentId === 'undefined') continue;
927
+ while (removedParents.has(o.parentId)) o.parentId = removedParents.get(o.parentId);
928
+ }
929
+
851
930
  const resultIds = new Set();
852
- for (const l of lines) {
853
- let o; try { o = JSON.parse(l); } catch { continue; }
931
+ for (const { value: o } of kept) {
854
932
  if (o && o.message && o.message.role === 'toolResult') resultIds.add(o.message.toolCallId);
855
933
  }
856
934
  const rnd = () => Math.random().toString(16).slice(2, 10);
857
935
  const out = [];
858
936
  let inserted = 0, prevSynthId = null;
859
- for (const l of lines) {
860
- let o; try { o = JSON.parse(l); } catch { out.push(l); continue; }
861
- if (prevSynthId && o && typeof o.parentId !== 'undefined') { o.parentId = prevSynthId; prevSynthId = null; }
937
+ for (const { raw, value: o } of kept) {
938
+ if (!o) { out.push(raw); continue; }
939
+ if (prevSynthId && typeof o.parentId !== 'undefined') { o.parentId = prevSynthId; prevSynthId = null; }
862
940
  out.push(JSON.stringify(o));
863
- const c = o && o.message && o.message.content;
864
- if (o && o.message && o.message.role === 'assistant' && Array.isArray(c)) {
941
+ const m = o.message;
942
+ const c = m && m.content;
943
+ if (m && m.role === 'assistant' && m.stopReason !== 'error' && m.stopReason !== 'aborted' && Array.isArray(c)) {
865
944
  for (const b of c) {
866
945
  if (b && b.type === 'toolCall' && b.id && !resultIds.has(b.id)) {
867
946
  const synth = {
@@ -881,30 +960,63 @@ function healOrphanedToolCalls(file) {
881
960
  }
882
961
  }
883
962
  }
884
- if (inserted > 0) {
885
- try { const bak = file + '.bak-orphanheal'; if (!fs.existsSync(bak)) fs.copyFileSync(file, bak); } catch {}
963
+ const changed = removedParents.size + inserted;
964
+ if (changed > 0) {
965
+ const bak = file + '.bak-orphanheal';
966
+ if (!fs.existsSync(bak)) fs.copyFileSync(file, bak);
886
967
  fs.writeFileSync(file, out.join('\n') + '\n');
887
968
  }
888
- return inserted;
889
- } catch { return 0; }
969
+ return changed;
970
+ } catch (error) {
971
+ throw new Error(`session repair failed: ${error.message}`);
972
+ }
890
973
  }
891
974
 
892
- function resumeSession(s) {
975
+ function loadResumeDefaults(settingsFile = SETTINGS_FILE) {
976
+ try {
977
+ const settings = JSON.parse(fs.readFileSync(settingsFile, 'utf8'));
978
+ return {
979
+ model: settings.defaultProvider && settings.defaultModel
980
+ ? `${settings.defaultProvider}/${settings.defaultModel}`
981
+ : '',
982
+ thinking: settings.defaultThinkingLevel || '',
983
+ };
984
+ } catch { return null; }
985
+ }
986
+
987
+ function buildResumeArgs(s, useCurrentDefaults = true, settingsFile = SETTINGS_FILE) {
988
+ const args = ['--session', s.id, '--session-dir', path.dirname(s.file)];
989
+ const defaults = useCurrentDefaults && loadResumeDefaults(settingsFile);
990
+ if (defaults?.model) args.push('--model', defaults.model);
991
+ if (defaults?.thinking) args.push('--thinking', defaults.thinking);
992
+ return args;
993
+ }
994
+
995
+ function resumeSession(s, useCurrentDefaults = true) {
893
996
  if (!s) return;
894
997
  // Heal orphaned tool calls before handing off, so resume can't crash the
895
998
  // spawned pi (see healOrphanedToolCalls).
896
- try { healOrphanedToolCalls(s.file); } catch {}
999
+ let repaired;
1000
+ try { repaired = healOrphanedToolCalls(s.file); }
1001
+ catch (error) {
1002
+ process.stdout.write(A.exitAlt + A.showC);
1003
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
1004
+ console.error(`pisesh: ${error.message}`);
1005
+ process.exit(1);
1006
+ }
897
1007
  // Leave alt screen + show cursor so the spawned pi takes over a clean
898
1008
  // main-buffer terminal (it will manage its own alt screen).
899
1009
  process.stdout.write(A.exitAlt + A.showC);
900
1010
  if (process.stdin.isTTY) process.stdin.setRawMode(false);
901
1011
  process.stdin.pause();
1012
+ if (repaired > 0) {
1013
+ console.log(`pisesh: repaired ${repaired} orphaned session entr${repaired === 1 ? 'y' : 'ies'}; backup: ${s.file}.bak-orphanheal`);
1014
+ }
902
1015
 
903
- const projectDir = path.dirname(s.file);
904
1016
  // Resume in the session's effective cwd (user override wins over recorded),
905
1017
  // falling back to pisesh's own cwd if that directory no longer exists.
906
1018
  const targetCwd = s.effectiveCwd;
907
- const child = spawn('pi', ['--session', s.id, '--session-dir', projectDir], {
1019
+ const child = spawn('pi', buildResumeArgs(s, useCurrentDefaults), {
908
1020
  stdio: 'inherit',
909
1021
  cwd: targetCwd && fs.existsSync(targetCwd) ? targetCwd : process.cwd(),
910
1022
  });
@@ -934,13 +1046,17 @@ Usage:
934
1046
  pisesh --json Dump favorites file as JSON
935
1047
  pisesh --star <id> Add a session id (partial UUID ok) to favorites
936
1048
  pisesh --unstar <id> Remove a session id from favorites
1049
+ pisesh --clean-favorites Remove favorites whose sessions no longer exist
1050
+ pisesh --version, -v Print the installed version
937
1051
  pisesh --help, -h This help
938
1052
 
939
1053
  TUI keys:
940
1054
  ↑↓ / j k move cursor
941
1055
  Tab / h l switch tab (★ Favorites → Today → Here → All)
942
1056
  f / Space toggle favorite
943
- Enter resume session (spawns 'pi --session …' in its cwd)
1057
+ x remove favorites whose sessions no longer exist
1058
+ Enter resume with current default model and thinking settings
1059
+ o resume with the session-recorded model and thinking settings
944
1060
  e edit name (custom display title)
945
1061
  p edit cwd — arrow-key directory browser
946
1062
  (↑↓ move, →/Enter open dir, ← parent, s select, Esc cancel)
@@ -970,6 +1086,13 @@ function main() {
970
1086
  const argv = process.argv.slice(2);
971
1087
 
972
1088
  if (argv.includes('-h') || argv.includes('--help')) { printHelp(); return; }
1089
+ if (argv.includes('-v') || argv.includes('--version')) { console.log(VERSION); return; }
1090
+ if (argv.includes('--clean-favorites')) {
1091
+ sessions = scanSessions();
1092
+ const stale = cleanStaleFavorites(sessions);
1093
+ console.log(`removed ${stale.length} stale favorite${stale.length === 1 ? '' : 's'}`);
1094
+ return;
1095
+ }
973
1096
  if (argv.includes('-l') || argv.includes('--list')) {
974
1097
  [...favorites].forEach(id => console.log(id));
975
1098
  return;
@@ -1010,4 +1133,5 @@ function main() {
1010
1133
  render();
1011
1134
  }
1012
1135
 
1013
- main();
1136
+ if (require.main === module) main();
1137
+ else module.exports = { buildResumeArgs, cleanStaleFavorites, healOrphanedToolCalls, loadResumeDefaults };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pisesh",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "Bookmark, search, and resume pi coding-agent sessions with a fast keyboard-driven TUI.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -46,7 +46,7 @@
46
46
  "image": "https://raw.githubusercontent.com/Blue-B/pisesh/main/assets/preview.png"
47
47
  },
48
48
  "scripts": {
49
- "test": "node --check bin/pisesh && echo 'pisesh: syntax OK'",
49
+ "test": "node --check bin/pisesh && node --test test/*.test.js",
50
50
  "prepublishOnly": "npm test"
51
51
  }
52
52
  }