pi-note 0.29.1 → 0.30.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 +8 -5
- package/package.json +1 -1
- package/remote/exit.ts +33 -20
package/README.md
CHANGED
|
@@ -63,11 +63,14 @@ No Kitty action handler is needed for this extension.
|
|
|
63
63
|
|
|
64
64
|
Completion keeps the conversation and editor. With an empty editor, **Ctrl+D**
|
|
65
65
|
(or your configured exit key) offers Cancel, Exit without completing, or Complete
|
|
66
|
-
task and exit.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
66
|
+
task and exit. **`/new`** offers Cancel, Start new session without completing, or
|
|
67
|
+
Complete task and start new session. With no selected task, `/new` proceeds normally.
|
|
68
|
+
Completion requires idle Pi and a current displayed revision; failures keep the
|
|
69
|
+
current session open. `/quit` exits without completing.
|
|
70
|
+
|
|
71
|
+
Reload/resume restores the active branch's selection. Starting a new session
|
|
72
|
+
without completing retains the selection; completing starts it unselected.
|
|
73
|
+
Unrelated fresh launches start unselected.
|
|
71
74
|
Changing endpoints starts a separate selection namespace. Unsaved Pi sessions
|
|
72
75
|
cannot guarantee selection persistence.
|
|
73
76
|
|
package/package.json
CHANGED
package/remote/exit.ts
CHANGED
|
@@ -3,13 +3,11 @@ import { truncateToWidth } from '@earendil-works/pi-tui';
|
|
|
3
3
|
import { clean } from './protocol.ts';
|
|
4
4
|
import type { RemoteSession } from './session.ts';
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
const complete = 'Complete task and exit';
|
|
8
|
-
|
|
9
|
-
/** Default remote companion. /quit and nonempty-editor input remain Pi-owned. */
|
|
6
|
+
/** Default remote companion. /quit and nonempty-editor exit input remain Pi-owned. */
|
|
10
7
|
export function registerRemoteExit(pi: ExtensionAPI, getSession: () => RemoteSession | undefined): void {
|
|
11
8
|
let invalidate: (() => void) | undefined;
|
|
12
9
|
let stop: (() => void) | undefined;
|
|
10
|
+
let confirm: ((reason: 'exit' | 'new') => Promise<boolean>) | undefined;
|
|
13
11
|
pi.on('session_start', (_event, ctx) => {
|
|
14
12
|
stop?.();
|
|
15
13
|
if (ctx.mode !== 'tui' || !ctx.hasUI) return;
|
|
@@ -17,33 +15,36 @@ export function registerRemoteExit(pi: ExtensionAPI, getSession: () => RemoteSes
|
|
|
17
15
|
let alive = true, pending = false, epoch = 0;
|
|
18
16
|
let controller: AbortController | undefined;
|
|
19
17
|
invalidate = () => { epoch++; controller?.abort(); };
|
|
20
|
-
|
|
21
|
-
if (!alive || pending) return;
|
|
18
|
+
confirm = async (reason) => {
|
|
19
|
+
if (!alive || pending) return false;
|
|
20
|
+
const leave = reason === 'new' ? 'Start new session without completing' : 'Exit without completing';
|
|
21
|
+
const complete = reason === 'new' ? 'Complete task and start new session' : 'Complete task and exit';
|
|
22
22
|
pending = true;
|
|
23
23
|
const generation = epoch, session = getSession();
|
|
24
24
|
const operation = controller = new AbortController();
|
|
25
25
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
26
|
+
const draft = ctx.ui.getEditorText();
|
|
26
27
|
const validSession = session?.current();
|
|
27
28
|
const current = () => alive && generation === epoch && !operation.signal.aborted &&
|
|
28
29
|
session === getSession() && !!validSession?.() &&
|
|
29
|
-
ctx.sessionManager.getSessionId() === sessionId &&
|
|
30
|
-
|
|
30
|
+
ctx.sessionManager.getSessionId() === sessionId &&
|
|
31
|
+
(reason === 'exit' ? ctx.ui.getEditorText().length === 0 : ctx.ui.getEditorText() === draft);
|
|
31
32
|
try {
|
|
32
33
|
if (!session) throw new Error('unavailable');
|
|
33
34
|
await session.refresh(true);
|
|
34
|
-
if (!current()) return;
|
|
35
|
+
if (!current()) return false;
|
|
35
36
|
const view = session.view;
|
|
36
37
|
if (!view) throw new Error('unavailable');
|
|
37
|
-
if (view.state.selectedId == null)
|
|
38
|
+
if (view.state.selectedId == null) return true;
|
|
38
39
|
const branch = ctx.sessionManager.getLeafId();
|
|
39
40
|
const done = view.controls.find((c) => c.id === 'done' && c.kind === 'action');
|
|
40
41
|
const title = truncateToWidth(clean((view.body ?? view.title).split('\n', 1)[0]), 70);
|
|
41
42
|
const choice = await ctx.ui.select(`Pinote · ${title}`, ['Cancel', leave, complete], {
|
|
42
43
|
signal: AbortSignal.any([operation.signal, session.signal]),
|
|
43
44
|
});
|
|
44
|
-
if (!current()) return;
|
|
45
|
-
if (choice === leave)
|
|
46
|
-
if (choice !== complete) return;
|
|
45
|
+
if (!current()) return false;
|
|
46
|
+
if (choice === leave) return true;
|
|
47
|
+
if (choice !== complete) return false;
|
|
47
48
|
const latest = session.view;
|
|
48
49
|
if (!ctx.isIdle() || ctx.sessionManager.getLeafId() !== branch || !done ||
|
|
49
50
|
!latest || latest.state.selectedId !== view.state.selectedId || latest.revision !== view.revision ||
|
|
@@ -51,13 +52,20 @@ export function registerRemoteExit(pi: ExtensionAPI, getSession: () => RemoteSes
|
|
|
51
52
|
throw new Error('changed');
|
|
52
53
|
}
|
|
53
54
|
// Approval belongs to this exact displayed view, never to a refreshed revision.
|
|
54
|
-
await session.action(view, done, undefined, () => ctx.isIdle());
|
|
55
|
-
if (!current()) return;
|
|
56
|
-
if (session.view && session.view.state.selectedId == null)
|
|
57
|
-
|
|
55
|
+
await session.action(view, done, undefined, () => current() && ctx.isIdle() && ctx.sessionManager.getLeafId() === branch);
|
|
56
|
+
if (!current()) return false;
|
|
57
|
+
if (session.view && session.view.state.selectedId == null) return true;
|
|
58
|
+
ctx.ui.notify('Pinote: Pi stayed in this session because the selection was not cleared. Refresh /pinote before retrying.', 'warning');
|
|
58
59
|
} catch {
|
|
59
|
-
if (alive && generation === epoch) ctx.ui.notify('Pinote: Pi stayed
|
|
60
|
+
if (alive && generation === epoch) ctx.ui.notify('Pinote: Pi stayed in this session. Completion was not confirmed and may have committed; refresh /pinote before retrying. /quit exits without completing.', 'warning');
|
|
60
61
|
} finally { pending = false; }
|
|
62
|
+
return false;
|
|
63
|
+
};
|
|
64
|
+
const exit = async () => {
|
|
65
|
+
if (await confirm?.('exit')) {
|
|
66
|
+
if (!ctx.isIdle()) ctx.abort();
|
|
67
|
+
ctx.shutdown();
|
|
68
|
+
}
|
|
61
69
|
};
|
|
62
70
|
const factory: NonNullable<ReturnType<typeof ctx.ui.getEditorComponent>> = (tui, theme, keybindings) => {
|
|
63
71
|
const editor = previous ? previous(tui, theme, keybindings) : new CustomEditor(tui, theme, keybindings);
|
|
@@ -75,10 +83,15 @@ export function registerRemoteExit(pi: ExtensionAPI, getSession: () => RemoteSes
|
|
|
75
83
|
if (ctx.ui.getEditorComponent() === factory) ctx.ui.setEditorComponent(previous);
|
|
76
84
|
};
|
|
77
85
|
});
|
|
78
|
-
pi.on('session_shutdown', () => { stop?.(); stop = invalidate = undefined; });
|
|
86
|
+
pi.on('session_shutdown', () => { stop?.(); stop = invalidate = confirm = undefined; });
|
|
79
87
|
const interrupted = () => { invalidate?.(); };
|
|
80
88
|
pi.on('agent_start', interrupted);
|
|
81
|
-
pi.on('session_before_switch',
|
|
89
|
+
pi.on('session_before_switch', async (event, ctx) => {
|
|
90
|
+
interrupted();
|
|
91
|
+
if (event.reason === 'new' && ctx.mode === 'tui' && ctx.hasUI && !await confirm?.('new')) {
|
|
92
|
+
return { cancel: true };
|
|
93
|
+
}
|
|
94
|
+
});
|
|
82
95
|
pi.on('session_before_fork', interrupted);
|
|
83
96
|
pi.on('session_before_tree', interrupted);
|
|
84
97
|
}
|