pi-note 0.15.1 → 0.16.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 +44 -31
- package/footer-settings.ts +21 -7
- package/index.ts +95 -88
- package/package.json +1 -1
- package/setup.ts +2 -20
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,15 +54,20 @@ 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).
|
|
@@ -72,8 +75,8 @@ The normal Pi status area shows selected-task controls and title without replaci
|
|
|
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
|
|
@@ -129,9 +132,16 @@ Configure `~/.pi/agent/pi-note.json` (or the agent directory set by
|
|
|
129
132
|
|
|
130
133
|
`taskOfferPolicy` controls the agent's task-offer guidance:
|
|
131
134
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
+
With no selected task, suggest a note only when implementation is expected to
|
|
136
|
+
span multiple turns. Skip questions, investigation, recon, and small edits (such
|
|
137
|
+
as changing a few configuration lines). If investigation develops into substantial
|
|
138
|
+
implementation, suggest then. No plan is required: a short title identifies the
|
|
139
|
+
work. Explicit user requests to create a task remain allowed under every policy.
|
|
140
|
+
|
|
141
|
+
- `always` (default): offer for qualifying implementation in any repository.
|
|
142
|
+
- `github-remote`: for qualifying implementation, first verify with Git that the
|
|
143
|
+
repository has an HTTPS or SSH remote hosted on `github.com`; otherwise do not
|
|
144
|
+
offer. Local paths and other
|
|
135
145
|
hosts do not qualify. This is agent guidance, not a network or PR-access check.
|
|
136
146
|
- `never`: do not offer tasks; explicit requests to create one remain allowed.
|
|
137
147
|
|
|
@@ -152,8 +162,11 @@ field needs no `/reload`. Its value is literal text, not a prompt template; use
|
|
|
152
162
|
`\n` in JSON strings for multiple lines. It must be a nonblank string without control
|
|
153
163
|
characters other than tabs and newlines. A missing file or key uses the default.
|
|
154
164
|
Invalid/unreadable configuration reports an error without starting/switching a
|
|
155
|
-
task or changing the editor. **Done
|
|
156
|
-
|
|
165
|
+
task or changing the editor. Menu **Done**, footer completion, and agent tools are
|
|
166
|
+
unaffected. If configuration is invalid or unreadable, Settings shows only task,
|
|
167
|
+
suggestion, and CLI actions; repair `pi-note.json` before editing preferences.
|
|
168
|
+
Invalid configuration is never replaced with defaults. This is user-level
|
|
169
|
+
configuration only; project-local files are not read.
|
|
157
170
|
|
|
158
171
|
### Agent tools
|
|
159
172
|
|
|
@@ -193,13 +206,13 @@ asking in chat. The pin footer shows a pending suggestion, distinct from a selec
|
|
|
193
206
|
|
|
194
207
|
- Muted **✕** dismisses without creating a note; further offers are suppressed until a new session.
|
|
195
208
|
- 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
|
|
209
|
+
- 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
210
|
|
|
198
211
|
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
212
|
|
|
200
213
|
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
214
|
|
|
202
|
-
Controls use the [Kitty preview handler](#display-only-preview), with no additional configuration. Fullscreen Kitty uses **Ctrl+Shift+click**.
|
|
215
|
+
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
216
|
|
|
204
217
|
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
218
|
|
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,16 +7,17 @@ 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
|
|
|
16
16
|
const handoffGuidance = "Keep agent notes to at most three short bullets total: outcome, blocker, next action, only when relevant. Replace stale notes; omit narration, repeated task text, and routine test logs. Keep a GitHub pull request in the PR field. Read pinote_fields to learn the user's globally configured footer field names and presentation. Populate those fields with meaningful task values when relevant using pinote_update_current; fields without values stay hidden. Footer fields and widths are chosen by the user's pi-note.json config. Set configured fields with Markdown values; links are clickable. When footer.fields is unset, Bar selects labels, one per line. Do not list PR in Bar. Remove a field to hide it. Do not modify config without user approval.";
|
|
17
|
+
const implementationOfferGuidance = "When no pinote task is selected, suggest one only when implementation is expected to span multiple turns. Skip suggestions for questions, investigation, recon, and small edits such as changing a few configuration lines. If investigation develops into substantial implementation, suggest then. No plan is required; use a short title describing the work. Explicit user requests to create one remain allowed.";
|
|
17
18
|
const offerGuidance = {
|
|
18
|
-
always:
|
|
19
|
-
"github-remote":
|
|
19
|
+
always: `${implementationOfferGuidance} For eligible work, use pinote_propose to show one suggested \`[tag] task\` in the bottom bar instead of asking in chat. In noninteractive modes, ask before creating it.`,
|
|
20
|
+
"github-remote": `${implementationOfferGuidance} For eligible work, first verify with Git that the current repository has a remote whose URL host is github.com (HTTPS or SSH). Only then use pinote_propose to show one suggested \`[tag] task\` in the bottom bar instead of asking in chat. In noninteractive modes, ask before creating it. Local paths, other hosts, and GitHub-looking URL paths do not qualify. If no GitHub remote is verified, do not offer a task.`,
|
|
20
21
|
never: "Do not offer to create a pinote task. Create one only when the user explicitly requests it.",
|
|
21
22
|
};
|
|
22
23
|
const consentGuidance = "The suggestion bar's plus creates, starts and selects the note; its cross dismisses it. Continue the requested work while the suggestion is pending; do not ask again in chat or create a note yourself. On a chat no, continue without a note; on a chat yes, call pinote_add with select true. Never add or select without consent. If a task is already selected, do not replace it unless the user asks to switch.";
|
|
@@ -179,7 +180,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
179
180
|
if (!alive || generation !== epoch || setupAbort) return;
|
|
180
181
|
cliState = action;
|
|
181
182
|
if (suggest && action === "upgrade") {
|
|
182
|
-
ctx.ui.notify(`Pinote CLI ${bundledCLIVersion} is bundled with this extension.
|
|
183
|
+
ctx.ui.notify(`Pinote CLI ${bundledCLIVersion} is bundled with this extension. Open /pi-note → Settings → Upgrade CLI to update your older CLI.`, "info");
|
|
183
184
|
}
|
|
184
185
|
};
|
|
185
186
|
|
|
@@ -306,7 +307,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
306
307
|
if (alive && generation === epoch && serial === refreshSerial) {
|
|
307
308
|
previewBridge?.invalidate();
|
|
308
309
|
clearTaskFooter(ctx);
|
|
309
|
-
ctx.ui.setStatus("pinote", suggestionStatus(ctx) ?? `${ctx.ui.theme.bold(noteIcon)} Pinote unavailable ·
|
|
310
|
+
ctx.ui.setStatus("pinote", suggestionStatus(ctx) ?? `${ctx.ui.theme.bold(noteIcon)} Pinote unavailable · /pi-note → Settings`);
|
|
310
311
|
}
|
|
311
312
|
}
|
|
312
313
|
};
|
|
@@ -349,10 +350,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
349
350
|
};
|
|
350
351
|
const timer = setTimeout(() => {
|
|
351
352
|
finish(false);
|
|
352
|
-
if (alive && activeContext === ctx) ctx.ui.notify("Pinote completion could not start.
|
|
353
|
+
if (alive && activeContext === ctx) ctx.ui.notify("Pinote completion could not start. Open /pi-note → Settings → Complete task.", "warning");
|
|
353
354
|
}, 4000);
|
|
354
355
|
completionDispatch = { token, selection, task: displayed, started: () => clearTimeout(timer), finish };
|
|
355
|
-
try { pi.sendUserMessage(`/pi-note
|
|
356
|
+
try { pi.sendUserMessage(`/pi-note ${token}`, { expandPromptTemplates: true }); }
|
|
356
357
|
catch { finish(false); }
|
|
357
358
|
});
|
|
358
359
|
};
|
|
@@ -404,18 +405,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
404
405
|
if (alive && generation === epoch) await refresh(ctx);
|
|
405
406
|
}
|
|
406
407
|
};
|
|
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
408
|
pi.on("session_start", async (_event, ctx) => {
|
|
420
409
|
setupAbort?.abort();
|
|
421
410
|
cancelCompletionDispatch();
|
|
@@ -445,7 +434,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
445
434
|
cliState = undefined;
|
|
446
435
|
if (ctx.hasUI && ctx.mode === "tui") {
|
|
447
436
|
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
437
|
}
|
|
450
438
|
if (ctx.hasUI && ctx.mode === "tui") {
|
|
451
439
|
const bridge = createPreviewBridge();
|
|
@@ -455,7 +443,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
455
443
|
} catch {
|
|
456
444
|
await bridge.stop();
|
|
457
445
|
if (previewBridge === bridge) previewBridge = undefined;
|
|
458
|
-
ctx.ui.notify("Pinote task link unavailable;
|
|
446
|
+
ctx.ui.notify("Pinote task link unavailable; open /pi-note → Settings → Preview task.", "warning");
|
|
459
447
|
}
|
|
460
448
|
}
|
|
461
449
|
if (!alive || generation !== epoch) return;
|
|
@@ -500,51 +488,49 @@ export default function (pi: ExtensionAPI) {
|
|
|
500
488
|
if (bridge) await bridge.stop();
|
|
501
489
|
});
|
|
502
490
|
|
|
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
|
-
}
|
|
491
|
+
const installCLI = async (upgrade: boolean, ctx: ExtensionCommandContext) => {
|
|
492
|
+
if (!ctx.hasUI || ctx.mode !== "tui") return;
|
|
493
|
+
if (pending || !ctx.isIdle()) {
|
|
494
|
+
ctx.ui.notify("Wait until Pi is idle and the pinote operation has finished.", "warning");
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
const operation = Symbol();
|
|
498
|
+
pending = operation;
|
|
499
|
+
const generation = epoch;
|
|
500
|
+
const controller = new AbortController();
|
|
501
|
+
setupAbort = controller;
|
|
502
|
+
clearTaskFooter(ctx);
|
|
503
|
+
const currentSession = () => alive && generation === epoch;
|
|
504
|
+
try {
|
|
505
|
+
await setupCLI(pi, ctx, upgrade,
|
|
506
|
+
() => currentSession() && ctx.isIdle(), controller.signal);
|
|
507
|
+
} catch (error) {
|
|
508
|
+
if (currentSession()) ctx.ui.notify(`Pinote setup: ${clean(String(error))}`, "error");
|
|
509
|
+
} finally {
|
|
510
|
+
if (pending === operation) pending = undefined;
|
|
511
|
+
if (setupAbort === controller) setupAbort = undefined;
|
|
512
|
+
const latestContext = activeContext ?? (currentSession() ? ctx : undefined);
|
|
513
|
+
if (alive && latestContext) {
|
|
514
|
+
const generation = epoch;
|
|
515
|
+
await checkCLI(latestContext, !currentSession());
|
|
516
|
+
if (!alive || generation !== epoch || setupAbort) return;
|
|
517
|
+
await refresh(latestContext);
|
|
537
518
|
}
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
registerInstallCommand("pi-note-setup");
|
|
541
|
-
registerInstallCommand("pi-note-upgrade");
|
|
519
|
+
}
|
|
520
|
+
};
|
|
542
521
|
|
|
543
522
|
pi.registerCommand("pi-note", {
|
|
544
523
|
description: "Continue, complete, switch tasks, or edit global settings (Tab)",
|
|
545
524
|
handler: async (args, ctx) => {
|
|
546
525
|
if (!ctx.hasUI || ctx.mode !== "tui") return;
|
|
547
|
-
|
|
526
|
+
// Footer completion needs a command context, but no extra slash command.
|
|
527
|
+
// Only the current one-use capability may dispatch; stale tokens remain inert.
|
|
528
|
+
const token = args.trim();
|
|
529
|
+
if (/^[a-f0-9]{32}:\d+:\d+:\d+:[a-f0-9]{16}$/u.test(token)) {
|
|
530
|
+
await completeSelected(token, ctx);
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
if (token) { ctx.ui.notify("Usage: /pi-note", "warning"); return; }
|
|
548
534
|
if (pending || !ctx.isIdle()) {
|
|
549
535
|
ctx.ui.notify("Wait until Pi is idle and the pinote operation has finished.", "warning");
|
|
550
536
|
return;
|
|
@@ -556,19 +542,46 @@ export default function (pi: ExtensionAPI) {
|
|
|
556
542
|
const currentSession = () => alive && generation === epoch;
|
|
557
543
|
const canAct = () => currentSession() && ctx.isIdle() && branch === branchEpoch;
|
|
558
544
|
try {
|
|
559
|
-
|
|
545
|
+
await checkCLI(ctx);
|
|
560
546
|
if (!canAct()) return;
|
|
561
|
-
|
|
547
|
+
const current = cliState === "ready" ? await selected(ctx) : null;
|
|
548
|
+
if (!canAct()) return;
|
|
549
|
+
let settings = cliState !== "ready";
|
|
562
550
|
while (canAct()) {
|
|
551
|
+
if (!settings && cliState !== "ready") {
|
|
552
|
+
ctx.ui.notify(setupHint, "warning");
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
563
555
|
if (settings) {
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
556
|
+
let config: PinoteSettings = { footer: { ...defaultFooterConfig, fields: [] }, handoffPrompt: defaultHandoffPrompt };
|
|
557
|
+
let expectedRaw: string | null = null;
|
|
558
|
+
let configurationError: string | undefined;
|
|
559
|
+
try {
|
|
560
|
+
const document = readFooterDocument();
|
|
561
|
+
const root = document.raw === null ? {} : JSON.parse(document.raw);
|
|
562
|
+
config = {
|
|
563
|
+
footer: { ...document.config, fields: effectiveFooterFields(document.config, current?.agent_notes) },
|
|
564
|
+
handoffPrompt: "handoffPrompt" in root ? parseHandoffPrompt(root.handoffPrompt) : defaultHandoffPrompt,
|
|
565
|
+
};
|
|
566
|
+
expectedRaw = document.raw;
|
|
567
|
+
} catch (error) {
|
|
568
|
+
// Broken preferences must not strand task actions or CLI recovery.
|
|
569
|
+
// Offer actions only; never save defaults over invalid user data.
|
|
570
|
+
configurationError = clean(error instanceof Error ? error.message : String(error));
|
|
571
|
+
}
|
|
572
|
+
const options: SettingsOption[] = [
|
|
573
|
+
...(current ? [
|
|
574
|
+
{ label: "Preview task", result: "preview" as const },
|
|
575
|
+
{ label: "Complete task (new session)", result: "done" as const },
|
|
576
|
+
] : []),
|
|
577
|
+
...(suggestion ? [
|
|
578
|
+
{ label: "Accept suggestion", result: "yes" as const },
|
|
579
|
+
{ label: "Dismiss suggestion", result: "no" as const },
|
|
580
|
+
] : []),
|
|
581
|
+
...(cliState === "setup" ? [{ label: "Install CLI", result: "setup" as const }]
|
|
582
|
+
: cliState === "upgrade" ? [{ label: "Upgrade CLI", result: "upgrade" as const }] : []),
|
|
583
|
+
];
|
|
584
|
+
const edited = await ctx.ui.custom<SettingsResult>((tui, theme, kb, done) =>
|
|
572
585
|
new FooterSettings(config, Object.keys(current?.agent_notes ?? {}).filter((name) => name !== "Bar"),
|
|
573
586
|
theme, (data, action) => kb.matches(data, action as Keybinding), done, () => tui.requestRender(),
|
|
574
587
|
() => new Editor(tui, {
|
|
@@ -578,14 +591,22 @@ export default function (pi: ExtensionAPI) {
|
|
|
578
591
|
noMatch: (line) => theme.fg("warning", line) },
|
|
579
592
|
}), (updated) => {
|
|
580
593
|
if (!canAct()) throw new Error("This session changed. Reopen settings before editing.");
|
|
594
|
+
if (configurationError) throw new Error(configurationError);
|
|
581
595
|
const saved = saveFooterConfig(updated.footer, expectedRaw, updated.handoffPrompt);
|
|
582
596
|
expectedRaw = saved.raw;
|
|
583
597
|
footerConfig = saved.config;
|
|
584
598
|
void refresh(ctx);
|
|
585
|
-
}));
|
|
599
|
+
}, options, configurationError));
|
|
586
600
|
if (!canAct() || edited === undefined) return;
|
|
587
|
-
settings = false;
|
|
588
|
-
|
|
601
|
+
if (edited === "tasks") { settings = false; continue; }
|
|
602
|
+
if (!options.some((option) => option.result === edited)) return;
|
|
603
|
+
if (pending === operation) pending = undefined;
|
|
604
|
+
if (edited === "preview") {
|
|
605
|
+
if (!await preview(ctx)) ctx.ui.notify("The task is no longer available for preview.", "warning");
|
|
606
|
+
} else if (edited === "done") await completeSelected("", ctx, current ?? undefined);
|
|
607
|
+
else if (edited === "yes" || edited === "no") await respondToSuggestion(edited, ctx);
|
|
608
|
+
else await installCLI(edited === "upgrade", ctx);
|
|
609
|
+
return;
|
|
589
610
|
}
|
|
590
611
|
const action = current
|
|
591
612
|
? await ctx.ui.custom<string | undefined>((tui, theme, kb, done) =>
|
|
@@ -733,20 +754,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
733
754
|
if (alive && generation === epoch) await refresh(ctx);
|
|
734
755
|
}
|
|
735
756
|
};
|
|
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
757
|
pi.registerTool({
|
|
751
758
|
name: "pinote_propose",
|
|
752
759
|
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;
|