@henryqw/pi-notes 0.1.0 → 0.1.1
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 +2 -3
- package/extensions/notes.ts +31 -38
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,11 +15,10 @@ pi install npm:@henryqw/pi-notes
|
|
|
15
15
|
| `/note <text>` | command | Add a note for current Git worktree (max 4). |
|
|
16
16
|
| `/note-rm` | command | Pick a note from current worktree to remove. |
|
|
17
17
|
| `/note-clear` | command | Clear current worktree's notes. |
|
|
18
|
-
| `/note-prune` | command | Delete notes for repositories and worktrees that no longer exist. |
|
|
19
18
|
|
|
20
|
-
Notes are isolated per Git worktree, render as a numbered widget above editor, and persist across sessions under `~/.pi/agent/config/pi-notes/`.
|
|
19
|
+
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.
|
|
21
20
|
|
|
22
|
-
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
|
|
21
|
+
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`.
|
|
23
22
|
|
|
24
23
|
## Remove
|
|
25
24
|
|
package/extensions/notes.ts
CHANGED
|
@@ -69,7 +69,11 @@ export function parseNotes(raw: string): NotesRecord {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
export function renderNotes(notes: string[]): string[] {
|
|
72
|
-
return notes.
|
|
72
|
+
return notes.map((note, i) => `${i + 1}. ${note}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function setNotesWidget(ctx: ExtensionContext, notes: string[]): void {
|
|
76
|
+
ctx.ui.setWidget(WIDGET_KEY, notes.length ? renderNotes(notes) : undefined);
|
|
73
77
|
}
|
|
74
78
|
|
|
75
79
|
async function resolveWorktree(pi: ExtensionAPI, cwd: string): Promise<WorktreeIdentity> {
|
|
@@ -151,22 +155,22 @@ async function refresh(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
|
|
|
151
155
|
try {
|
|
152
156
|
const identity = await resolveWorktree(pi, ctx.cwd);
|
|
153
157
|
const loaded = await loadNotes(identity);
|
|
154
|
-
|
|
158
|
+
const message = issueMessage(loaded.issue);
|
|
159
|
+
if (message) ctx.ui.setWidget(WIDGET_KEY, [message]);
|
|
160
|
+
else setNotesWidget(ctx, loaded.notes);
|
|
155
161
|
} catch {
|
|
156
162
|
ctx.ui.setWidget(WIDGET_KEY, undefined);
|
|
157
163
|
}
|
|
158
164
|
}
|
|
159
165
|
|
|
160
|
-
async function pruneStale(pi: ExtensionAPI): Promise<
|
|
166
|
+
async function pruneStale(pi: ExtensionAPI): Promise<void> {
|
|
161
167
|
let entries;
|
|
162
168
|
try {
|
|
163
169
|
entries = await readdir(configDir(), { withFileTypes: true });
|
|
164
170
|
} catch (error) {
|
|
165
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") return
|
|
171
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
|
|
166
172
|
throw error;
|
|
167
173
|
}
|
|
168
|
-
let removed = 0;
|
|
169
|
-
let skipped = 0;
|
|
170
174
|
for (const entry of entries) {
|
|
171
175
|
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
172
176
|
const path = join(configDir(), entry.name);
|
|
@@ -174,13 +178,9 @@ async function pruneStale(pi: ExtensionAPI): Promise<{ removed: number; skipped:
|
|
|
174
178
|
try {
|
|
175
179
|
record = parseNotes(await readFile(path, "utf8"));
|
|
176
180
|
} catch {
|
|
177
|
-
skipped++;
|
|
178
|
-
continue;
|
|
179
|
-
}
|
|
180
|
-
if (path !== notesPath(record.worktree)) {
|
|
181
|
-
skipped++;
|
|
182
181
|
continue;
|
|
183
182
|
}
|
|
183
|
+
if (path !== notesPath(record.worktree)) continue;
|
|
184
184
|
let stale = false;
|
|
185
185
|
try {
|
|
186
186
|
const [worktree, repository, gitDir] = await Promise.all([
|
|
@@ -194,31 +194,34 @@ async function pruneStale(pi: ExtensionAPI): Promise<{ removed: number; skipped:
|
|
|
194
194
|
|| await worktreeGeneration(gitDir) !== record.generation;
|
|
195
195
|
} catch (error) {
|
|
196
196
|
if (isMissing(error)) stale = true;
|
|
197
|
-
else
|
|
198
|
-
skipped++;
|
|
199
|
-
continue;
|
|
200
|
-
}
|
|
197
|
+
else continue;
|
|
201
198
|
}
|
|
202
199
|
if (!stale) {
|
|
203
200
|
let current: WorktreeIdentity;
|
|
204
201
|
try {
|
|
205
202
|
current = await resolveWorktree(pi, record.worktree);
|
|
206
203
|
} catch {
|
|
207
|
-
skipped++;
|
|
208
204
|
continue;
|
|
209
205
|
}
|
|
210
206
|
stale = !isDeepStrictEqual(current, recordIdentity(record));
|
|
211
207
|
}
|
|
212
|
-
if (stale) {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
208
|
+
if (stale) await rm(path, { force: true });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function pruneStaleNotes(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
|
|
213
|
+
try {
|
|
214
|
+
await pruneStale(pi);
|
|
215
|
+
} catch (error) {
|
|
216
|
+
ctx.ui.notify(`Failed to prune stale notes: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
216
217
|
}
|
|
217
|
-
return { removed, skipped };
|
|
218
218
|
}
|
|
219
219
|
|
|
220
220
|
export default function notesExtension(pi: ExtensionAPI): void {
|
|
221
|
-
pi.on("session_start", (_event, ctx) =>
|
|
221
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
222
|
+
await pruneStaleNotes(pi, ctx);
|
|
223
|
+
await refresh(pi, ctx);
|
|
224
|
+
});
|
|
222
225
|
|
|
223
226
|
pi.registerCommand("note", {
|
|
224
227
|
description: `Add a note to this Git worktree (max ${MAX_NOTES})`,
|
|
@@ -240,7 +243,8 @@ export default function notesExtension(pi: ExtensionAPI): void {
|
|
|
240
243
|
}
|
|
241
244
|
current.notes.push(text);
|
|
242
245
|
await persist(current.identity, current.notes);
|
|
243
|
-
ctx
|
|
246
|
+
setNotesWidget(ctx, current.notes);
|
|
247
|
+
await pruneStaleNotes(pi, ctx);
|
|
244
248
|
},
|
|
245
249
|
});
|
|
246
250
|
|
|
@@ -268,7 +272,8 @@ export default function notesExtension(pi: ExtensionAPI): void {
|
|
|
268
272
|
}
|
|
269
273
|
current.notes.splice(index, 1);
|
|
270
274
|
await persist(current.identity, current.notes);
|
|
271
|
-
ctx
|
|
275
|
+
setNotesWidget(ctx, current.notes);
|
|
276
|
+
await pruneStaleNotes(pi, ctx);
|
|
272
277
|
},
|
|
273
278
|
});
|
|
274
279
|
|
|
@@ -282,23 +287,11 @@ export default function notesExtension(pi: ExtensionAPI): void {
|
|
|
282
287
|
try {
|
|
283
288
|
const identity = await resolveWorktree(pi, ctx.cwd);
|
|
284
289
|
await rm(notesPath(identity.worktree), { force: true });
|
|
285
|
-
ctx
|
|
290
|
+
setNotesWidget(ctx, []);
|
|
291
|
+
await pruneStaleNotes(pi, ctx);
|
|
286
292
|
} catch (error) {
|
|
287
293
|
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
288
294
|
}
|
|
289
295
|
},
|
|
290
296
|
});
|
|
291
|
-
|
|
292
|
-
pi.registerCommand("note-prune", {
|
|
293
|
-
description: "Delete notes for removed repositories and worktrees",
|
|
294
|
-
handler: async (args, ctx) => {
|
|
295
|
-
if (args.trim()) {
|
|
296
|
-
ctx.ui.notify("Usage: /note-prune", "warning");
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
|
-
const { removed, skipped } = await pruneStale(pi);
|
|
300
|
-
ctx.ui.notify(`Removed ${removed} stale worktree note file${removed === 1 ? "" : "s"}; preserved ${skipped} unchecked or invalid file${skipped === 1 ? "" : "s"}.`, "info");
|
|
301
|
-
await refresh(pi, ctx);
|
|
302
|
-
},
|
|
303
|
-
});
|
|
304
297
|
}
|