pi-note 0.17.1 → 0.18.1
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 +20 -5
- package/footer-config.ts +14 -4
- package/footer-settings.ts +21 -6
- package/index.ts +61 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -84,7 +84,7 @@ The agent can still read the task separately with `pinote_get_current`.
|
|
|
84
84
|
Click previews work while the agent is running, provided no other Pinote operation
|
|
85
85
|
is open. Each click saves a fresh snapshot in the transcript, above any currently
|
|
86
86
|
streaming response. Later output scrolls it upward; it does not expire. Preview
|
|
87
|
-
reads never lock out the agent's task updates. Settings
|
|
87
|
+
reads never lock out the agent's task updates. Settings also works during a running turn.
|
|
88
88
|
|
|
89
89
|

|
|
90
90
|
|
|
@@ -112,8 +112,11 @@ are rejected. The **Preview task** Settings action is also available when the
|
|
|
112
112
|
socket cannot start; the task remains visible without a link.
|
|
113
113
|
|
|
114
114
|
Each Pi session remembers its own task, including several sessions in one folder.
|
|
115
|
-
Resume restores that session's task
|
|
116
|
-
|
|
115
|
+
Resume restores that session's task. `/new` keeps the current task selected and
|
|
116
|
+
prepopulates the editor with `newSessionPrompt`; nothing is submitted automatically.
|
|
117
|
+
A fresh Pi launch starts unselected and does not import an older per-folder selection.
|
|
118
|
+
Completion still starts a clean, unselected session. Retention requires normal
|
|
119
|
+
file-backed Pi sessions; `--no-session` does not retain the selection across `/new`. Pi saves the session after the first submitted message;
|
|
117
120
|
quitting before that leaves the next launch unselected, while the task stays in progress.
|
|
118
121
|
Use **Continue**
|
|
119
122
|
to insert the same configured prompt. Completing, removing, or scheduling a task clears it
|
|
@@ -131,6 +134,7 @@ Configure `~/.pi/agent/pi-note.json` (or the agent directory set by
|
|
|
131
134
|
```json
|
|
132
135
|
{
|
|
133
136
|
"handoffPrompt": "Read the current Pinote task. Summarize your understanding and proposed approach, but don’t start work yet.",
|
|
137
|
+
"newSessionPrompt": "Read the current Pinote task. Summarize your understanding and proposed approach, but don’t start work yet.",
|
|
134
138
|
"taskOfferPolicy": "github-remote"
|
|
135
139
|
}
|
|
136
140
|
```
|
|
@@ -158,6 +162,13 @@ warns until you fix the file and reload. No project-local configuration is read.
|
|
|
158
162
|
In `/pi-note → Settings`, select **Task prompt** to edit the text inserted after
|
|
159
163
|
selection or **Continue**. **Shift+Enter** or **Ctrl+J** adds a newline; **Enter** saves it
|
|
160
164
|
globally, **Ctrl+C** clears the edit, and **Esc** cancels that unfinished edit.
|
|
165
|
+
|
|
166
|
+
**New session prompt** edits the separate draft inserted by `/new` when a task is
|
|
167
|
+
retained. Its default matches the built-in task prompt; an empty/whitespace-only
|
|
168
|
+
value disables insertion without clearing the task. `/new` rereads it each time,
|
|
169
|
+
so changes need no `/reload`. Invalid configuration warns and leaves the retained
|
|
170
|
+
task selected without inserting a prompt. Both prompts are literal text, not templates.
|
|
171
|
+
|
|
161
172
|
Settings and fields save automatically after each confirmed change; no separate
|
|
162
173
|
Save action is needed. Leaving Settings with **Esc** or **Tab** keeps saved changes
|
|
163
174
|
and discards only unfinished input. Saving never changes existing editor input or submits anything.
|
|
@@ -309,7 +320,7 @@ String definitions and the older `{ "label": "Next", "width": 24 }` form still
|
|
|
309
320
|
work. Absent/`null` fields retain the legacy automatic PR link plus `Bar` fields;
|
|
310
321
|
the first Settings save turns this legacy list into explicit global definitions.
|
|
311
322
|
|
|
312
|
-
Settings saves apply immediately in this Pi session. Run `/reload` after manual
|
|
323
|
+
Settings saves include both prompts and apply immediately in this Pi session. Run `/reload` after manual
|
|
313
324
|
footer edits or in other running sessions; settings are also reread on session start.
|
|
314
325
|
Invalid footer configuration warns and uses footer defaults; missing files silently
|
|
315
326
|
use defaults. The task prompt is also read separately on selection/Continue as
|
|
@@ -347,7 +358,11 @@ Pinote no longer polls GitHub, runs `gh`, watches completed tasks, or prompts on
|
|
|
347
358
|
PR merges. No GitHub authentication is required. `PINOTE_PR_POLL_SECONDS` is no
|
|
348
359
|
longer used. Other separately installed Pi PR/Git extensions are unaffected.
|
|
349
360
|
|
|
350
|
-
Tools work without a TUI
|
|
361
|
+
Tools work without a TUI. `/pi-note` works in a TUI while the agent is running:
|
|
362
|
+
you can select/switch tasks, use Continue, preview tasks, respond to suggestions,
|
|
363
|
+
and edit settings without interrupting the turn or submitting input. Completion
|
|
364
|
+
and CLI installation/upgrades still require an idle session. Another pending
|
|
365
|
+
Pinote operation blocks the menu. Open menus do not block agent handoff updates.
|
|
351
366
|
This package does not synchronize databases or paths between machines. Note text
|
|
352
367
|
and fields loaded into Pi are sent to the configured model when used as context;
|
|
353
368
|
avoid secrets. Agent commands do not send desktop notifications.
|
package/footer-config.ts
CHANGED
|
@@ -4,13 +4,20 @@ import { join } from "node:path";
|
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
5
|
|
|
6
6
|
export const defaultHandoffPrompt = "Read the current Pinote task. Summarize your understanding, but don’t start work yet.";
|
|
7
|
+
export const defaultNewSessionPrompt = defaultHandoffPrompt;
|
|
8
|
+
export function parseNewSessionPrompt(value: unknown): string {
|
|
9
|
+
if (typeof value !== "string" || /[\x00-\x08\x0b-\x1f\x7f-\x9f]/u.test(value)) {
|
|
10
|
+
throw new Error("newSessionPrompt in pi-note.json must be a string without control characters (except tabs/newlines).");
|
|
11
|
+
}
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
7
14
|
export function parseHandoffPrompt(value: unknown): string {
|
|
8
15
|
if (typeof value !== "string" || !value.trim() || /[\x00-\x08\x0b-\x1f\x7f-\x9f]/u.test(value)) {
|
|
9
16
|
throw new Error("handoffPrompt in pi-note.json must be a nonblank string without control characters (except tabs/newlines).");
|
|
10
17
|
}
|
|
11
18
|
return value;
|
|
12
19
|
}
|
|
13
|
-
export type PinoteSettings = { footer: FooterConfig; handoffPrompt: string };
|
|
20
|
+
export type PinoteSettings = { footer: FooterConfig; handoffPrompt: string; newSessionPrompt?: string };
|
|
14
21
|
|
|
15
22
|
export type FooterField = { name: string; label: string; link: boolean; format: string; width?: number };
|
|
16
23
|
export type FooterConfig = { titleWidth: number; fieldWidth: number; maxFields: number; fields: FooterField[] | null };
|
|
@@ -90,9 +97,12 @@ export function loadFooterConfig(warn: (message: string) => void): FooterConfig
|
|
|
90
97
|
|
|
91
98
|
// Synchronous compare-and-replace: reject edits made while the settings dialog was open,
|
|
92
99
|
// preserve unrelated keys, and never leave partial JSON behind.
|
|
93
|
-
export function saveFooterConfig(config: FooterConfig, expectedRaw: string | null, handoffPrompt?: string) {
|
|
100
|
+
export function saveFooterConfig(config: FooterConfig, expectedRaw: string | null, handoffPrompt?: string, newSessionPrompt?: string) {
|
|
94
101
|
const checked = parseFooterConfig({ footer: config });
|
|
95
|
-
const
|
|
102
|
+
const prompts = {
|
|
103
|
+
...(handoffPrompt === undefined ? {} : { handoffPrompt: parseHandoffPrompt(handoffPrompt) }),
|
|
104
|
+
...(newSessionPrompt === undefined ? {} : { newSessionPrompt: parseNewSessionPrompt(newSessionPrompt) }),
|
|
105
|
+
};
|
|
96
106
|
const directory = getAgentDir();
|
|
97
107
|
mkdirSync(directory, { recursive: true });
|
|
98
108
|
const path = join(directory, "pi-note.json");
|
|
@@ -107,7 +117,7 @@ export function saveFooterConfig(config: FooterConfig, expectedRaw: string | nul
|
|
|
107
117
|
if (lstatSync(path, { throwIfNoEntry: false })?.isSymbolicLink()) throw new Error("pi-note.json is a symlink. Edit its target manually rather than replacing the link.");
|
|
108
118
|
const root = expectedRaw === null ? {} : JSON.parse(expectedRaw);
|
|
109
119
|
if (!object(root)) throw new Error("Invalid pi-note configuration; expected an object.");
|
|
110
|
-
raw = `${JSON.stringify({ ...root, ...
|
|
120
|
+
raw = `${JSON.stringify({ ...root, ...prompts, footer: checked }, null, 2)}\n`;
|
|
111
121
|
writeFileSync(temp, raw, { flag: "wx", mode: 0o600 });
|
|
112
122
|
renameSync(temp, path);
|
|
113
123
|
} finally { rmSync(temp, { force: true }); closeSync(descriptor); rmSync(lock, { force: true }); }
|
package/footer-settings.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Editor, Input, matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
|
-
import { defaultFooterField, parseFooterConfig, parseHandoffPrompt, type PinoteSettings, type FooterConfig, type FooterField } from "./footer-config.ts";
|
|
3
|
+
import { defaultFooterField, defaultNewSessionPrompt, parseFooterConfig, parseHandoffPrompt, parseNewSessionPrompt, type PinoteSettings, type FooterConfig, type FooterField } from "./footer-config.ts";
|
|
4
4
|
|
|
5
5
|
type Matches = (data: string, action: string) => boolean;
|
|
6
6
|
type Row = { label: string; action: () => void };
|
|
@@ -42,7 +42,9 @@ export class TaskMenu {
|
|
|
42
42
|
export class FooterSettings {
|
|
43
43
|
private draft: FooterConfig;
|
|
44
44
|
private prompt: string;
|
|
45
|
+
private newSessionPrompt: string;
|
|
45
46
|
private editor?: Editor;
|
|
47
|
+
private editorMode?: "handoff" | "newSession";
|
|
46
48
|
private createEditor: () => Editor;
|
|
47
49
|
private knownFields: string[];
|
|
48
50
|
private theme: Theme;
|
|
@@ -65,6 +67,7 @@ export class FooterSettings {
|
|
|
65
67
|
this.configurationError = configurationError;
|
|
66
68
|
this.draft = structuredClone(config.footer);
|
|
67
69
|
this.prompt = config.handoffPrompt;
|
|
70
|
+
this.newSessionPrompt = config.newSessionPrompt === undefined ? defaultNewSessionPrompt : parseNewSessionPrompt(config.newSessionPrompt);
|
|
68
71
|
this.createEditor = createEditor;
|
|
69
72
|
this.save = save;
|
|
70
73
|
this.draft.fields ??= [];
|
|
@@ -86,11 +89,13 @@ export class FooterSettings {
|
|
|
86
89
|
private validate(field: FooterField): FooterField {
|
|
87
90
|
return parseFooterConfig({ footer: { fields: [field] } }).fields![0];
|
|
88
91
|
}
|
|
89
|
-
private persist(footer = this.draft, handoffPrompt = this.prompt) {
|
|
90
|
-
const settings = { footer: parseFooterConfig({ footer }), handoffPrompt: parseHandoffPrompt(handoffPrompt)
|
|
92
|
+
private persist(footer = this.draft, handoffPrompt = this.prompt, newSessionPrompt = this.newSessionPrompt) {
|
|
93
|
+
const settings = { footer: parseFooterConfig({ footer }), handoffPrompt: parseHandoffPrompt(handoffPrompt),
|
|
94
|
+
newSessionPrompt: parseNewSessionPrompt(newSessionPrompt) };
|
|
91
95
|
this.save(structuredClone(settings));
|
|
92
96
|
this.draft = settings.footer;
|
|
93
97
|
this.prompt = settings.handoffPrompt;
|
|
98
|
+
this.newSessionPrompt = settings.newSessionPrompt;
|
|
94
99
|
}
|
|
95
100
|
private persistField(field: FooterField) {
|
|
96
101
|
const candidate = this.validate(field);
|
|
@@ -119,11 +124,19 @@ export class FooterSettings {
|
|
|
119
124
|
}),
|
|
120
125
|
}));
|
|
121
126
|
rows.push({ label: `Task prompt: ${display(this.prompt)}`, action: () => {
|
|
127
|
+
this.editorMode = "handoff";
|
|
122
128
|
this.editor = this.createEditor();
|
|
123
129
|
this.editor.disableSubmit = true;
|
|
124
130
|
this.editor.setText(this.prompt);
|
|
125
131
|
this.editor.focused = this.hasFocus;
|
|
126
132
|
} });
|
|
133
|
+
rows.push({ label: `New session prompt: ${display(this.newSessionPrompt).trim() || "(disabled)"}`, action: () => {
|
|
134
|
+
this.editorMode = "newSession";
|
|
135
|
+
this.editor = this.createEditor();
|
|
136
|
+
this.editor.disableSubmit = true;
|
|
137
|
+
this.editor.setText(this.newSessionPrompt);
|
|
138
|
+
this.editor.focused = this.hasFocus;
|
|
139
|
+
} });
|
|
127
140
|
const configured = this.draft.fields!.map((field) => field.name);
|
|
128
141
|
for (const name of [...configured, ...this.knownFields.filter((name) => !configured.includes(name))]) {
|
|
129
142
|
rows.push({ label: `Field: ${display(name)}${configured.includes(name) ? "" : " (not configured)"}`, action: () => this.edit(name) });
|
|
@@ -168,11 +181,13 @@ export class FooterSettings {
|
|
|
168
181
|
if (isTab(data)) this.done("tasks");
|
|
169
182
|
else if (this.editor) {
|
|
170
183
|
if (matchesKey(data, "ctrl+c")) this.editor.setText("");
|
|
171
|
-
else if (this.matches(data, "tui.select.cancel")) this.editor = undefined;
|
|
184
|
+
else if (this.matches(data, "tui.select.cancel")) { this.editor = undefined; this.editorMode = undefined; }
|
|
172
185
|
else if (this.matches(data, "tui.input.newLine")) this.editor.handleInput(data);
|
|
173
186
|
else if (this.matches(data, "tui.select.confirm")) {
|
|
174
|
-
this.persist(this.draft, this.editor.getExpandedText());
|
|
187
|
+
if (this.editorMode === "newSession") this.persist(this.draft, this.prompt, this.editor.getExpandedText());
|
|
188
|
+
else this.persist(this.draft, this.editor.getExpandedText(), this.newSessionPrompt);
|
|
175
189
|
this.editor = undefined;
|
|
190
|
+
this.editorMode = undefined;
|
|
176
191
|
} else this.editor.handleInput(data);
|
|
177
192
|
} else if (this.input) {
|
|
178
193
|
if (this.matches(data, "tui.select.cancel")) this.input = undefined;
|
|
@@ -197,7 +212,7 @@ export class FooterSettings {
|
|
|
197
212
|
const lines = [this.theme.fg("accent", "Pinote — Global Settings"), this.theme.fg("dim", "Tasks (Tab) · Settings · Fields")];
|
|
198
213
|
if (this.configurationError) lines.push(this.theme.fg("warning", this.configurationError));
|
|
199
214
|
if (this.editor) {
|
|
200
|
-
lines.push(this.theme.fg("accent", "Task prompt"), ...this.editor.render(width),
|
|
215
|
+
lines.push(this.theme.fg("accent", this.editorMode === "newSession" ? "New session prompt" : "Task prompt"), ...this.editor.render(width),
|
|
201
216
|
this.theme.fg("dim", "Enter save · Shift+Enter/Ctrl+J newline · Ctrl+C clear · Esc cancel edit"));
|
|
202
217
|
} else if (this.input) {
|
|
203
218
|
lines.push(this.theme.fg("accent", this.input.heading), ...this.input.widget.render(width),
|
package/index.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { join } from "node:path";
|
|
|
6
6
|
import { stripVTControlCharacters } from "node:util";
|
|
7
7
|
import { Type } from "typebox";
|
|
8
8
|
import { updateTaskFooter, clearTaskFooter } from "./footer-status.ts";
|
|
9
|
-
import { defaultFooterConfig, defaultHandoffPrompt, effectiveFooterFields, loadFooterConfig, parseHandoffPrompt, readFooterDocument, saveFooterConfig, type FooterConfig, type PinoteSettings } from "./footer-config.ts";
|
|
9
|
+
import { defaultFooterConfig, defaultHandoffPrompt, defaultNewSessionPrompt, effectiveFooterFields, loadFooterConfig, parseHandoffPrompt, parseNewSessionPrompt, readFooterDocument, saveFooterConfig, type FooterConfig, type PinoteSettings } from "./footer-config.ts";
|
|
10
10
|
import { FooterSettings, TaskMenu, withSettingsTab, type SettingsOption, type SettingsResult } from "./footer-settings.ts";
|
|
11
11
|
import { TaskPicker, taskState, taskTag } from "./task-picker.ts";
|
|
12
12
|
import { bundledCLIVersion, detectCLI, setupCLI, setupHint, versionAtLeast, type CLIAction } from "./setup.ts";
|
|
@@ -36,6 +36,13 @@ type Summary = Pick<Task, "id" | "text" | "state" | "tag">;
|
|
|
36
36
|
// The standard Pi footer accepts text, so ship a portable terminal glyph, not a theme icon.
|
|
37
37
|
const noteIcon = readFileSync(new URL("./icons/note.txt", import.meta.url), "utf8").trim();
|
|
38
38
|
const previewType = "pinote-preview";
|
|
39
|
+
// /new replaces the extension runtime, even before the outgoing session is saved.
|
|
40
|
+
// Transfer only the ID, keyed by the exact destination session file, within this process.
|
|
41
|
+
const continuationKey = Symbol.for("pi-note.new-session-selections");
|
|
42
|
+
const processState = globalThis as typeof globalThis & {
|
|
43
|
+
[continuationKey]?: Map<string, number>;
|
|
44
|
+
};
|
|
45
|
+
const continuations = processState[continuationKey] ??= new Map<string, number>();
|
|
39
46
|
const compatible = "Incompatible note CLI response. Install pinote 0.3.0+ and check note on PATH.";
|
|
40
47
|
const validId = (value: unknown): value is number => Number.isSafeInteger(value) && Number(value) > 0;
|
|
41
48
|
const record = (value: unknown): value is Record<string, unknown> =>
|
|
@@ -162,6 +169,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
162
169
|
let epoch = 0;
|
|
163
170
|
let refreshSerial = 0;
|
|
164
171
|
let pending: symbol | undefined;
|
|
172
|
+
let menuPending: symbol | undefined;
|
|
165
173
|
let previewPending = false;
|
|
166
174
|
let setupAbort: AbortController | undefined;
|
|
167
175
|
let activeContext: ExtensionContext | undefined;
|
|
@@ -311,7 +319,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
311
319
|
}
|
|
312
320
|
};
|
|
313
321
|
const preview = async (ctx: ExtensionContext, requestSignal?: AbortSignal): Promise<boolean> => {
|
|
314
|
-
if (!ctx.hasUI || ctx.mode !== "tui" || pending || previewPending || !validId(selectedId)) return false;
|
|
322
|
+
if (!ctx.hasUI || ctx.mode !== "tui" || pending || menuPending || previewPending || !validId(selectedId)) return false;
|
|
315
323
|
const generation = epoch;
|
|
316
324
|
const branch = branchEpoch;
|
|
317
325
|
const id = selectedId;
|
|
@@ -339,7 +347,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
339
347
|
}
|
|
340
348
|
};
|
|
341
349
|
const dispatchCompletion = (ctx: ExtensionContext, displayed: Task, selection: string): Promise<boolean> => {
|
|
342
|
-
if (!ctx.isIdle() || pending || completionDispatch) return Promise.resolve(false);
|
|
350
|
+
if (!ctx.isIdle() || pending || menuPending || completionDispatch) return Promise.resolve(false);
|
|
343
351
|
const token = `${selection}:${randomBytes(8).toString("hex")}`;
|
|
344
352
|
return new Promise((resolve) => {
|
|
345
353
|
const finish = (accepted: boolean) => {
|
|
@@ -360,7 +368,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
360
368
|
const token = args.trim();
|
|
361
369
|
const request = token ? completionDispatch : undefined;
|
|
362
370
|
if (token && (request?.token !== token || request.selection !== `${runtimeToken}:${epoch}:${branchEpoch}:${selectedId}`)) return;
|
|
363
|
-
if (!alive || !ctx.hasUI || ctx.mode !== "tui" || !ctx.isIdle() || pending || setupAbort || !validId(selectedId)) {
|
|
371
|
+
if (!alive || !ctx.hasUI || ctx.mode !== "tui" || !ctx.isIdle() || pending || menuPending || setupAbort || !validId(selectedId)) {
|
|
364
372
|
request?.finish(false);
|
|
365
373
|
if (ctx.hasUI) ctx.ui.notify("Select a task and wait until Pi is idle and the Pinote operation has finished.", "warning");
|
|
366
374
|
return;
|
|
@@ -420,6 +428,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
420
428
|
alive = true;
|
|
421
429
|
activeContext = ctx;
|
|
422
430
|
selectedId = readSelection(ctx);
|
|
431
|
+
const sessionFile = ctx.sessionManager.getSessionFile?.();
|
|
432
|
+
const continuation = sessionFile ? continuations.get(sessionFile) : undefined;
|
|
433
|
+
if (sessionFile) continuations.delete(sessionFile);
|
|
434
|
+
const carrying = _event.reason === "new" && validId(continuation);
|
|
435
|
+
if (carrying) remember(continuation);
|
|
423
436
|
restoreSuggestion(ctx);
|
|
424
437
|
if (_event.reason === "new" || _event.reason === "fork") {
|
|
425
438
|
// A fork may include an ancestor proposal whose consent was consumed elsewhere.
|
|
@@ -447,10 +460,27 @@ export default function (pi: ExtensionAPI) {
|
|
|
447
460
|
if (!alive || generation !== epoch) return;
|
|
448
461
|
// Keep the setup lock until its aborted subprocess has actually settled.
|
|
449
462
|
if (!setupAbort) pending = undefined;
|
|
463
|
+
menuPending = undefined;
|
|
450
464
|
await checkCLI(ctx, true);
|
|
451
465
|
if (!alive || generation !== epoch) return;
|
|
452
466
|
if (suggestion) previewBridge?.setSuggestion((choice) => respondToSuggestion(choice, ctx));
|
|
453
467
|
await refresh(ctx);
|
|
468
|
+
if (carrying && alive && generation === epoch) {
|
|
469
|
+
try {
|
|
470
|
+
const branch = branchEpoch;
|
|
471
|
+
const current = await selected(ctx);
|
|
472
|
+
if (!current || !alive || generation !== epoch || branch !== branchEpoch || selectedId !== current.id ||
|
|
473
|
+
!ctx.hasUI || ctx.mode !== "tui") return;
|
|
474
|
+
const config = readConfig();
|
|
475
|
+
const prompt = "newSessionPrompt" in config ? parseNewSessionPrompt(config.newSessionPrompt) : defaultNewSessionPrompt;
|
|
476
|
+
if (prompt.trim()) {
|
|
477
|
+
const draft = ctx.ui.getEditorText();
|
|
478
|
+
ctx.ui.setEditorText(draft ? `${draft}\n\n${prompt}` : prompt);
|
|
479
|
+
}
|
|
480
|
+
} catch (error) {
|
|
481
|
+
if (alive && generation === epoch && ctx.hasUI) ctx.ui.notify(`Pinote new session: ${clean(String(error))}`, "warning");
|
|
482
|
+
}
|
|
483
|
+
}
|
|
454
484
|
});
|
|
455
485
|
pi.on("session_tree", async (_event, ctx) => {
|
|
456
486
|
if (!alive) return;
|
|
@@ -470,6 +500,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
470
500
|
pi.on("before_agent_start", async (_event, ctx) => { await refresh(ctx); });
|
|
471
501
|
pi.on("agent_end", async (_event, ctx) => { await refresh(ctx); });
|
|
472
502
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
503
|
+
// Shutdown means replacement was accepted; cancelled /new never leaves a transfer.
|
|
504
|
+
if (_event.reason === "new" && _event.targetSessionFile && validId(selectedId)) {
|
|
505
|
+
// Bound orphaned transfers if creating the replacement runtime fails.
|
|
506
|
+
if (continuations.size >= 32) continuations.delete(continuations.keys().next().value!);
|
|
507
|
+
continuations.set(_event.targetSessionFile, selectedId);
|
|
508
|
+
}
|
|
473
509
|
setupAbort?.abort();
|
|
474
510
|
cancelCompletionDispatch();
|
|
475
511
|
clearSuggestion(ctx, false);
|
|
@@ -488,7 +524,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
488
524
|
|
|
489
525
|
const installCLI = async (upgrade: boolean, ctx: ExtensionCommandContext) => {
|
|
490
526
|
if (!ctx.hasUI || ctx.mode !== "tui") return;
|
|
491
|
-
if (pending || !ctx.isIdle()) {
|
|
527
|
+
if (pending || menuPending || !ctx.isIdle()) {
|
|
492
528
|
ctx.ui.notify("Wait until Pi is idle and the pinote operation has finished.", "warning");
|
|
493
529
|
return;
|
|
494
530
|
}
|
|
@@ -529,16 +565,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
529
565
|
return;
|
|
530
566
|
}
|
|
531
567
|
if (token) { ctx.ui.notify("Usage: /pi-note", "warning"); return; }
|
|
532
|
-
if (pending ||
|
|
533
|
-
ctx.ui.notify("Wait until
|
|
568
|
+
if (pending || menuPending) {
|
|
569
|
+
ctx.ui.notify("Wait until the pinote operation has finished.", "warning");
|
|
534
570
|
return;
|
|
535
571
|
}
|
|
536
572
|
const operation = Symbol();
|
|
537
|
-
|
|
573
|
+
menuPending = operation;
|
|
538
574
|
const generation = epoch;
|
|
539
575
|
const branch = branchEpoch;
|
|
540
576
|
const currentSession = () => alive && generation === epoch;
|
|
541
|
-
|
|
577
|
+
// Menus, task selection and preference edits do not interrupt the agent.
|
|
578
|
+
// Completion and CLI setup retain their own idle-only guards.
|
|
579
|
+
const canAct = () => currentSession() && branch === branchEpoch && menuPending === operation;
|
|
542
580
|
try {
|
|
543
581
|
await checkCLI(ctx);
|
|
544
582
|
if (!canAct()) return;
|
|
@@ -551,7 +589,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
551
589
|
return;
|
|
552
590
|
}
|
|
553
591
|
if (settings) {
|
|
554
|
-
let config: PinoteSettings = { footer: { ...defaultFooterConfig, fields: [] }, handoffPrompt: defaultHandoffPrompt };
|
|
592
|
+
let config: PinoteSettings = { footer: { ...defaultFooterConfig, fields: [] }, handoffPrompt: defaultHandoffPrompt, newSessionPrompt: defaultNewSessionPrompt };
|
|
555
593
|
let expectedRaw: string | null = null;
|
|
556
594
|
let configurationError: string | undefined;
|
|
557
595
|
try {
|
|
@@ -560,6 +598,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
560
598
|
config = {
|
|
561
599
|
footer: { ...document.config, fields: effectiveFooterFields(document.config, current?.agent_notes) },
|
|
562
600
|
handoffPrompt: "handoffPrompt" in root ? parseHandoffPrompt(root.handoffPrompt) : defaultHandoffPrompt,
|
|
601
|
+
newSessionPrompt: "newSessionPrompt" in root ? parseNewSessionPrompt(root.newSessionPrompt) : defaultNewSessionPrompt,
|
|
563
602
|
};
|
|
564
603
|
expectedRaw = document.raw;
|
|
565
604
|
} catch (error) {
|
|
@@ -590,7 +629,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
590
629
|
}), (updated) => {
|
|
591
630
|
if (!canAct()) throw new Error("This session changed. Reopen settings before editing.");
|
|
592
631
|
if (configurationError) throw new Error(configurationError);
|
|
593
|
-
const saved = saveFooterConfig(updated.footer, expectedRaw, updated.handoffPrompt);
|
|
632
|
+
const saved = saveFooterConfig(updated.footer, expectedRaw, updated.handoffPrompt, updated.newSessionPrompt);
|
|
594
633
|
expectedRaw = saved.raw;
|
|
595
634
|
footerConfig = saved.config;
|
|
596
635
|
void refresh(ctx);
|
|
@@ -598,7 +637,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
598
637
|
if (!canAct() || edited === undefined) return;
|
|
599
638
|
if (edited === "tasks") { settings = false; continue; }
|
|
600
639
|
if (!options.some((option) => option.result === edited)) return;
|
|
601
|
-
if (
|
|
640
|
+
if (menuPending === operation) menuPending = undefined;
|
|
602
641
|
if (edited === "preview") {
|
|
603
642
|
if (!await preview(ctx)) ctx.ui.notify("The task is no longer available for preview.", "warning");
|
|
604
643
|
} else if (edited === "done") await completeSelected("", ctx, current ?? undefined);
|
|
@@ -614,7 +653,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
614
653
|
if (!action || !canAct()) return;
|
|
615
654
|
if (action === "Settings") { settings = true; continue; }
|
|
616
655
|
if (action === "Done" && current) {
|
|
617
|
-
if (
|
|
656
|
+
if (menuPending === operation) menuPending = undefined;
|
|
618
657
|
await completeSelected("", ctx, current);
|
|
619
658
|
return;
|
|
620
659
|
}
|
|
@@ -632,6 +671,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
632
671
|
id = choice;
|
|
633
672
|
} else return;
|
|
634
673
|
const handoff = handoffPrompt();
|
|
674
|
+
if (pending) {
|
|
675
|
+
ctx.ui.notify("Wait until the pinote operation has finished.", "warning");
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
pending = operation;
|
|
635
679
|
refreshSerial++;
|
|
636
680
|
const chosen = await startTask(id, canAct);
|
|
637
681
|
if (!chosen || !canAct()) return;
|
|
@@ -644,6 +688,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
644
688
|
if (currentSession()) ctx.ui.notify(`Pinote: ${clean(error instanceof Error ? error.message : String(error))}`, "error");
|
|
645
689
|
} finally {
|
|
646
690
|
if (pending === operation) pending = undefined;
|
|
691
|
+
if (menuPending === operation) menuPending = undefined;
|
|
647
692
|
if (currentSession()) await refresh(ctx);
|
|
648
693
|
}
|
|
649
694
|
},
|
|
@@ -720,7 +765,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
720
765
|
clearSuggestion(ctx);
|
|
721
766
|
return true;
|
|
722
767
|
}
|
|
723
|
-
if (pending || setupAbort) return false;
|
|
768
|
+
if (pending || menuPending || setupAbort) return false;
|
|
724
769
|
const generation = epoch;
|
|
725
770
|
const branch = branchEpoch;
|
|
726
771
|
const operation = Symbol();
|
|
@@ -769,7 +814,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
769
814
|
const current = await selected(ctx, signal);
|
|
770
815
|
if (!alive || generation !== epoch || branch !== branchEpoch) throw new Error("Pinote operation cancelled: the session changed.");
|
|
771
816
|
if (current || selectedId !== null) throw new Error("A task is already selected; keep it unless the user asks to switch.");
|
|
772
|
-
if (pending || setupAbort) throw new Error("A pinote operation is already open. Retry after it finishes.");
|
|
817
|
+
if (pending || menuPending || setupAbort) throw new Error("A pinote operation is already open. Retry after it finishes.");
|
|
773
818
|
const result = (status: string) => ({ content: [{ type: "text" as const, text: JSON.stringify({ status }) }], details: { status } });
|
|
774
819
|
suggestion = { text: params.text, ...(params.tag === undefined ? {} : { tag: params.tag.trim() }) };
|
|
775
820
|
saveSuggestion();
|
|
@@ -799,7 +844,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
799
844
|
const generation = epoch;
|
|
800
845
|
const branch = branchEpoch;
|
|
801
846
|
if (params.select !== true) return toolResult(await writeTask(ctx, signal, argv, undefined, "0.4.0"));
|
|
802
|
-
if (pending) throw new Error("A pinote operation is already open. Retry after it finishes.");
|
|
847
|
+
if (pending || menuPending) throw new Error("A pinote operation is already open. Retry after it finishes.");
|
|
803
848
|
const operation = Symbol();
|
|
804
849
|
const previousSelection = selectedId;
|
|
805
850
|
pending = operation;
|