pi-note 0.10.0 → 0.11.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 +17 -7
- package/footer-config.ts +16 -4
- package/footer-settings.ts +69 -30
- package/index.ts +26 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -62,8 +62,8 @@ The agent fetches current task text and saved handoff fields when you submit;
|
|
|
62
62
|
they are not copied into the draft. The default prompt asks for a summary, not implementation.
|
|
63
63
|
Existing drafts are preserved; nothing is submitted automatically.
|
|
64
64
|
With a selected task it offers **Continue**, **Done**, **Switch task**, and **Settings**.
|
|
65
|
-
Press **Tab** from the task menu or picker to open global
|
|
66
|
-
returns to tasks
|
|
65
|
+
Press **Tab** from the task menu or picker to open global settings; Tab
|
|
66
|
+
returns to tasks, keeping saved changes. Settings also lists fields found on the current
|
|
67
67
|
task, so existing values can be configured without creating another task.
|
|
68
68
|
Type in the task picker to filter by text, ID, tag, or state (all words must match).
|
|
69
69
|
Use ↑/↓ and Enter to select, or Esc to cancel. Rows show **●** for in progress or
|
|
@@ -109,6 +109,13 @@ and selected tasks. Run `/reload` after changing this setting. Invalid policy
|
|
|
109
109
|
values or unreadable/malformed configuration suppress offers; interactive startup
|
|
110
110
|
warns until you fix the file and reload. No project-local configuration is read.
|
|
111
111
|
|
|
112
|
+
In `/pi-note → Settings`, select **Task prompt** to edit the text inserted after
|
|
113
|
+
selection or **Continue**. **Shift+Enter** or **Ctrl+J** adds a newline; **Enter** saves it
|
|
114
|
+
globally, **Ctrl+C** clears the edit, and **Esc** cancels that unfinished edit.
|
|
115
|
+
Settings and fields save automatically after each confirmed change; no separate
|
|
116
|
+
Save action is needed. Leaving Settings with **Esc** or **Tab** keeps saved changes
|
|
117
|
+
and discards only unfinished input. Saving never changes existing editor input or submits anything.
|
|
118
|
+
|
|
112
119
|
Task selection and **Continue** reread `handoffPrompt` each time; changing that
|
|
113
120
|
field needs no `/reload`. Its value is literal text, not a prompt template; use
|
|
114
121
|
`\n` in JSON strings for multiple lines. It must be a nonblank string without control
|
|
@@ -203,9 +210,12 @@ accepted. Missing/blank values do not display and do not consume the field limit
|
|
|
203
210
|
In `/pi-note → Settings`, **Add field** adds a global display definition, not a
|
|
204
211
|
task value. Select any field to edit **Link**, **Label**, **Format**, and **Width**.
|
|
205
212
|
Label starts as the field name; clear it for no prefix. Link off displays plain
|
|
206
|
-
text without a clickable link.
|
|
207
|
-
|
|
208
|
-
|
|
213
|
+
text without a clickable link. Toggles, field additions/removals, and edits confirmed
|
|
214
|
+
with **Enter** save immediately. **Esc** returns from a field to settings without
|
|
215
|
+
undoing saved changes. Invalid edits and failed saves stay open with an error;
|
|
216
|
+
failed saves leave saved values unchanged and can be retried. Saves include the task
|
|
217
|
+
prompt, preserve unrelated settings (such as `taskOfferPolicy`), and reject external
|
|
218
|
+
configuration changes made since opening settings or the last successful save.
|
|
209
219
|
Concurrent saves use `pi-note.json.lock`; remove stale locks only when no save is
|
|
210
220
|
running. Symlinked configuration must be edited manually; saves never replace the link.
|
|
211
221
|
|
|
@@ -221,8 +231,8 @@ the first Settings save turns this legacy list into explicit global definitions.
|
|
|
221
231
|
Settings saves apply immediately in this Pi session. Run `/reload` after manual
|
|
222
232
|
footer edits or in other running sessions; settings are also reread on session start.
|
|
223
233
|
Invalid footer configuration warns and uses footer defaults; missing files silently
|
|
224
|
-
use defaults.
|
|
225
|
-
|
|
234
|
+
use defaults. The task prompt is also read separately on selection/Continue as
|
|
235
|
+
described above; prompt changes need no reload in any running session.
|
|
226
236
|
This is user-level configuration, not task data; agents should not change it
|
|
227
237
|
without approval. Task values still come from `pinote_update_current`.
|
|
228
238
|
|
package/footer-config.ts
CHANGED
|
@@ -3,6 +3,15 @@ import { closeSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, rm
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
5
|
|
|
6
|
+
export const defaultHandoffPrompt = "Read the current Pinote task. Summarize your understanding, but don’t start work yet.";
|
|
7
|
+
export function parseHandoffPrompt(value: unknown): string {
|
|
8
|
+
if (typeof value !== "string" || !value.trim() || /[\x00-\x08\x0b-\x1f\x7f-\x9f]/u.test(value)) {
|
|
9
|
+
throw new Error("handoffPrompt in pi-note.json must be a nonblank string without control characters (except tabs/newlines).");
|
|
10
|
+
}
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
export type PinoteSettings = { footer: FooterConfig; handoffPrompt: string };
|
|
14
|
+
|
|
6
15
|
export type FooterField = { name: string; label: string; link: boolean; format: string; width?: number };
|
|
7
16
|
export type FooterConfig = { titleWidth: number; fieldWidth: number; maxFields: number; fields: FooterField[] | null };
|
|
8
17
|
export const defaultFooterConfig: FooterConfig = { titleWidth: 60, fieldWidth: 60, maxFields: 4, fields: null };
|
|
@@ -80,9 +89,10 @@ export function loadFooterConfig(warn: (message: string) => void): FooterConfig
|
|
|
80
89
|
}
|
|
81
90
|
|
|
82
91
|
// Synchronous compare-and-replace: reject edits made while the settings dialog was open,
|
|
83
|
-
// preserve unrelated keys
|
|
84
|
-
export function saveFooterConfig(config: FooterConfig, expectedRaw: string | null)
|
|
92
|
+
// preserve unrelated keys, and never leave partial JSON behind.
|
|
93
|
+
export function saveFooterConfig(config: FooterConfig, expectedRaw: string | null, handoffPrompt?: string) {
|
|
85
94
|
const checked = parseFooterConfig({ footer: config });
|
|
95
|
+
const prompt = handoffPrompt === undefined ? {} : { handoffPrompt: parseHandoffPrompt(handoffPrompt) };
|
|
86
96
|
const directory = getAgentDir();
|
|
87
97
|
mkdirSync(directory, { recursive: true });
|
|
88
98
|
const path = join(directory, "pi-note.json");
|
|
@@ -91,15 +101,17 @@ export function saveFooterConfig(config: FooterConfig, expectedRaw: string | nul
|
|
|
91
101
|
try { descriptor = openSync(lock, "wx", 0o600); }
|
|
92
102
|
catch { throw new Error("Pinote settings are locked. Retry after another save finishes; remove a stale pi-note.json.lock only when no save is running."); }
|
|
93
103
|
const temp = join(directory, `.pi-note-${randomUUID()}.tmp`);
|
|
104
|
+
let raw: string;
|
|
94
105
|
try {
|
|
95
106
|
if (readRaw() !== expectedRaw) throw new Error("Pi-note configuration changed while settings were open. Reopen settings before saving.");
|
|
96
107
|
if (lstatSync(path, { throwIfNoEntry: false })?.isSymbolicLink()) throw new Error("pi-note.json is a symlink. Edit its target manually rather than replacing the link.");
|
|
97
108
|
const root = expectedRaw === null ? {} : JSON.parse(expectedRaw);
|
|
98
109
|
if (!object(root)) throw new Error("Invalid pi-note configuration; expected an object.");
|
|
99
|
-
|
|
110
|
+
raw = `${JSON.stringify({ ...root, ...prompt, footer: checked }, null, 2)}\n`;
|
|
111
|
+
writeFileSync(temp, raw, { flag: "wx", mode: 0o600 });
|
|
100
112
|
renameSync(temp, path);
|
|
101
113
|
} finally { rmSync(temp, { force: true }); closeSync(descriptor); rmSync(lock, { force: true }); }
|
|
102
|
-
return checked;
|
|
114
|
+
return { config: checked, raw };
|
|
103
115
|
}
|
|
104
116
|
|
|
105
117
|
export function effectiveFooterFields(config: FooterConfig, notes: Record<string, string> = {}): FooterField[] {
|
package/footer-settings.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { Input, matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
|
-
import { defaultFooterField, parseFooterConfig, type FooterConfig, type FooterField } from "./footer-config.ts";
|
|
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";
|
|
4
4
|
|
|
5
5
|
type Matches = (data: string, action: string) => boolean;
|
|
6
6
|
type Row = { label: string; action: () => void };
|
|
@@ -38,10 +38,14 @@ export class TaskMenu {
|
|
|
38
38
|
|
|
39
39
|
export class FooterSettings {
|
|
40
40
|
private draft: FooterConfig;
|
|
41
|
+
private prompt: string;
|
|
42
|
+
private editor?: Editor;
|
|
43
|
+
private createEditor: () => Editor;
|
|
41
44
|
private knownFields: string[];
|
|
42
45
|
private theme: Theme;
|
|
43
46
|
private matches: Matches;
|
|
44
|
-
private done: (result:
|
|
47
|
+
private done: (result: "tasks" | undefined) => void;
|
|
48
|
+
private save: (settings: PinoteSettings) => void;
|
|
45
49
|
private requestRender: () => void;
|
|
46
50
|
private index = 0;
|
|
47
51
|
private field?: FooterField;
|
|
@@ -49,15 +53,23 @@ export class FooterSettings {
|
|
|
49
53
|
private error = "";
|
|
50
54
|
private hasFocus = false;
|
|
51
55
|
|
|
52
|
-
constructor(config:
|
|
53
|
-
done: (result:
|
|
54
|
-
|
|
56
|
+
constructor(config: PinoteSettings, knownFields: string[], theme: Theme, matches: Matches,
|
|
57
|
+
done: (result: "tasks" | undefined) => void, requestRender: () => void, createEditor: () => Editor,
|
|
58
|
+
save: (settings: PinoteSettings) => void) {
|
|
59
|
+
this.draft = structuredClone(config.footer);
|
|
60
|
+
this.prompt = config.handoffPrompt;
|
|
61
|
+
this.createEditor = createEditor;
|
|
62
|
+
this.save = save;
|
|
55
63
|
this.draft.fields ??= [];
|
|
56
64
|
this.knownFields = [...new Set(knownFields)].filter((name) => name !== "Bar");
|
|
57
65
|
this.theme = theme; this.matches = matches; this.done = done; this.requestRender = requestRender;
|
|
58
66
|
}
|
|
59
67
|
get focused() { return this.hasFocus; }
|
|
60
|
-
set focused(value: boolean) {
|
|
68
|
+
set focused(value: boolean) {
|
|
69
|
+
this.hasFocus = value;
|
|
70
|
+
if (this.input) this.input.widget.focused = value;
|
|
71
|
+
if (this.editor) this.editor.focused = value;
|
|
72
|
+
}
|
|
61
73
|
|
|
62
74
|
private ask(heading: string, value: string, apply: (value: string) => void) {
|
|
63
75
|
const widget = new Input();
|
|
@@ -67,6 +79,20 @@ export class FooterSettings {
|
|
|
67
79
|
private validate(field: FooterField): FooterField {
|
|
68
80
|
return parseFooterConfig({ footer: { fields: [field] } }).fields![0];
|
|
69
81
|
}
|
|
82
|
+
private persist(footer = this.draft, handoffPrompt = this.prompt) {
|
|
83
|
+
const settings = { footer: parseFooterConfig({ footer }), handoffPrompt: parseHandoffPrompt(handoffPrompt) };
|
|
84
|
+
this.save(structuredClone(settings));
|
|
85
|
+
this.draft = settings.footer;
|
|
86
|
+
this.prompt = settings.handoffPrompt;
|
|
87
|
+
}
|
|
88
|
+
private persistField(field: FooterField) {
|
|
89
|
+
const candidate = this.validate(field);
|
|
90
|
+
const fields = this.draft.fields!.filter((entry) => entry.name !== candidate.name);
|
|
91
|
+
const index = this.draft.fields!.findIndex((entry) => entry.name === candidate.name);
|
|
92
|
+
fields.splice(index < 0 ? fields.length : index, 0, candidate);
|
|
93
|
+
this.persist({ ...this.draft, fields });
|
|
94
|
+
this.field = candidate;
|
|
95
|
+
}
|
|
70
96
|
private edit(name: string) {
|
|
71
97
|
this.field = structuredClone(this.draft.fields!.find((field) => field.name === name) ?? defaultFooterField(name));
|
|
72
98
|
this.index = 0; this.error = "";
|
|
@@ -77,56 +103,66 @@ export class FooterSettings {
|
|
|
77
103
|
action: () => this.ask(key, String(this.draft[key]), (value) => {
|
|
78
104
|
const candidate = { ...this.draft, [key]: Number(value) };
|
|
79
105
|
if (!value.trim()) throw new Error("Enter a number.");
|
|
80
|
-
this.
|
|
106
|
+
this.persist(candidate);
|
|
81
107
|
}),
|
|
82
108
|
}));
|
|
109
|
+
rows.push({ label: `Task prompt: ${display(this.prompt)}`, action: () => {
|
|
110
|
+
this.editor = this.createEditor();
|
|
111
|
+
this.editor.disableSubmit = true;
|
|
112
|
+
this.editor.setText(this.prompt);
|
|
113
|
+
this.editor.focused = this.hasFocus;
|
|
114
|
+
} });
|
|
83
115
|
const configured = this.draft.fields!.map((field) => field.name);
|
|
84
116
|
for (const name of [...configured, ...this.knownFields.filter((name) => !configured.includes(name))]) {
|
|
85
117
|
rows.push({ label: `Field: ${display(name)}${configured.includes(name) ? "" : " (not configured)"}`, action: () => this.edit(name) });
|
|
86
118
|
}
|
|
87
119
|
rows.push({ label: "+ Add field", action: () => this.ask("Field name", "", (value) => {
|
|
88
120
|
const field = this.validate(defaultFooterField(value));
|
|
89
|
-
this.edit(field.name);
|
|
90
|
-
|
|
121
|
+
if (this.draft.fields!.some((entry) => entry.name === field.name)) this.edit(field.name);
|
|
122
|
+
else {
|
|
123
|
+
this.persistField(field);
|
|
124
|
+
this.index = 0;
|
|
125
|
+
}
|
|
126
|
+
}) },
|
|
91
127
|
{ label: "Return to tasks", action: () => this.done("tasks") });
|
|
92
128
|
return rows;
|
|
93
129
|
}
|
|
94
130
|
private fieldRows(): Row[] {
|
|
95
131
|
const field = this.field!;
|
|
96
132
|
const rows: Row[] = [
|
|
97
|
-
{ label: `Link: ${field.link ? "on" : "off (plain text)"}`, action: () => { field
|
|
133
|
+
{ label: `Link: ${field.link ? "on" : "off (plain text)"}`, action: () => this.persistField({ ...field, link: !field.link }) },
|
|
98
134
|
{ label: `Label: ${display(field.label) || "(none)"}`, action: () => this.ask("Label (empty = no prefix)", field.label, (value) => {
|
|
99
|
-
this.
|
|
135
|
+
this.persistField({ ...field, label: value });
|
|
100
136
|
}) },
|
|
101
137
|
{ label: `Format: ${display(field.format) || "(empty)"}`, action: () => this.ask("Format: <value>, <url>, <number>", field.format, (value) => {
|
|
102
|
-
this.
|
|
138
|
+
this.persistField({ ...field, format: value });
|
|
103
139
|
}) },
|
|
104
140
|
{ label: `Width: ${field.width ?? `default (${this.draft.fieldWidth})`}`, action: () => this.ask("Width (empty = default)", field.width === undefined ? "" : String(field.width), (value) => {
|
|
105
141
|
const candidate = { ...field };
|
|
106
142
|
if (value.trim()) candidate.width = Number(value); else delete candidate.width;
|
|
107
|
-
this.
|
|
143
|
+
this.persistField(candidate);
|
|
108
144
|
}) },
|
|
109
|
-
{ label: "Save field", action: () => {
|
|
110
|
-
const candidate = this.validate(field);
|
|
111
|
-
const fields = this.draft.fields!.filter((entry) => entry.name !== candidate.name);
|
|
112
|
-
const index = this.draft.fields!.findIndex((entry) => entry.name === candidate.name);
|
|
113
|
-
fields.splice(index < 0 ? fields.length : index, 0, candidate);
|
|
114
|
-
this.draft = parseFooterConfig({ footer: { ...this.draft, fields } });
|
|
115
|
-
this.field = undefined; this.index = 0;
|
|
116
|
-
} },
|
|
117
145
|
];
|
|
118
146
|
if (this.draft.fields!.some((entry) => entry.name === field.name)) rows.push({ label: "Remove field", action: () => {
|
|
119
|
-
this.draft
|
|
147
|
+
this.persist({ ...this.draft, fields: this.draft.fields!.filter((entry) => entry.name !== field.name) });
|
|
120
148
|
this.field = undefined; this.index = 0;
|
|
121
149
|
} });
|
|
122
|
-
rows.push({ label: "
|
|
150
|
+
rows.push({ label: "Back to settings", action: () => { this.field = undefined; this.index = 0; } });
|
|
123
151
|
return rows;
|
|
124
152
|
}
|
|
125
153
|
handleInput(data: string) {
|
|
126
154
|
this.error = "";
|
|
127
155
|
try {
|
|
128
156
|
if (isTab(data)) this.done("tasks");
|
|
129
|
-
else if (this.
|
|
157
|
+
else if (this.editor) {
|
|
158
|
+
if (matchesKey(data, "ctrl+c")) this.editor.setText("");
|
|
159
|
+
else if (this.matches(data, "tui.select.cancel")) this.editor = undefined;
|
|
160
|
+
else if (this.matches(data, "tui.input.newLine")) this.editor.handleInput(data);
|
|
161
|
+
else if (this.matches(data, "tui.select.confirm")) {
|
|
162
|
+
this.persist(this.draft, this.editor.getExpandedText());
|
|
163
|
+
this.editor = undefined;
|
|
164
|
+
} else this.editor.handleInput(data);
|
|
165
|
+
} else if (this.input) {
|
|
130
166
|
if (this.matches(data, "tui.select.cancel")) this.input = undefined;
|
|
131
167
|
else if (this.matches(data, "tui.select.confirm")) {
|
|
132
168
|
const input = this.input;
|
|
@@ -147,9 +183,12 @@ export class FooterSettings {
|
|
|
147
183
|
}
|
|
148
184
|
render(width: number) {
|
|
149
185
|
const lines = [this.theme.fg("accent", "Pinote — Global Settings"), this.theme.fg("dim", "Tasks (Tab) · Settings · Fields")];
|
|
150
|
-
if (this.
|
|
186
|
+
if (this.editor) {
|
|
187
|
+
lines.push(this.theme.fg("accent", "Task prompt"), ...this.editor.render(width),
|
|
188
|
+
this.theme.fg("dim", "Enter save · Shift+Enter/Ctrl+J newline · Ctrl+C clear · Esc cancel edit"));
|
|
189
|
+
} else if (this.input) {
|
|
151
190
|
lines.push(this.theme.fg("accent", this.input.heading), ...this.input.widget.render(width),
|
|
152
|
-
this.theme.fg("dim", "Enter
|
|
191
|
+
this.theme.fg("dim", "Enter save · Esc cancel edit"));
|
|
153
192
|
} else {
|
|
154
193
|
if (this.field) lines.push(this.theme.fg("accent", `Field: ${display(this.field.name)}`));
|
|
155
194
|
const rows = this.field ? this.fieldRows() : this.rootRows();
|
|
@@ -158,13 +197,13 @@ export class FooterSettings {
|
|
|
158
197
|
lines.push(...rows.slice(start, start + 8).map((row, offset) => offset + start === this.index
|
|
159
198
|
? this.theme.fg("accent", `> ${row.label}`) : ` ${row.label}`));
|
|
160
199
|
if (rows.length > 8) lines.push(this.theme.fg("dim", `${this.index + 1}/${rows.length}`));
|
|
161
|
-
lines.push(this.theme.fg("dim", "↑↓ navigate · Enter edit · Esc
|
|
200
|
+
lines.push(this.theme.fg("dim", "↑↓ navigate · Enter edit · Esc back"));
|
|
162
201
|
}
|
|
163
202
|
if (this.error) lines.push(this.theme.fg("error", this.error));
|
|
164
|
-
lines.push(this.theme.fg("dim", "
|
|
203
|
+
lines.push(this.theme.fg("dim", "Changes save automatically · Tab: tasks"));
|
|
165
204
|
return lines.map((line) => truncateToWidth(line, width));
|
|
166
205
|
}
|
|
167
|
-
invalidate() { this.input?.widget.invalidate(); }
|
|
206
|
+
invalidate() { this.input?.widget.invalidate(); this.editor?.invalidate(); }
|
|
168
207
|
}
|
|
169
208
|
|
|
170
209
|
export function withSettingsTab<T extends { handleInput?(data: string): void; render(width: number): string[]; invalidate(): void; focused?: boolean }>(
|
package/index.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { getAgentDir, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { truncateToWidth, type Keybinding } from "@earendil-works/pi-tui";
|
|
2
|
+
import { Editor, truncateToWidth, type Keybinding } from "@earendil-works/pi-tui";
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { stripVTControlCharacters } from "node:util";
|
|
6
6
|
import { Type } from "typebox";
|
|
7
7
|
import { createPRWatcher } from "./pr-watch.ts";
|
|
8
|
-
import { defaultFooterConfig, effectiveFooterFields, loadFooterConfig, readFooterDocument, saveFooterConfig, type FooterConfig } from "./footer-config.ts";
|
|
8
|
+
import { defaultFooterConfig, defaultHandoffPrompt, effectiveFooterFields, loadFooterConfig, parseHandoffPrompt, readFooterDocument, saveFooterConfig, type FooterConfig, type PinoteSettings } from "./footer-config.ts";
|
|
9
9
|
import { FooterSettings, TaskMenu, withSettingsTab } from "./footer-settings.ts";
|
|
10
10
|
import { TaskPicker, taskState, taskTag } from "./task-picker.ts";
|
|
11
11
|
import { bundledCLIVersion, cliMenu, detectCLI, setupCLI, setupHint, versionAtLeast, type CLIAction } from "./setup.ts";
|
|
@@ -40,7 +40,6 @@ const clean = (value: string) => stripVTControlCharacters(value).replace(/[\x00-
|
|
|
40
40
|
const firstLine = (task: Summary) => clean(task.text.split("\n", 1)[0]).replace(/\s+/gu, " ").trim();
|
|
41
41
|
const title = (task: Summary) => truncateToWidth(`#${task.id} ${firstLine(task)}`, 60);
|
|
42
42
|
|
|
43
|
-
const defaultHandoffPrompt = "Read the current Pinote task. Summarize your understanding, but don’t start work yet.";
|
|
44
43
|
function readConfig(): Record<string, unknown> {
|
|
45
44
|
const path = join(getAgentDir(), "pi-note.json");
|
|
46
45
|
let raw: string;
|
|
@@ -77,12 +76,7 @@ function creationGuidance(): { guidelines: string[]; error?: string } {
|
|
|
77
76
|
|
|
78
77
|
function handoffPrompt(): string {
|
|
79
78
|
const config = readConfig();
|
|
80
|
-
|
|
81
|
-
if (!("handoffPrompt" in config)) return defaultHandoffPrompt;
|
|
82
|
-
if (!text(config.handoffPrompt) || !config.handoffPrompt.trim()) {
|
|
83
|
-
throw new Error(`handoffPrompt in ${path} must be a nonblank string without control characters (except tabs/newlines).`);
|
|
84
|
-
}
|
|
85
|
-
return config.handoffPrompt;
|
|
79
|
+
return "handoffPrompt" in config ? parseHandoffPrompt(config.handoffPrompt) : defaultHandoffPrompt;
|
|
86
80
|
}
|
|
87
81
|
|
|
88
82
|
function parse(raw: string): unknown {
|
|
@@ -360,7 +354,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
360
354
|
registerInstallCommand("pi-note-upgrade");
|
|
361
355
|
|
|
362
356
|
pi.registerCommand("pi-note", {
|
|
363
|
-
description: "Continue, complete, switch tasks, or edit global
|
|
357
|
+
description: "Continue, complete, switch tasks, or edit global settings (Tab)",
|
|
364
358
|
handler: async (args, ctx) => {
|
|
365
359
|
if (!ctx.hasUI || ctx.mode !== "tui") return;
|
|
366
360
|
if (args.trim()) { ctx.ui.notify("Usage: /pi-note", "warning"); return; }
|
|
@@ -381,15 +375,30 @@ export default function (pi: ExtensionAPI) {
|
|
|
381
375
|
while (canAct()) {
|
|
382
376
|
if (settings) {
|
|
383
377
|
const document = readFooterDocument();
|
|
384
|
-
const
|
|
385
|
-
const
|
|
378
|
+
const root = document.raw === null ? {} : JSON.parse(document.raw);
|
|
379
|
+
const config: PinoteSettings = {
|
|
380
|
+
footer: { ...document.config, fields: effectiveFooterFields(document.config, current?.agent_notes) },
|
|
381
|
+
handoffPrompt: "handoffPrompt" in root ? parseHandoffPrompt(root.handoffPrompt) : defaultHandoffPrompt,
|
|
382
|
+
};
|
|
383
|
+
let expectedRaw = document.raw;
|
|
384
|
+
const edited = await ctx.ui.custom<"tasks" | undefined>((tui, theme, kb, done) =>
|
|
386
385
|
new FooterSettings(config, Object.keys(current?.agent_notes ?? {}).filter((name) => name !== "Bar"),
|
|
387
|
-
theme, (data, action) => kb.matches(data, action as Keybinding), done, () => tui.requestRender()
|
|
386
|
+
theme, (data, action) => kb.matches(data, action as Keybinding), done, () => tui.requestRender(),
|
|
387
|
+
() => new Editor(tui, {
|
|
388
|
+
borderColor: (line) => theme.fg("accent", line),
|
|
389
|
+
selectList: { selectedPrefix: (line) => theme.fg("accent", line), selectedText: (line) => theme.fg("accent", line),
|
|
390
|
+
description: (line) => theme.fg("muted", line), scrollInfo: (line) => theme.fg("dim", line),
|
|
391
|
+
noMatch: (line) => theme.fg("warning", line) },
|
|
392
|
+
}), (updated) => {
|
|
393
|
+
if (!canAct()) throw new Error("This session changed. Reopen settings before editing.");
|
|
394
|
+
const saved = saveFooterConfig(updated.footer, expectedRaw, updated.handoffPrompt);
|
|
395
|
+
expectedRaw = saved.raw;
|
|
396
|
+
footerConfig = saved.config;
|
|
397
|
+
void refresh(ctx);
|
|
398
|
+
}));
|
|
388
399
|
if (!canAct() || edited === undefined) return;
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
ctx.ui.notify("Global Pinote footer settings saved.", "info");
|
|
392
|
-
return;
|
|
400
|
+
settings = false;
|
|
401
|
+
continue;
|
|
393
402
|
}
|
|
394
403
|
const action = current
|
|
395
404
|
? await ctx.ui.custom<string | undefined>((tui, theme, kb, done) =>
|