pi-note 0.16.0 → 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 +18 -6
- package/index.ts +17 -20
- 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.
|
|
@@ -132,9 +137,16 @@ Configure `~/.pi/agent/pi-note.json` (or the agent directory set by
|
|
|
132
137
|
|
|
133
138
|
`taskOfferPolicy` controls the agent's task-offer guidance:
|
|
134
139
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
140
|
+
With no selected task, suggest a note only when implementation is expected to
|
|
141
|
+
span multiple turns. Skip questions, investigation, recon, and small edits (such
|
|
142
|
+
as changing a few configuration lines). If investigation develops into substantial
|
|
143
|
+
implementation, suggest then. No plan is required: a short title identifies the
|
|
144
|
+
work. Explicit user requests to create a task remain allowed under every policy.
|
|
145
|
+
|
|
146
|
+
- `always` (default): offer for qualifying implementation in any repository.
|
|
147
|
+
- `github-remote`: for qualifying implementation, first verify with Git that the
|
|
148
|
+
repository has an HTTPS or SSH remote hosted on `github.com`; otherwise do not
|
|
149
|
+
offer. Local paths and other
|
|
138
150
|
hosts do not qualify. This is agent guidance, not a network or PR-access check.
|
|
139
151
|
- `never`: do not offer tasks; explicit requests to create one remain allowed.
|
|
140
152
|
|
|
@@ -197,7 +209,7 @@ asking in chat. The pin footer shows a pending suggestion, distinct from a selec
|
|
|
197
209
|

|
|
198
210
|

|
|
199
211
|
|
|
200
|
-
- 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.
|
|
201
213
|
- Accent **+** creates, starts, and selects the suggested task.
|
|
202
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.
|
|
203
215
|
|
|
@@ -209,7 +221,7 @@ Controls use the [Kitty preview handler](#display-only-preview), with no additio
|
|
|
209
221
|
|
|
210
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.
|
|
211
223
|
|
|
212
|
-
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
|
|
213
225
|
tag name when it fits.
|
|
214
226
|
Task text starts with a short, action-oriented title (aim for at most 60 characters).
|
|
215
227
|
Put context, URLs, commands, and acceptance criteria after a blank line; the title
|
package/index.ts
CHANGED
|
@@ -14,12 +14,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
|
-
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.";
|
|
23
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.";
|
|
24
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.";
|
|
25
26
|
type Task = {
|
|
@@ -135,8 +136,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
135
136
|
let previewBridge: ReturnType<typeof createPreviewBridge> | undefined;
|
|
136
137
|
const suggestionType = "pinote-suggestion";
|
|
137
138
|
let suggestion: { text: string; tag?: string } | undefined;
|
|
138
|
-
|
|
139
|
-
const saveSuggestion = () => pi.appendEntry(suggestionType, { suggestion: suggestion ?? null, declined: offerDeclined });
|
|
139
|
+
const saveSuggestion = () => pi.appendEntry(suggestionType, { suggestion: suggestion ?? null });
|
|
140
140
|
const clearSuggestion = (ctx: ExtensionContext, persist = true) => {
|
|
141
141
|
const hadSuggestion = suggestion !== undefined;
|
|
142
142
|
previewBridge?.clearSuggestion();
|
|
@@ -146,23 +146,23 @@ export default function (pi: ExtensionAPI) {
|
|
|
146
146
|
};
|
|
147
147
|
const restoreSuggestion = (ctx: ExtensionContext) => {
|
|
148
148
|
suggestion = undefined;
|
|
149
|
-
offerDeclined = false;
|
|
150
149
|
for (const entry of ctx.sessionManager.getBranch()) {
|
|
151
150
|
if (entry.type !== "custom" || entry.customType !== suggestionType) continue;
|
|
152
151
|
const data = entry.data;
|
|
153
|
-
if (!record(data)
|
|
152
|
+
if (!record(data)) continue;
|
|
154
153
|
const value = data.suggestion;
|
|
155
|
-
|
|
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() &&
|
|
156
156
|
(value.tag === undefined || (text(value.tag) && value.tag.trim() && value.tag.length <= 64))
|
|
157
157
|
? { text: value.text, ...(value.tag === undefined ? {} : { tag: value.tag as string }) } : undefined;
|
|
158
|
-
offerDeclined = data.declined;
|
|
159
158
|
}
|
|
160
|
-
if (selectedId !== null
|
|
159
|
+
if (selectedId !== null) suggestion = undefined;
|
|
161
160
|
};
|
|
162
161
|
let alive = true;
|
|
163
162
|
let epoch = 0;
|
|
164
163
|
let refreshSerial = 0;
|
|
165
164
|
let pending: symbol | undefined;
|
|
165
|
+
let previewPending = false;
|
|
166
166
|
let setupAbort: AbortController | undefined;
|
|
167
167
|
let activeContext: ExtensionContext | undefined;
|
|
168
168
|
let cliState: CLIAction | undefined;
|
|
@@ -311,18 +311,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
311
311
|
}
|
|
312
312
|
};
|
|
313
313
|
const preview = async (ctx: ExtensionContext, requestSignal?: AbortSignal): Promise<boolean> => {
|
|
314
|
-
if (!ctx.hasUI || ctx.mode !== "tui" ||
|
|
314
|
+
if (!ctx.hasUI || ctx.mode !== "tui" || pending || previewPending || !validId(selectedId)) return false;
|
|
315
315
|
const generation = epoch;
|
|
316
316
|
const branch = branchEpoch;
|
|
317
317
|
const id = selectedId;
|
|
318
|
-
|
|
319
|
-
|
|
318
|
+
// Serialize clicks without blocking the agent's task updates.
|
|
319
|
+
previewPending = true;
|
|
320
320
|
// Complete before the bridge's 5s and helper's 6s deadlines; disconnection cancels reads too.
|
|
321
321
|
const controller = new AbortController();
|
|
322
322
|
const deadline = Date.now() + 4000;
|
|
323
323
|
const timer = setTimeout(() => controller.abort(), 4000);
|
|
324
324
|
const signal = requestSignal ? AbortSignal.any([requestSignal, controller.signal]) : controller.signal;
|
|
325
|
-
const canUse = () => alive && generation === epoch && branch === branchEpoch && selectedId === id &&
|
|
325
|
+
const canUse = () => alive && generation === epoch && branch === branchEpoch && selectedId === id && !pending &&
|
|
326
326
|
!signal.aborted && Date.now() < deadline;
|
|
327
327
|
try {
|
|
328
328
|
const current = await currentTask(signal, canUse);
|
|
@@ -335,7 +335,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
335
335
|
return false;
|
|
336
336
|
} finally {
|
|
337
337
|
clearTimeout(timer);
|
|
338
|
-
|
|
338
|
+
previewPending = false;
|
|
339
339
|
}
|
|
340
340
|
};
|
|
341
341
|
const dispatchCompletion = (ctx: ExtensionContext, displayed: Task, selection: string): Promise<boolean> => {
|
|
@@ -424,7 +424,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
424
424
|
if (_event.reason === "new" || _event.reason === "fork") {
|
|
425
425
|
// A fork may include an ancestor proposal whose consent was consumed elsewhere.
|
|
426
426
|
clearSuggestion(ctx, false);
|
|
427
|
-
offerDeclined = false;
|
|
428
427
|
if (ctx.sessionManager.getBranch().some((entry) => entry.type === "custom" && entry.customType === suggestionType)) saveSuggestion();
|
|
429
428
|
}
|
|
430
429
|
footerConfig = loadFooterConfig((message) => {
|
|
@@ -718,7 +717,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
718
717
|
const proposed = suggestion;
|
|
719
718
|
if (!alive || !proposed || selectedId !== null) return false;
|
|
720
719
|
if (choice === "no") {
|
|
721
|
-
offerDeclined = true;
|
|
722
720
|
clearSuggestion(ctx);
|
|
723
721
|
return true;
|
|
724
722
|
}
|
|
@@ -757,7 +755,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
757
755
|
name: "pinote_propose",
|
|
758
756
|
label: "Pinote suggest task",
|
|
759
757
|
promptGuidelines: createGuidance,
|
|
760
|
-
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.",
|
|
761
759
|
parameters: Type.Object({
|
|
762
760
|
text: Type.String({ minLength: 1, description: "Short action-oriented title, then optional details after a blank line." }),
|
|
763
761
|
tag: Type.Optional(Type.String({ minLength: 1, maxLength: 64 })),
|
|
@@ -773,13 +771,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
773
771
|
if (current || selectedId !== null) throw new Error("A task is already selected; keep it unless the user asks to switch.");
|
|
774
772
|
if (pending || setupAbort) throw new Error("A pinote operation is already open. Retry after it finishes.");
|
|
775
773
|
const result = (status: string) => ({ content: [{ type: "text" as const, text: JSON.stringify({ status }) }], details: { status } });
|
|
776
|
-
if (offerDeclined) return result("dismissed; continue without a note and do not offer again");
|
|
777
774
|
if (suggestion) return result("pending; existing suggestion retained, continue work without asking again");
|
|
778
775
|
suggestion = { text: params.text, ...(params.tag === undefined ? {} : { tag: params.tag.trim() }) };
|
|
779
776
|
saveSuggestion();
|
|
780
777
|
previewBridge?.setSuggestion((choice) => respondToSuggestion(choice, ctx));
|
|
781
778
|
ctx.ui.setStatus("pinote", suggestionStatus(ctx));
|
|
782
|
-
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");
|
|
783
780
|
},
|
|
784
781
|
});
|
|
785
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
|
}
|