pi-anchor 0.4.0 → 0.5.0
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 +35 -34
- package/README_zh.md +33 -37
- package/assets/hero-en.svg +47 -30
- package/assets/hero-zh.svg +45 -28
- package/bin/anchor.js +39 -8
- package/dist/{chunk-LKY6WIKH.js → chunk-VXEQDTK4.js} +270 -60
- package/dist/{extension-B1-fzCox.d.cts → extension-Cq1NH1jT.d.cts} +42 -4
- package/dist/{extension-B1-fzCox.d.ts → extension-Cq1NH1jT.d.ts} +42 -4
- package/dist/extension.cjs +269 -60
- package/dist/extension.d.cts +1 -1
- package/dist/extension.d.ts +1 -1
- package/dist/extension.js +3 -1
- package/dist/index.cjs +156 -30
- package/dist/index.d.cts +15 -10
- package/dist/index.d.ts +15 -10
- package/dist/index.js +7 -1
- package/package.json +2 -2
- package/src/extension.ts +169 -42
- package/src/pi-types.d.ts +5 -1
- package/src/store.ts +93 -5
- package/src/tui.ts +89 -35
- package/src/types.ts +14 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/extension.ts
|
|
2
2
|
import { Type } from "@sinclair/typebox";
|
|
3
|
+
import fs3 from "fs";
|
|
3
4
|
|
|
4
5
|
// src/store.ts
|
|
5
6
|
import fs from "fs";
|
|
@@ -53,6 +54,32 @@ function extractPathsFromText(text) {
|
|
|
53
54
|
}
|
|
54
55
|
return Array.from(results);
|
|
55
56
|
}
|
|
57
|
+
function extractProgressFromText(rawTitle) {
|
|
58
|
+
if (!rawTitle) return { cleanTitle: "" };
|
|
59
|
+
const stepParts = Array.from(rawTitle.matchAll(/(?:^|\s)(\d+)\.\s*([^\d]+?)(?=\s+\d+\.\s|$)/g));
|
|
60
|
+
if (stepParts.length >= 2) {
|
|
61
|
+
const steps = stepParts.map((m) => m[2].trim().replace(/[;;,,]+$/, "")).filter(Boolean);
|
|
62
|
+
const firstMatchIdx = rawTitle.indexOf(stepParts[0][0]);
|
|
63
|
+
const prefix = rawTitle.slice(0, firstMatchIdx).replace(/[::\-—\s]+$/, "").trim();
|
|
64
|
+
return {
|
|
65
|
+
cleanTitle: prefix || steps.join(" \u2192 "),
|
|
66
|
+
progress: { done: 0, total: steps.length, steps }
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const ratioMatch = rawTitle.match(/(?:\[(\d{1,3})\/(\d{1,3})\]|\((\d{1,3})\/(\d{1,3})\)|(?:^|\s)(\d{1,3})\/(\d{1,3})(?=\s|$))/);
|
|
70
|
+
if (ratioMatch) {
|
|
71
|
+
const done = parseInt(ratioMatch[1] ?? ratioMatch[3] ?? ratioMatch[5], 10);
|
|
72
|
+
const total = parseInt(ratioMatch[2] ?? ratioMatch[4] ?? ratioMatch[6], 10);
|
|
73
|
+
if (!isNaN(done) && !isNaN(total) && total >= 2 && total <= 500 && done >= 0 && done <= total) {
|
|
74
|
+
const stripped = rawTitle.replace(ratioMatch[0], " ").replace(/\s+/g, " ").trim();
|
|
75
|
+
return {
|
|
76
|
+
cleanTitle: stripped || rawTitle.trim(),
|
|
77
|
+
progress: { done, total }
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return { cleanTitle: rawTitle.trim() };
|
|
82
|
+
}
|
|
56
83
|
function isAnchorTargetingWorkspace(anchor, targetCwd) {
|
|
57
84
|
if (!targetCwd) return true;
|
|
58
85
|
const normTarget = targetCwd.trim().replace(/\\/g, "/").replace(/\/+$/, "");
|
|
@@ -195,8 +222,9 @@ function renderColdStartAnchorsContext(store, cwdOrNow, nowArg) {
|
|
|
195
222
|
const lines = activeNotes.map((a) => {
|
|
196
223
|
const num = a.id.replace(/^anc-/, "");
|
|
197
224
|
const files = a.files.length > 0 ? ` (${a.files.slice(0, 2).join(", ")})` : "";
|
|
225
|
+
const prog = a.progress ? ` [${a.progress.done}/${a.progress.total}${a.progress.steps && a.progress.steps[a.progress.done] ? ` \xB7 next: ${a.progress.steps[a.progress.done]}` : ""}]` : "";
|
|
198
226
|
const desc = a.description ? ` \u2014 ${a.description}` : "";
|
|
199
|
-
return `\u2022 #${num} [${a.project}] ${a.title}${files}${desc}`;
|
|
227
|
+
return `\u2022 #${num} [${a.project}] ${a.title}${prog}${files}${desc}`;
|
|
200
228
|
});
|
|
201
229
|
return [
|
|
202
230
|
`<sticky-notes count="${activeNotes.length}">`,
|
|
@@ -371,8 +399,9 @@ var AnchorStore = class {
|
|
|
371
399
|
const state = this.loadState();
|
|
372
400
|
const now = Date.now();
|
|
373
401
|
const id = this.generateId(state);
|
|
402
|
+
const { cleanTitle, progress: parsedProgress } = extractProgressFromText(input.title);
|
|
374
403
|
const explicitFiles = (input.files || []).map((f) => normalizePath(f)).filter(Boolean);
|
|
375
|
-
const inferredFiles = explicitFiles.length > 0 ? explicitFiles : extractPathsFromText(`${
|
|
404
|
+
const inferredFiles = explicitFiles.length > 0 ? explicitFiles : extractPathsFromText(`${cleanTitle} ${input.description || ""}`);
|
|
376
405
|
let cwd = input.cwd ? normalizePath(input.cwd) : "";
|
|
377
406
|
if (input.project && (/^[a-zA-Z]:[\\/]/.test(input.project.trim()) || input.project.trim().startsWith("/"))) {
|
|
378
407
|
cwd = normalizePath(input.project.trim());
|
|
@@ -383,7 +412,7 @@ var AnchorStore = class {
|
|
|
383
412
|
const projectName = input.project ? input.project.trim() : cwd ? path.basename(cwd) : "global";
|
|
384
413
|
const anchor = {
|
|
385
414
|
id,
|
|
386
|
-
title:
|
|
415
|
+
title: cleanTitle,
|
|
387
416
|
description: input.description?.trim(),
|
|
388
417
|
priority: input.priority || "p1",
|
|
389
418
|
status: "active",
|
|
@@ -393,7 +422,9 @@ var AnchorStore = class {
|
|
|
393
422
|
updatedAt: now,
|
|
394
423
|
lastTouchedAt: now,
|
|
395
424
|
files: inferredFiles,
|
|
396
|
-
tags: (input.tags || []).map((t) => t.trim()).filter(Boolean)
|
|
425
|
+
tags: (input.tags || []).map((t) => t.trim()).filter(Boolean),
|
|
426
|
+
...input.sessionFile ? { sessionFile: normalizePath(input.sessionFile) } : {},
|
|
427
|
+
...input.progress || parsedProgress ? { progress: input.progress || parsedProgress } : {}
|
|
397
428
|
};
|
|
398
429
|
state.anchors.push(anchor);
|
|
399
430
|
this.saveState(state);
|
|
@@ -442,7 +473,7 @@ var AnchorStore = class {
|
|
|
442
473
|
return updated;
|
|
443
474
|
});
|
|
444
475
|
}
|
|
445
|
-
touch(id, timestamp = Date.now()) {
|
|
476
|
+
touch(id, timestamp = Date.now(), sessionFile) {
|
|
446
477
|
return this.withLock(() => {
|
|
447
478
|
const state = this.loadState();
|
|
448
479
|
const idx = this.findAnchorIndex(state.anchors, id);
|
|
@@ -453,11 +484,44 @@ var AnchorStore = class {
|
|
|
453
484
|
if (current.status === "sleeping") {
|
|
454
485
|
current.status = "active";
|
|
455
486
|
}
|
|
487
|
+
if (sessionFile) {
|
|
488
|
+
current.sessionFile = normalizePath(sessionFile);
|
|
489
|
+
}
|
|
456
490
|
state.anchors[idx] = current;
|
|
457
491
|
this.saveState(state);
|
|
458
492
|
return current;
|
|
459
493
|
});
|
|
460
494
|
}
|
|
495
|
+
/**
|
|
496
|
+
* Advance (or set) progress on a sticky note:
|
|
497
|
+
* - If delta is provided (default +1), increments `done` up to `total`.
|
|
498
|
+
* - If note had no progress yet, initializes a 4-step bar (`1/4`).
|
|
499
|
+
*/
|
|
500
|
+
step(id, delta = 1, explicitTotal) {
|
|
501
|
+
return this.withLock(() => {
|
|
502
|
+
const state = this.loadState();
|
|
503
|
+
const idx = this.findAnchorIndex(state.anchors, id);
|
|
504
|
+
if (idx === -1) throw new Error(`Note not found: ${id}`);
|
|
505
|
+
const current = state.anchors[idx];
|
|
506
|
+
const now = Date.now();
|
|
507
|
+
const total = explicitTotal && explicitTotal >= 2 ? explicitTotal : current.progress?.total || 4;
|
|
508
|
+
const prevDone = current.progress?.done || 0;
|
|
509
|
+
const nextDone = Math.max(0, Math.min(total, prevDone + delta));
|
|
510
|
+
current.progress = {
|
|
511
|
+
...current.progress || {},
|
|
512
|
+
done: nextDone,
|
|
513
|
+
total
|
|
514
|
+
};
|
|
515
|
+
current.lastTouchedAt = now;
|
|
516
|
+
current.updatedAt = now;
|
|
517
|
+
if (current.status === "sleeping") {
|
|
518
|
+
current.status = "active";
|
|
519
|
+
}
|
|
520
|
+
state.anchors[idx] = current;
|
|
521
|
+
this.saveState(state);
|
|
522
|
+
return { anchor: current, completed: nextDone >= total };
|
|
523
|
+
});
|
|
524
|
+
}
|
|
461
525
|
/**
|
|
462
526
|
* Tear off (settle) a completed sticky note and archive it to archive.jsonl
|
|
463
527
|
*/
|
|
@@ -528,6 +592,7 @@ var AnchorStore = class {
|
|
|
528
592
|
};
|
|
529
593
|
|
|
530
594
|
// src/tui.ts
|
|
595
|
+
import fs2 from "fs";
|
|
531
596
|
import path2 from "path";
|
|
532
597
|
var ANSI_REGEX = /\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g;
|
|
533
598
|
function stripAnsi(str) {
|
|
@@ -594,6 +659,14 @@ function padToWidth(str, targetWidth) {
|
|
|
594
659
|
if (current >= targetWidth) return truncated;
|
|
595
660
|
return truncated + " ".repeat(targetWidth - current);
|
|
596
661
|
}
|
|
662
|
+
function formatProgressBar(progress, cells = 4) {
|
|
663
|
+
if (!progress || !progress.total || progress.total < 1) return "";
|
|
664
|
+
const done = Math.max(0, Math.min(progress.total, progress.done || 0));
|
|
665
|
+
const filled = Math.min(cells, Math.max(done > 0 ? 1 : 0, Math.round(done / progress.total * cells)));
|
|
666
|
+
const finalFilled = done >= progress.total ? cells : done === 0 ? 0 : filled;
|
|
667
|
+
const bar = "\u25A0".repeat(finalFilled) + "\u25A1".repeat(Math.max(0, cells - finalFilled));
|
|
668
|
+
return `[${bar} ${done}/${progress.total}]`;
|
|
669
|
+
}
|
|
597
670
|
function formatRelativeAge(timestamp, now = Date.now()) {
|
|
598
671
|
const diffSec = Math.max(0, Math.floor((now - timestamp) / 1e3));
|
|
599
672
|
if (diffSec < 60) return "just now";
|
|
@@ -647,6 +720,7 @@ function updateAnchorStatusBar(ctx, store) {
|
|
|
647
720
|
try {
|
|
648
721
|
const active = store.list({ status: "active", cwd: ctx.cwd });
|
|
649
722
|
ctx.ui.setStatus("anchor", active.length > 0 ? `\u2316 ${active.length}` : void 0);
|
|
723
|
+
updateStartupBanner(ctx, store);
|
|
650
724
|
} catch {
|
|
651
725
|
}
|
|
652
726
|
}
|
|
@@ -655,41 +729,41 @@ function updateStartupBanner(ctx, store) {
|
|
|
655
729
|
try {
|
|
656
730
|
const active = store.list({ status: "active", cwd: ctx.cwd });
|
|
657
731
|
if (active.length === 0) {
|
|
658
|
-
ctx.ui.setWidget("anchor-startup", void 0);
|
|
732
|
+
ctx.ui.setWidget("anchor-startup", void 0, { placement: "belowEditor" });
|
|
659
733
|
return;
|
|
660
734
|
}
|
|
661
735
|
const sorted = sortAnchorsForDisplay(active, ctx.cwd);
|
|
662
|
-
const topItems = sorted.slice(0, 3);
|
|
663
736
|
const factory = (_tui, theme) => ({
|
|
664
737
|
render: (width) => {
|
|
665
|
-
const maxTitleWidth = Math.max(14, width - 34);
|
|
666
738
|
const fg = theme?.fg ? theme.fg.bind(theme) : (_c, text) => text;
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
const num = a.id.replace(/^anc-/, "").padStart(2, "0");
|
|
739
|
+
if (sorted.length === 1) {
|
|
740
|
+
const a = sorted[0];
|
|
741
|
+
const num = a.id.replace(/^anc-/, "");
|
|
742
|
+
const bar = a.progress ? `${fg("accent", formatProgressBar(a.progress))} ` : "";
|
|
743
|
+
const nextStep = a.progress?.steps && a.progress.steps[a.progress.done] ? fg("muted", ` \u25B8 ${a.progress.steps[a.progress.done]}`) : "";
|
|
673
744
|
const origin = formatOrigin(a);
|
|
674
|
-
const
|
|
675
|
-
const
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
)
|
|
681
|
-
);
|
|
745
|
+
const sessionIcon = a.sessionFile ? " \u21AA" : "";
|
|
746
|
+
const prefix = `${fg("accent", "\u2316")} ${fg("dim", `#${num}`)} ${bar}`;
|
|
747
|
+
const suffix = ` ${fg("muted", `(${origin}${sessionIcon})`)}`;
|
|
748
|
+
const availTitle = Math.max(12, width - getDisplayWidth(prefix) - getDisplayWidth(nextStep) - getDisplayWidth(suffix) - 2);
|
|
749
|
+
const titleClipped = truncateToWidth(a.title, availTitle);
|
|
750
|
+
return [truncateToWidth(`${prefix}${titleClipped}${nextStep}${suffix}`, width)];
|
|
682
751
|
}
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
752
|
+
const countTag = fg("accent", `\u2316 ${sorted.length} notes: `);
|
|
753
|
+
const segments = sorted.slice(0, 3).map((a) => {
|
|
754
|
+
const num = a.id.replace(/^anc-/, "");
|
|
755
|
+
const bar = a.progress ? `${formatProgressBar(a.progress)} ` : "";
|
|
756
|
+
const shortTitle = truncateToWidth(a.title, Math.max(14, Math.floor((width - 20) / Math.min(3, sorted.length)) - 12));
|
|
757
|
+
return `${fg("dim", `#${num}`)} ${bar}${shortTitle}`;
|
|
758
|
+
});
|
|
759
|
+
const more = sorted.length > 3 ? fg("dim", ` +${sorted.length - 3}`) : "";
|
|
760
|
+
return [truncateToWidth(`${countTag}${segments.join(fg("dim", " \xB7 "))}${more}`, width)];
|
|
687
761
|
},
|
|
688
762
|
invalidate: () => {
|
|
689
763
|
}
|
|
690
764
|
});
|
|
691
765
|
const content = ctx.ui?.theme ? factory : factory().render(80);
|
|
692
|
-
ctx.ui.setWidget("anchor-startup", content, { placement: "
|
|
766
|
+
ctx.ui.setWidget("anchor-startup", content, { placement: "belowEditor" });
|
|
693
767
|
} catch {
|
|
694
768
|
}
|
|
695
769
|
}
|
|
@@ -697,7 +771,7 @@ async function openAnchorDashboard(ctx, store) {
|
|
|
697
771
|
if (!hasActiveUI(ctx)) return;
|
|
698
772
|
const allNotes = store.list({ cwd: ctx.cwd, all: true }).filter((a) => a.status === "active" || a.status === "sleeping");
|
|
699
773
|
if (allNotes.length === 0) {
|
|
700
|
-
ctx.ui.notify("\u2316 No sticky notes.
|
|
774
|
+
ctx.ui.notify("\u2316 No sticky notes. Type `pin: <note>` or `/pin <note>` to stick one.", "info");
|
|
701
775
|
return;
|
|
702
776
|
}
|
|
703
777
|
const sorted = sortAnchorsForDisplay(allNotes, ctx.cwd);
|
|
@@ -708,10 +782,11 @@ async function openAnchorDashboard(ctx, store) {
|
|
|
708
782
|
const titleWidth = isNarrow ? Math.max(20, termCols - 30) : 42;
|
|
709
783
|
for (const a of sorted) {
|
|
710
784
|
const num = a.id.replace(/^anc-/, "").padStart(2, "0");
|
|
711
|
-
const
|
|
785
|
+
const bar = a.progress ? `${formatProgressBar(a.progress)} ` : "";
|
|
786
|
+
const origin = formatOrigin(a) + (a.sessionFile ? " \u21AA" : "");
|
|
712
787
|
const age = a.status === "sleeping" ? `${formatRelativeAge(a.createdAt)} \xB7 quiet` : formatRelativeAge(a.createdAt);
|
|
713
788
|
const colNum = `${num} `;
|
|
714
|
-
const colTitle = padToWidth(a.title
|
|
789
|
+
const colTitle = padToWidth(`${bar}${a.title}`, titleWidth);
|
|
715
790
|
const colOrigin = padToWidth(origin, 22);
|
|
716
791
|
const label = isNarrow ? `${colNum}${colTitle} ${age}`.trimEnd() : `${colNum}${colTitle} ${colOrigin} ${age}`.trimEnd();
|
|
717
792
|
optionMap.set(label, a);
|
|
@@ -721,8 +796,15 @@ async function openAnchorDashboard(ctx, store) {
|
|
|
721
796
|
if (!selected) return;
|
|
722
797
|
const anchor = optionMap.get(selected);
|
|
723
798
|
if (!anchor) return;
|
|
799
|
+
const currentSessionFile = ctx.sessionManager?.getSessionFile?.();
|
|
800
|
+
const canJumpSession = Boolean(
|
|
801
|
+
anchor.sessionFile && fs2.existsSync(anchor.sessionFile) && typeof ctx.switchSession === "function" && (!currentSessionFile || path2.resolve(anchor.sessionFile) !== path2.resolve(currentSessionFile))
|
|
802
|
+
);
|
|
803
|
+
const stepLabel = anchor.progress ? `\u25B6 Advance step (${anchor.progress.done}/${anchor.progress.total} \u2794 ${Math.min(anchor.progress.total, anchor.progress.done + 1)}/${anchor.progress.total})` : "\u25B6 Start progress bar (1/4)";
|
|
724
804
|
const actions = [
|
|
725
805
|
"\u2713 Done (tear off note)",
|
|
806
|
+
stepLabel,
|
|
807
|
+
...canJumpSession ? ["\u21AA Jump to session (resume linked conversation)"] : [],
|
|
726
808
|
...anchor.status === "sleeping" ? ["\u26A1 Wake up (mark active)"] : [],
|
|
727
809
|
"\u2139 Read full note",
|
|
728
810
|
"\u21A9 Back"
|
|
@@ -733,6 +815,23 @@ async function openAnchorDashboard(ctx, store) {
|
|
|
733
815
|
store.settle(anchor.id, { settledBy: "manual" });
|
|
734
816
|
ctx.ui.notify(`\u2316 Torn off #${anchor.id.replace(/^anc-/, "")}: "${anchor.title}" (/anchor undo to put back)`, "info");
|
|
735
817
|
updateAnchorStatusBar(ctx, store);
|
|
818
|
+
} else if (action.startsWith("\u25B6")) {
|
|
819
|
+
const { anchor: updated, completed } = store.step(anchor.id, 1);
|
|
820
|
+
updateAnchorStatusBar(ctx, store);
|
|
821
|
+
if (completed) {
|
|
822
|
+
ctx.ui.notify(
|
|
823
|
+
`\u2316 #${updated.id.replace(/^anc-/, "")} reached ${formatProgressBar(updated.progress)}! Run /done ${updated.id.replace(/^anc-/, "")} to tear off.`,
|
|
824
|
+
"info"
|
|
825
|
+
);
|
|
826
|
+
} else {
|
|
827
|
+
ctx.ui.notify(`\u2316 Advanced #${updated.id.replace(/^anc-/, "")} to ${formatProgressBar(updated.progress)}`, "info");
|
|
828
|
+
}
|
|
829
|
+
} else if (action.startsWith("\u21AA") && anchor.sessionFile) {
|
|
830
|
+
try {
|
|
831
|
+
await ctx.switchSession(anchor.sessionFile);
|
|
832
|
+
} catch (err) {
|
|
833
|
+
ctx.ui.notify(`Could not switch session: ${err.message}`, "error");
|
|
834
|
+
}
|
|
736
835
|
} else if (action.startsWith("\u26A1")) {
|
|
737
836
|
store.touch(anchor.id);
|
|
738
837
|
ctx.ui.notify(`\u2316 Woke up #${anchor.id.replace(/^anc-/, "")}: "${anchor.title}"`, "info");
|
|
@@ -740,8 +839,10 @@ async function openAnchorDashboard(ctx, store) {
|
|
|
740
839
|
} else if (action.startsWith("\u2139")) {
|
|
741
840
|
const details = [
|
|
742
841
|
`\u2316 Note #${anchor.id.replace(/^anc-/, "")}: ${anchor.title}`,
|
|
842
|
+
anchor.progress ? `Progress: ${formatProgressBar(anchor.progress)}${anchor.progress.steps ? ` (${anchor.progress.steps.join(" \u2192 ")})` : ""}` : null,
|
|
743
843
|
anchor.description ? `Details: ${anchor.description}` : null,
|
|
744
844
|
anchor.files.length > 0 ? `Files: ${anchor.files.join(", ")}` : null,
|
|
845
|
+
anchor.sessionFile ? `Session: ${path2.basename(anchor.sessionFile)}` : null,
|
|
745
846
|
`Workspace: ${anchor.project || "global"} \xB7 Pinned ${formatRelativeAge(anchor.createdAt)}`
|
|
746
847
|
].filter(Boolean).join("\n");
|
|
747
848
|
ctx.ui.notify(details, "info");
|
|
@@ -765,11 +866,30 @@ function parseAnchorCommandInput(raw) {
|
|
|
765
866
|
targetId: settleMatch[1].trim().replace(/^#/, "")
|
|
766
867
|
};
|
|
767
868
|
}
|
|
869
|
+
const stepMatch = trimmed.match(/^(?:step|\+)\s*(\S+)(?:\s+([+-]?\d+))?$/i);
|
|
870
|
+
if (stepMatch) {
|
|
871
|
+
return {
|
|
872
|
+
mode: "step",
|
|
873
|
+
targetId: stepMatch[1].trim().replace(/^#/, ""),
|
|
874
|
+
delta: stepMatch[2] ? parseInt(stepMatch[2], 10) : 1
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
const jumpMatch = trimmed.match(/^(?:jump|resume|session)\s+(.+)$/i);
|
|
878
|
+
if (jumpMatch) {
|
|
879
|
+
return {
|
|
880
|
+
mode: "jump",
|
|
881
|
+
targetId: jumpMatch[1].trim().replace(/^#/, "")
|
|
882
|
+
};
|
|
883
|
+
}
|
|
768
884
|
return {
|
|
769
885
|
mode: "pin",
|
|
770
886
|
title: trimmed
|
|
771
887
|
};
|
|
772
888
|
}
|
|
889
|
+
function parseQuickNoteInput(text) {
|
|
890
|
+
const m = (text || "").trim().match(/^(?:pin|note|便签|备忘)\s*[::]\s*(.+)$/i);
|
|
891
|
+
return m && m[1].trim() ? m[1].trim() : null;
|
|
892
|
+
}
|
|
773
893
|
function resolveAnchorTarget(store, idOrIndex, cwd) {
|
|
774
894
|
const clean = idOrIndex.trim().replace(/^#/, "");
|
|
775
895
|
if (/^\d+$/.test(clean)) {
|
|
@@ -782,33 +902,60 @@ function resolveAnchorTarget(store, idOrIndex, cwd) {
|
|
|
782
902
|
}
|
|
783
903
|
return store.get(clean)?.id;
|
|
784
904
|
}
|
|
905
|
+
function getSessionFileSafe(ctx) {
|
|
906
|
+
try {
|
|
907
|
+
const file = ctx?.sessionManager?.getSessionFile?.();
|
|
908
|
+
return typeof file === "string" && file.trim() ? file.trim() : void 0;
|
|
909
|
+
} catch {
|
|
910
|
+
return void 0;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
785
913
|
function extension_default(pi) {
|
|
786
914
|
const store = new AnchorStore();
|
|
787
915
|
const annotatedThisSession = /* @__PURE__ */ new Set();
|
|
788
|
-
|
|
916
|
+
const recentSessionFiles = [];
|
|
917
|
+
function rememberTouchedFile(filePath, cwd) {
|
|
918
|
+
const norm = normalizePath(filePath, cwd);
|
|
919
|
+
if (!norm) return;
|
|
920
|
+
const idx = recentSessionFiles.indexOf(norm);
|
|
921
|
+
if (idx !== -1) recentSessionFiles.splice(idx, 1);
|
|
922
|
+
recentSessionFiles.unshift(norm);
|
|
923
|
+
if (recentSessionFiles.length > 5) recentSessionFiles.pop();
|
|
924
|
+
}
|
|
925
|
+
function createStickyNoteFromUser(title, ctx) {
|
|
926
|
+
const sessionFile = getSessionFileSafe(ctx);
|
|
927
|
+
const anc = store.create({
|
|
928
|
+
title,
|
|
929
|
+
cwd: ctx.cwd,
|
|
930
|
+
sessionFile
|
|
931
|
+
});
|
|
932
|
+
if (anc.files.length === 0 && recentSessionFiles.length > 0) {
|
|
933
|
+
store.update(anc.id, { files: [recentSessionFiles[0]] });
|
|
934
|
+
}
|
|
935
|
+
updateAnchorStatusBar(ctx, store);
|
|
936
|
+
return store.get(anc.id) || anc;
|
|
937
|
+
}
|
|
938
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
789
939
|
annotatedThisSession.clear();
|
|
940
|
+
recentSessionFiles.length = 0;
|
|
790
941
|
sweepQuietNotes(store);
|
|
791
942
|
const quietForHere = store.list({ status: "sleeping", all: true }).filter((a) => (a.files.length > 0 || a.project && a.project !== "global" || a.cwd) && isAnchorTargetingWorkspace(a, ctx.cwd));
|
|
792
943
|
for (const q of quietForHere) {
|
|
793
944
|
store.touch(q.id);
|
|
794
945
|
}
|
|
795
946
|
updateAnchorStatusBar(ctx, store);
|
|
796
|
-
|
|
797
|
-
updateStartupBanner(ctx, store);
|
|
798
|
-
} else if (hasActiveUI(ctx)) {
|
|
799
|
-
try {
|
|
800
|
-
ctx.ui.setWidget("anchor-startup", void 0);
|
|
801
|
-
} catch {
|
|
802
|
-
}
|
|
803
|
-
}
|
|
947
|
+
updateStartupBanner(ctx, store);
|
|
804
948
|
});
|
|
805
|
-
pi.on("
|
|
949
|
+
pi.on("input", async (event, ctx) => {
|
|
950
|
+
if (event?.source === "extension") return { action: "continue" };
|
|
951
|
+
const quickNote = parseQuickNoteInput(String(event?.text || ""));
|
|
952
|
+
if (!quickNote) return { action: "continue" };
|
|
953
|
+
const anc = createStickyNoteFromUser(quickNote, ctx);
|
|
954
|
+
const bar = anc.progress ? ` ${formatProgressBar(anc.progress)}` : "";
|
|
806
955
|
if (hasActiveUI(ctx)) {
|
|
807
|
-
|
|
808
|
-
ctx.ui.setWidget("anchor-startup", void 0);
|
|
809
|
-
} catch {
|
|
810
|
-
}
|
|
956
|
+
ctx.ui.notify(`\u2316 Pinned #${anc.id.replace(/^anc-/, "")}${bar}: "${anc.title}"`, "info");
|
|
811
957
|
}
|
|
958
|
+
return { action: "handled" };
|
|
812
959
|
});
|
|
813
960
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
814
961
|
const entries = ctx.sessionManager?.getEntries() || [];
|
|
@@ -825,6 +972,7 @@ ${snippet}` };
|
|
|
825
972
|
pi.on("tool_result", async (event, ctx) => {
|
|
826
973
|
if (event.isError) return;
|
|
827
974
|
const lowerTool = String(event.toolName || "").toLowerCase();
|
|
975
|
+
const currentSessionFile = getSessionFileSafe(ctx);
|
|
828
976
|
if (lowerTool === "bash" || lowerTool === "powershell") {
|
|
829
977
|
const cmd = String(event.input?.command || "");
|
|
830
978
|
if (cmd.includes("git commit")) {
|
|
@@ -849,12 +997,18 @@ ${snippet}` };
|
|
|
849
997
|
}
|
|
850
998
|
const pathInput = event.input?.path;
|
|
851
999
|
if (typeof pathInput !== "string") return;
|
|
1000
|
+
const isMutation = MUTATION_TOOLS.has(lowerTool);
|
|
1001
|
+
if (isMutation) {
|
|
1002
|
+
rememberTouchedFile(pathInput, ctx.cwd);
|
|
1003
|
+
}
|
|
852
1004
|
const touchedPath = normalizePath(pathInput, ctx.cwd);
|
|
853
1005
|
const notes = store.list({ cwd: ctx.cwd }).filter((a) => a.status === "active" || a.status === "sleeping");
|
|
854
1006
|
const matches = findMatchedAnchors(notes, [touchedPath]);
|
|
855
1007
|
if (matches.length === 0) return;
|
|
856
|
-
if (
|
|
857
|
-
for (const m of matches)
|
|
1008
|
+
if (isMutation) {
|
|
1009
|
+
for (const m of matches) {
|
|
1010
|
+
store.touch(m.anchor.id, Date.now(), currentSessionFile);
|
|
1011
|
+
}
|
|
858
1012
|
updateAnchorStatusBar(ctx, store);
|
|
859
1013
|
}
|
|
860
1014
|
const targetNote = matches[0].anchor;
|
|
@@ -876,10 +1030,10 @@ ${snippet}` };
|
|
|
876
1030
|
pi.registerTool({
|
|
877
1031
|
name: "anchor",
|
|
878
1032
|
label: "Anchor (Sticky Note)",
|
|
879
|
-
description: "Pin, list, or tear off cross-session sticky notes stored in ~/.anchor/ (zero git repo clutter). Actions: pin (stick a new note), list (view notes), settle (tear off completed note).",
|
|
1033
|
+
description: "Pin, list, step, or tear off cross-session sticky notes stored in ~/.anchor/ (zero git repo clutter). Actions: pin (stick a new note), list (view notes), step (advance progress bar), settle (tear off completed note).",
|
|
880
1034
|
promptSnippet: "Pin or tear off cross-session sticky notes that survive terminal restarts",
|
|
881
1035
|
promptGuidelines: [
|
|
882
|
-
|
|
1036
|
+
"Use `anchor` when the user explicitly asks to remember something for later/tomorrow or across sessions.",
|
|
883
1037
|
"BOUNDARY WITH TODO: `todo` is for multi-step work right now in this session; `anchor` is a sticky note for future sessions.",
|
|
884
1038
|
'When a pinned note is completed, call `anchor` with action "settle" to tear it off.'
|
|
885
1039
|
],
|
|
@@ -887,22 +1041,23 @@ ${snippet}` };
|
|
|
887
1041
|
action: Type.Union([
|
|
888
1042
|
Type.Literal("pin"),
|
|
889
1043
|
Type.Literal("list"),
|
|
1044
|
+
Type.Literal("step"),
|
|
890
1045
|
Type.Literal("settle"),
|
|
891
1046
|
Type.Literal("touch"),
|
|
892
1047
|
Type.Literal("sweep")
|
|
893
1048
|
]),
|
|
894
|
-
title: Type.Optional(Type.String({ description: "Sticky note text (
|
|
1049
|
+
title: Type.Optional(Type.String({ description: "Sticky note text (supports [1/4] or 1. A 2. B progress steps)" })),
|
|
895
1050
|
description: Type.Optional(Type.String({ description: "Optional extra details" })),
|
|
896
1051
|
project: Type.Optional(Type.String({ description: "Optional project name or folder" })),
|
|
897
1052
|
files: Type.Optional(Type.Array(Type.String(), { description: "Optional associated file paths" })),
|
|
898
|
-
id: Type.Optional(Type.String({ description: "Note ID, e.g. anc-1 or 1 (for settle)" }))
|
|
1053
|
+
id: Type.Optional(Type.String({ description: "Note ID, e.g. anc-1 or 1 (for step/settle)" }))
|
|
899
1054
|
}),
|
|
900
1055
|
renderCall(args, theme) {
|
|
901
1056
|
const action = args.action || "list";
|
|
902
1057
|
let title = theme.fg("toolTitle", theme.bold("\u2316 anchor ")) + theme.fg("accent", action);
|
|
903
1058
|
if (action === "pin" && args.title) {
|
|
904
1059
|
title += ` "${args.title}"`;
|
|
905
|
-
} else if ((action === "settle" || action === "touch") && args.id) {
|
|
1060
|
+
} else if ((action === "settle" || action === "touch" || action === "step") && args.id) {
|
|
906
1061
|
title += ` ${theme.fg("dim", `#${String(args.id).replace(/^anc-/, "")}`)}`;
|
|
907
1062
|
}
|
|
908
1063
|
return {
|
|
@@ -932,7 +1087,8 @@ ${snippet}` };
|
|
|
932
1087
|
description: params.description,
|
|
933
1088
|
project: params.project,
|
|
934
1089
|
cwd: ctx.cwd,
|
|
935
|
-
files: params.files
|
|
1090
|
+
files: params.files?.length ? params.files : recentSessionFiles[0] ? [recentSessionFiles[0]] : [],
|
|
1091
|
+
sessionFile: getSessionFileSafe(ctx)
|
|
936
1092
|
});
|
|
937
1093
|
updateAnchorStatusBar(ctx, store);
|
|
938
1094
|
return {
|
|
@@ -947,6 +1103,20 @@ ${snippet}` };
|
|
|
947
1103
|
isError: false
|
|
948
1104
|
};
|
|
949
1105
|
}
|
|
1106
|
+
if (params.action === "step") {
|
|
1107
|
+
const rawId = params.id || "1";
|
|
1108
|
+
try {
|
|
1109
|
+
const targetId = resolveAnchorTarget(store, String(rawId), ctx.cwd) || String(rawId);
|
|
1110
|
+
const { anchor: updated } = store.step(targetId, 1);
|
|
1111
|
+
updateAnchorStatusBar(ctx, store);
|
|
1112
|
+
return {
|
|
1113
|
+
content: [{ type: "text", text: `Advanced #${updated.id.replace(/^anc-/, "")} to ${formatProgressBar(updated.progress)}` }],
|
|
1114
|
+
isError: false
|
|
1115
|
+
};
|
|
1116
|
+
} catch (err) {
|
|
1117
|
+
return { content: [{ type: "text", text: err.message }], isError: true };
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
950
1120
|
if (params.action === "settle") {
|
|
951
1121
|
if (!params.id) {
|
|
952
1122
|
return { content: [{ type: "text", text: "Error: id is required for settle" }], isError: true };
|
|
@@ -965,7 +1135,7 @@ ${snippet}` };
|
|
|
965
1135
|
}
|
|
966
1136
|
if (params.action === "touch" && params.id) {
|
|
967
1137
|
try {
|
|
968
|
-
const touched = store.touch(String(params.id));
|
|
1138
|
+
const touched = store.touch(String(params.id), Date.now(), getSessionFileSafe(ctx));
|
|
969
1139
|
updateAnchorStatusBar(ctx, store);
|
|
970
1140
|
return { content: [{ type: "text", text: `Refreshed note #${touched.id}` }], isError: false };
|
|
971
1141
|
} catch (err) {
|
|
@@ -981,7 +1151,7 @@ ${snippet}` };
|
|
|
981
1151
|
}
|
|
982
1152
|
});
|
|
983
1153
|
pi.registerCommand("anchor", {
|
|
984
|
-
description: "View, pin, or tear off sticky notes (/anchor, /anchor
|
|
1154
|
+
description: "View, pin, step, jump to session, or tear off sticky notes (/anchor, /anchor step 1, /anchor jump 1, /anchor done 1)",
|
|
985
1155
|
handler: async (args, ctx) => {
|
|
986
1156
|
const parsed = parseAnchorCommandInput(args);
|
|
987
1157
|
if (parsed.mode === "dashboard") {
|
|
@@ -998,6 +1168,27 @@ ${snippet}` };
|
|
|
998
1168
|
}
|
|
999
1169
|
return;
|
|
1000
1170
|
}
|
|
1171
|
+
if (parsed.mode === "step" && parsed.targetId) {
|
|
1172
|
+
const resolvedId = resolveAnchorTarget(store, parsed.targetId, ctx.cwd);
|
|
1173
|
+
if (!resolvedId) {
|
|
1174
|
+
ctx.ui.notify(`\u2316 Note not found: ${parsed.targetId}`, "error");
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
const { anchor: updated } = store.step(resolvedId, parsed.delta || 1);
|
|
1178
|
+
ctx.ui.notify(`\u2316 Advanced #${updated.id.replace(/^anc-/, "")} to ${formatProgressBar(updated.progress)}`, "info");
|
|
1179
|
+
updateAnchorStatusBar(ctx, store);
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
if (parsed.mode === "jump" && parsed.targetId) {
|
|
1183
|
+
const resolvedId = resolveAnchorTarget(store, parsed.targetId, ctx.cwd);
|
|
1184
|
+
const note = resolvedId ? store.get(resolvedId) : void 0;
|
|
1185
|
+
if (!note || !note.sessionFile || !fs3.existsSync(note.sessionFile)) {
|
|
1186
|
+
ctx.ui.notify(`\u2316 No saved conversation session found for #${parsed.targetId}`, "error");
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
await ctx.switchSession(note.sessionFile);
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1001
1192
|
if (parsed.mode === "settle" && parsed.targetId) {
|
|
1002
1193
|
const resolvedId = resolveAnchorTarget(store, parsed.targetId, ctx.cwd);
|
|
1003
1194
|
if (!resolvedId) {
|
|
@@ -1019,21 +1210,37 @@ ${snippet}` };
|
|
|
1019
1210
|
return;
|
|
1020
1211
|
}
|
|
1021
1212
|
}
|
|
1022
|
-
const anc =
|
|
1023
|
-
|
|
1024
|
-
|
|
1213
|
+
const anc = createStickyNoteFromUser(parsed.title || rawTrimmed, ctx);
|
|
1214
|
+
const bar = anc.progress ? ` ${formatProgressBar(anc.progress)}` : "";
|
|
1215
|
+
ctx.ui.notify(`\u2316 Pinned #${anc.id.replace(/^anc-/, "")}${bar}: "${anc.title}"`, "info");
|
|
1025
1216
|
}
|
|
1026
1217
|
});
|
|
1027
1218
|
pi.registerCommand("pin", {
|
|
1028
|
-
description: "Stick a quick note across sessions (e.g. /pin
|
|
1219
|
+
description: "Stick a quick note across sessions (e.g. /pin Migrate auth [1/3])",
|
|
1029
1220
|
handler: async (args, ctx) => {
|
|
1030
1221
|
const text = (args || "").trim();
|
|
1031
1222
|
if (!text) {
|
|
1032
1223
|
await openAnchorDashboard(ctx, store);
|
|
1033
1224
|
return;
|
|
1034
1225
|
}
|
|
1035
|
-
const anc =
|
|
1036
|
-
|
|
1226
|
+
const anc = createStickyNoteFromUser(text, ctx);
|
|
1227
|
+
const bar = anc.progress ? ` ${formatProgressBar(anc.progress)}` : "";
|
|
1228
|
+
ctx.ui.notify(`\u2316 Pinned #${anc.id.replace(/^anc-/, "")}${bar}: "${anc.title}"`, "info");
|
|
1229
|
+
}
|
|
1230
|
+
});
|
|
1231
|
+
pi.registerCommand("step", {
|
|
1232
|
+
description: "Advance progress bar on a sticky note (e.g. /step 1)",
|
|
1233
|
+
handler: async (args, ctx) => {
|
|
1234
|
+
const tokens = (args || "").trim().split(/\s+/).filter(Boolean);
|
|
1235
|
+
const target = tokens[0] || "1";
|
|
1236
|
+
const delta = tokens[1] ? parseInt(tokens[1], 10) : 1;
|
|
1237
|
+
const resolvedId = resolveAnchorTarget(store, target, ctx.cwd);
|
|
1238
|
+
if (!resolvedId) {
|
|
1239
|
+
ctx.ui.notify(`\u2316 Note not found: ${target}`, "error");
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
const { anchor: updated } = store.step(resolvedId, isNaN(delta) ? 1 : delta);
|
|
1243
|
+
ctx.ui.notify(`\u2316 Advanced #${updated.id.replace(/^anc-/, "")} to ${formatProgressBar(updated.progress)}`, "info");
|
|
1037
1244
|
updateAnchorStatusBar(ctx, store);
|
|
1038
1245
|
}
|
|
1039
1246
|
});
|
|
@@ -1070,6 +1277,7 @@ export {
|
|
|
1070
1277
|
isSameOrSubPath,
|
|
1071
1278
|
globToRegExp,
|
|
1072
1279
|
extractPathsFromText,
|
|
1280
|
+
extractProgressFromText,
|
|
1073
1281
|
isAnchorTargetingWorkspace,
|
|
1074
1282
|
matchAnchorAgainstTouchedFiles,
|
|
1075
1283
|
findMatchedAnchors,
|
|
@@ -1088,6 +1296,7 @@ export {
|
|
|
1088
1296
|
hasActiveUI,
|
|
1089
1297
|
truncateToWidth,
|
|
1090
1298
|
padToWidth,
|
|
1299
|
+
formatProgressBar,
|
|
1091
1300
|
formatRelativeAge,
|
|
1092
1301
|
formatOrigin,
|
|
1093
1302
|
sortAnchorsForDisplay,
|
|
@@ -1095,6 +1304,7 @@ export {
|
|
|
1095
1304
|
updateStartupBanner,
|
|
1096
1305
|
openAnchorDashboard,
|
|
1097
1306
|
parseAnchorCommandInput,
|
|
1307
|
+
parseQuickNoteInput,
|
|
1098
1308
|
resolveAnchorTarget,
|
|
1099
1309
|
extension_default
|
|
1100
1310
|
};
|