pi-note 0.16.1 → 0.16.2
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 +8 -3
- package/index.ts +14 -18
- package/package.json +1 -1
- package/preview-click.cjs +1 -1
package/README.md
CHANGED
|
@@ -81,7 +81,12 @@ to model**: it is a local custom session entry, excluded from subsequent model
|
|
|
81
81
|
requests, compaction and branch summaries. It survives session resume but does not
|
|
82
82
|
modify the note, editor draft or task selection, and never starts a model call.
|
|
83
83
|
The agent can still read the task separately with `pinote_get_current`.
|
|
84
|
-
|
|
84
|
+
Click previews work while the agent is running, provided no other Pinote operation
|
|
85
|
+
is open. Each click saves a fresh snapshot in the transcript, above any currently
|
|
86
|
+
streaming response. Later output scrolls it upward; it does not expire. Preview
|
|
87
|
+
reads never lock out the agent's task updates. Settings still requires Pi to be idle.
|
|
88
|
+
|
|
89
|
+

|
|
85
90
|
|
|
86
91
|
On Linux, the task text (pin, tag and title) is a clickable OSC 8 link to a
|
|
87
92
|
private per-session Unix socket. Overflow at the configured title width stays linked.
|
|
@@ -204,7 +209,7 @@ asking in chat. The pin footer shows a pending suggestion, distinct from a selec
|
|
|
204
209
|

|
|
205
210
|

|
|
206
211
|
|
|
207
|
-
- Muted **✕** dismisses without creating a note;
|
|
212
|
+
- Muted **✕** dismisses only the current suggestion without creating a note; the agent can suggest another task while none is selected.
|
|
208
213
|
- Accent **+** creates, starts, and selects the suggested task.
|
|
209
214
|
- 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.
|
|
210
215
|
|
|
@@ -216,7 +221,7 @@ Controls use the [Kitty preview handler](#display-only-preview), with no additio
|
|
|
216
221
|
|
|
217
222
|
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.
|
|
218
223
|
|
|
219
|
-
An existing suggestion is retained rather than replaced. 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. 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
|
|
224
|
+
An existing suggestion is retained rather than replaced. 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
|
|
220
225
|
tag name when it fits.
|
|
221
226
|
Task text starts with a short, action-oriented title (aim for at most 60 characters).
|
|
222
227
|
Put context, URLs, commands, and acceptance criteria after a blank line; the title
|
package/index.ts
CHANGED
|
@@ -20,7 +20,7 @@ const offerGuidance = {
|
|
|
20
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.`,
|
|
21
21
|
never: "Do not offer to create a pinote task. Create one only when the user explicitly requests it.",
|
|
22
22
|
};
|
|
23
|
-
const consentGuidance = "The suggestion bar's plus creates, starts and selects the note; its cross dismisses
|
|
23
|
+
const consentGuidance = "The suggestion bar's plus creates, starts and selects the note; its cross dismisses only that suggestion, not future suggestions. 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.";
|
|
24
24
|
const titleGuidance = "Start task text with a short, action-oriented summary (aim for at most 60 characters). Put context, URLs, commands, and acceptance criteria after a blank line. Never put implementation details in the title.";
|
|
25
25
|
const tagGuidance = "Reuse a pinote_tags name when it fits. One tag; case-sensitive, trimmed, at most 64 characters. Tags are set when creating tasks with pinote_add; agent tools cannot retag existing tasks. Do not use pinote_update_current for tags or task text.";
|
|
26
26
|
type Task = {
|
|
@@ -136,8 +136,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
136
136
|
let previewBridge: ReturnType<typeof createPreviewBridge> | undefined;
|
|
137
137
|
const suggestionType = "pinote-suggestion";
|
|
138
138
|
let suggestion: { text: string; tag?: string } | undefined;
|
|
139
|
-
|
|
140
|
-
const saveSuggestion = () => pi.appendEntry(suggestionType, { suggestion: suggestion ?? null, declined: offerDeclined });
|
|
139
|
+
const saveSuggestion = () => pi.appendEntry(suggestionType, { suggestion: suggestion ?? null });
|
|
141
140
|
const clearSuggestion = (ctx: ExtensionContext, persist = true) => {
|
|
142
141
|
const hadSuggestion = suggestion !== undefined;
|
|
143
142
|
previewBridge?.clearSuggestion();
|
|
@@ -147,23 +146,23 @@ export default function (pi: ExtensionAPI) {
|
|
|
147
146
|
};
|
|
148
147
|
const restoreSuggestion = (ctx: ExtensionContext) => {
|
|
149
148
|
suggestion = undefined;
|
|
150
|
-
offerDeclined = false;
|
|
151
149
|
for (const entry of ctx.sessionManager.getBranch()) {
|
|
152
150
|
if (entry.type !== "custom" || entry.customType !== suggestionType) continue;
|
|
153
151
|
const data = entry.data;
|
|
154
|
-
if (!record(data)
|
|
152
|
+
if (!record(data)) continue;
|
|
155
153
|
const value = data.suggestion;
|
|
156
|
-
|
|
154
|
+
// Legacy dismissal entries clear their proposal, but never block a new one.
|
|
155
|
+
suggestion = data.declined !== true && record(value) && text(value.text) && value.text.trim() &&
|
|
157
156
|
(value.tag === undefined || (text(value.tag) && value.tag.trim() && value.tag.length <= 64))
|
|
158
157
|
? { text: value.text, ...(value.tag === undefined ? {} : { tag: value.tag as string }) } : undefined;
|
|
159
|
-
offerDeclined = data.declined;
|
|
160
158
|
}
|
|
161
|
-
if (selectedId !== null
|
|
159
|
+
if (selectedId !== null) suggestion = undefined;
|
|
162
160
|
};
|
|
163
161
|
let alive = true;
|
|
164
162
|
let epoch = 0;
|
|
165
163
|
let refreshSerial = 0;
|
|
166
164
|
let pending: symbol | undefined;
|
|
165
|
+
let previewPending = false;
|
|
167
166
|
let setupAbort: AbortController | undefined;
|
|
168
167
|
let activeContext: ExtensionContext | undefined;
|
|
169
168
|
let cliState: CLIAction | undefined;
|
|
@@ -312,18 +311,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
312
311
|
}
|
|
313
312
|
};
|
|
314
313
|
const preview = async (ctx: ExtensionContext, requestSignal?: AbortSignal): Promise<boolean> => {
|
|
315
|
-
if (!ctx.hasUI || ctx.mode !== "tui" ||
|
|
314
|
+
if (!ctx.hasUI || ctx.mode !== "tui" || pending || previewPending || !validId(selectedId)) return false;
|
|
316
315
|
const generation = epoch;
|
|
317
316
|
const branch = branchEpoch;
|
|
318
317
|
const id = selectedId;
|
|
319
|
-
|
|
320
|
-
|
|
318
|
+
// Serialize clicks without blocking the agent's task updates.
|
|
319
|
+
previewPending = true;
|
|
321
320
|
// Complete before the bridge's 5s and helper's 6s deadlines; disconnection cancels reads too.
|
|
322
321
|
const controller = new AbortController();
|
|
323
322
|
const deadline = Date.now() + 4000;
|
|
324
323
|
const timer = setTimeout(() => controller.abort(), 4000);
|
|
325
324
|
const signal = requestSignal ? AbortSignal.any([requestSignal, controller.signal]) : controller.signal;
|
|
326
|
-
const canUse = () => alive && generation === epoch && branch === branchEpoch && selectedId === id &&
|
|
325
|
+
const canUse = () => alive && generation === epoch && branch === branchEpoch && selectedId === id && !pending &&
|
|
327
326
|
!signal.aborted && Date.now() < deadline;
|
|
328
327
|
try {
|
|
329
328
|
const current = await currentTask(signal, canUse);
|
|
@@ -336,7 +335,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
336
335
|
return false;
|
|
337
336
|
} finally {
|
|
338
337
|
clearTimeout(timer);
|
|
339
|
-
|
|
338
|
+
previewPending = false;
|
|
340
339
|
}
|
|
341
340
|
};
|
|
342
341
|
const dispatchCompletion = (ctx: ExtensionContext, displayed: Task, selection: string): Promise<boolean> => {
|
|
@@ -425,7 +424,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
425
424
|
if (_event.reason === "new" || _event.reason === "fork") {
|
|
426
425
|
// A fork may include an ancestor proposal whose consent was consumed elsewhere.
|
|
427
426
|
clearSuggestion(ctx, false);
|
|
428
|
-
offerDeclined = false;
|
|
429
427
|
if (ctx.sessionManager.getBranch().some((entry) => entry.type === "custom" && entry.customType === suggestionType)) saveSuggestion();
|
|
430
428
|
}
|
|
431
429
|
footerConfig = loadFooterConfig((message) => {
|
|
@@ -719,7 +717,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
719
717
|
const proposed = suggestion;
|
|
720
718
|
if (!alive || !proposed || selectedId !== null) return false;
|
|
721
719
|
if (choice === "no") {
|
|
722
|
-
offerDeclined = true;
|
|
723
720
|
clearSuggestion(ctx);
|
|
724
721
|
return true;
|
|
725
722
|
}
|
|
@@ -758,7 +755,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
758
755
|
name: "pinote_propose",
|
|
759
756
|
label: "Pinote suggest task",
|
|
760
757
|
promptGuidelines: createGuidance,
|
|
761
|
-
description: "Show a suggested task beside the pin icon in the footer, without creating it or asking in chat. + creates, starts and selects it; ✕ dismisses it. Continue the requested work while awaiting consent. Requires an interactive TUI with no selected task. An existing suggestion is retained;
|
|
758
|
+
description: "Show a suggested task beside the pin icon in the footer, without creating it or asking in chat. + creates, starts and selects it; ✕ dismisses it. Continue the requested work while awaiting consent. Requires an interactive TUI with no selected task. An existing suggestion is retained; dismissal clears only that suggestion and allows later proposals. In noninteractive modes ask in chat, then use pinote_add only after consent.",
|
|
762
759
|
parameters: Type.Object({
|
|
763
760
|
text: Type.String({ minLength: 1, description: "Short action-oriented title, then optional details after a blank line." }),
|
|
764
761
|
tag: Type.Optional(Type.String({ minLength: 1, maxLength: 64 })),
|
|
@@ -774,13 +771,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
774
771
|
if (current || selectedId !== null) throw new Error("A task is already selected; keep it unless the user asks to switch.");
|
|
775
772
|
if (pending || setupAbort) throw new Error("A pinote operation is already open. Retry after it finishes.");
|
|
776
773
|
const result = (status: string) => ({ content: [{ type: "text" as const, text: JSON.stringify({ status }) }], details: { status } });
|
|
777
|
-
if (offerDeclined) return result("dismissed; continue without a note and do not offer again");
|
|
778
774
|
if (suggestion) return result("pending; existing suggestion retained, continue work without asking again");
|
|
779
775
|
suggestion = { text: params.text, ...(params.tag === undefined ? {} : { tag: params.tag.trim() }) };
|
|
780
776
|
saveSuggestion();
|
|
781
777
|
previewBridge?.setSuggestion((choice) => respondToSuggestion(choice, ctx));
|
|
782
778
|
ctx.ui.setStatus("pinote", suggestionStatus(ctx));
|
|
783
|
-
return result("pending; user can click
|
|
779
|
+
return result("pending; user can click + to add and select or ✕ to dismiss, continue work without asking again");
|
|
784
780
|
},
|
|
785
781
|
});
|
|
786
782
|
pi.registerTool({
|
package/package.json
CHANGED
package/preview-click.cjs
CHANGED
|
@@ -59,7 +59,7 @@ if (require.main === module) {
|
|
|
59
59
|
process.exitCode = 2;
|
|
60
60
|
} else {
|
|
61
61
|
preview(process.argv[2]).catch(() => {
|
|
62
|
-
console.error('Pinote
|
|
62
|
+
console.error('Pinote action unavailable; select a task and retry after any Pinote operation finishes. Done also requires Pi to be idle.');
|
|
63
63
|
process.exitCode = 1;
|
|
64
64
|
});
|
|
65
65
|
}
|