pi-note 0.11.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 +12 -8
- package/footer-config.ts +5 -3
- package/footer-settings.ts +37 -24
- package/index.ts +11 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ they are not copied into the draft. The default prompt asks for a summary, not i
|
|
|
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
65
|
Press **Tab** from the task menu or picker to open global settings; Tab
|
|
66
|
-
returns to tasks
|
|
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
|
|
@@ -110,10 +110,11 @@ values or unreadable/malformed configuration suppress offers; interactive startu
|
|
|
110
110
|
warns until you fix the file and reload. No project-local configuration is read.
|
|
111
111
|
|
|
112
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**
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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.
|
|
117
118
|
|
|
118
119
|
Task selection and **Continue** reread `handoffPrompt` each time; changing that
|
|
119
120
|
field needs no `/reload`. Its value is literal text, not a prompt template; use
|
|
@@ -209,9 +210,12 @@ accepted. Missing/blank values do not display and do not consume the field limit
|
|
|
209
210
|
In `/pi-note → Settings`, **Add field** adds a global display definition, not a
|
|
210
211
|
task value. Select any field to edit **Link**, **Label**, **Format**, and **Width**.
|
|
211
212
|
Label starts as the field name; clear it for no prefix. Link off displays plain
|
|
212
|
-
text without a clickable link.
|
|
213
|
-
|
|
214
|
-
|
|
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.
|
|
215
219
|
Concurrent saves use `pi-note.json.lock`; remove stale locks only when no save is
|
|
216
220
|
running. Symlinked configuration must be edited manually; saves never replace the link.
|
|
217
221
|
|
package/footer-config.ts
CHANGED
|
@@ -90,7 +90,7 @@ export function loadFooterConfig(warn: (message: string) => void): FooterConfig
|
|
|
90
90
|
|
|
91
91
|
// Synchronous compare-and-replace: reject edits made while the settings dialog was open,
|
|
92
92
|
// preserve unrelated keys, and never leave partial JSON behind.
|
|
93
|
-
export function saveFooterConfig(config: FooterConfig, expectedRaw: string | null, handoffPrompt?: string)
|
|
93
|
+
export function saveFooterConfig(config: FooterConfig, expectedRaw: string | null, handoffPrompt?: string) {
|
|
94
94
|
const checked = parseFooterConfig({ footer: config });
|
|
95
95
|
const prompt = handoffPrompt === undefined ? {} : { handoffPrompt: parseHandoffPrompt(handoffPrompt) };
|
|
96
96
|
const directory = getAgentDir();
|
|
@@ -101,15 +101,17 @@ export function saveFooterConfig(config: FooterConfig, expectedRaw: string | nul
|
|
|
101
101
|
try { descriptor = openSync(lock, "wx", 0o600); }
|
|
102
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."); }
|
|
103
103
|
const temp = join(directory, `.pi-note-${randomUUID()}.tmp`);
|
|
104
|
+
let raw: string;
|
|
104
105
|
try {
|
|
105
106
|
if (readRaw() !== expectedRaw) throw new Error("Pi-note configuration changed while settings were open. Reopen settings before saving.");
|
|
106
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.");
|
|
107
108
|
const root = expectedRaw === null ? {} : JSON.parse(expectedRaw);
|
|
108
109
|
if (!object(root)) throw new Error("Invalid pi-note configuration; expected an object.");
|
|
109
|
-
|
|
110
|
+
raw = `${JSON.stringify({ ...root, ...prompt, footer: checked }, null, 2)}\n`;
|
|
111
|
+
writeFileSync(temp, raw, { flag: "wx", mode: 0o600 });
|
|
110
112
|
renameSync(temp, path);
|
|
111
113
|
} finally { rmSync(temp, { force: true }); closeSync(descriptor); rmSync(lock, { force: true }); }
|
|
112
|
-
return checked;
|
|
114
|
+
return { config: checked, raw };
|
|
113
115
|
}
|
|
114
116
|
|
|
115
117
|
export function effectiveFooterFields(config: FooterConfig, notes: Record<string, string> = {}): FooterField[] {
|
package/footer-settings.ts
CHANGED
|
@@ -44,7 +44,8 @@ export class FooterSettings {
|
|
|
44
44
|
private knownFields: string[];
|
|
45
45
|
private theme: Theme;
|
|
46
46
|
private matches: Matches;
|
|
47
|
-
private done: (result:
|
|
47
|
+
private done: (result: "tasks" | undefined) => void;
|
|
48
|
+
private save: (settings: PinoteSettings) => void;
|
|
48
49
|
private requestRender: () => void;
|
|
49
50
|
private index = 0;
|
|
50
51
|
private field?: FooterField;
|
|
@@ -53,10 +54,12 @@ export class FooterSettings {
|
|
|
53
54
|
private hasFocus = false;
|
|
54
55
|
|
|
55
56
|
constructor(config: PinoteSettings, knownFields: string[], theme: Theme, matches: Matches,
|
|
56
|
-
done: (result:
|
|
57
|
+
done: (result: "tasks" | undefined) => void, requestRender: () => void, createEditor: () => Editor,
|
|
58
|
+
save: (settings: PinoteSettings) => void) {
|
|
57
59
|
this.draft = structuredClone(config.footer);
|
|
58
60
|
this.prompt = config.handoffPrompt;
|
|
59
61
|
this.createEditor = createEditor;
|
|
62
|
+
this.save = save;
|
|
60
63
|
this.draft.fields ??= [];
|
|
61
64
|
this.knownFields = [...new Set(knownFields)].filter((name) => name !== "Bar");
|
|
62
65
|
this.theme = theme; this.matches = matches; this.done = done; this.requestRender = requestRender;
|
|
@@ -76,6 +79,20 @@ export class FooterSettings {
|
|
|
76
79
|
private validate(field: FooterField): FooterField {
|
|
77
80
|
return parseFooterConfig({ footer: { fields: [field] } }).fields![0];
|
|
78
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
|
+
}
|
|
79
96
|
private edit(name: string) {
|
|
80
97
|
this.field = structuredClone(this.draft.fields!.find((field) => field.name === name) ?? defaultFooterField(name));
|
|
81
98
|
this.index = 0; this.error = "";
|
|
@@ -86,7 +103,7 @@ export class FooterSettings {
|
|
|
86
103
|
action: () => this.ask(key, String(this.draft[key]), (value) => {
|
|
87
104
|
const candidate = { ...this.draft, [key]: Number(value) };
|
|
88
105
|
if (!value.trim()) throw new Error("Enter a number.");
|
|
89
|
-
this.
|
|
106
|
+
this.persist(candidate);
|
|
90
107
|
}),
|
|
91
108
|
}));
|
|
92
109
|
rows.push({ label: `Task prompt: ${display(this.prompt)}`, action: () => {
|
|
@@ -101,40 +118,36 @@ export class FooterSettings {
|
|
|
101
118
|
}
|
|
102
119
|
rows.push({ label: "+ Add field", action: () => this.ask("Field name", "", (value) => {
|
|
103
120
|
const field = this.validate(defaultFooterField(value));
|
|
104
|
-
this.edit(field.name);
|
|
105
|
-
|
|
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
|
+
}) },
|
|
106
127
|
{ label: "Return to tasks", action: () => this.done("tasks") });
|
|
107
128
|
return rows;
|
|
108
129
|
}
|
|
109
130
|
private fieldRows(): Row[] {
|
|
110
131
|
const field = this.field!;
|
|
111
132
|
const rows: Row[] = [
|
|
112
|
-
{ 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 }) },
|
|
113
134
|
{ label: `Label: ${display(field.label) || "(none)"}`, action: () => this.ask("Label (empty = no prefix)", field.label, (value) => {
|
|
114
|
-
this.
|
|
135
|
+
this.persistField({ ...field, label: value });
|
|
115
136
|
}) },
|
|
116
137
|
{ label: `Format: ${display(field.format) || "(empty)"}`, action: () => this.ask("Format: <value>, <url>, <number>", field.format, (value) => {
|
|
117
|
-
this.
|
|
138
|
+
this.persistField({ ...field, format: value });
|
|
118
139
|
}) },
|
|
119
140
|
{ label: `Width: ${field.width ?? `default (${this.draft.fieldWidth})`}`, action: () => this.ask("Width (empty = default)", field.width === undefined ? "" : String(field.width), (value) => {
|
|
120
141
|
const candidate = { ...field };
|
|
121
142
|
if (value.trim()) candidate.width = Number(value); else delete candidate.width;
|
|
122
|
-
this.
|
|
143
|
+
this.persistField(candidate);
|
|
123
144
|
}) },
|
|
124
|
-
{ label: "Save field", action: () => {
|
|
125
|
-
const candidate = this.validate(field);
|
|
126
|
-
const fields = this.draft.fields!.filter((entry) => entry.name !== candidate.name);
|
|
127
|
-
const index = this.draft.fields!.findIndex((entry) => entry.name === candidate.name);
|
|
128
|
-
fields.splice(index < 0 ? fields.length : index, 0, candidate);
|
|
129
|
-
this.draft = parseFooterConfig({ footer: { ...this.draft, fields } });
|
|
130
|
-
this.field = undefined; this.index = 0;
|
|
131
|
-
} },
|
|
132
145
|
];
|
|
133
146
|
if (this.draft.fields!.some((entry) => entry.name === field.name)) rows.push({ label: "Remove field", action: () => {
|
|
134
|
-
this.draft
|
|
147
|
+
this.persist({ ...this.draft, fields: this.draft.fields!.filter((entry) => entry.name !== field.name) });
|
|
135
148
|
this.field = undefined; this.index = 0;
|
|
136
149
|
} });
|
|
137
|
-
rows.push({ label: "
|
|
150
|
+
rows.push({ label: "Back to settings", action: () => { this.field = undefined; this.index = 0; } });
|
|
138
151
|
return rows;
|
|
139
152
|
}
|
|
140
153
|
handleInput(data: string) {
|
|
@@ -146,7 +159,7 @@ export class FooterSettings {
|
|
|
146
159
|
else if (this.matches(data, "tui.select.cancel")) this.editor = undefined;
|
|
147
160
|
else if (this.matches(data, "tui.input.newLine")) this.editor.handleInput(data);
|
|
148
161
|
else if (this.matches(data, "tui.select.confirm")) {
|
|
149
|
-
this.
|
|
162
|
+
this.persist(this.draft, this.editor.getExpandedText());
|
|
150
163
|
this.editor = undefined;
|
|
151
164
|
} else this.editor.handleInput(data);
|
|
152
165
|
} else if (this.input) {
|
|
@@ -172,10 +185,10 @@ export class FooterSettings {
|
|
|
172
185
|
const lines = [this.theme.fg("accent", "Pinote — Global Settings"), this.theme.fg("dim", "Tasks (Tab) · Settings · Fields")];
|
|
173
186
|
if (this.editor) {
|
|
174
187
|
lines.push(this.theme.fg("accent", "Task prompt"), ...this.editor.render(width),
|
|
175
|
-
this.theme.fg("dim", "Enter
|
|
188
|
+
this.theme.fg("dim", "Enter save · Shift+Enter/Ctrl+J newline · Ctrl+C clear · Esc cancel edit"));
|
|
176
189
|
} else if (this.input) {
|
|
177
190
|
lines.push(this.theme.fg("accent", this.input.heading), ...this.input.widget.render(width),
|
|
178
|
-
this.theme.fg("dim", "Enter
|
|
191
|
+
this.theme.fg("dim", "Enter save · Esc cancel edit"));
|
|
179
192
|
} else {
|
|
180
193
|
if (this.field) lines.push(this.theme.fg("accent", `Field: ${display(this.field.name)}`));
|
|
181
194
|
const rows = this.field ? this.fieldRows() : this.rootRows();
|
|
@@ -184,10 +197,10 @@ export class FooterSettings {
|
|
|
184
197
|
lines.push(...rows.slice(start, start + 8).map((row, offset) => offset + start === this.index
|
|
185
198
|
? this.theme.fg("accent", `> ${row.label}`) : ` ${row.label}`));
|
|
186
199
|
if (rows.length > 8) lines.push(this.theme.fg("dim", `${this.index + 1}/${rows.length}`));
|
|
187
|
-
lines.push(this.theme.fg("dim", "↑↓ navigate · Enter edit · Esc
|
|
200
|
+
lines.push(this.theme.fg("dim", "↑↓ navigate · Enter edit · Esc back"));
|
|
188
201
|
}
|
|
189
202
|
if (this.error) lines.push(this.theme.fg("error", this.error));
|
|
190
|
-
lines.push(this.theme.fg("dim", "
|
|
203
|
+
lines.push(this.theme.fg("dim", "Changes save automatically · Tab: tasks"));
|
|
191
204
|
return lines.map((line) => truncateToWidth(line, width));
|
|
192
205
|
}
|
|
193
206
|
invalidate() { this.input?.widget.invalidate(); this.editor?.invalidate(); }
|
package/index.ts
CHANGED
|
@@ -380,7 +380,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
380
380
|
footer: { ...document.config, fields: effectiveFooterFields(document.config, current?.agent_notes) },
|
|
381
381
|
handoffPrompt: "handoffPrompt" in root ? parseHandoffPrompt(root.handoffPrompt) : defaultHandoffPrompt,
|
|
382
382
|
};
|
|
383
|
-
|
|
383
|
+
let expectedRaw = document.raw;
|
|
384
|
+
const edited = await ctx.ui.custom<"tasks" | undefined>((tui, theme, kb, done) =>
|
|
384
385
|
new FooterSettings(config, Object.keys(current?.agent_notes ?? {}).filter((name) => name !== "Bar"),
|
|
385
386
|
theme, (data, action) => kb.matches(data, action as Keybinding), done, () => tui.requestRender(),
|
|
386
387
|
() => new Editor(tui, {
|
|
@@ -388,12 +389,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
388
389
|
selectList: { selectedPrefix: (line) => theme.fg("accent", line), selectedText: (line) => theme.fg("accent", line),
|
|
389
390
|
description: (line) => theme.fg("muted", line), scrollInfo: (line) => theme.fg("dim", line),
|
|
390
391
|
noMatch: (line) => theme.fg("warning", line) },
|
|
391
|
-
}))
|
|
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
|
+
}));
|
|
392
399
|
if (!canAct() || edited === undefined) return;
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
ctx.ui.notify("Global Pinote settings saved.", "info");
|
|
396
|
-
return;
|
|
400
|
+
settings = false;
|
|
401
|
+
continue;
|
|
397
402
|
}
|
|
398
403
|
const action = current
|
|
399
404
|
? await ctx.ui.custom<string | undefined>((tui, theme, kb, done) =>
|