pi-note 0.13.1 → 0.13.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
@@ -171,8 +171,8 @@ user-level configuration only; project-local files are not read.
171
171
  populating relevant values with `pinote_update_current`. Empty/missing values
172
172
  stay hidden; this tool does not edit configuration.
173
173
  - `pinote_tags`: list saved tag names.
174
- - `pinote_propose`: show a nonmodal suggested-task bar below the input without
175
- creating a note. Optional `tag`; full text is retained, but the bar shows only
174
+ - `pinote_propose`: show a suggested task beside the pin icon in the footer without
175
+ creating a note. Optional `tag`; full text is retained, but the footer shows only
176
176
  its title. Requires a TUI with no selected task. The agent continues your work
177
177
  while the suggestion awaits your choice.
178
178
  - `pinote_add`: create an active task. Optional `tag`. `select: true` starts and
@@ -183,12 +183,25 @@ and removes `pinote_tag`, without aliases. Agents cannot read/update tasks by ID
183
183
  or retag existing tasks; use `/pi-note` to select an existing task.
184
184
 
185
185
  When allowed by `taskOfferPolicy`, the agent uses `pinote_propose` instead of
186
- asking in chat. A bar below the input shows `[tag] Task title ✓ ✕`:
186
+ asking in chat. The pin footer shows a pending suggestion, distinct from a selected task:
187
187
 
188
- - **✓** creates the note, starts it, and selects it for this session.
189
- - **✕** hides the suggestion without creating a note; further offers are suppressed
188
+ ```text
189
+ 📌 Suggested: ✓ Add ✕ Dismiss · [tag] Task title...
190
+ ```
191
+
192
+ ![Suggested task beside the footer pin](../docs/images/pi-task-suggestion.png)
193
+
194
+ - **✓ Add** creates the note, starts it, and selects it for this session.
195
+ - **✕ Dismiss** hides the suggestion without creating a note; further offers are suppressed
190
196
  until a new session or reload.
191
197
 
198
+ The configured `footer.titleWidth` bounds the whole suggestion (default 60 columns).
199
+ Controls precede the variable text so long titles cannot push them off the right edge.
200
+ The tag is omitted when space is tight; the first-line title truncates with `...`.
201
+ Smaller budgets shorten controls to ✓ and ✕; tiny budgets show only the suggestion cue.
202
+ Pi can still clip the combined status row on narrow terminals; use the commands below
203
+ if the controls are not visible. Accepting replaces the suggestion with the selected task.
204
+
192
205
  Neither action submits input or starts an agent turn; editor drafts stay unchanged.
193
206
  The icons reuse the [Kitty preview handler](#display-only-preview), with no additional
194
207
  configuration. Fullscreen Kitty uses **Ctrl+Shift+click**. Keyboard alternatives are
@@ -216,7 +229,7 @@ Values are Markdown strings; no fields are required. `PR` enables the watcher be
216
229
  `Bar` chooses extra footer fields unless configuration overrides it:
217
230
 
218
231
  ```markdown
219
- # Agent
232
+ ## Agent
220
233
  PR: [Task selection #42](https://github.com/org/repo/pull/42)
221
234
  Dashboard: [Metrics](http://127.0.0.1:3000/d/app)
222
235
  Next: Address review comments
package/index.ts CHANGED
@@ -131,7 +131,7 @@ export default function (pi: ExtensionAPI) {
131
131
  let offerDeclined = false;
132
132
  const clearSuggestion = (ctx: ExtensionContext) => {
133
133
  previewBridge?.clearSuggestion();
134
- if (suggestion && ctx.hasUI && ctx.mode === "tui") ctx.ui.setWidget("pinote-suggestion", undefined);
134
+ if (suggestion && ctx.hasUI && ctx.mode === "tui") ctx.ui.setStatus("pinote", undefined);
135
135
  suggestion = undefined;
136
136
  };
137
137
  let alive = true;
@@ -142,6 +142,11 @@ export default function (pi: ExtensionAPI) {
142
142
  let activeContext: ExtensionContext | undefined;
143
143
  let cliState: CLIAction | undefined;
144
144
  let footerConfig = { ...defaultFooterConfig };
145
+ const suggestionStatus = (ctx: ExtensionContext) => suggestion ? renderSuggestion(
146
+ suggestion, footerConfig.titleWidth, ctx.ui.theme, {
147
+ yes: previewBridge?.suggestionUrl("yes"), no: previewBridge?.suggestionUrl("no"),
148
+ },
149
+ ) : undefined;
145
150
  const checkCLI = async (ctx: ExtensionContext, suggest = false) => {
146
151
  if (!ctx.hasUI || ctx.mode !== "tui" || setupAbort) return;
147
152
  const generation = epoch;
@@ -266,13 +271,13 @@ export default function (pi: ExtensionAPI) {
266
271
  const url = previewBridge?.url();
267
272
  // Truncate before linking so the task label, including configured overflow, is clickable.
268
273
  const label = current ? truncateToWidth(`${noteIcon} ${taskTag(current)} ${firstLine(current)}`, footerConfig.titleWidth, "...") : undefined;
269
- ctx.ui.setStatus("pinote", label && url ? `\x1b]8;;${url}\x07${label}\x1b]8;;\x07` : label);
274
+ ctx.ui.setStatus("pinote", current ? (label && url ? `\x1b]8;;${url}\x07${label}\x1b]8;;\x07` : label) : suggestionStatus(ctx));
270
275
  watcher.update(ctx, current);
271
276
  }
272
277
  } catch {
273
278
  if (alive && generation === epoch && serial === refreshSerial) {
274
279
  previewBridge?.invalidate();
275
- ctx.ui.setStatus("pinote", `${noteIcon} Pinote unavailable · ${cliState === "upgrade" ? "/pi-note-upgrade" : cliState === "setup" ? "/pi-note-setup" : "/pi-note"}`);
280
+ ctx.ui.setStatus("pinote", suggestionStatus(ctx) ?? `${noteIcon} Pinote unavailable · ${cliState === "upgrade" ? "/pi-note-upgrade" : cliState === "setup" ? "/pi-note-setup" : "/pi-note"}`);
276
281
  }
277
282
  }
278
283
  };
@@ -661,7 +666,7 @@ export default function (pi: ExtensionAPI) {
661
666
  name: "pinote_propose",
662
667
  label: "Pinote suggest task",
663
668
  promptGuidelines: createGuidance,
664
- description: "Show a suggested task in a nonmodal bar below the input, 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.",
669
+ 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.",
665
670
  parameters: Type.Object({
666
671
  text: Type.String({ minLength: 1, description: "Short action-oriented title, then optional details after a blank line." }),
667
672
  tag: Type.Optional(Type.String({ minLength: 1, maxLength: 64 })),
@@ -681,13 +686,7 @@ export default function (pi: ExtensionAPI) {
681
686
  if (suggestion) return result("pending; existing suggestion retained, continue work without asking again");
682
687
  suggestion = { text: params.text, ...(params.tag === undefined ? {} : { tag: params.tag.trim() }) };
683
688
  previewBridge?.setSuggestion((choice) => respondToSuggestion(choice, ctx));
684
- const proposed = suggestion;
685
- ctx.ui.setWidget("pinote-suggestion", (_tui, theme) => ({
686
- render: (width) => [renderSuggestion(proposed, width, theme, {
687
- yes: previewBridge?.suggestionUrl("yes"), no: previewBridge?.suggestionUrl("no"),
688
- })],
689
- invalidate() {},
690
- }), { placement: "belowEditor" });
689
+ ctx.ui.setStatus("pinote", suggestionStatus(ctx));
691
690
  return result("pending; user can click ✓ to add and select or ✕ to dismiss, continue work without asking again");
692
691
  },
693
692
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-note",
3
- "version": "0.13.1",
3
+ "version": "0.13.2",
4
4
  "description": "Persistent pinote tasks, Markdown handoffs, and task status for Pi",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -3,6 +3,7 @@ import { stripVTControlCharacters } from "node:util";
3
3
  import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
4
4
  import type { Theme } from "@earendil-works/pi-coding-agent";
5
5
 
6
+ const pin = readFileSync(new URL("./icons/note.txt", import.meta.url), "utf8").trim();
6
7
  const check = readFileSync(new URL("./icons/check.txt", import.meta.url), "utf8").trim();
7
8
  const cross = readFileSync(new URL("./icons/cross.txt", import.meta.url), "utf8").trim();
8
9
  const link = (label: string, url?: string) => url ? `\x1b]8;;${url}\x07${label}\x1b]8;;\x07` : label;
@@ -11,10 +12,21 @@ export function renderSuggestion(
11
12
  task: { text: string; tag?: string }, width: number, theme: Pick<Theme, "fg">,
12
13
  urls: { yes?: string; no?: string },
13
14
  ): string {
14
- const actions = `${theme.fg("success", link(check, urls.yes))} ${theme.fg("error", link(cross, urls.no))}`;
15
- const actionsWidth = visibleWidth(actions);
16
- if (width <= actionsWidth + 2) return truncateToWidth(actions, width, "");
15
+ const cue = `${pin} ${theme.fg("accent", "Suggested:")}`;
16
+ const actions = (compact: boolean) => `${theme.fg("success", link(compact ? check : `${check} Add`, urls.yes))} ${theme.fg("muted", link(compact ? cross : `${cross} Dismiss`, urls.no))}`;
17
+ let prefix = `${cue} ${actions(false)}`;
18
+ if (visibleWidth(prefix) > width) prefix = `${cue} ${actions(true)}`;
19
+ // Never show unexplained action icons when even the suggestion cue cannot fit.
20
+ if (visibleWidth(prefix) > width) return truncateToWidth(cue, width, "");
21
+ const remaining = width - visibleWidth(prefix) - 3; // separator: " · "
22
+ if (remaining < 4) return prefix;
17
23
  const clean = (value: string) => stripVTControlCharacters(value).replace(/[\x00-\x1f\x7f-\x9f]/gu, " ").replace(/\s+/gu, " ").trim();
18
- const label = `[${clean(task.tag ?? "Untagged")}] ${clean(task.text.split("\n", 1)[0])}`;
19
- return `${theme.fg("muted", truncateToWidth(label, Math.max(0, width - actionsWidth - 2), "..."))} ${actions}`;
24
+ const tag = `[${clean(task.tag ?? "Untagged")}]`;
25
+ const title = clean(task.text.split("\n", 1)[0]);
26
+ // Drop the tag before sacrificing the title to a long tag or a small budget.
27
+ const tagWidth = visibleWidth(tag) + 1;
28
+ const label = remaining >= tagWidth + Math.min(12, visibleWidth(title))
29
+ ? `${theme.fg("muted", tag)} ${theme.fg("text", truncateToWidth(title, remaining - tagWidth, "..."))}`
30
+ : theme.fg("text", truncateToWidth(title, remaining, "..."));
31
+ return `${prefix}${theme.fg("muted", " · ")}${label}`;
20
32
  }