pi-note 0.32.0 → 0.33.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/LOCAL.md +12 -12
- package/README.md +20 -2
- package/dist-mcp/mcp/core.js +1 -1
- package/index.ts +12 -12
- package/mcp/core.ts +1 -1
- package/package.json +1 -1
- package/remote/exit.ts +24 -3
- package/remote/ui.ts +14 -3
- package/remote.ts +11 -5
- package/setup.ts +4 -4
package/LOCAL.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[npm](https://www.npmjs.com/package/pi-note) · [Pi package directory](https://pi.dev/packages/pi-note)
|
|
4
4
|
|
|
5
5
|
Persistent [pinote](https://github.com/kvidzibo/pinote) tasks and Markdown handoffs
|
|
6
|
-
in Pi: selected-task footer status, `/
|
|
6
|
+
in Pi: selected-task footer status, `/pinote`, and agent current-task read/update,
|
|
7
7
|
add, and tag-listing tools.
|
|
8
8
|
The extension and Python app share a repository but install separately.
|
|
9
9
|
|
|
@@ -30,8 +30,8 @@ Install [uv](https://docs.astral.sh/uv/getting-started/installation/) first and
|
|
|
30
30
|
restart Pi with `uv` on PATH. At startup the extension checks `note --version`
|
|
31
31
|
against its bundled CLI version (currently 0.4.0), without network requests:
|
|
32
32
|
|
|
33
|
-
- Missing/unrecognized CLI: `/
|
|
34
|
-
- Older CLI: `/
|
|
33
|
+
- Missing/unrecognized CLI: `/pinote` opens Settings directly, where **Install CLI** is available.
|
|
34
|
+
- Older CLI: `/pinote` opens Settings directly, where **Upgrade CLI** is available.
|
|
35
35
|
- Equal/newer CLI: neither installation action is shown; no upgrade notification.
|
|
36
36
|
|
|
37
37
|
Install and upgrade recheck the CLI version and ask before installing into uv's
|
|
@@ -54,7 +54,7 @@ Check `note --version`, then restart Pi or `/reload`. Network/build failures and
|
|
|
54
54
|
conflicting executables are reported without forcibly overwriting another
|
|
55
55
|
installer's commands. Fix the reported cause and retry; installs time out after
|
|
56
56
|
three minutes. Installation actions are unavailable while installation runs.
|
|
57
|
-
Setup hints direct users to `/
|
|
57
|
+
Setup hints direct users to `/pinote` → Settings.
|
|
58
58
|
Missing uv is reported with its installation link, not installed
|
|
59
59
|
automatically. The [GTK desktop app](../README.md#optional-gtk-checklist) is optional
|
|
60
60
|
and still requires separate system dependencies.
|
|
@@ -65,16 +65,16 @@ standalone `settings/pi/extensions/pinote.ts` entry if installed; load only one
|
|
|
65
65
|
|
|
66
66
|
## Use
|
|
67
67
|
|
|
68
|
-
When the CLI is ready and no task is selected, `/
|
|
68
|
+
When the CLI is ready and no task is selected, `/pinote` opens the task picker.
|
|
69
69
|
It marks the chosen task in progress and adds this default prompt to the editor:
|
|
70
70
|
“Read the current Pinote task. Summarize your understanding, but don’t start work yet.”
|
|
71
|
-
If the CLI is missing or older than the bundled version, `/
|
|
71
|
+
If the CLI is missing or older than the bundled version, `/pinote` opens Settings
|
|
72
72
|
directly so the relevant CLI installation action remains accessible. **Tab** opens
|
|
73
73
|
Settings from the picker or task view; **Tab** from Settings returns to tasks.
|
|
74
74
|
The agent fetches current task text and saved handoff fields when you submit;
|
|
75
75
|
they are not copied into the draft. The default prompt asks for a summary, not implementation.
|
|
76
76
|
Existing drafts are preserved; nothing is submitted automatically.
|
|
77
|
-
With a selected task, `/
|
|
77
|
+
With a selected task, `/pinote` offers **Continue**, **Done**, **Switch task**, and
|
|
78
78
|
**Settings**. Settings provides **Preview task**, **Complete task (new session)**,
|
|
79
79
|
**Accept suggestion**, and **Dismiss suggestion** when applicable, plus
|
|
80
80
|
**Install CLI** or **Upgrade CLI** only when needed. Settings also lists fields found
|
|
@@ -170,7 +170,7 @@ and selected tasks. Run `/reload` after changing this setting. Invalid policy
|
|
|
170
170
|
values or unreadable/malformed configuration suppress offers; interactive startup
|
|
171
171
|
warns until you fix the file and reload. No project-local configuration is read.
|
|
172
172
|
|
|
173
|
-
In `/
|
|
173
|
+
In `/pinote → Settings`, select **Task prompt** to edit the text inserted after
|
|
174
174
|
selection or **Continue**. **Shift+Enter** or **Ctrl+J** adds a newline; **Enter** saves it
|
|
175
175
|
globally, **Ctrl+C** clears the edit, and **Esc** cancels that unfinished edit.
|
|
176
176
|
|
|
@@ -219,7 +219,7 @@ configuration only; project-local files are not read.
|
|
|
219
219
|
|
|
220
220
|
Version 0.7.0 replaces `pinote_get`/`pinote_update` with these current-task tools
|
|
221
221
|
and removes `pinote_tag`, without aliases. Agents cannot read/update tasks by ID
|
|
222
|
-
or retag existing tasks; use `/
|
|
222
|
+
or retag existing tasks; use `/pinote` to select an existing task.
|
|
223
223
|
|
|
224
224
|
When allowed by `taskOfferPolicy`, the agent uses `pinote_propose` instead of
|
|
225
225
|
asking in chat. The pin footer shows a pending suggestion, distinct from a selected task:
|
|
@@ -242,7 +242,7 @@ The configured `footer.titleWidth` bounds the entire row (default 60 columns). C
|
|
|
242
242
|
|
|
243
243
|
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.
|
|
244
244
|
|
|
245
|
-
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 `/
|
|
245
|
+
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 `/pinote` 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.
|
|
246
246
|
|
|
247
247
|
If another task fits better, the agent can replace an unaccepted suggestion by calling `pinote_propose` again, without waiting for dismissal. Replacement invalidates the old ✚/✖ links; only the latest suggestion can be accepted or dismissed. Pending suggestions survive `/reload` and session resume, including their full text and tag; new links replace stale capabilities. Accepted or dismissed suggestions never reappear on reload, but dismissal does not block new proposals—even in sessions saved by older versions. New sessions and forks start without a suggestion; selecting a task or navigating the tree clears it. Suggestion state is local, display-only session metadata, not model context. Pi saves the session after the first submitted message; quitting before that can still lose unsaved session state. Repeated acceptance cannot duplicate a note. A busy Pinote operation blocks acceptance. Noninteractive agents still ask in chat before using `pinote_add` with `select: true`. An existing selection is not replaced unless the user asks to switch. Reuse a saved
|
|
248
248
|
tag name when it fits.
|
|
@@ -310,7 +310,7 @@ Names are case-sensitive, trimmed and NFC-normalized, at most 64 characters;
|
|
|
310
310
|
accepted. Missing/blank values do not display and do not consume the field limit.
|
|
311
311
|
`[]` or `maxFields: 0` hides configured fields, including PR.
|
|
312
312
|
|
|
313
|
-
In `/
|
|
313
|
+
In `/pinote → Settings`, **Add field** adds a global display definition, not a
|
|
314
314
|
task value. Select any field to edit **Link**, **Label**, **Format**, and **Width**.
|
|
315
315
|
Label starts as the field name; clear it for no prefix. Link off displays plain
|
|
316
316
|
text without a clickable link. Toggles, field additions/removals, and edits confirmed
|
|
@@ -369,7 +369,7 @@ Pinote no longer polls GitHub, runs `gh`, watches completed tasks, or prompts on
|
|
|
369
369
|
PR merges. No GitHub authentication is required. `PINOTE_PR_POLL_SECONDS` is no
|
|
370
370
|
longer used. Other separately installed Pi PR/Git extensions are unaffected.
|
|
371
371
|
|
|
372
|
-
Tools work without a TUI. `/
|
|
372
|
+
Tools work without a TUI. `/pinote` works in a TUI while the agent is running:
|
|
373
373
|
you can select/switch tasks, use Continue, preview tasks, respond to suggestions,
|
|
374
374
|
and edit settings without interrupting the turn or submitting input. Completion
|
|
375
375
|
and CLI installation/upgrades still require an idle session. Another pending
|
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@ state routing, tools and UI.
|
|
|
48
48
|
|
|
49
49
|
## Use
|
|
50
50
|
|
|
51
|
-
Open **`/pinote
|
|
51
|
+
Open **`/pinote`**. Select or create a task, preview it,
|
|
52
52
|
clear the selection, complete it, respond to suggestions, or edit shared settings.
|
|
53
53
|
Selecting an existing task starts it immediately and returns focus to the prompt,
|
|
54
54
|
with no extra confirmation. Creation and settings changes still require confirmation.
|
|
@@ -62,7 +62,8 @@ title previews it and double-clicking completes it. A suggested title requires a
|
|
|
62
62
|
double-click to accept. Regular mode shows status; `/pinote` provides all actions.
|
|
63
63
|
No Kitty action handler is needed for this extension.
|
|
64
64
|
|
|
65
|
-
Completion keeps the conversation and editor
|
|
65
|
+
Completion keeps the conversation and editor, except for dedicated **Start in Kitty…**
|
|
66
|
+
sessions (see below). With an empty editor, **Ctrl+D**
|
|
66
67
|
(or your configured exit key) offers Cancel, Exit without completing, or Complete
|
|
67
68
|
task and exit. **`/new`** offers Cancel, Start new session without completing, or
|
|
68
69
|
Complete task and start new session. With no selected task, `/new` proceeds normally.
|
|
@@ -77,6 +78,8 @@ cannot guarantee selection persistence.
|
|
|
77
78
|
|
|
78
79
|
The GTK app's **Start in Kitty…** action (note 0.13.0+) chooses a working directory
|
|
79
80
|
and opens a named tab with this task selected, without submitting a prompt.
|
|
81
|
+
In note 0.16.0+, it uses the tag's default folder when available, otherwise keeps
|
|
82
|
+
the directory chooser. Set or clear defaults in **Manage tags…**.
|
|
80
83
|
With note 0.15.0+ and pi-note 0.32.0+, successful selection prefills an empty Pi editor
|
|
81
84
|
with `is task clear?`. Configure `gui.kitty_start_prompt` in the
|
|
82
85
|
[GUI config](../docs/gui.md#gui-configuration), or set it to `""` to disable; existing
|
|
@@ -87,6 +90,17 @@ client-local and is never sent to the service or persisted as launch state.
|
|
|
87
90
|
Only a fresh session can consume it. The service must share the GUI's database;
|
|
88
91
|
missing/stale revisions fail closed. Resume, reload and `/new` never replay the
|
|
89
92
|
handoff or replace another selection. Pi keeps its normal endpoint/token settings.
|
|
93
|
+
With pi-note 0.33.1+, completing the launched task from the menu, footer or
|
|
94
|
+
**Complete task and exit** runs `git pull --ff-only --no-rebase --no-autostash` in
|
|
95
|
+
Pi's working directory before exiting, so Kitty closes its dedicated tab (or the
|
|
96
|
+
standalone window's last tab). It never merges, rebases or stashes local changes.
|
|
97
|
+
If Git fails, times out (30 seconds), or the session/draft changes, Pi stays open;
|
|
98
|
+
the task remains completed and is not retried. Check Git manually, then `/quit`.
|
|
99
|
+
Non-Git directories or branches without an upstream also stay open after the
|
|
100
|
+
failed pull. `/quit` and Exit without completing do not pull.
|
|
101
|
+
This ownership survives `/reload`, but not `/new`, resume, fork, tree navigation
|
|
102
|
+
or process restart. Completing a different task or an externally completed task
|
|
103
|
+
does not pull or close Pi; failed/uncertain completion stays open.
|
|
90
104
|
|
|
91
105
|
### Agent tools and context
|
|
92
106
|
|
|
@@ -121,6 +135,10 @@ separate from global display settings.
|
|
|
121
135
|
|
|
122
136
|
## Migration
|
|
123
137
|
|
|
138
|
+
Since pi-note 0.33.0, `/pinote` is the only slash command in both modes.
|
|
139
|
+
The `/pi-note` alias is removed; update saved shortcuts to `/pinote`.
|
|
140
|
+
The npm package name and `pi-note.json` configuration filename are unchanged.
|
|
141
|
+
|
|
124
142
|
1. Back up the service machine's Pinote data; keep `notes.db` local to that machine.
|
|
125
143
|
2. Start the standalone service against that same database and settings directory.
|
|
126
144
|
3. Remove Pinote's service entry and its unused package entry from the gateway
|
package/dist-mcp/mcp/core.js
CHANGED
|
@@ -183,7 +183,7 @@ function view(data, scoped = false, secondary = false, features = {}) {
|
|
|
183
183
|
// Footer actions remain valid on every screen, including Settings.
|
|
184
184
|
if (task) {
|
|
185
185
|
action('preview', 'Preview full task', { description: 'Display only; never sent to the model' });
|
|
186
|
-
action('done', 'Complete task', { icon: icons.check, ...(features.footerNativeActions ? { description: 'Complete
|
|
186
|
+
action('done', 'Complete task', { icon: icons.check, ...(features.footerNativeActions ? { description: 'Complete the selected task' } : {}) });
|
|
187
187
|
}
|
|
188
188
|
else if (state.proposal) {
|
|
189
189
|
const acceptance = 'task_id' in state.proposal ? `Start and select existing task #${state.proposal.task_id}; no new task is created.` : 'Create, start, and select this suggestion.';
|
package/index.ts
CHANGED
|
@@ -107,7 +107,7 @@ function task(raw: string): Task | null {
|
|
|
107
107
|
}
|
|
108
108
|
function requiredTask(raw: string, id?: number): Task {
|
|
109
109
|
const value = task(raw);
|
|
110
|
-
if (!value) throw new Error("No selected pinote task. Select one with /
|
|
110
|
+
if (!value) throw new Error("No selected pinote task. Select one with /pinote first.");
|
|
111
111
|
if (id !== undefined && value.id !== id) throw new Error(compatible);
|
|
112
112
|
return value;
|
|
113
113
|
}
|
|
@@ -187,7 +187,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
187
187
|
if (!alive || generation !== epoch || setupAbort) return;
|
|
188
188
|
cliState = action;
|
|
189
189
|
if (suggest && action === "upgrade") {
|
|
190
|
-
ctx.ui.notify(`Pinote CLI ${bundledCLIVersion} is bundled with this extension. Open /
|
|
190
|
+
ctx.ui.notify(`Pinote CLI ${bundledCLIVersion} is bundled with this extension. Open /pinote → Settings → Upgrade CLI to update your older CLI.`, "info");
|
|
191
191
|
}
|
|
192
192
|
};
|
|
193
193
|
|
|
@@ -314,7 +314,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
314
314
|
if (alive && generation === epoch && serial === refreshSerial) {
|
|
315
315
|
previewBridge?.invalidate();
|
|
316
316
|
clearTaskFooter(ctx);
|
|
317
|
-
ctx.ui.setStatus("pinote", suggestionStatus(ctx) ?? `${ctx.ui.theme.bold(noteIcon)} Pinote unavailable · /
|
|
317
|
+
ctx.ui.setStatus("pinote", suggestionStatus(ctx) ?? `${ctx.ui.theme.bold(noteIcon)} Pinote unavailable · /pinote → Settings`);
|
|
318
318
|
}
|
|
319
319
|
}
|
|
320
320
|
};
|
|
@@ -357,10 +357,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
357
357
|
};
|
|
358
358
|
const timer = setTimeout(() => {
|
|
359
359
|
finish(false);
|
|
360
|
-
if (alive && activeContext === ctx) ctx.ui.notify("Pinote completion could not start. Open /
|
|
360
|
+
if (alive && activeContext === ctx) ctx.ui.notify("Pinote completion could not start. Open /pinote → Settings → Complete task.", "warning");
|
|
361
361
|
}, 4000);
|
|
362
362
|
completionDispatch = { token, selection, task: displayed, started: () => clearTimeout(timer), finish };
|
|
363
|
-
try { pi.sendUserMessage(`/
|
|
363
|
+
try { pi.sendUserMessage(`/pinote ${token}`, { expandPromptTemplates: true }); }
|
|
364
364
|
catch { finish(false); }
|
|
365
365
|
});
|
|
366
366
|
};
|
|
@@ -454,7 +454,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
454
454
|
} catch {
|
|
455
455
|
await bridge.stop();
|
|
456
456
|
if (previewBridge === bridge) previewBridge = undefined;
|
|
457
|
-
ctx.ui.notify("Pinote task link unavailable; open /
|
|
457
|
+
ctx.ui.notify("Pinote task link unavailable; open /pinote → Settings → Preview task.", "warning");
|
|
458
458
|
}
|
|
459
459
|
}
|
|
460
460
|
if (!alive || generation !== epoch) return;
|
|
@@ -553,7 +553,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
553
553
|
}
|
|
554
554
|
};
|
|
555
555
|
|
|
556
|
-
pi.registerCommand("
|
|
556
|
+
pi.registerCommand("pinote", {
|
|
557
557
|
description: "Continue, complete, switch tasks, or edit global settings (Tab)",
|
|
558
558
|
handler: async (args, ctx) => {
|
|
559
559
|
if (!ctx.hasUI || ctx.mode !== "tui") return;
|
|
@@ -564,7 +564,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
564
564
|
await completeSelected(token, ctx);
|
|
565
565
|
return;
|
|
566
566
|
}
|
|
567
|
-
if (token) { ctx.ui.notify("Usage: /
|
|
567
|
+
if (token) { ctx.ui.notify("Usage: /pinote", "warning"); return; }
|
|
568
568
|
if (pending || menuPending) {
|
|
569
569
|
ctx.ui.notify("Wait until the pinote operation has finished.", "warning");
|
|
570
570
|
return;
|
|
@@ -724,7 +724,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
724
724
|
const canRun = () => alive && generation === epoch && branch === branchEpoch && selectedId === id;
|
|
725
725
|
const current = await selected(ctx, signal);
|
|
726
726
|
if (!canRun()) throw new Error("Pinote operation cancelled: the session or selected task changed.");
|
|
727
|
-
if (!current) throw new Error("No selected pinote task. Select one with /
|
|
727
|
+
if (!current) throw new Error("No selected pinote task. Select one with /pinote first.");
|
|
728
728
|
const argv = ["agent", "update", String(current.id), "--expected-updated-at", params.expected_updated_at,
|
|
729
729
|
"--set-json", JSON.stringify(params.set ?? {})];
|
|
730
730
|
for (const label of params.remove ?? []) argv.push(`--remove=${label}`);
|
|
@@ -789,7 +789,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
789
789
|
return true;
|
|
790
790
|
} catch (error) {
|
|
791
791
|
if (alive && generation === epoch) ctx.ui.notify(
|
|
792
|
-
`Pinote suggestion: ${clean(String(error))}${created ? ` Note #${created.id} was created; use /
|
|
792
|
+
`Pinote suggestion: ${clean(String(error))}${created ? ` Note #${created.id} was created; use /pinote to select it. Do not add it again.` : " A write may have committed; check /pinote before retrying."}`, "error");
|
|
793
793
|
return false;
|
|
794
794
|
} finally {
|
|
795
795
|
if (pending === operation) pending = undefined;
|
|
@@ -854,10 +854,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
854
854
|
clearSuggestion(ctx);
|
|
855
855
|
try {
|
|
856
856
|
const created = requiredTask(await run(argv, signal, canAct, "0.4.0"));
|
|
857
|
-
if (!canAct()) throw new Error("Pinote operation cancelled: the session changed. The note was created; check /
|
|
857
|
+
if (!canAct()) throw new Error("Pinote operation cancelled: the session changed. The note was created; check /pinote before retrying.");
|
|
858
858
|
const chosen = await startTask(created.id, canAct, signal);
|
|
859
859
|
if (!chosen || !alive || generation !== epoch || branch !== branchEpoch || selectedId !== chosen.id) {
|
|
860
|
-
throw new Error("Pinote operation cancelled: the session changed. The note may have started; check /
|
|
860
|
+
throw new Error("Pinote operation cancelled: the session changed. The note may have started; check /pinote before retrying.");
|
|
861
861
|
}
|
|
862
862
|
return toolResult(chosen);
|
|
863
863
|
} finally {
|
package/mcp/core.ts
CHANGED
|
@@ -162,7 +162,7 @@ function view(data: Snapshot, scoped = false, secondary = false, features: { foo
|
|
|
162
162
|
// Footer actions remain valid on every screen, including Settings.
|
|
163
163
|
if (task) {
|
|
164
164
|
action('preview', 'Preview full task', { description: 'Display only; never sent to the model' });
|
|
165
|
-
action('done', 'Complete task', { icon: icons.check, ...(features.footerNativeActions ? { description: 'Complete
|
|
165
|
+
action('done', 'Complete task', { icon: icons.check, ...(features.footerNativeActions ? { description: 'Complete the selected task' } : {}) });
|
|
166
166
|
} else if (state.proposal) {
|
|
167
167
|
const acceptance = 'task_id' in state.proposal ? `Start and select existing task #${state.proposal.task_id}; no new task is created.` : 'Create, start, and select this suggestion.';
|
|
168
168
|
action('proposal:accept', 'Accept suggestion', { icon: icons.add, description: `${acceptance} ${features.footerNativeActions ? 'Double-click the suggested footer title to accept; use Dismiss suggestion to dismiss.' : 'Left-click to accept; right-click to dismiss.'}\n${suggestion!.text.slice(0, 4000)}` });
|
package/package.json
CHANGED
package/remote/exit.ts
CHANGED
|
@@ -1,10 +1,26 @@
|
|
|
1
|
-
import { CustomEditor, type ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
1
|
+
import { CustomEditor, type ExtensionAPI, type ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
2
2
|
import { truncateToWidth } from '@earendil-works/pi-tui';
|
|
3
3
|
import { clean } from './protocol.ts';
|
|
4
4
|
import type { RemoteSession } from './session.ts';
|
|
5
5
|
|
|
6
|
+
/** Completion is already committed: pull failures must never retry the task action. */
|
|
7
|
+
export async function pullBeforeExit(pi: ExtensionAPI, ctx: ExtensionContext, signal: AbortSignal, current: () => boolean): Promise<boolean> {
|
|
8
|
+
const sessionId = ctx.sessionManager.getSessionId(), branch = ctx.sessionManager.getLeafId();
|
|
9
|
+
const draft = ctx.ui.getEditorText();
|
|
10
|
+
const unchanged = () => current() && !signal.aborted && ctx.isIdle() &&
|
|
11
|
+
ctx.sessionManager.getSessionId() === sessionId && ctx.sessionManager.getLeafId() === branch && ctx.ui.getEditorText() === draft;
|
|
12
|
+
if (!unchanged()) return false;
|
|
13
|
+
ctx.ui.notify('Task completed. Running git pull --ff-only before closing Pi…', 'info');
|
|
14
|
+
try {
|
|
15
|
+
const result = await pi.exec('git', ['pull', '--ff-only', '--no-rebase', '--no-autostash'], { cwd: ctx.cwd, signal, timeout: 30_000 });
|
|
16
|
+
if (result.code === 0 && !result.killed && unchanged()) return true;
|
|
17
|
+
} catch { /* Completion succeeded; only the local pull failed. */ }
|
|
18
|
+
if (current()) ctx.ui.notify('Task completed, but Pi stayed open: git pull failed or the session changed. Check Git manually, then /quit. Completion will not be retried.', 'warning');
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
6
22
|
/** Default remote companion. /quit and nonempty-editor exit input remain Pi-owned. */
|
|
7
|
-
export function registerRemoteExit(pi: ExtensionAPI, getSession: () => RemoteSession | undefined): void {
|
|
23
|
+
export function registerRemoteExit(pi: ExtensionAPI, getSession: () => RemoteSession | undefined, ownsLaunch = (_taskId: number) => false): void {
|
|
8
24
|
let invalidate: (() => void) | undefined;
|
|
9
25
|
let stop: (() => void) | undefined;
|
|
10
26
|
let confirm: ((reason: 'exit' | 'new') => Promise<boolean>) | undefined;
|
|
@@ -54,7 +70,12 @@ export function registerRemoteExit(pi: ExtensionAPI, getSession: () => RemoteSes
|
|
|
54
70
|
// Approval belongs to this exact displayed view, never to a refreshed revision.
|
|
55
71
|
await session.action(view, done, undefined, () => current() && ctx.isIdle() && ctx.sessionManager.getLeafId() === branch);
|
|
56
72
|
if (!current()) return false;
|
|
57
|
-
if (session.view && session.view.state.selectedId == null)
|
|
73
|
+
if (session.view && session.view.state.selectedId == null) {
|
|
74
|
+
if (reason === 'exit' && typeof view.state.selectedId === 'number' && ownsLaunch(view.state.selectedId)) {
|
|
75
|
+
return await pullBeforeExit(pi, ctx, operation.signal, current);
|
|
76
|
+
}
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
58
79
|
ctx.ui.notify('Pinote: Pi stayed in this session because the selection was not cleared. Refresh /pinote before retrying.', 'warning');
|
|
59
80
|
} catch {
|
|
60
81
|
if (alive && generation === epoch) ctx.ui.notify('Pinote: Pi stayed in this session. Completion was not confirmed and may have committed; refresh /pinote before retrying. /quit exits without completing.', 'warning');
|
package/remote/ui.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { NativeFooter } from './footer.ts';
|
|
|
4
4
|
import { clean, cleanUnbounded, editorText, footerText, literal, type NativeControl, type NativeView } from './protocol.ts';
|
|
5
5
|
import { RemoteFailure } from './client.ts';
|
|
6
6
|
import { RemoteSession } from './session.ts';
|
|
7
|
+
import { pullBeforeExit } from './exit.ts';
|
|
7
8
|
import { inlineHeight, remoteMenu, type MenuState } from './menu.ts';
|
|
8
9
|
|
|
9
10
|
const line = (text: string) => clean(text).replace(/\n/g, ' ');
|
|
@@ -63,10 +64,12 @@ export class RemoteUI {
|
|
|
63
64
|
private repaint = () => {};
|
|
64
65
|
private menuChanged?: () => void;
|
|
65
66
|
private opening = false;
|
|
67
|
+
private closing = false;
|
|
66
68
|
private pi: ExtensionAPI;
|
|
67
69
|
private session: RemoteSession;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
+
private exitOnComplete: (taskId: number) => boolean;
|
|
71
|
+
constructor(pi: ExtensionAPI, session: RemoteSession, exitOnComplete = (_taskId: number) => false) {
|
|
72
|
+
this.pi = pi; this.session = session; this.exitOnComplete = exitOnComplete;
|
|
70
73
|
session.onChange = () => { this.render(); this.menuChanged?.(); };
|
|
71
74
|
}
|
|
72
75
|
attach(ctx: ExtensionContext): void {
|
|
@@ -109,6 +112,14 @@ export class RemoteUI {
|
|
|
109
112
|
if (!current()) return false;
|
|
110
113
|
const result = await this.session.action(view, control, value, () => control.id !== 'done' || ctx.isIdle());
|
|
111
114
|
if (!current()) return false;
|
|
115
|
+
if (control.id === 'done' && ctx.isIdle() && typeof view.state.selectedId === 'number' &&
|
|
116
|
+
this.session.view && this.session.view.state.selectedId == null && this.exitOnComplete(view.state.selectedId)) {
|
|
117
|
+
if (await pullBeforeExit(this.pi, ctx, signal, current)) {
|
|
118
|
+
this.closing = true;
|
|
119
|
+
ctx.shutdown();
|
|
120
|
+
}
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
112
123
|
if (result.preview) this.pi.appendEntry('pinote-remote-preview', { title: line(view.title), text: clean(result.preview) });
|
|
113
124
|
if (result.draft && await review(ctx, 'Insert server draft?', result.draft, signal) && current()) {
|
|
114
125
|
if (ctx.ui.getEditorText().length) ctx.ui.notify('Draft not inserted: clear the editor first.', 'warning');
|
|
@@ -131,7 +142,7 @@ export class RemoteUI {
|
|
|
131
142
|
const menus = new Map<string, MenuState>();
|
|
132
143
|
try {
|
|
133
144
|
await this.session.refresh(true);
|
|
134
|
-
while (current() && !signal.aborted) {
|
|
145
|
+
while (current() && !signal.aborted && !this.closing) {
|
|
135
146
|
const view = this.session.view;
|
|
136
147
|
if (!view) return;
|
|
137
148
|
const screen = String(view.state.screen ?? 'tasks');
|
package/remote.ts
CHANGED
|
@@ -11,10 +11,13 @@ import { editorText } from './remote/protocol.ts';
|
|
|
11
11
|
// Reload/resume/fork restore branch entries, never this map or a folder-global store.
|
|
12
12
|
const key = Symbol.for('pinote.remote.new-transfer.v1');
|
|
13
13
|
const transfers: Map<string, SavedState> = ((globalThis as any)[key] ??= new Map());
|
|
14
|
+
// Process-local launch ownership survives /reload, never resume in another process.
|
|
15
|
+
const launches: Map<string, number> = ((globalThis as any)[Symbol.for('pinote.remote.kitty-launch.v1')] ??= new Map());
|
|
14
16
|
|
|
15
17
|
export default function pinoteRemote(pi: ExtensionAPI): void {
|
|
16
18
|
let session: RemoteSession | undefined, ui: RemoteUI | undefined;
|
|
17
|
-
let file: string | undefined;
|
|
19
|
+
let file: string | undefined, launchKey = '';
|
|
20
|
+
const ownsLaunch = (taskId: number) => launches.get(launchKey) === taskId;
|
|
18
21
|
let launchRequest = process.env.PINOTE_LAUNCH_TASK;
|
|
19
22
|
delete process.env.PINOTE_LAUNCH_TASK; // Never inherit consent into another Pi process or /reload.
|
|
20
23
|
const getSession = () => { if (!session) throw new RemoteFailure(); return session; };
|
|
@@ -35,15 +38,16 @@ export default function pinoteRemote(pi: ExtensionAPI): void {
|
|
|
35
38
|
await ui.open(ctx);
|
|
36
39
|
};
|
|
37
40
|
pi.registerCommand('pinote', { description: 'Open remote Pinote tasks and settings', handler: open });
|
|
38
|
-
pi.registerCommand('pi-note', { description: 'Open remote Pinote (compatibility alias)', handler: open });
|
|
39
41
|
const start = async (ctx: ExtensionContext, reason?: string, previous?: string) => {
|
|
40
42
|
const request = launchRequest; launchRequest = undefined; // Never replay an uncertain start.
|
|
43
|
+
if (reason !== 'reload') launches.clear();
|
|
41
44
|
ui?.detach(); await session?.stop();
|
|
42
45
|
ui = undefined; session = undefined;
|
|
43
46
|
try {
|
|
44
47
|
const config = remoteConfig();
|
|
45
48
|
session = new RemoteSession(config, (saved) => pi.appendEntry(ENTRY_TYPE, saved));
|
|
46
|
-
|
|
49
|
+
launchKey = `${config.scope}:${ctx.sessionManager.getSessionId()}`;
|
|
50
|
+
ui = new RemoteUI(pi, session, ownsLaunch);
|
|
47
51
|
ui.attach(ctx);
|
|
48
52
|
file = ctx.sessionManager.getSessionFile();
|
|
49
53
|
const branch = ctx.sessionManager.getBranch();
|
|
@@ -74,6 +78,7 @@ export default function pinoteRemote(pi: ExtensionAPI): void {
|
|
|
74
78
|
const control = view.controls.find((item) => item.id === `select:${launch.task_id}`);
|
|
75
79
|
if (!control) throw new Error('Launch task changed or belongs to another service');
|
|
76
80
|
await session.action(view, control); // The directory chooser's Start click granted consent.
|
|
81
|
+
if (session.view?.state.selectedId === launch.task_id) launches.set(launchKey, launch.task_id);
|
|
77
82
|
if (prompt?.trim() && ctx.mode === 'tui' && ctx.isIdle()) {
|
|
78
83
|
if (ctx.ui.getEditorText().length) ctx.ui.notify('Launch draft not inserted: the editor already contains a draft.', 'warning');
|
|
79
84
|
else ctx.ui.setEditorText(prompt);
|
|
@@ -87,7 +92,8 @@ export default function pinoteRemote(pi: ExtensionAPI): void {
|
|
|
87
92
|
};
|
|
88
93
|
pi.on('session_start', (event, ctx) => start(ctx, event.reason, event.previousSessionFile));
|
|
89
94
|
pi.on('session_tree', (_event, ctx) => start(ctx));
|
|
90
|
-
pi.on('session_shutdown', async () => {
|
|
95
|
+
pi.on('session_shutdown', async (event) => {
|
|
96
|
+
if (event.reason !== 'reload') launches.clear();
|
|
91
97
|
const saved = session?.snapshot();
|
|
92
98
|
if (file && saved?.retainOnNew) {
|
|
93
99
|
if (transfers.size >= 32) transfers.delete(transfers.keys().next().value!);
|
|
@@ -106,5 +112,5 @@ export default function pinoteRemote(pi: ExtensionAPI): void {
|
|
|
106
112
|
(char) => `\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}`),
|
|
107
113
|
].join('\n\n');
|
|
108
114
|
});
|
|
109
|
-
registerRemoteExit(pi, () => session);
|
|
115
|
+
registerRemoteExit(pi, () => session, ownsLaunch);
|
|
110
116
|
}
|
package/setup.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { stripVTControlCharacters } from "node:util";
|
|
|
4
4
|
// Immutable source: the Python app is not yet published to PyPI.
|
|
5
5
|
export const cliSource = "https://github.com/kvidzibo/pinote/archive/1597baf60bff8bc96893be40781f152a001623d1.tar.gz";
|
|
6
6
|
export const bundledCLIVersion = "0.4.0";
|
|
7
|
-
export const setupHint = "Open /
|
|
7
|
+
export const setupHint = "Open /pinote → Settings to install or upgrade the CLI; note must be on PATH.";
|
|
8
8
|
export type CLIAction = "setup" | "upgrade" | "ready";
|
|
9
9
|
|
|
10
10
|
export function cliAction(version: string): CLIAction {
|
|
@@ -51,10 +51,10 @@ export async function setupCLI(
|
|
|
51
51
|
const action = await detectCLI(pi, signal);
|
|
52
52
|
if (!canAct() || signal.aborted) return;
|
|
53
53
|
if (action === "ready") {
|
|
54
|
-
ctx.ui.notify(`Pinote CLI is ready (bundled version ${bundledCLIVersion} or newer). Use /
|
|
54
|
+
ctx.ui.notify(`Pinote CLI is ready (bundled version ${bundledCLIVersion} or newer). Use /pinote.`, "info");
|
|
55
55
|
return;
|
|
56
56
|
}
|
|
57
|
-
const command = `/
|
|
57
|
+
const command = `/pinote → Settings → ${action === "upgrade" ? "Upgrade CLI" : "Install CLI"}`;
|
|
58
58
|
if (upgrade !== (action === "upgrade")) {
|
|
59
59
|
ctx.ui.notify(`Use ${command} ${action === "upgrade" ? "to upgrade the older CLI" : "to install the missing CLI"}.`, "info");
|
|
60
60
|
return;
|
|
@@ -88,7 +88,7 @@ export async function setupCLI(
|
|
|
88
88
|
try {
|
|
89
89
|
const version = await exec("note", ["--version"]);
|
|
90
90
|
if (version.killed || version.code !== 0 || cliAction(version.stdout) !== "ready") throw new Error("note unavailable or older than bundled CLI");
|
|
91
|
-
if (canAct()) ctx.ui.notify(`${clean(version.stdout.trim())} is ready. Use /
|
|
91
|
+
if (canAct()) ctx.ui.notify(`${clean(version.stdout.trim())} is ready. Use /pinote. GTK is optional and installed separately.`, "info");
|
|
92
92
|
} catch {
|
|
93
93
|
if (canAct()) ctx.ui.notify("Pinote installed, but note on PATH is missing or older than the bundled CLI. Run uv tool dir --bin in your terminal, add that directory to PATH before older note executables, then restart Pi.", "warning");
|
|
94
94
|
}
|