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 +31 -16
- package/index.ts +14 -17
- package/kitty/open-actions.conf +1 -1
- package/package.json +1 -1
- package/task-suggestion.ts +17 -5
- package/icons/eye.txt +0 -1
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
|
|
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
|
|
73
|
-
The
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
174
|
-
creating a note. Optional `tag`; full text is retained, but the
|
|
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.
|
|
186
|
+
asking in chat. The pin footer shows a pending suggestion, distinct from a selected task:
|
|
186
187
|
|
|
187
|
-
|
|
188
|
-
|
|
188
|
+
```text
|
|
189
|
+
📌 Suggested: ✓ Add ✕ Dismiss · [tag] Task title...
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+

|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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
|
-
//
|
|
269
|
-
const
|
|
270
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
});
|
package/kitty/open-actions.conf
CHANGED
|
@@ -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
|
|
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
package/task-suggestion.ts
CHANGED
|
@@ -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
|
|
15
|
-
const
|
|
16
|
-
|
|
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
|
|
19
|
-
|
|
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
|
-
👁
|