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 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. Completion requires idle Pi and a current displayed revision;
67
- failures leave Pi open. `/quit` exits without completing.
68
-
69
- Reload/resume restores the active
70
- branch's selection; `/new` retains it. Unrelated fresh launches start unselected.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-note",
3
- "version": "0.29.1",
3
+ "version": "0.30.0",
4
4
  "description": "Persistent pinote tasks, Markdown handoffs, and task status for Pi",
5
5
  "license": "MIT",
6
6
  "type": "module",
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
- const leave = 'Exit without completing';
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
- const exit = async () => {
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 && ctx.ui.getEditorText().length === 0;
30
- const shutdown = () => { if (!ctx.isIdle()) ctx.abort(); ctx.shutdown(); };
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) { shutdown(); return; }
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) { shutdown(); return; }
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) ctx.shutdown();
57
- else ctx.ui.notify('Pinote: Pi stayed open because the selection was not cleared. Refresh /pinote before retrying.', 'warning');
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 open. Completion was not confirmed and may have committed; refresh /pinote before retrying. /quit exits without completing.', 'warning');
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', interrupted);
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
  }