pisesh 0.2.0 → 0.3.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/CHANGELOG.md +10 -0
- package/README.md +5 -3
- package/bin/pisesh +94 -11
- package/extensions/sesh.ts +222 -50
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.3.0] - 2026-08-22
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- Make `/sesh` return the selected session to its extension and switch through pi's official `ctx.switchSession()` API instead of launching a nested pi process. Standalone `pisesh` still launches pi.
|
|
9
|
+
- Preserve `Enter` current-default and `o` session-recorded model and thinking behavior across native session switches.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- Forward custom cwd overrides when supported by pi, warn when pi ignores them, and skip switching when the selected session is already active.
|
|
13
|
+
- Keep interrupted-tool-call repair after confirming pi does not synthesize missing results during session loading.
|
|
14
|
+
|
|
5
15
|
## [0.2.0] - 2026-08-01
|
|
6
16
|
|
|
7
17
|
### Added
|
package/README.md
CHANGED
|
@@ -73,6 +73,8 @@ pi install npm:pisesh
|
|
|
73
73
|
|
|
74
74
|
This registers pisesh as a pi extension. Inside any pi session, type `/sesh`. The extension runs its bundled CLI, so a global npm installation is not required.
|
|
75
75
|
|
|
76
|
+
`/sesh` does not start a second pi process. The picker returns the selected session and options to the extension, which calls pi's official `ctx.switchSession()` API. Standalone `pisesh` keeps its shell behavior and starts `pi --session`. Custom cwd overrides require a pi version that supports `cwdOverride` on extension session switches; pisesh warns if pi ignores one.
|
|
77
|
+
|
|
76
78
|
### Install the standalone CLI
|
|
77
79
|
|
|
78
80
|
```bash
|
|
@@ -142,10 +144,10 @@ pisesh --help
|
|
|
142
144
|
| Alt screen buffer | `\x1b[?1049h` / `\x1b[?1049l`, the same primitive `vim`, `less`, `htop`, and droid CLI use |
|
|
143
145
|
| Input | Node's `readline.emitKeypressEvents` in raw mode |
|
|
144
146
|
| Width calculation | UAX #11 East Asian Width ranges, compressed to ~10 inline range checks |
|
|
145
|
-
| Pi extension | TypeScript factory using
|
|
147
|
+
| Pi extension | TypeScript factory using `ui.custom`, `tui.stop`, and `ctx.switchSession()` |
|
|
146
148
|
| Storage | Two JSON files under `$PI_AGENT_DIR`: `favorites.json` and `pisesh-meta.json` |
|
|
147
149
|
| Session discovery | Direct filesystem scan of `~/.pi/agent/sessions/<projectSlug>/*.jsonl`; first 96 KB parsed |
|
|
148
|
-
| Process model |
|
|
150
|
+
| Process model | `/sesh` runs pisesh as a selector and switches the current runtime; standalone starts `pi` |
|
|
149
151
|
| Resume settings | `Enter` uses current defaults; `o` preserves the model and thinking recorded in the session |
|
|
150
152
|
| Custom paths | Honors `PI_AGENT_DIR` and `PI_SESSION_DIR`, including a flat custom session directory |
|
|
151
153
|
| Title generation | Ephemeral `pi --print --no-session` call using the model and effort selected in pisesh |
|
|
@@ -198,7 +200,7 @@ Korean / Chinese / Japanese / fullwidth characters render **2 cells wide** in te
|
|
|
198
200
|
- Windows: **Windows Terminal**, **WezTerm**, **Alacritty** ✅
|
|
199
201
|
- macOS: **iTerm2**, **Terminal.app**, **WezTerm**, **Alacritty**, **Kitty** ✅
|
|
200
202
|
- Linux: **GNOME Terminal**, **Konsole**, **xterm**, **Alacritty**, **Kitty** ✅
|
|
201
|
-
- [`pi`](https://www.npmjs.com/package/@earendil-works/pi-coding-agent) on `$PATH`
|
|
203
|
+
- [`pi`](https://www.npmjs.com/package/@earendil-works/pi-coding-agent) on `$PATH` when using standalone `pisesh`
|
|
202
204
|
|
|
203
205
|
## Contributing
|
|
204
206
|
|
package/bin/pisesh
CHANGED
|
@@ -24,6 +24,12 @@ function piAgentEnv(baseEnv = process.env, agentDir = AGENT_DIR) {
|
|
|
24
24
|
return { ...baseEnv, PI_CODING_AGENT_DIR: agentDir };
|
|
25
25
|
}
|
|
26
26
|
const PI_ENV = piAgentEnv();
|
|
27
|
+
// The /sesh extension gives the picker a private result pipe. Standalone
|
|
28
|
+
// pisesh has no such fd and keeps launching pi itself.
|
|
29
|
+
const SELECT_FD = (() => {
|
|
30
|
+
const fd = Number(process.env.PISESH_SELECT_FD);
|
|
31
|
+
return Number.isInteger(fd) && fd >= 3 ? fd : null;
|
|
32
|
+
})();
|
|
27
33
|
const VERSION = (() => {
|
|
28
34
|
for (const file of [
|
|
29
35
|
path.resolve(__dirname, '../package.json'),
|
|
@@ -1477,6 +1483,38 @@ function loadResumeDefaults(settingsFile = SETTINGS_FILE) {
|
|
|
1477
1483
|
} catch { return null; }
|
|
1478
1484
|
}
|
|
1479
1485
|
|
|
1486
|
+
function loadSessionSettings(file) {
|
|
1487
|
+
try {
|
|
1488
|
+
const entries = fs.readFileSync(file, 'utf8').split('\n').filter(Boolean).flatMap(line => {
|
|
1489
|
+
try { return [JSON.parse(line)]; } catch { return []; }
|
|
1490
|
+
}).filter(entry => entry && entry.type !== 'session');
|
|
1491
|
+
const byId = new Map(entries.filter(entry => entry.id).map(entry => [entry.id, entry]));
|
|
1492
|
+
const branch = [];
|
|
1493
|
+
const visited = new Set();
|
|
1494
|
+
let entry = entries[entries.length - 1];
|
|
1495
|
+
while (entry && !visited.has(entry.id)) {
|
|
1496
|
+
branch.push(entry);
|
|
1497
|
+
visited.add(entry.id);
|
|
1498
|
+
entry = entry.parentId ? byId.get(entry.parentId) : undefined;
|
|
1499
|
+
}
|
|
1500
|
+
branch.reverse();
|
|
1501
|
+
|
|
1502
|
+
let model = '';
|
|
1503
|
+
let thinking = '';
|
|
1504
|
+
for (const item of branch) {
|
|
1505
|
+
if (item.type === 'thinking_level_change' && typeof item.thinkingLevel === 'string') {
|
|
1506
|
+
thinking = item.thinkingLevel;
|
|
1507
|
+
} else if (item.type === 'model_change' && typeof item.provider === 'string' && typeof item.modelId === 'string') {
|
|
1508
|
+
model = `${item.provider}/${item.modelId}`;
|
|
1509
|
+
} else if (item.type === 'message' && item.message?.role === 'assistant'
|
|
1510
|
+
&& typeof item.message.provider === 'string' && typeof item.message.model === 'string') {
|
|
1511
|
+
model = `${item.message.provider}/${item.message.model}`;
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
return { model, thinking };
|
|
1515
|
+
} catch { return null; }
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1480
1518
|
function buildResumeArgs(s, useCurrentDefaults = true, settingsFile = SETTINGS_FILE) {
|
|
1481
1519
|
const args = ['--session', s.id, '--session-dir', path.dirname(s.file)];
|
|
1482
1520
|
const defaults = useCurrentDefaults && loadResumeDefaults(settingsFile);
|
|
@@ -1485,27 +1523,70 @@ function buildResumeArgs(s, useCurrentDefaults = true, settingsFile = SETTINGS_F
|
|
|
1485
1523
|
return args;
|
|
1486
1524
|
}
|
|
1487
1525
|
|
|
1526
|
+
function buildSelection(s, useCurrentDefaults = true, repaired = 0, settingsFile = SETTINGS_FILE) {
|
|
1527
|
+
const defaults = loadResumeDefaults(settingsFile);
|
|
1528
|
+
const recorded = useCurrentDefaults ? null : loadSessionSettings(s.file);
|
|
1529
|
+
const settings = useCurrentDefaults ? defaults : {
|
|
1530
|
+
model: recorded?.model || defaults?.model || '',
|
|
1531
|
+
thinking: recorded?.thinking || defaults?.thinking || '',
|
|
1532
|
+
};
|
|
1533
|
+
return {
|
|
1534
|
+
version: 1,
|
|
1535
|
+
sessionPath: path.resolve(s.file),
|
|
1536
|
+
resumeMode: useCurrentDefaults ? 'defaults' : 'session',
|
|
1537
|
+
...(s.cwdOverride ? { cwdOverride: s.cwdOverride } : {}),
|
|
1538
|
+
...(settings?.model ? { model: settings.model } : {}),
|
|
1539
|
+
...(settings?.thinking ? { thinking: settings.thinking } : {}),
|
|
1540
|
+
...(repaired > 0 ? { repaired } : {}),
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
function leaveTui() {
|
|
1545
|
+
process.stdout.write(A.exitAlt + A.showC);
|
|
1546
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
1547
|
+
process.stdin.pause();
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
function returnSelection(selection) {
|
|
1551
|
+
try {
|
|
1552
|
+
fs.writeSync(SELECT_FD, `${JSON.stringify(selection)}\n`);
|
|
1553
|
+
} catch (error) {
|
|
1554
|
+
throw new Error(`could not return selection to pi: ${error.message}`);
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1488
1558
|
function resumeSession(s, useCurrentDefaults = true) {
|
|
1489
1559
|
if (!s) return;
|
|
1490
|
-
// A resumed pi takes over this terminal, so stop
|
|
1491
|
-
//
|
|
1560
|
+
// A resumed pi or the parent extension takes over this terminal, so stop
|
|
1561
|
+
// background title work and suppress its asynchronous final render.
|
|
1492
1562
|
handingOff = true;
|
|
1493
1563
|
cancelGeneration();
|
|
1494
|
-
|
|
1495
|
-
//
|
|
1564
|
+
|
|
1565
|
+
// Selecting the already-active session must not rewrite its live transcript.
|
|
1566
|
+
if (SELECT_FD !== null && s.isCurrent) {
|
|
1567
|
+
leaveTui();
|
|
1568
|
+
try { returnSelection(buildSelection(s, useCurrentDefaults)); }
|
|
1569
|
+
catch (error) { console.error(`pisesh: ${error.message}`); process.exit(1); }
|
|
1570
|
+
process.exit(0);
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
// Pi still preserves a valid unfinished toolCall without adding its missing
|
|
1574
|
+
// result, so repair it before either native switching or standalone resume.
|
|
1496
1575
|
let repaired;
|
|
1497
1576
|
try { repaired = healOrphanedToolCalls(s.file); }
|
|
1498
1577
|
catch (error) {
|
|
1499
|
-
|
|
1500
|
-
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
1578
|
+
leaveTui();
|
|
1501
1579
|
console.error(`pisesh: ${error.message}`);
|
|
1502
1580
|
process.exit(1);
|
|
1503
1581
|
}
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1582
|
+
leaveTui();
|
|
1583
|
+
|
|
1584
|
+
if (SELECT_FD !== null) {
|
|
1585
|
+
try { returnSelection(buildSelection(s, useCurrentDefaults, repaired)); }
|
|
1586
|
+
catch (error) { console.error(`pisesh: ${error.message}`); process.exit(1); }
|
|
1587
|
+
process.exit(0);
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1509
1590
|
if (repaired > 0) {
|
|
1510
1591
|
console.log(`pisesh: repaired ${repaired} orphaned session entr${repaired === 1 ? 'y' : 'ies'}; backup: ${s.file}.bak-orphanheal`);
|
|
1511
1592
|
}
|
|
@@ -1640,11 +1721,13 @@ function main() {
|
|
|
1640
1721
|
if (require.main === module) main();
|
|
1641
1722
|
else module.exports = {
|
|
1642
1723
|
buildResumeArgs,
|
|
1724
|
+
buildSelection,
|
|
1643
1725
|
buildTitlePrompt,
|
|
1644
1726
|
cleanGeneratedTitle,
|
|
1645
1727
|
cleanStaleFavorites,
|
|
1646
1728
|
healOrphanedToolCalls,
|
|
1647
1729
|
loadResumeDefaults,
|
|
1730
|
+
loadSessionSettings,
|
|
1648
1731
|
parseModelEntries,
|
|
1649
1732
|
parseModelList,
|
|
1650
1733
|
piAgentEnv,
|
package/extensions/sesh.ts
CHANGED
|
@@ -1,97 +1,269 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* pisesh slash command
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* 1. Pauses pi's TUI (releases the terminal)
|
|
8
|
-
* 2. Spawns the external `pisesh` TUI (bookmark/resume picker)
|
|
9
|
-
* 3. When pisesh exits — whether the user resumed a nested session and quit it,
|
|
10
|
-
* or just pressed `q` — control returns to the original pi session and the
|
|
11
|
-
* TUI is restored.
|
|
12
|
-
*
|
|
13
|
-
* Bundled CLI tool: ../bin/pisesh (single-file Node TUI, no deps)
|
|
14
|
-
* Favorites file: ~/.pi/agent/favorites.json
|
|
4
|
+
* `/sesh` temporarily hands the terminal to the bundled picker. The picker
|
|
5
|
+
* returns a session path on a private fd; this extension then asks pi to switch
|
|
6
|
+
* its current runtime. Standalone `pisesh` still launches pi itself.
|
|
15
7
|
*/
|
|
16
8
|
|
|
17
9
|
import { spawn } from "node:child_process";
|
|
18
10
|
import path from "node:path";
|
|
11
|
+
import type { Readable } from "node:stream";
|
|
19
12
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
20
13
|
|
|
21
14
|
// Static package path, no user-controlled segments.
|
|
22
15
|
const PISESH_CLI = path.resolve(__dirname, "../bin/pisesh"); // pi-lens-ignore: ts-path-traversal
|
|
16
|
+
const THINKING_LEVELS = [
|
|
17
|
+
"off",
|
|
18
|
+
"minimal",
|
|
19
|
+
"low",
|
|
20
|
+
"medium",
|
|
21
|
+
"high",
|
|
22
|
+
"xhigh",
|
|
23
|
+
"max",
|
|
24
|
+
] as const;
|
|
25
|
+
type ThinkingLevel = (typeof THINKING_LEVELS)[number];
|
|
26
|
+
|
|
27
|
+
type PiseshSelection = {
|
|
28
|
+
version: 1;
|
|
29
|
+
sessionPath: string;
|
|
30
|
+
resumeMode: "defaults" | "session";
|
|
31
|
+
cwdOverride?: string;
|
|
32
|
+
model?: string;
|
|
33
|
+
thinking?: ThinkingLevel;
|
|
34
|
+
repaired?: number;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type PickerResult = {
|
|
38
|
+
code: number | null;
|
|
39
|
+
selection?: PiseshSelection;
|
|
40
|
+
error?: string;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
type PendingSwitch = Pick<
|
|
44
|
+
PiseshSelection,
|
|
45
|
+
"sessionPath" | "cwdOverride" | "model" | "thinking" | "repaired"
|
|
46
|
+
>;
|
|
23
47
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
48
|
+
const processState = globalThis as typeof globalThis & {
|
|
49
|
+
__piseshPendingSwitch?: PendingSwitch;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function parseSelection(raw: string): PiseshSelection | undefined {
|
|
53
|
+
if (!raw.trim()) return undefined;
|
|
54
|
+
|
|
55
|
+
let value: unknown;
|
|
56
|
+
try {
|
|
57
|
+
value = JSON.parse(raw);
|
|
58
|
+
} catch {
|
|
59
|
+
throw new Error("picker returned invalid JSON");
|
|
60
|
+
}
|
|
61
|
+
if (!value || typeof value !== "object") {
|
|
62
|
+
throw new Error("picker returned a non-object selection");
|
|
63
|
+
}
|
|
64
|
+
const data = value as Record<string, unknown>;
|
|
65
|
+
if (
|
|
66
|
+
data.version !== 1 ||
|
|
67
|
+
typeof data.sessionPath !== "string" ||
|
|
68
|
+
!path.isAbsolute(data.sessionPath) ||
|
|
69
|
+
(data.resumeMode !== "defaults" && data.resumeMode !== "session")
|
|
70
|
+
) {
|
|
71
|
+
throw new Error("picker returned an invalid session selection");
|
|
72
|
+
}
|
|
73
|
+
if (data.cwdOverride !== undefined && typeof data.cwdOverride !== "string") {
|
|
74
|
+
throw new Error("picker returned an invalid cwd override");
|
|
75
|
+
}
|
|
76
|
+
if (data.model !== undefined && typeof data.model !== "string") {
|
|
77
|
+
throw new Error("picker returned an invalid model");
|
|
78
|
+
}
|
|
79
|
+
if (
|
|
80
|
+
data.thinking !== undefined &&
|
|
81
|
+
!THINKING_LEVELS.includes(data.thinking as ThinkingLevel)
|
|
82
|
+
) {
|
|
83
|
+
throw new Error("picker returned an invalid thinking level");
|
|
84
|
+
}
|
|
85
|
+
if (
|
|
86
|
+
data.repaired !== undefined &&
|
|
87
|
+
(!Number.isInteger(data.repaired) || (data.repaired as number) < 0)
|
|
88
|
+
) {
|
|
89
|
+
throw new Error("picker returned an invalid repair count");
|
|
90
|
+
}
|
|
91
|
+
return data as PiseshSelection;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function runPisesh(currentSessionId: string | undefined): Promise<PickerResult> {
|
|
27
95
|
return new Promise((resolve) => {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
96
|
+
let output = "";
|
|
97
|
+
let settled = false;
|
|
98
|
+
const finish = (result: PickerResult) => {
|
|
99
|
+
if (settled) return;
|
|
100
|
+
settled = true;
|
|
101
|
+
resolve(result);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// fd 3 carries one small JSON result while stdin/stdout/stderr remain the
|
|
105
|
+
// real terminal used by the full-screen picker.
|
|
32
106
|
const child = spawn("node", [PISESH_CLI], {
|
|
33
|
-
stdio: "inherit",
|
|
107
|
+
stdio: ["inherit", "inherit", "inherit", "pipe"],
|
|
34
108
|
env: {
|
|
35
109
|
...process.env,
|
|
36
|
-
|
|
37
|
-
// that belong to the directory this pi instance is attached to.
|
|
110
|
+
PISESH_SELECT_FD: "3",
|
|
38
111
|
PISESH_CWD: process.cwd(),
|
|
39
112
|
...(currentSessionId
|
|
40
113
|
? { PISESH_CURRENT_SESSION: currentSessionId }
|
|
41
114
|
: {}),
|
|
42
115
|
},
|
|
43
116
|
});
|
|
44
|
-
child.
|
|
45
|
-
|
|
46
|
-
|
|
117
|
+
const resultPipe = child.stdio[3] as Readable | null;
|
|
118
|
+
resultPipe?.setEncoding("utf8");
|
|
119
|
+
resultPipe?.on("data", (chunk: string) => {
|
|
120
|
+
output += chunk;
|
|
121
|
+
});
|
|
122
|
+
child.on("close", (code) => {
|
|
123
|
+
if (code !== 0) return finish({ code });
|
|
124
|
+
try {
|
|
125
|
+
finish({ code, selection: parseSelection(output) });
|
|
126
|
+
} catch (error) {
|
|
127
|
+
finish({
|
|
128
|
+
code,
|
|
129
|
+
error: error instanceof Error ? error.message : String(error),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
child.on("error", (error) => {
|
|
47
134
|
process.stdout.write(
|
|
48
|
-
`\x1b[31mpisesh failed to launch: ${
|
|
135
|
+
`\x1b[31mpisesh failed to launch: ${error.message}\x1b[0m\n`,
|
|
49
136
|
);
|
|
50
|
-
|
|
137
|
+
finish({ code: 127, error: error.message });
|
|
51
138
|
});
|
|
52
139
|
});
|
|
53
140
|
}
|
|
54
141
|
|
|
142
|
+
function sameSession(left: string | undefined, right: string): boolean {
|
|
143
|
+
return left ? path.resolve(left) === path.resolve(right) : false;
|
|
144
|
+
}
|
|
145
|
+
|
|
55
146
|
export default function (pi: ExtensionAPI) {
|
|
147
|
+
// A successful switch loads a fresh extension instance before the old command
|
|
148
|
+
// returns. Plain pending data on globalThis lets that new instance apply the
|
|
149
|
+
// selected model and thinking without touching stale pre-switch pi/ctx objects.
|
|
150
|
+
pi.on("session_start", async (event, ctx) => {
|
|
151
|
+
const pending = processState.__piseshPendingSwitch;
|
|
152
|
+
if (
|
|
153
|
+
event.reason !== "resume" ||
|
|
154
|
+
!pending ||
|
|
155
|
+
!sameSession(ctx.sessionManager.getSessionFile(), pending.sessionPath)
|
|
156
|
+
) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
processState.__piseshPendingSwitch = undefined;
|
|
160
|
+
|
|
161
|
+
if (pending.model) {
|
|
162
|
+
const separator = pending.model.indexOf("/");
|
|
163
|
+
const model =
|
|
164
|
+
separator > 0
|
|
165
|
+
? ctx.modelRegistry.find(
|
|
166
|
+
pending.model.slice(0, separator),
|
|
167
|
+
pending.model.slice(separator + 1),
|
|
168
|
+
)
|
|
169
|
+
: undefined;
|
|
170
|
+
if (!model || !(await pi.setModel(model))) {
|
|
171
|
+
ctx.ui.notify(
|
|
172
|
+
`Could not apply resume model: ${pending.model}`,
|
|
173
|
+
"warning",
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (pending.thinking) {
|
|
178
|
+
pi.setThinkingLevel(
|
|
179
|
+
pending.thinking as Parameters<typeof pi.setThinkingLevel>[0],
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
if (pending.repaired) {
|
|
183
|
+
ctx.ui.notify(
|
|
184
|
+
`Repaired ${pending.repaired} interrupted tool call${pending.repaired === 1 ? "" : "s"} before resume`,
|
|
185
|
+
"warning",
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
if (
|
|
189
|
+
pending.cwdOverride &&
|
|
190
|
+
path.resolve(ctx.cwd) !== path.resolve(pending.cwdOverride)
|
|
191
|
+
) {
|
|
192
|
+
ctx.ui.notify(
|
|
193
|
+
"This pi version did not apply the selected cwd override; update pi to a version that supports it",
|
|
194
|
+
"warning",
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
56
199
|
pi.registerCommand("sesh", {
|
|
57
200
|
description: "Browse, star, and resume pi sessions (opens pisesh TUI)",
|
|
58
201
|
handler: async (_args, ctx) => {
|
|
59
|
-
if (
|
|
60
|
-
ctx.ui
|
|
202
|
+
if (ctx.mode !== "tui") {
|
|
203
|
+
ctx.ui.notify("/sesh requires pi's interactive TUI", "warning");
|
|
61
204
|
return;
|
|
62
205
|
}
|
|
63
206
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
} catch {
|
|
68
|
-
currentId = undefined;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const code = await ctx.ui.custom<number | null>(
|
|
72
|
-
(tui, _theme, _kb, done) => {
|
|
73
|
-
// Hand over the terminal
|
|
207
|
+
const currentId = ctx.sessionManager.getSessionId();
|
|
208
|
+
const result = await ctx.ui.custom<PickerResult>(
|
|
209
|
+
(tui, _theme, _keybindings, done) => {
|
|
74
210
|
tui.stop();
|
|
75
211
|
process.stdout.write("\x1b[2J\x1b[H");
|
|
76
|
-
|
|
77
|
-
runPisesh(currentId).then((exitCode) => {
|
|
78
|
-
// Restore pi's TUI
|
|
212
|
+
void runPisesh(currentId).then((pickerResult) => {
|
|
79
213
|
tui.start();
|
|
80
214
|
tui.requestRender(true);
|
|
81
|
-
done(
|
|
215
|
+
done(pickerResult);
|
|
82
216
|
});
|
|
83
|
-
|
|
84
|
-
// Return a no-op component (custom() requires one synchronously)
|
|
85
217
|
return { render: () => [], invalidate: () => {} };
|
|
86
218
|
},
|
|
87
219
|
);
|
|
88
220
|
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
|
|
221
|
+
if (!result) return;
|
|
222
|
+
if (result.error) {
|
|
223
|
+
ctx.ui.notify(`pisesh: ${result.error}`, "error");
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (result.code !== 0 && result.code !== null) {
|
|
227
|
+
ctx.ui.notify(`pisesh exited with code ${result.code}`, "warning");
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const selection = result.selection;
|
|
231
|
+
if (!selection) return;
|
|
232
|
+
|
|
233
|
+
const currentFile = ctx.sessionManager.getSessionFile();
|
|
234
|
+
if (sameSession(currentFile, selection.sessionPath)) {
|
|
235
|
+
ctx.ui.notify("That session is already active", "info");
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const pending: PendingSwitch = {
|
|
240
|
+
sessionPath: selection.sessionPath,
|
|
241
|
+
cwdOverride: selection.cwdOverride,
|
|
242
|
+
model: selection.model,
|
|
243
|
+
thinking: selection.thinking,
|
|
244
|
+
repaired: selection.repaired,
|
|
245
|
+
};
|
|
246
|
+
processState.__piseshPendingSwitch = pending;
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
const switchSession = ctx.switchSession as (
|
|
250
|
+
sessionPath: string,
|
|
251
|
+
options?: { cwdOverride?: string },
|
|
252
|
+
) => Promise<{ cancelled: boolean }>;
|
|
253
|
+
const switched = await switchSession(
|
|
254
|
+
selection.sessionPath,
|
|
255
|
+
selection.cwdOverride
|
|
256
|
+
? { cwdOverride: selection.cwdOverride }
|
|
257
|
+
: undefined,
|
|
258
|
+
);
|
|
259
|
+
if (switched.cancelled) {
|
|
260
|
+
processState.__piseshPendingSwitch = undefined;
|
|
261
|
+
ctx.ui.notify("Resume cancelled", "info");
|
|
262
|
+
}
|
|
263
|
+
} finally {
|
|
264
|
+
if (processState.__piseshPendingSwitch === pending) {
|
|
265
|
+
processState.__piseshPendingSwitch = undefined;
|
|
266
|
+
}
|
|
95
267
|
}
|
|
96
268
|
},
|
|
97
269
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pisesh",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Bookmark, search, and resume pi coding-agent sessions with a fast keyboard-driven TUI.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"author": "Blue-B <source_vs@naver.com>",
|
|
28
28
|
"type": "commonjs",
|
|
29
29
|
"bin": {
|
|
30
|
-
"pisesh": "
|
|
30
|
+
"pisesh": "bin/pisesh"
|
|
31
31
|
},
|
|
32
32
|
"files": [
|
|
33
33
|
"bin/",
|