pi-note 0.15.0 → 0.16.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 +37 -31
- package/footer-settings.ts +21 -7
- package/index.ts +92 -86
- package/package.json +1 -1
- package/setup.ts +2 -20
- package/task-suggestion.ts +5 -5
package/README.md
CHANGED
|
@@ -9,10 +9,9 @@ The extension and Python app share a repository but install separately.
|
|
|
9
9
|
|
|
10
10
|
Requires Pi 0.99.1+, Node.js 22.19+ and **pinote 0.3.0+** (`note` on PATH). Add and tag listing need **pinote 0.4.0+**.
|
|
11
11
|
From this checkout, install the extension with `pi install ./pi`, then run
|
|
12
|
-
`/reload` in interactive Pi.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
It is not published yet. npm installation does not run Python installers.
|
|
12
|
+
`/reload` in interactive Pi. The planned npm package name is `pi-note`; after its
|
|
13
|
+
first release use `pi install npm:pi-note` instead. It is not published yet. npm
|
|
14
|
+
installation does not run Python installers.
|
|
16
15
|
|
|
17
16
|
### CLI setup and upgrades
|
|
18
17
|
|
|
@@ -20,21 +19,19 @@ Install [uv](https://docs.astral.sh/uv/getting-started/installation/) first and
|
|
|
20
19
|
restart Pi with `uv` on PATH. At startup the extension checks `note --version`
|
|
21
20
|
against its bundled CLI version (currently 0.4.0), without network requests:
|
|
22
21
|
|
|
23
|
-
- Missing/unrecognized CLI:
|
|
24
|
-
- Older CLI:
|
|
25
|
-
- Equal/newer CLI:
|
|
22
|
+
- Missing/unrecognized CLI: `/pi-note` opens Settings directly, where **Install CLI** is available.
|
|
23
|
+
- Older CLI: `/pi-note` opens Settings directly, where **Upgrade CLI** is available.
|
|
24
|
+
- Equal/newer CLI: neither installation action is shown; no upgrade notification.
|
|
26
25
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
reinstalls an equal/newer CLI or intentionally downgrades it.
|
|
26
|
+
Install and upgrade recheck the CLI version and ask before installing into uv's
|
|
27
|
+
isolated tool environment. Restart Pi or `/reload` after external CLI changes;
|
|
28
|
+
installation actions never reinstall an equal/newer CLI or intentionally downgrade it.
|
|
31
29
|
Python 3.11+ is required; uv can download a suitable interpreter when missing.
|
|
32
|
-
|
|
30
|
+
Installation downloads a pinned GitHub source archive and build dependencies,
|
|
33
31
|
not a similarly named PyPI package. It does not install GTK dependencies, edit
|
|
34
32
|
shell configuration, or change tasks. This checks the Python CLI bundled with
|
|
35
33
|
the installed extension, not the latest npm/GitHub release. Update the extension
|
|
36
|
-
first to receive a newer bundled CLI.
|
|
37
|
-
`/pi-note-upgrade`.
|
|
34
|
+
first to receive a newer bundled CLI.
|
|
38
35
|
|
|
39
36
|
Back up your database before upgrading and update an optional GUI separately.
|
|
40
37
|
For checkout development, use `uv tool install --reinstall .` at the repo root
|
|
@@ -45,7 +42,8 @@ put that directory before older `note` executables on PATH, and restart Pi.
|
|
|
45
42
|
Check `note --version`, then restart Pi or `/reload`. Network/build failures and
|
|
46
43
|
conflicting executables are reported without forcibly overwriting another
|
|
47
44
|
installer's commands. Fix the reported cause and retry; installs time out after
|
|
48
|
-
three minutes.
|
|
45
|
+
three minutes. Installation actions are unavailable while installation runs.
|
|
46
|
+
Setup hints direct users to `/pi-note` → Settings.
|
|
49
47
|
Missing uv is reported with its installation link, not installed
|
|
50
48
|
automatically. The [GTK desktop app](../README.md#optional-gtk-checklist) is optional
|
|
51
49
|
and still requires separate system dependencies.
|
|
@@ -56,24 +54,29 @@ standalone `settings/pi/extensions/pinote.ts` entry if installed; load only one
|
|
|
56
54
|
|
|
57
55
|
## Use
|
|
58
56
|
|
|
59
|
-
|
|
57
|
+
When the CLI is ready and no task is selected, `/pi-note` opens the task picker.
|
|
58
|
+
It marks the chosen task in progress and adds this default prompt to the editor:
|
|
60
59
|
“Read the current Pinote task. Summarize your understanding, but don’t start work yet.”
|
|
60
|
+
If the CLI is missing or older than the bundled version, `/pi-note` opens Settings
|
|
61
|
+
directly so the relevant CLI installation action remains accessible. **Tab** opens
|
|
62
|
+
Settings from the picker or task view; **Tab** from Settings returns to tasks.
|
|
61
63
|
The agent fetches current task text and saved handoff fields when you submit;
|
|
62
64
|
they are not copied into the draft. The default prompt asks for a summary, not implementation.
|
|
63
65
|
Existing drafts are preserved; nothing is submitted automatically.
|
|
64
|
-
With a selected task
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
With a selected task, `/pi-note` offers **Continue**, **Done**, **Switch task**, and
|
|
67
|
+
**Settings**. Settings provides **Preview task**, **Complete task (new session)**,
|
|
68
|
+
**Accept suggestion**, and **Dismiss suggestion** when applicable, plus
|
|
69
|
+
**Install CLI** or **Upgrade CLI** only when needed. Settings also lists fields found
|
|
70
|
+
on the current task, so existing values can be configured without creating another task.
|
|
68
71
|
Type in the task picker to filter by text, ID, tag, or state (all words must match).
|
|
69
72
|
Use ↑/↓ and Enter to select, or Esc to cancel. Rows show **●** for in progress or
|
|
70
73
|
**○** for active, followed by the tag (`[Untagged]` when absent).
|
|
71
|
-
The normal Pi status area shows selected-task controls and title without replacing other footers. The footer omits the task ID and state and shows only the first line, bounded by `footer.titleWidth` (default 60 terminal columns including the pin, controls, tag and title). Overflow ends with `...`. The title remains clickable for preview; task controls are separate. At very small widths controls are hidden (pin only). The pin and controls are bundled in `icons/note.txt`, `icons/check.txt`, `icons/cross.txt`, and `icons/add.txt`; they use terminal fonts, not a Nerd Font or icon theme.
|
|
74
|
+
The normal Pi status area shows selected-task controls and title without replacing other footers. The footer omits the task ID and state and shows only the first line, bounded by `footer.titleWidth` (default 60 terminal columns including the pin, controls, tag and title). Overflow ends with `...`. The title remains clickable for preview; task controls are separate. At very small widths controls are hidden (pin only). The pin and controls are bundled in `icons/note.txt`, `icons/check.txt`, `icons/cross.txt`, and `icons/add.txt`; they use terminal fonts, not a Nerd Font or icon theme. Icons are bold; their apparent weight depends on the terminal font, and emoji may look unchanged.
|
|
72
75
|
|
|
73
76
|
### Display-only preview
|
|
74
77
|
|
|
75
|
-
|
|
76
|
-
agent fields in the transcript. The preview is labeled **display only · not sent
|
|
78
|
+
Use **Preview task** in Settings to print the selected task's full Markdown text
|
|
79
|
+
and saved agent fields in the transcript. The preview is labeled **display only · not sent
|
|
77
80
|
to model**: it is a local custom session entry, excluded from subsequent model
|
|
78
81
|
requests, compaction and branch summaries. It survives session resume but does not
|
|
79
82
|
modify the note, editor draft or task selection, and never starts a model call.
|
|
@@ -100,8 +103,8 @@ Reload Kitty with **Ctrl+Shift+F5**. In Pi's regular mode, click the task text;
|
|
|
100
103
|
fullscreen mode, use **Ctrl+Shift+click** so Kitty handles the link instead of
|
|
101
104
|
Pi's system URL opener. The helper sends only an authenticated selection
|
|
102
105
|
identifier, never note content or a model prompt. Stale links after task switching, tree navigation, reload or session exit
|
|
103
|
-
are rejected.
|
|
104
|
-
start
|
|
106
|
+
are rejected. The **Preview task** Settings action is also available when the
|
|
107
|
+
socket cannot start; the task remains visible without a link.
|
|
105
108
|
|
|
106
109
|
Each Pi session remembers its own task, including several sessions in one folder.
|
|
107
110
|
Resume restores that session's task; a new session starts unselected and does not import
|
|
@@ -152,8 +155,11 @@ field needs no `/reload`. Its value is literal text, not a prompt template; use
|
|
|
152
155
|
`\n` in JSON strings for multiple lines. It must be a nonblank string without control
|
|
153
156
|
characters other than tabs and newlines. A missing file or key uses the default.
|
|
154
157
|
Invalid/unreadable configuration reports an error without starting/switching a
|
|
155
|
-
task or changing the editor. **Done
|
|
156
|
-
|
|
158
|
+
task or changing the editor. Menu **Done**, footer completion, and agent tools are
|
|
159
|
+
unaffected. If configuration is invalid or unreadable, Settings shows only task,
|
|
160
|
+
suggestion, and CLI actions; repair `pi-note.json` before editing preferences.
|
|
161
|
+
Invalid configuration is never replaced with defaults. This is user-level
|
|
162
|
+
configuration only; project-local files are not read.
|
|
157
163
|
|
|
158
164
|
### Agent tools
|
|
159
165
|
|
|
@@ -185,7 +191,7 @@ asking in chat. The pin footer shows a pending suggestion, distinct from a selec
|
|
|
185
191
|
|
|
186
192
|
```text
|
|
187
193
|
📌 ✕ + · [tag] Task title...
|
|
188
|
-
📌 ✓
|
|
194
|
+
📌 ✓ · [tag] Task title...
|
|
189
195
|
```
|
|
190
196
|
|
|
191
197
|

|
|
@@ -193,13 +199,13 @@ asking in chat. The pin footer shows a pending suggestion, distinct from a selec
|
|
|
193
199
|
|
|
194
200
|
- Muted **✕** dismisses without creating a note; further offers are suppressed until a new session.
|
|
195
201
|
- Accent **+** creates, starts, and selects the suggested task.
|
|
196
|
-
- Green **✓** completes the selected task, starts a clean unselected session, and reloads Pi without a completion prompt. Menu **Done** and
|
|
202
|
+
- Green **✓** completes the selected task, starts a clean unselected session, and reloads Pi without a completion prompt. Menu **Done** and **Complete task** in Settings do the same.
|
|
197
203
|
|
|
198
|
-
Done occupies the former ✕ position, not the + position, so clicking Add twice cannot accidentally complete the new task. The former + cell
|
|
204
|
+
Done occupies the former ✕ position, not the + position, so clicking Add twice cannot accidentally complete the new task. The selected row removes the former + cell's padding. The selected task's title is a separate preview link.
|
|
199
205
|
|
|
200
206
|
The configured `footer.titleWidth` bounds the entire row (default 60 columns). Controls stay first; the tag is dropped before truncating the first-line title with `...`. Tiny widths hide controls (pin only). Pi can still clip the combined status row on narrow terminals.
|
|
201
207
|
|
|
202
|
-
Controls use the [Kitty preview handler](#display-only-preview), with no additional configuration. Fullscreen Kitty uses **Ctrl+Shift+click**.
|
|
208
|
+
Controls use the [Kitty preview handler](#display-only-preview), with no additional configuration. Fullscreen Kitty uses **Ctrl+Shift+click**. The Settings actions **Accept suggestion** and **Dismiss suggestion** perform the corresponding footer actions. No action submits input or starts an agent turn. Add and Dismiss preserve editor drafts; **Complete task** starts a clean session and reloads Pi, discarding the editor draft, so save unfinished input before completing a task.
|
|
203
209
|
|
|
204
210
|
Completion requires an idle Pi session and no pending Pinote operation. It rejects stale selected-task links and externally changed revisions. Accepted mutations drain even if the helper disconnects. On an error, check `/pi-note` before retrying, because a write may already have committed. Failed or stale writes never restart Pi. If another extension cancels the new session, the task remains completed and Pi reports the cancellation. The reload runs through the new session's fresh context; retired contexts are not reused.
|
|
205
211
|
|
package/footer-settings.ts
CHANGED
|
@@ -4,6 +4,9 @@ import { defaultFooterField, parseFooterConfig, parseHandoffPrompt, type PinoteS
|
|
|
4
4
|
|
|
5
5
|
type Matches = (data: string, action: string) => boolean;
|
|
6
6
|
type Row = { label: string; action: () => void };
|
|
7
|
+
export type SettingsAction = "preview" | "done" | "yes" | "no" | "setup" | "upgrade";
|
|
8
|
+
export type SettingsResult = "tasks" | SettingsAction | undefined;
|
|
9
|
+
export type SettingsOption = { label: string; result: SettingsAction };
|
|
7
10
|
const isTab = (data: string) => matchesKey(data, "tab") || matchesKey(data, "shift+tab");
|
|
8
11
|
const display = (value: string) => value.replace(/[\x00-\x1f\x7f-\x9f]/gu, " ");
|
|
9
12
|
const actions = ["Continue", "Done", "Switch task", "Settings"];
|
|
@@ -44,7 +47,9 @@ export class FooterSettings {
|
|
|
44
47
|
private knownFields: string[];
|
|
45
48
|
private theme: Theme;
|
|
46
49
|
private matches: Matches;
|
|
47
|
-
private done: (result:
|
|
50
|
+
private done: (result: SettingsResult) => void;
|
|
51
|
+
private options: SettingsOption[];
|
|
52
|
+
private configurationError?: string;
|
|
48
53
|
private save: (settings: PinoteSettings) => void;
|
|
49
54
|
private requestRender: () => void;
|
|
50
55
|
private index = 0;
|
|
@@ -54,8 +59,10 @@ export class FooterSettings {
|
|
|
54
59
|
private hasFocus = false;
|
|
55
60
|
|
|
56
61
|
constructor(config: PinoteSettings, knownFields: string[], theme: Theme, matches: Matches,
|
|
57
|
-
done: (result:
|
|
58
|
-
save: (settings: PinoteSettings) => void) {
|
|
62
|
+
done: (result: SettingsResult) => void, requestRender: () => void, createEditor: () => Editor,
|
|
63
|
+
save: (settings: PinoteSettings) => void, options: SettingsOption[] = [], configurationError?: string) {
|
|
64
|
+
this.options = options;
|
|
65
|
+
this.configurationError = configurationError;
|
|
59
66
|
this.draft = structuredClone(config.footer);
|
|
60
67
|
this.prompt = config.handoffPrompt;
|
|
61
68
|
this.createEditor = createEditor;
|
|
@@ -97,7 +104,12 @@ export class FooterSettings {
|
|
|
97
104
|
this.field = structuredClone(this.draft.fields!.find((field) => field.name === name) ?? defaultFooterField(name));
|
|
98
105
|
this.index = 0; this.error = "";
|
|
99
106
|
}
|
|
107
|
+
private actionRows(): Row[] {
|
|
108
|
+
return [...this.options.map(({ label, result }) => ({ label, action: () => this.done(result) })),
|
|
109
|
+
{ label: "Return to tasks", action: () => this.done("tasks") }];
|
|
110
|
+
}
|
|
100
111
|
private rootRows(): Row[] {
|
|
112
|
+
if (this.configurationError) return this.actionRows();
|
|
101
113
|
const rows: Row[] = (["titleWidth", "fieldWidth", "maxFields"] as const).map((key) => ({
|
|
102
114
|
label: `${{ titleWidth: "Title width", fieldWidth: "Default field width", maxFields: "Max fields" }[key]}: ${this.draft[key]}`,
|
|
103
115
|
action: () => this.ask(key, String(this.draft[key]), (value) => {
|
|
@@ -123,8 +135,8 @@ export class FooterSettings {
|
|
|
123
135
|
this.persistField(field);
|
|
124
136
|
this.index = 0;
|
|
125
137
|
}
|
|
126
|
-
}) }
|
|
127
|
-
|
|
138
|
+
}) });
|
|
139
|
+
rows.push(...this.actionRows());
|
|
128
140
|
return rows;
|
|
129
141
|
}
|
|
130
142
|
private fieldRows(): Row[] {
|
|
@@ -183,6 +195,7 @@ export class FooterSettings {
|
|
|
183
195
|
}
|
|
184
196
|
render(width: number) {
|
|
185
197
|
const lines = [this.theme.fg("accent", "Pinote — Global Settings"), this.theme.fg("dim", "Tasks (Tab) · Settings · Fields")];
|
|
198
|
+
if (this.configurationError) lines.push(this.theme.fg("warning", this.configurationError));
|
|
186
199
|
if (this.editor) {
|
|
187
200
|
lines.push(this.theme.fg("accent", "Task prompt"), ...this.editor.render(width),
|
|
188
201
|
this.theme.fg("dim", "Enter save · Shift+Enter/Ctrl+J newline · Ctrl+C clear · Esc cancel edit"));
|
|
@@ -197,10 +210,11 @@ export class FooterSettings {
|
|
|
197
210
|
lines.push(...rows.slice(start, start + 8).map((row, offset) => offset + start === this.index
|
|
198
211
|
? this.theme.fg("accent", `> ${row.label}`) : ` ${row.label}`));
|
|
199
212
|
if (rows.length > 8) lines.push(this.theme.fg("dim", `${this.index + 1}/${rows.length}`));
|
|
200
|
-
lines.push(this.theme.fg("dim", "↑↓ navigate · Enter
|
|
213
|
+
lines.push(this.theme.fg("dim", "↑↓ navigate · Enter select · Esc back"));
|
|
201
214
|
}
|
|
202
215
|
if (this.error) lines.push(this.theme.fg("error", this.error));
|
|
203
|
-
lines.push(this.theme.fg("dim",
|
|
216
|
+
lines.push(this.theme.fg("dim", this.configurationError
|
|
217
|
+
? "Fix pi-note.json to edit settings · Tab: tasks" : "Changes save automatically · Tab: tasks"));
|
|
204
218
|
return lines.map((line) => truncateToWidth(line, width));
|
|
205
219
|
}
|
|
206
220
|
invalidate() { this.input?.widget.invalidate(); this.editor?.invalidate(); }
|
package/index.ts
CHANGED
|
@@ -7,9 +7,9 @@ import { stripVTControlCharacters } from "node:util";
|
|
|
7
7
|
import { Type } from "typebox";
|
|
8
8
|
import { updateTaskFooter, clearTaskFooter } from "./footer-status.ts";
|
|
9
9
|
import { defaultFooterConfig, defaultHandoffPrompt, effectiveFooterFields, loadFooterConfig, parseHandoffPrompt, readFooterDocument, saveFooterConfig, type FooterConfig, type PinoteSettings } from "./footer-config.ts";
|
|
10
|
-
import { FooterSettings, TaskMenu, withSettingsTab } from "./footer-settings.ts";
|
|
10
|
+
import { FooterSettings, TaskMenu, withSettingsTab, type SettingsOption, type SettingsResult } from "./footer-settings.ts";
|
|
11
11
|
import { TaskPicker, taskState, taskTag } from "./task-picker.ts";
|
|
12
|
-
import { bundledCLIVersion,
|
|
12
|
+
import { bundledCLIVersion, detectCLI, setupCLI, setupHint, versionAtLeast, type CLIAction } from "./setup.ts";
|
|
13
13
|
import { createPreviewBridge } from "./preview-bridge.ts";
|
|
14
14
|
import { renderSuggestion, renderSelectedTask } from "./task-suggestion.ts";
|
|
15
15
|
|
|
@@ -179,7 +179,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
179
179
|
if (!alive || generation !== epoch || setupAbort) return;
|
|
180
180
|
cliState = action;
|
|
181
181
|
if (suggest && action === "upgrade") {
|
|
182
|
-
ctx.ui.notify(`Pinote CLI ${bundledCLIVersion} is bundled with this extension.
|
|
182
|
+
ctx.ui.notify(`Pinote CLI ${bundledCLIVersion} is bundled with this extension. Open /pi-note → Settings → Upgrade CLI to update your older CLI.`, "info");
|
|
183
183
|
}
|
|
184
184
|
};
|
|
185
185
|
|
|
@@ -306,7 +306,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
306
306
|
if (alive && generation === epoch && serial === refreshSerial) {
|
|
307
307
|
previewBridge?.invalidate();
|
|
308
308
|
clearTaskFooter(ctx);
|
|
309
|
-
ctx.ui.setStatus("pinote", suggestionStatus(ctx) ?? `${noteIcon} Pinote unavailable ·
|
|
309
|
+
ctx.ui.setStatus("pinote", suggestionStatus(ctx) ?? `${ctx.ui.theme.bold(noteIcon)} Pinote unavailable · /pi-note → Settings`);
|
|
310
310
|
}
|
|
311
311
|
}
|
|
312
312
|
};
|
|
@@ -349,10 +349,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
349
349
|
};
|
|
350
350
|
const timer = setTimeout(() => {
|
|
351
351
|
finish(false);
|
|
352
|
-
if (alive && activeContext === ctx) ctx.ui.notify("Pinote completion could not start.
|
|
352
|
+
if (alive && activeContext === ctx) ctx.ui.notify("Pinote completion could not start. Open /pi-note → Settings → Complete task.", "warning");
|
|
353
353
|
}, 4000);
|
|
354
354
|
completionDispatch = { token, selection, task: displayed, started: () => clearTimeout(timer), finish };
|
|
355
|
-
try { pi.sendUserMessage(`/pi-note
|
|
355
|
+
try { pi.sendUserMessage(`/pi-note ${token}`, { expandPromptTemplates: true }); }
|
|
356
356
|
catch { finish(false); }
|
|
357
357
|
});
|
|
358
358
|
};
|
|
@@ -404,18 +404,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
404
404
|
if (alive && generation === epoch) await refresh(ctx);
|
|
405
405
|
}
|
|
406
406
|
};
|
|
407
|
-
pi.registerCommand("pi-note-done", {
|
|
408
|
-
description: "Complete the selected task, start a clean session, and reload Pi",
|
|
409
|
-
handler: (args, ctx) => completeSelected(args, ctx),
|
|
410
|
-
});
|
|
411
|
-
pi.registerCommand("pi-note-preview", {
|
|
412
|
-
description: "Display the selected task locally without sending it to the model",
|
|
413
|
-
handler: async (args, ctx) => {
|
|
414
|
-
if (!ctx.hasUI || ctx.mode !== "tui") return;
|
|
415
|
-
if (args.trim()) { ctx.ui.notify("Usage: /pi-note-preview", "warning"); return; }
|
|
416
|
-
if (!await preview(ctx)) ctx.ui.notify("Select a task with /pi-note and wait until Pi is idle before previewing.", "warning");
|
|
417
|
-
},
|
|
418
|
-
});
|
|
419
407
|
pi.on("session_start", async (_event, ctx) => {
|
|
420
408
|
setupAbort?.abort();
|
|
421
409
|
cancelCompletionDispatch();
|
|
@@ -445,7 +433,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
445
433
|
cliState = undefined;
|
|
446
434
|
if (ctx.hasUI && ctx.mode === "tui") {
|
|
447
435
|
if (offerConfigError) ctx.ui.notify(`Pinote task offers disabled: ${clean(offerConfigError)} Fix the configuration and /reload.`, "warning");
|
|
448
|
-
ctx.ui.addAutocompleteProvider((current) => cliMenu(current, () => setupAbort ? undefined : cliState));
|
|
449
436
|
}
|
|
450
437
|
if (ctx.hasUI && ctx.mode === "tui") {
|
|
451
438
|
const bridge = createPreviewBridge();
|
|
@@ -455,7 +442,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
455
442
|
} catch {
|
|
456
443
|
await bridge.stop();
|
|
457
444
|
if (previewBridge === bridge) previewBridge = undefined;
|
|
458
|
-
ctx.ui.notify("Pinote task link unavailable;
|
|
445
|
+
ctx.ui.notify("Pinote task link unavailable; open /pi-note → Settings → Preview task.", "warning");
|
|
459
446
|
}
|
|
460
447
|
}
|
|
461
448
|
if (!alive || generation !== epoch) return;
|
|
@@ -500,51 +487,49 @@ export default function (pi: ExtensionAPI) {
|
|
|
500
487
|
if (bridge) await bridge.stop();
|
|
501
488
|
});
|
|
502
489
|
|
|
503
|
-
const
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
if (
|
|
529
|
-
|
|
530
|
-
const latestContext = activeContext ?? (currentSession() ? ctx : undefined);
|
|
531
|
-
if (alive && latestContext) {
|
|
532
|
-
const generation = epoch;
|
|
533
|
-
await checkCLI(latestContext, !currentSession());
|
|
534
|
-
if (!alive || generation !== epoch || setupAbort) return;
|
|
535
|
-
await refresh(latestContext);
|
|
536
|
-
}
|
|
490
|
+
const installCLI = async (upgrade: boolean, ctx: ExtensionCommandContext) => {
|
|
491
|
+
if (!ctx.hasUI || ctx.mode !== "tui") return;
|
|
492
|
+
if (pending || !ctx.isIdle()) {
|
|
493
|
+
ctx.ui.notify("Wait until Pi is idle and the pinote operation has finished.", "warning");
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
const operation = Symbol();
|
|
497
|
+
pending = operation;
|
|
498
|
+
const generation = epoch;
|
|
499
|
+
const controller = new AbortController();
|
|
500
|
+
setupAbort = controller;
|
|
501
|
+
clearTaskFooter(ctx);
|
|
502
|
+
const currentSession = () => alive && generation === epoch;
|
|
503
|
+
try {
|
|
504
|
+
await setupCLI(pi, ctx, upgrade,
|
|
505
|
+
() => currentSession() && ctx.isIdle(), controller.signal);
|
|
506
|
+
} catch (error) {
|
|
507
|
+
if (currentSession()) ctx.ui.notify(`Pinote setup: ${clean(String(error))}`, "error");
|
|
508
|
+
} finally {
|
|
509
|
+
if (pending === operation) pending = undefined;
|
|
510
|
+
if (setupAbort === controller) setupAbort = undefined;
|
|
511
|
+
const latestContext = activeContext ?? (currentSession() ? ctx : undefined);
|
|
512
|
+
if (alive && latestContext) {
|
|
513
|
+
const generation = epoch;
|
|
514
|
+
await checkCLI(latestContext, !currentSession());
|
|
515
|
+
if (!alive || generation !== epoch || setupAbort) return;
|
|
516
|
+
await refresh(latestContext);
|
|
537
517
|
}
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
registerInstallCommand("pi-note-setup");
|
|
541
|
-
registerInstallCommand("pi-note-upgrade");
|
|
518
|
+
}
|
|
519
|
+
};
|
|
542
520
|
|
|
543
521
|
pi.registerCommand("pi-note", {
|
|
544
522
|
description: "Continue, complete, switch tasks, or edit global settings (Tab)",
|
|
545
523
|
handler: async (args, ctx) => {
|
|
546
524
|
if (!ctx.hasUI || ctx.mode !== "tui") return;
|
|
547
|
-
|
|
525
|
+
// Footer completion needs a command context, but no extra slash command.
|
|
526
|
+
// Only the current one-use capability may dispatch; stale tokens remain inert.
|
|
527
|
+
const token = args.trim();
|
|
528
|
+
if (/^[a-f0-9]{32}:\d+:\d+:\d+:[a-f0-9]{16}$/u.test(token)) {
|
|
529
|
+
await completeSelected(token, ctx);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
if (token) { ctx.ui.notify("Usage: /pi-note", "warning"); return; }
|
|
548
533
|
if (pending || !ctx.isIdle()) {
|
|
549
534
|
ctx.ui.notify("Wait until Pi is idle and the pinote operation has finished.", "warning");
|
|
550
535
|
return;
|
|
@@ -556,19 +541,46 @@ export default function (pi: ExtensionAPI) {
|
|
|
556
541
|
const currentSession = () => alive && generation === epoch;
|
|
557
542
|
const canAct = () => currentSession() && ctx.isIdle() && branch === branchEpoch;
|
|
558
543
|
try {
|
|
559
|
-
|
|
544
|
+
await checkCLI(ctx);
|
|
560
545
|
if (!canAct()) return;
|
|
561
|
-
|
|
546
|
+
const current = cliState === "ready" ? await selected(ctx) : null;
|
|
547
|
+
if (!canAct()) return;
|
|
548
|
+
let settings = cliState !== "ready";
|
|
562
549
|
while (canAct()) {
|
|
550
|
+
if (!settings && cliState !== "ready") {
|
|
551
|
+
ctx.ui.notify(setupHint, "warning");
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
563
554
|
if (settings) {
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
555
|
+
let config: PinoteSettings = { footer: { ...defaultFooterConfig, fields: [] }, handoffPrompt: defaultHandoffPrompt };
|
|
556
|
+
let expectedRaw: string | null = null;
|
|
557
|
+
let configurationError: string | undefined;
|
|
558
|
+
try {
|
|
559
|
+
const document = readFooterDocument();
|
|
560
|
+
const root = document.raw === null ? {} : JSON.parse(document.raw);
|
|
561
|
+
config = {
|
|
562
|
+
footer: { ...document.config, fields: effectiveFooterFields(document.config, current?.agent_notes) },
|
|
563
|
+
handoffPrompt: "handoffPrompt" in root ? parseHandoffPrompt(root.handoffPrompt) : defaultHandoffPrompt,
|
|
564
|
+
};
|
|
565
|
+
expectedRaw = document.raw;
|
|
566
|
+
} catch (error) {
|
|
567
|
+
// Broken preferences must not strand task actions or CLI recovery.
|
|
568
|
+
// Offer actions only; never save defaults over invalid user data.
|
|
569
|
+
configurationError = clean(error instanceof Error ? error.message : String(error));
|
|
570
|
+
}
|
|
571
|
+
const options: SettingsOption[] = [
|
|
572
|
+
...(current ? [
|
|
573
|
+
{ label: "Preview task", result: "preview" as const },
|
|
574
|
+
{ label: "Complete task (new session)", result: "done" as const },
|
|
575
|
+
] : []),
|
|
576
|
+
...(suggestion ? [
|
|
577
|
+
{ label: "Accept suggestion", result: "yes" as const },
|
|
578
|
+
{ label: "Dismiss suggestion", result: "no" as const },
|
|
579
|
+
] : []),
|
|
580
|
+
...(cliState === "setup" ? [{ label: "Install CLI", result: "setup" as const }]
|
|
581
|
+
: cliState === "upgrade" ? [{ label: "Upgrade CLI", result: "upgrade" as const }] : []),
|
|
582
|
+
];
|
|
583
|
+
const edited = await ctx.ui.custom<SettingsResult>((tui, theme, kb, done) =>
|
|
572
584
|
new FooterSettings(config, Object.keys(current?.agent_notes ?? {}).filter((name) => name !== "Bar"),
|
|
573
585
|
theme, (data, action) => kb.matches(data, action as Keybinding), done, () => tui.requestRender(),
|
|
574
586
|
() => new Editor(tui, {
|
|
@@ -578,14 +590,22 @@ export default function (pi: ExtensionAPI) {
|
|
|
578
590
|
noMatch: (line) => theme.fg("warning", line) },
|
|
579
591
|
}), (updated) => {
|
|
580
592
|
if (!canAct()) throw new Error("This session changed. Reopen settings before editing.");
|
|
593
|
+
if (configurationError) throw new Error(configurationError);
|
|
581
594
|
const saved = saveFooterConfig(updated.footer, expectedRaw, updated.handoffPrompt);
|
|
582
595
|
expectedRaw = saved.raw;
|
|
583
596
|
footerConfig = saved.config;
|
|
584
597
|
void refresh(ctx);
|
|
585
|
-
}));
|
|
598
|
+
}, options, configurationError));
|
|
586
599
|
if (!canAct() || edited === undefined) return;
|
|
587
|
-
settings = false;
|
|
588
|
-
|
|
600
|
+
if (edited === "tasks") { settings = false; continue; }
|
|
601
|
+
if (!options.some((option) => option.result === edited)) return;
|
|
602
|
+
if (pending === operation) pending = undefined;
|
|
603
|
+
if (edited === "preview") {
|
|
604
|
+
if (!await preview(ctx)) ctx.ui.notify("The task is no longer available for preview.", "warning");
|
|
605
|
+
} else if (edited === "done") await completeSelected("", ctx, current ?? undefined);
|
|
606
|
+
else if (edited === "yes" || edited === "no") await respondToSuggestion(edited, ctx);
|
|
607
|
+
else await installCLI(edited === "upgrade", ctx);
|
|
608
|
+
return;
|
|
589
609
|
}
|
|
590
610
|
const action = current
|
|
591
611
|
? await ctx.ui.custom<string | undefined>((tui, theme, kb, done) =>
|
|
@@ -733,20 +753,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
733
753
|
if (alive && generation === epoch) await refresh(ctx);
|
|
734
754
|
}
|
|
735
755
|
};
|
|
736
|
-
pi.registerCommand("pi-note-yes", {
|
|
737
|
-
description: "Create, start and select the suggested task (same as +)",
|
|
738
|
-
handler: async (args, ctx) => {
|
|
739
|
-
if (!ctx.hasUI || ctx.mode !== "tui") return;
|
|
740
|
-
if (args.trim() || !await respondToSuggestion("yes", ctx)) ctx.ui.notify("No available suggestion, or a Pinote operation is busy. Retry when it finishes.", "warning");
|
|
741
|
-
},
|
|
742
|
-
});
|
|
743
|
-
pi.registerCommand("pi-note-no", {
|
|
744
|
-
description: "Dismiss the suggested task without creating a note (same as ✕)",
|
|
745
|
-
handler: async (args, ctx) => {
|
|
746
|
-
if (!ctx.hasUI || ctx.mode !== "tui") return;
|
|
747
|
-
if (args.trim() || !await respondToSuggestion("no", ctx)) ctx.ui.notify("No available suggestion. Usage: /pi-note-no", "warning");
|
|
748
|
-
},
|
|
749
|
-
});
|
|
750
756
|
pi.registerTool({
|
|
751
757
|
name: "pinote_propose",
|
|
752
758
|
label: "Pinote suggest task",
|
package/package.json
CHANGED
package/setup.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { stripVTControlCharacters } from "node:util";
|
|
3
|
-
import type { AutocompleteProvider } from "@earendil-works/pi-tui";
|
|
4
3
|
|
|
5
4
|
// Immutable source: the Python app is not yet published to PyPI.
|
|
6
5
|
export const cliSource = "https://github.com/kvidzibo/pinote/archive/1597baf60bff8bc96893be40781f152a001623d1.tar.gz";
|
|
7
6
|
export const bundledCLIVersion = "0.4.0";
|
|
8
|
-
export const setupHint = "
|
|
7
|
+
export const setupHint = "Open /pi-note → Settings to install or upgrade the CLI; note must be on PATH.";
|
|
9
8
|
export type CLIAction = "setup" | "upgrade" | "ready";
|
|
10
9
|
|
|
11
10
|
export function cliAction(version: string): CLIAction {
|
|
@@ -27,23 +26,6 @@ export async function detectCLI(pi: ExtensionAPI, signal?: AbortSignal): Promise
|
|
|
27
26
|
} catch { return "setup"; }
|
|
28
27
|
}
|
|
29
28
|
|
|
30
|
-
export function cliMenu(current: AutocompleteProvider, action: () => CLIAction | undefined): AutocompleteProvider {
|
|
31
|
-
return {
|
|
32
|
-
triggerCharacters: current.triggerCharacters,
|
|
33
|
-
async getSuggestions(...args) {
|
|
34
|
-
const suggestions = await current.getSuggestions(...args);
|
|
35
|
-
if (!suggestions || !/^\/\S*$/u.test(suggestions.prefix)) return suggestions;
|
|
36
|
-
const items = suggestions.items.filter(({ value }) => {
|
|
37
|
-
const name = value.replace(/^\//u, "");
|
|
38
|
-
return name === "pi-note-setup" ? action() === "setup"
|
|
39
|
-
: name === "pi-note-upgrade" ? action() === "upgrade" : true;
|
|
40
|
-
});
|
|
41
|
-
return items.length ? { ...suggestions, items } : null;
|
|
42
|
-
},
|
|
43
|
-
applyCompletion: (...args) => current.applyCompletion(...args),
|
|
44
|
-
shouldTriggerFileCompletion: (...args) => current.shouldTriggerFileCompletion?.(...args) ?? true,
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
29
|
export function versionAtLeast(version: string, minimum: string): boolean {
|
|
48
30
|
const match = /^pinote (\d+)\.(\d+)\.(\d+)$/u.exec(version.trim());
|
|
49
31
|
if (!match) return false;
|
|
@@ -72,7 +54,7 @@ export async function setupCLI(
|
|
|
72
54
|
ctx.ui.notify(`Pinote CLI is ready (bundled version ${bundledCLIVersion} or newer). Use /pi-note.`, "info");
|
|
73
55
|
return;
|
|
74
56
|
}
|
|
75
|
-
const command = action === "upgrade" ? "
|
|
57
|
+
const command = `/pi-note → Settings → ${action === "upgrade" ? "Upgrade CLI" : "Install CLI"}`;
|
|
76
58
|
if (upgrade !== (action === "upgrade")) {
|
|
77
59
|
ctx.ui.notify(`Use ${command} ${action === "upgrade" ? "to upgrade the older CLI" : "to install the missing CLI"}.`, "info");
|
|
78
60
|
return;
|
package/task-suggestion.ts
CHANGED
|
@@ -11,11 +11,11 @@ const add = readFileSync(new URL("./icons/add.txt", import.meta.url), "utf8").tr
|
|
|
11
11
|
const gap = "\u00a0";
|
|
12
12
|
const link = (label: string, url?: string) => url ? `\x1b]8;;${url}\x07${label}\x1b]8;;\x07` : label;
|
|
13
13
|
type Task = { text: string; tag?: string | null };
|
|
14
|
-
type Colors = Pick<Theme, "fg">;
|
|
14
|
+
type Colors = Pick<Theme, "fg" | "bold">;
|
|
15
15
|
|
|
16
16
|
function renderTask(task: Task, width: number, theme: Colors, prefix: string, previewUrl?: string): string {
|
|
17
17
|
// All controls or none: never let a narrow budget reveal only one pending choice.
|
|
18
|
-
if (visibleWidth(prefix) > width) return truncateToWidth(pin, width, "");
|
|
18
|
+
if (visibleWidth(prefix) > width) return truncateToWidth(theme.bold(pin), width, "");
|
|
19
19
|
const remaining = width - visibleWidth(prefix) - 3; // separator: " · "
|
|
20
20
|
if (remaining < 4) return prefix.trimEnd();
|
|
21
21
|
const clean = (value: string) => stripVTControlCharacters(value).replace(/[\x00-\x1f\x7f-\x9f]/gu, " ").replace(/\s+/gu, " ").trim();
|
|
@@ -30,12 +30,12 @@ function renderTask(task: Task, width: number, theme: Colors, prefix: string, pr
|
|
|
30
30
|
|
|
31
31
|
export function renderSuggestion(task: Task, width: number, theme: Colors, urls: { yes?: string; no?: string }): string {
|
|
32
32
|
return renderTask(task, width, theme,
|
|
33
|
-
`${pin} ${theme.fg("muted", link(cross, urls.no))}${gap.repeat(2)}${theme.fg("accent", link(add, urls.yes))}`);
|
|
33
|
+
`${theme.bold(pin)} ${theme.fg("muted", link(theme.bold(cross), urls.no))}${gap.repeat(2)}${theme.fg("accent", link(theme.bold(add), urls.yes))}`);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export function renderSelectedTask(task: Task, width: number, theme: Colors, urls: { preview?: string; done?: string }): string {
|
|
37
|
-
//
|
|
37
|
+
// Done stays in the former '✕' cell, never the Add cell.
|
|
38
38
|
return renderTask(task, width, theme,
|
|
39
|
-
`${pin} ${theme.fg("success", link(check, urls.done))}
|
|
39
|
+
`${theme.bold(pin)} ${theme.fg("success", link(theme.bold(check), urls.done))}`,
|
|
40
40
|
urls.preview);
|
|
41
41
|
}
|