Skip to content

Commit 1657087

Browse files
authored
feat(pi-notes): limit notes to two lines (#179)
1 parent bbdb237 commit 1657087

5 files changed

Lines changed: 47 additions & 11 deletions

File tree

package-lock.json

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/pi-notes/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
# `@henryqw/pi-notes`
22

3-
Persistent notes shown in a Pi widget, managed with slash commands.
3+
Persistent post-it reminders shown in a Pi widget, managed with slash commands.
44

55
## Why
66

7-
- **Created for**: Keeping persistent scratch notes visible per worktree without leaving the Pi session.
8-
- **Advantage**: Slash-command-managed notes render in a widget and survive across sessions without leaking between worktrees.
7+
- **Created for**: Keeping a few brief, post-it-style reminders visible per worktree without leaving the Pi session.
8+
- **Advantage**: Notes stay intentionally bounded and visible instead of becoming clipboard storage or history.
99

1010
## Install
1111

@@ -21,7 +21,7 @@ pi install npm:@henryqw/pi-notes
2121
| `/note-rm` | command | Pick a note from current worktree to remove. |
2222
| `/note-clear` | command | Clear current worktree's notes. |
2323

24-
Notes are isolated per Git worktree, render as a numbered widget above editor, and persist across sessions under `~/.pi/agent/config/pi-notes/`. Empty worktrees show no widget. Stale files for removed repositories and worktrees are deleted silently when a session starts or notes change.
24+
Notes are isolated per Git worktree, render as a numbered widget above editor with at most two lines per note, and persist across sessions under `~/.pi/agent/config/pi-notes/`. Empty worktrees show no widget. Stale files for removed repositories and worktrees are deleted silently when a session starts or notes change.
2525

2626
Each worktree file is validated as untrusted data. Malformed files are preserved and block mutation for affected worktree until fixed or reset with `/note-clear`.
2727

packages/pi-notes/extensions/notes.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
type ExtensionAPI,
88
type ExtensionContext,
99
} from "@earendil-works/pi-coding-agent";
10+
import { truncateToWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
1011

1112
const MAX_NOTES = 4;
1213
const WIDGET_KEY = "pi-notes";
@@ -70,8 +71,29 @@ export function renderNotes(notes: string[]): string[] {
7071
return notes.map((note, i) => `${i + 1}. ${note}`);
7172
}
7273

74+
export function renderNotesWidget(notes: string[], width: number): string[] {
75+
const renderWidth = Math.max(1, width);
76+
return renderNotes(notes).flatMap((note) => {
77+
const lines = wrapTextWithAnsi(note, renderWidth);
78+
return lines.length > 2
79+
? [lines[0]!, truncateToWidth(`${lines[1]}…`, renderWidth, "…")]
80+
: lines;
81+
});
82+
}
83+
7384
function setNotesWidget(ctx: ExtensionContext, notes: string[]): void {
74-
ctx.ui.setWidget(WIDGET_KEY, notes.length ? renderNotes(notes) : undefined);
85+
if (!notes.length) {
86+
ctx.ui.setWidget(WIDGET_KEY, undefined);
87+
return;
88+
}
89+
if (ctx.mode !== "tui") {
90+
ctx.ui.setWidget(WIDGET_KEY, renderNotes(notes));
91+
return;
92+
}
93+
ctx.ui.setWidget(WIDGET_KEY, () => ({
94+
invalidate() {},
95+
render: (width) => renderNotesWidget(notes, width),
96+
}));
7597
}
7698

7799
async function resolveWorktree(pi: ExtensionAPI, cwd: string): Promise<WorktreeIdentity> {

packages/pi-notes/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@henryqw/pi-notes",
3-
"version": "0.1.4",
3+
"version": "0.2.0",
44
"description": "Persistent notes in a Pi widget, managed with /note commands.",
55
"keywords": [
66
"pi-package",
@@ -24,7 +24,8 @@
2424
"pack:check": "npm pack --dry-run"
2525
},
2626
"peerDependencies": {
27-
"@earendil-works/pi-coding-agent": "^0.84.2"
27+
"@earendil-works/pi-coding-agent": "^0.84.2",
28+
"@earendil-works/pi-tui": "^0.84.2"
2829
},
2930
"repository": {
3031
"type": "git",

packages/pi-notes/test/notes.test.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { tmpdir } from "node:os";
44
import { join } from "node:path";
55
import test, { type TestContext } from "node:test";
66
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
7-
import notesExtension, { parseNotes, renderNotes } from "../extensions/notes.ts";
7+
import notesExtension, { parseNotes, renderNotes, renderNotesWidget } from "../extensions/notes.ts";
88

99
type CommandHandler = (args: string, ctx: ExtensionContext) => Promise<void>;
1010
type Identity = { repository: string; worktree: string; gitDir: string };
@@ -52,8 +52,13 @@ async function harness(t: TestContext) {
5252
let select: ((options: string[]) => Promise<string | undefined>) | undefined;
5353
const context = (cwd: string) => ({
5454
cwd,
55+
mode: "tui",
5556
ui: {
56-
setWidget(_key: string, content: string[] | undefined) { widget = content; },
57+
setWidget(_key: string, content: unknown) {
58+
widget = typeof content === "function"
59+
? (content as () => { render(width: number): string[] })().render(80)
60+
: content as string[] | undefined;
61+
},
5762
notify(message: string) { notified = message; },
5863
select(_title: string, options: string[]) { return select ? select(options) : Promise.resolve(undefined); },
5964
},
@@ -97,6 +102,13 @@ test("renderNotes numbers entries", () => {
97102
assert.deepEqual(renderNotes([]), []);
98103
});
99104

105+
test("widget limits each note to two lines", () => {
106+
assert.deepEqual(
107+
renderNotesWidget(["short", "alpha beta gamma delta"], 12),
108+
["1. short", "2. alpha", "beta gamma…"],
109+
);
110+
});
111+
100112
test("notes persist independently per worktree without creating config on startup", async (t) => {
101113
const h = await harness(t);
102114
const firstCtx = h.context(h.first);

0 commit comments

Comments
 (0)