pi-note 0.13.0 → 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
@@ -68,9 +68,9 @@ task, so existing values can be configured without creating another task.
68
68
  Type in the task picker to filter by text, ID, tag, or state (all words must match).
69
69
  Use ↑/↓ and Enter to select, or Esc to cancel. Rows show **●** for in progress or
70
70
  **○** for active, followed by the tag (`[Untagged]` when absent).
71
- The normal Pi status area shows `👁 📌 [tag] Task title` without replacing other footers.
72
- The footer omits the task ID and state and shows only the first line, truncated to the configured width (default 60 terminal columns including the eye, pin and tag). Overflow ends with `...`.
73
- The glyphs are bundled in `icons/eye.txt` and `icons/note.txt`; they use terminal fonts, not a Nerd Font or icon theme.
71
+ The normal Pi status area shows `📌 [tag] Task title` without replacing other footers.
72
+ The footer omits the task ID and state and shows only the first line, truncated to the configured width (default 60 terminal columns including the pin and tag). Overflow ends with `...`.
73
+ The pin glyph is bundled in `icons/note.txt`; it uses terminal fonts, not a Nerd Font or icon theme.
74
74
 
75
75
  ### Display-only preview
76
76
 
@@ -82,12 +82,13 @@ modify the note, editor draft or task selection, and never starts a model call.
82
82
  The agent can still read the task separately with `pinote_get_current`.
83
83
  Previewing requires Pi to be idle with no other Pinote operation open.
84
84
 
85
- On Linux, the eye is a clickable OSC 8 link to a private per-session Unix socket.
85
+ On Linux, the task text (pin, tag and title) is a clickable OSC 8 link to a
86
+ private per-session Unix socket. Overflow at the configured title width stays linked.
86
87
  The package ships [a Kitty configuration example](kitty/open-actions.conf).
87
88
  Append its block to `~/.config/kitty/open-actions.conf`, preserving existing
88
89
  actions. Replace `/absolute/path/to/pi-note` with the installed package directory
89
90
  containing `preview-click.cjs`; for this checkout, that directory is `pi/`.
90
- Keep `${URL}` literal: Kitty substitutes the clicked eye's link.
91
+ Keep `${URL}` literal: Kitty substitutes the clicked task's link.
91
92
 
92
93
  ```conf
93
94
  protocol pi-note-preview
@@ -97,12 +98,12 @@ action launch --type=background node /absolute/path/to/pi-note/preview-click.cjs
97
98
  Do not replace or symlink your entire Kitty configuration to the example: it
98
99
  contains only Pinote's handler. Package installation does not edit Kitty files.
99
100
 
100
- Reload Kitty with **Ctrl+Shift+F5**. In Pi's regular mode, click the eye; in
101
+ Reload Kitty with **Ctrl+Shift+F5**. In Pi's regular mode, click the task text; in
101
102
  fullscreen mode, use **Ctrl+Shift+click** so Kitty handles the link instead of
102
103
  Pi's system URL opener. The helper sends only an authenticated selection
103
104
  identifier, never note content or a model prompt. Stale links after task switching, tree navigation, reload or session exit
104
105
  are rejected. Other terminals can use `/pi-note-preview`; if the socket cannot
105
- start, the eye is omitted and that command remains available.
106
+ start, the task remains visible without a link and that command remains available.
106
107
 
107
108
  Each Pi session remembers its own task, including several sessions in one folder.
108
109
  Resume restores that session's task; a new session starts unselected and does not import
@@ -170,8 +171,8 @@ user-level configuration only; project-local files are not read.
170
171
  populating relevant values with `pinote_update_current`. Empty/missing values
171
172
  stay hidden; this tool does not edit configuration.
172
173
  - `pinote_tags`: list saved tag names.
173
- - `pinote_propose`: show a nonmodal suggested-task bar below the input without
174
- 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
175
176
  its title. Requires a TUI with no selected task. The agent continues your work
176
177
  while the suggestion awaits your choice.
177
178
  - `pinote_add`: create an active task. Optional `tag`. `select: true` starts and
@@ -182,12 +183,25 @@ and removes `pinote_tag`, without aliases. Agents cannot read/update tasks by ID
182
183
  or retag existing tasks; use `/pi-note` to select an existing task.
183
184
 
184
185
  When allowed by `taskOfferPolicy`, the agent uses `pinote_propose` instead of
185
- 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:
186
187
 
187
- - **✓** creates the note, starts it, and selects it for this session.
188
- - **✕** 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
189
196
  until a new session or reload.
190
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
+
191
205
  Neither action submits input or starts an agent turn; editor drafts stay unchanged.
192
206
  The icons reuse the [Kitty preview handler](#display-only-preview), with no additional
193
207
  configuration. Fullscreen Kitty uses **Ctrl+Shift+click**. Keyboard alternatives are
@@ -215,7 +229,7 @@ Values are Markdown strings; no fields are required. `PR` enables the watcher be
215
229
  `Bar` chooses extra footer fields unless configuration overrides it:
216
230
 
217
231
  ```markdown
218
- # Agent
232
+ ## Agent
219
233
  PR: [Task selection #42](https://github.com/org/repo/pull/42)
220
234
  Dashboard: [Metrics](http://127.0.0.1:3000/d/app)
221
235
  Next: Address review comments
@@ -251,7 +265,7 @@ Add `footer` to `~/.pi/agent/pi-note.json` (or `pi-note.json` under
251
265
  }
252
266
  ```
253
267
 
254
- Widths are terminal columns, including the eye, pin, tags or field labels, and must be
268
+ Widths are terminal columns, including the pin, tags or field labels, and must be
255
269
  integers from 3 to 1000. Defaults: `titleWidth: 60`, `fieldWidth: 60`,
256
270
  `maxFields: 4` (allowed 0–64). Each field can override `fieldWidth` with `width`.
257
271
  `fields` selects task field names in order, regardless of a task's `Bar`.
@@ -303,8 +317,9 @@ By default, at most four extra fields are shown, each truncated to 60 columns.
303
317
  Overflow ends with `...`, including cuts between Markdown/link segments. Removing
304
318
  a listed field hides it even if configuration or `Bar` still names it. Terminal
305
319
  OSC 8 support is required for clicking links. Pi joins footer statuses on one
306
- line and truncates the combined line with `...` when the terminal is narrow;
307
- this extension does not replace other footers. Other fields, such as `Jira` and
320
+ line and truncates the combined line with `...` when the terminal is narrow.
321
+ Pi's final ellipsis is plain text; click the remaining task text to preview it.
322
+ This extension does not replace other footers. Other fields, such as `Jira` and
308
323
  `CWD` in the example, stay off the footer unless selected.
309
324
 
310
325
  ### PR merge watcher
package/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { getAgentDir, getMarkdownTheme, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import { Container, Editor, Markdown, Text, truncateToWidth, visibleWidth, type Keybinding } from "@earendil-works/pi-tui";
2
+ import { Container, Editor, Markdown, Text, truncateToWidth, type Keybinding } from "@earendil-works/pi-tui";
3
3
  import { readFileSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { stripVTControlCharacters } from "node:util";
@@ -33,7 +33,6 @@ type Task = {
33
33
  type Summary = Pick<Task, "id" | "text" | "state" | "tag">;
34
34
  // The standard Pi footer accepts text, so ship a portable terminal glyph, not a theme icon.
35
35
  const noteIcon = readFileSync(new URL("./icons/note.txt", import.meta.url), "utf8").trim();
36
- const eyeIcon = readFileSync(new URL("./icons/eye.txt", import.meta.url), "utf8").trim();
37
36
  const previewType = "pinote-preview";
38
37
  const compatible = "Incompatible note CLI response. Install pinote 0.3.0+ and check note on PATH.";
39
38
  const validId = (value: unknown): value is number => Number.isSafeInteger(value) && Number(value) > 0;
@@ -132,7 +131,7 @@ export default function (pi: ExtensionAPI) {
132
131
  let offerDeclined = false;
133
132
  const clearSuggestion = (ctx: ExtensionContext) => {
134
133
  previewBridge?.clearSuggestion();
135
- 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);
136
135
  suggestion = undefined;
137
136
  };
138
137
  let alive = true;
@@ -143,6 +142,11 @@ export default function (pi: ExtensionAPI) {
143
142
  let activeContext: ExtensionContext | undefined;
144
143
  let cliState: CLIAction | undefined;
145
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;
146
150
  const checkCLI = async (ctx: ExtensionContext, suggest = false) => {
147
151
  if (!ctx.hasUI || ctx.mode !== "tui" || setupAbort) return;
148
152
  const generation = epoch;
@@ -265,16 +269,15 @@ export default function (pi: ExtensionAPI) {
265
269
  previewBridge?.setTask(previewId, (signal) => alive && generation === epoch && previewBranch === branchEpoch &&
266
270
  selectedId === previewId ? preview(ctx, signal) : Promise.resolve(false));
267
271
  const url = previewBridge?.url();
268
- // Keep the eye outside title truncation, including the minimum title width.
269
- const eye = url ? `\x1b]8;;${url}\x07${eyeIcon}\x1b]8;;\x07 ` : "";
270
- const width = Math.max(0, footerConfig.titleWidth - (eye ? visibleWidth(eyeIcon) + 1 : 0));
271
- ctx.ui.setStatus("pinote", current ? eye + truncateToWidth(`${noteIcon} ${taskTag(current)} ${firstLine(current)}`, width, "...") : undefined);
272
+ // Truncate before linking so the task label, including configured overflow, is clickable.
273
+ const label = current ? truncateToWidth(`${noteIcon} ${taskTag(current)} ${firstLine(current)}`, footerConfig.titleWidth, "...") : undefined;
274
+ ctx.ui.setStatus("pinote", current ? (label && url ? `\x1b]8;;${url}\x07${label}\x1b]8;;\x07` : label) : suggestionStatus(ctx));
272
275
  watcher.update(ctx, current);
273
276
  }
274
277
  } catch {
275
278
  if (alive && generation === epoch && serial === refreshSerial) {
276
279
  previewBridge?.invalidate();
277
- 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"}`);
278
281
  }
279
282
  }
280
283
  };
@@ -365,7 +368,7 @@ export default function (pi: ExtensionAPI) {
365
368
  } catch {
366
369
  await bridge.stop();
367
370
  if (previewBridge === bridge) previewBridge = undefined;
368
- ctx.ui.notify("Pinote eye link unavailable; use /pi-note-preview.", "warning");
371
+ ctx.ui.notify("Pinote task link unavailable; use /pi-note-preview.", "warning");
369
372
  }
370
373
  }
371
374
  if (!alive || generation !== epoch) return;
@@ -663,7 +666,7 @@ export default function (pi: ExtensionAPI) {
663
666
  name: "pinote_propose",
664
667
  label: "Pinote suggest task",
665
668
  promptGuidelines: createGuidance,
666
- 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.",
667
670
  parameters: Type.Object({
668
671
  text: Type.String({ minLength: 1, description: "Short action-oriented title, then optional details after a blank line." }),
669
672
  tag: Type.Optional(Type.String({ minLength: 1, maxLength: 64 })),
@@ -683,13 +686,7 @@ export default function (pi: ExtensionAPI) {
683
686
  if (suggestion) return result("pending; existing suggestion retained, continue work without asking again");
684
687
  suggestion = { text: params.text, ...(params.tag === undefined ? {} : { tag: params.tag.trim() }) };
685
688
  previewBridge?.setSuggestion((choice) => respondToSuggestion(choice, ctx));
686
- const proposed = suggestion;
687
- ctx.ui.setWidget("pinote-suggestion", (_tui, theme) => ({
688
- render: (width) => [renderSuggestion(proposed, width, theme, {
689
- yes: previewBridge?.suggestionUrl("yes"), no: previewBridge?.suggestionUrl("no"),
690
- })],
691
- invalidate() {},
692
- }), { placement: "belowEditor" });
689
+ ctx.ui.setStatus("pinote", suggestionStatus(ctx));
693
690
  return result("pending; user can click ✓ to add and select or ✕ to dismiss, continue work without asking again");
694
691
  },
695
692
  });
@@ -1,5 +1,5 @@
1
1
  # Append this block to ~/.config/kitty/open-actions.conf; preserve existing actions.
2
2
  # Replace /absolute/path/to/pi-note with the directory containing preview-click.cjs.
3
- # Keep ${URL} literal: Kitty substitutes the clicked eye's link.
3
+ # Keep ${URL} literal: Kitty substitutes the clicked task's link.
4
4
  protocol pi-note-preview
5
5
  action launch --type=background node /absolute/path/to/pi-note/preview-click.cjs ${URL}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-note",
3
- "version": "0.13.0",
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
  }
package/icons/eye.txt DELETED
@@ -1 +0,0 @@
1
- 👁