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 +19 -6
- package/index.ts +10 -11
- package/package.json +1 -1
- package/task-suggestion.ts +17 -5
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
|
|
175
|
-
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
|
|
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.
|
|
186
|
+
asking in chat. The pin footer shows a pending suggestion, distinct from a selected task:
|
|
187
187
|
|
|
188
|
-
|
|
189
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
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
|
}
|