pi-note 0.10.0 → 0.11.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 +11 -5
- package/footer-config.ts +13 -3
- package/footer-settings.ts +37 -11
- package/index.ts +19 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -62,7 +62,7 @@ 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
|
|
65
|
+
Press **Tab** from the task menu or picker to open global settings; Tab
|
|
66
66
|
returns to tasks without saving. 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).
|
|
@@ -109,6 +109,12 @@ 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** applies it to
|
|
114
|
+
the settings draft, **Ctrl+C** clears it, and **Esc** cancels that edit. Select **Save settings** to
|
|
115
|
+
persist it globally; leaving Settings with **Esc** or **Tab** discards the draft.
|
|
116
|
+
Saving never changes existing editor input or submits anything.
|
|
117
|
+
|
|
112
118
|
Task selection and **Continue** reread `handoffPrompt` each time; changing that
|
|
113
119
|
field needs no `/reload`. Its value is literal text, not a prompt template; use
|
|
114
120
|
`\n` in JSON strings for multiple lines. It must be a nonblank string without control
|
|
@@ -204,8 +210,8 @@ In `/pi-note → Settings`, **Add field** adds a global display definition, not
|
|
|
204
210
|
task value. Select any field to edit **Link**, **Label**, **Format**, and **Width**.
|
|
205
211
|
Label starts as the field name; clear it for no prefix. Link off displays plain
|
|
206
212
|
text without a clickable link. **Save** persists the draft; cancellation leaves
|
|
207
|
-
configuration untouched. Saves preserve unrelated
|
|
208
|
-
`taskOfferPolicy`) and reject configuration changes made while the dialog was open.
|
|
213
|
+
configuration untouched. Saves include the task prompt and preserve unrelated
|
|
214
|
+
settings (such as `taskOfferPolicy`) and reject configuration changes made while the dialog was open.
|
|
209
215
|
Concurrent saves use `pi-note.json.lock`; remove stale locks only when no save is
|
|
210
216
|
running. Symlinked configuration must be edited manually; saves never replace the link.
|
|
211
217
|
|
|
@@ -221,8 +227,8 @@ the first Settings save turns this legacy list into explicit global definitions.
|
|
|
221
227
|
Settings saves apply immediately in this Pi session. Run `/reload` after manual
|
|
222
228
|
footer edits or in other running sessions; settings are also reread on session start.
|
|
223
229
|
Invalid footer configuration warns and uses footer defaults; missing files silently
|
|
224
|
-
use defaults.
|
|
225
|
-
|
|
230
|
+
use defaults. The task prompt is also read separately on selection/Continue as
|
|
231
|
+
described above; prompt changes need no reload in any running session.
|
|
226
232
|
This is user-level configuration, not task data; agents should not change it
|
|
227
233
|
without approval. Task values still come from `pinote_update_current`.
|
|
228
234
|
|
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): FooterConfig {
|
|
92
|
+
// preserve unrelated keys, and never leave partial JSON behind.
|
|
93
|
+
export function saveFooterConfig(config: FooterConfig, expectedRaw: string | null, handoffPrompt?: string): FooterConfig {
|
|
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");
|
|
@@ -96,7 +106,7 @@ export function saveFooterConfig(config: FooterConfig, expectedRaw: string | nul
|
|
|
96
106
|
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
107
|
const root = expectedRaw === null ? {} : JSON.parse(expectedRaw);
|
|
98
108
|
if (!object(root)) throw new Error("Invalid pi-note configuration; expected an object.");
|
|
99
|
-
writeFileSync(temp, `${JSON.stringify({ ...root, footer: checked }, null, 2)}\n`, { flag: "wx", mode: 0o600 });
|
|
109
|
+
writeFileSync(temp, `${JSON.stringify({ ...root, ...prompt, footer: checked }, null, 2)}\n`, { flag: "wx", mode: 0o600 });
|
|
100
110
|
renameSync(temp, path);
|
|
101
111
|
} finally { rmSync(temp, { force: true }); closeSync(descriptor); rmSync(lock, { force: true }); }
|
|
102
112
|
return checked;
|
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,13 @@ 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: PinoteSettings | "tasks" | undefined) => void;
|
|
45
48
|
private requestRender: () => void;
|
|
46
49
|
private index = 0;
|
|
47
50
|
private field?: FooterField;
|
|
@@ -49,15 +52,21 @@ export class FooterSettings {
|
|
|
49
52
|
private error = "";
|
|
50
53
|
private hasFocus = false;
|
|
51
54
|
|
|
52
|
-
constructor(config:
|
|
53
|
-
done: (result:
|
|
54
|
-
this.draft = structuredClone(config);
|
|
55
|
+
constructor(config: PinoteSettings, knownFields: string[], theme: Theme, matches: Matches,
|
|
56
|
+
done: (result: PinoteSettings | "tasks" | undefined) => void, requestRender: () => void, createEditor: () => Editor) {
|
|
57
|
+
this.draft = structuredClone(config.footer);
|
|
58
|
+
this.prompt = config.handoffPrompt;
|
|
59
|
+
this.createEditor = createEditor;
|
|
55
60
|
this.draft.fields ??= [];
|
|
56
61
|
this.knownFields = [...new Set(knownFields)].filter((name) => name !== "Bar");
|
|
57
62
|
this.theme = theme; this.matches = matches; this.done = done; this.requestRender = requestRender;
|
|
58
63
|
}
|
|
59
64
|
get focused() { return this.hasFocus; }
|
|
60
|
-
set focused(value: boolean) {
|
|
65
|
+
set focused(value: boolean) {
|
|
66
|
+
this.hasFocus = value;
|
|
67
|
+
if (this.input) this.input.widget.focused = value;
|
|
68
|
+
if (this.editor) this.editor.focused = value;
|
|
69
|
+
}
|
|
61
70
|
|
|
62
71
|
private ask(heading: string, value: string, apply: (value: string) => void) {
|
|
63
72
|
const widget = new Input();
|
|
@@ -80,6 +89,12 @@ export class FooterSettings {
|
|
|
80
89
|
this.draft = parseFooterConfig({ footer: candidate });
|
|
81
90
|
}),
|
|
82
91
|
}));
|
|
92
|
+
rows.push({ label: `Task prompt: ${display(this.prompt)}`, action: () => {
|
|
93
|
+
this.editor = this.createEditor();
|
|
94
|
+
this.editor.disableSubmit = true;
|
|
95
|
+
this.editor.setText(this.prompt);
|
|
96
|
+
this.editor.focused = this.hasFocus;
|
|
97
|
+
} });
|
|
83
98
|
const configured = this.draft.fields!.map((field) => field.name);
|
|
84
99
|
for (const name of [...configured, ...this.knownFields.filter((name) => !configured.includes(name))]) {
|
|
85
100
|
rows.push({ label: `Field: ${display(name)}${configured.includes(name) ? "" : " (not configured)"}`, action: () => this.edit(name) });
|
|
@@ -87,7 +102,7 @@ export class FooterSettings {
|
|
|
87
102
|
rows.push({ label: "+ Add field", action: () => this.ask("Field name", "", (value) => {
|
|
88
103
|
const field = this.validate(defaultFooterField(value));
|
|
89
104
|
this.edit(field.name);
|
|
90
|
-
}) }, { label: "Save settings", action: () => this.done(parseFooterConfig({ footer: this.draft })) },
|
|
105
|
+
}) }, { label: "Save settings", action: () => this.done({ footer: parseFooterConfig({ footer: this.draft }), handoffPrompt: parseHandoffPrompt(this.prompt) }) },
|
|
91
106
|
{ label: "Return to tasks", action: () => this.done("tasks") });
|
|
92
107
|
return rows;
|
|
93
108
|
}
|
|
@@ -126,7 +141,15 @@ export class FooterSettings {
|
|
|
126
141
|
this.error = "";
|
|
127
142
|
try {
|
|
128
143
|
if (isTab(data)) this.done("tasks");
|
|
129
|
-
else if (this.
|
|
144
|
+
else if (this.editor) {
|
|
145
|
+
if (matchesKey(data, "ctrl+c")) this.editor.setText("");
|
|
146
|
+
else if (this.matches(data, "tui.select.cancel")) this.editor = undefined;
|
|
147
|
+
else if (this.matches(data, "tui.input.newLine")) this.editor.handleInput(data);
|
|
148
|
+
else if (this.matches(data, "tui.select.confirm")) {
|
|
149
|
+
this.prompt = parseHandoffPrompt(this.editor.getExpandedText());
|
|
150
|
+
this.editor = undefined;
|
|
151
|
+
} else this.editor.handleInput(data);
|
|
152
|
+
} else if (this.input) {
|
|
130
153
|
if (this.matches(data, "tui.select.cancel")) this.input = undefined;
|
|
131
154
|
else if (this.matches(data, "tui.select.confirm")) {
|
|
132
155
|
const input = this.input;
|
|
@@ -147,7 +170,10 @@ export class FooterSettings {
|
|
|
147
170
|
}
|
|
148
171
|
render(width: number) {
|
|
149
172
|
const lines = [this.theme.fg("accent", "Pinote — Global Settings"), this.theme.fg("dim", "Tasks (Tab) · Settings · Fields")];
|
|
150
|
-
if (this.
|
|
173
|
+
if (this.editor) {
|
|
174
|
+
lines.push(this.theme.fg("accent", "Task prompt"), ...this.editor.render(width),
|
|
175
|
+
this.theme.fg("dim", "Enter apply · Shift+Enter/Ctrl+J newline · Ctrl+C clear · Esc cancel"));
|
|
176
|
+
} else if (this.input) {
|
|
151
177
|
lines.push(this.theme.fg("accent", this.input.heading), ...this.input.widget.render(width),
|
|
152
178
|
this.theme.fg("dim", "Enter apply to draft · Esc cancel"));
|
|
153
179
|
} else {
|
|
@@ -164,7 +190,7 @@ export class FooterSettings {
|
|
|
164
190
|
lines.push(this.theme.fg("dim", "Tab: tasks, discard draft · Save settings: persist globally"));
|
|
165
191
|
return lines.map((line) => truncateToWidth(line, width));
|
|
166
192
|
}
|
|
167
|
-
invalidate() { this.input?.widget.invalidate(); }
|
|
193
|
+
invalidate() { this.input?.widget.invalidate(); this.editor?.invalidate(); }
|
|
168
194
|
}
|
|
169
195
|
|
|
170
196
|
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,14 +375,24 @@ 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
|
+
const edited = await ctx.ui.custom<PinoteSettings | "tasks" | undefined>((tui, theme, kb, done) =>
|
|
386
384
|
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()
|
|
385
|
+
theme, (data, action) => kb.matches(data, action as Keybinding), done, () => tui.requestRender(),
|
|
386
|
+
() => new Editor(tui, {
|
|
387
|
+
borderColor: (line) => theme.fg("accent", line),
|
|
388
|
+
selectList: { selectedPrefix: (line) => theme.fg("accent", line), selectedText: (line) => theme.fg("accent", line),
|
|
389
|
+
description: (line) => theme.fg("muted", line), scrollInfo: (line) => theme.fg("dim", line),
|
|
390
|
+
noMatch: (line) => theme.fg("warning", line) },
|
|
391
|
+
})));
|
|
388
392
|
if (!canAct() || edited === undefined) return;
|
|
389
393
|
if (edited === "tasks") { settings = false; continue; }
|
|
390
|
-
footerConfig = saveFooterConfig(edited, document.raw);
|
|
391
|
-
ctx.ui.notify("Global Pinote
|
|
394
|
+
footerConfig = saveFooterConfig(edited.footer, document.raw, edited.handoffPrompt);
|
|
395
|
+
ctx.ui.notify("Global Pinote settings saved.", "info");
|
|
392
396
|
return;
|
|
393
397
|
}
|
|
394
398
|
const action = current
|