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 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
- Previewing requires Pi to be idle with no other Pinote operation open.
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
+ ![Preview above a running response](../docs/images/pi-note-active-preview.png)
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
- - `always` (default): offer a task when given work with no task selected.
136
- - `github-remote`: first verify with Git that the repository has an HTTPS or SSH
137
- remote hosted on `github.com`; otherwise do not offer. Local paths and other
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
  ![Suggested task controls](../docs/images/pi-task-suggestion.png)
198
210
  ![Selected task with Done control](../docs/images/pi-task-selected.png)
199
211
 
200
- - Muted **✕** dismisses without creating a note; further offers are suppressed until a new session.
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: "When the user gives work and no pinote task is selected, 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.",
19
- "github-remote": "When the user gives work and no pinote task is selected, 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; explicit user requests to create one remain allowed.",
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 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.";
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
- let offerDeclined = false;
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) || typeof data.declined !== "boolean") continue;
152
+ if (!record(data)) continue;
154
153
  const value = data.suggestion;
155
- suggestion = record(value) && text(value.text) && value.text.trim() &&
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 || offerDeclined) suggestion = undefined;
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" || !ctx.isIdle() || pending || !validId(selectedId)) return false;
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
- const operation = Symbol();
319
- pending = operation;
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 && ctx.isIdle() &&
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
- if (pending === operation) pending = undefined;
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; a dismissed offer is not repeated. In noninteractive modes ask in chat, then use pinote_add only after consent.",
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 ✓ to add and select or ✕ to dismiss, continue work without asking again");
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-note",
3
- "version": "0.16.0",
3
+ "version": "0.16.2",
4
4
  "description": "Persistent pinote tasks, Markdown handoffs, and task status for Pi",
5
5
  "license": "MIT",
6
6
  "type": "module",
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 preview or suggestion unavailable; select a task and wait until Pi is idle.');
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
  }