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
package/dist/extension.cjs
CHANGED
|
@@ -32,10 +32,12 @@ var extension_exports = {};
|
|
|
32
32
|
__export(extension_exports, {
|
|
33
33
|
default: () => extension_default,
|
|
34
34
|
parseAnchorCommandInput: () => parseAnchorCommandInput,
|
|
35
|
+
parseQuickNoteInput: () => parseQuickNoteInput,
|
|
35
36
|
resolveAnchorTarget: () => resolveAnchorTarget
|
|
36
37
|
});
|
|
37
38
|
module.exports = __toCommonJS(extension_exports);
|
|
38
39
|
var import_typebox = require("@sinclair/typebox");
|
|
40
|
+
var import_node_fs3 = __toESM(require("fs"), 1);
|
|
39
41
|
|
|
40
42
|
// src/store.ts
|
|
41
43
|
var import_node_fs = __toESM(require("fs"), 1);
|
|
@@ -89,6 +91,32 @@ function extractPathsFromText(text) {
|
|
|
89
91
|
}
|
|
90
92
|
return Array.from(results);
|
|
91
93
|
}
|
|
94
|
+
function extractProgressFromText(rawTitle) {
|
|
95
|
+
if (!rawTitle) return { cleanTitle: "" };
|
|
96
|
+
const stepParts = Array.from(rawTitle.matchAll(/(?:^|\s)(\d+)\.\s*([^\d]+?)(?=\s+\d+\.\s|$)/g));
|
|
97
|
+
if (stepParts.length >= 2) {
|
|
98
|
+
const steps = stepParts.map((m) => m[2].trim().replace(/[;;,,]+$/, "")).filter(Boolean);
|
|
99
|
+
const firstMatchIdx = rawTitle.indexOf(stepParts[0][0]);
|
|
100
|
+
const prefix = rawTitle.slice(0, firstMatchIdx).replace(/[::\-—\s]+$/, "").trim();
|
|
101
|
+
return {
|
|
102
|
+
cleanTitle: prefix || steps.join(" \u2192 "),
|
|
103
|
+
progress: { done: 0, total: steps.length, steps }
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
const ratioMatch = rawTitle.match(/(?:\[(\d{1,3})\/(\d{1,3})\]|\((\d{1,3})\/(\d{1,3})\)|(?:^|\s)(\d{1,3})\/(\d{1,3})(?=\s|$))/);
|
|
107
|
+
if (ratioMatch) {
|
|
108
|
+
const done = parseInt(ratioMatch[1] ?? ratioMatch[3] ?? ratioMatch[5], 10);
|
|
109
|
+
const total = parseInt(ratioMatch[2] ?? ratioMatch[4] ?? ratioMatch[6], 10);
|
|
110
|
+
if (!isNaN(done) && !isNaN(total) && total >= 2 && total <= 500 && done >= 0 && done <= total) {
|
|
111
|
+
const stripped = rawTitle.replace(ratioMatch[0], " ").replace(/\s+/g, " ").trim();
|
|
112
|
+
return {
|
|
113
|
+
cleanTitle: stripped || rawTitle.trim(),
|
|
114
|
+
progress: { done, total }
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return { cleanTitle: rawTitle.trim() };
|
|
119
|
+
}
|
|
92
120
|
function isAnchorTargetingWorkspace(anchor, targetCwd) {
|
|
93
121
|
if (!targetCwd) return true;
|
|
94
122
|
const normTarget = targetCwd.trim().replace(/\\/g, "/").replace(/\/+$/, "");
|
|
@@ -231,8 +259,9 @@ function renderColdStartAnchorsContext(store, cwdOrNow, nowArg) {
|
|
|
231
259
|
const lines = activeNotes.map((a) => {
|
|
232
260
|
const num = a.id.replace(/^anc-/, "");
|
|
233
261
|
const files = a.files.length > 0 ? ` (${a.files.slice(0, 2).join(", ")})` : "";
|
|
262
|
+
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]}` : ""}]` : "";
|
|
234
263
|
const desc = a.description ? ` \u2014 ${a.description}` : "";
|
|
235
|
-
return `\u2022 #${num} [${a.project}] ${a.title}${files}${desc}`;
|
|
264
|
+
return `\u2022 #${num} [${a.project}] ${a.title}${prog}${files}${desc}`;
|
|
236
265
|
});
|
|
237
266
|
return [
|
|
238
267
|
`<sticky-notes count="${activeNotes.length}">`,
|
|
@@ -406,8 +435,9 @@ var AnchorStore = class {
|
|
|
406
435
|
const state = this.loadState();
|
|
407
436
|
const now = Date.now();
|
|
408
437
|
const id = this.generateId(state);
|
|
438
|
+
const { cleanTitle, progress: parsedProgress } = extractProgressFromText(input.title);
|
|
409
439
|
const explicitFiles = (input.files || []).map((f) => normalizePath(f)).filter(Boolean);
|
|
410
|
-
const inferredFiles = explicitFiles.length > 0 ? explicitFiles : extractPathsFromText(`${
|
|
440
|
+
const inferredFiles = explicitFiles.length > 0 ? explicitFiles : extractPathsFromText(`${cleanTitle} ${input.description || ""}`);
|
|
411
441
|
let cwd = input.cwd ? normalizePath(input.cwd) : "";
|
|
412
442
|
if (input.project && (/^[a-zA-Z]:[\\/]/.test(input.project.trim()) || input.project.trim().startsWith("/"))) {
|
|
413
443
|
cwd = normalizePath(input.project.trim());
|
|
@@ -418,7 +448,7 @@ var AnchorStore = class {
|
|
|
418
448
|
const projectName = input.project ? input.project.trim() : cwd ? import_node_path.default.basename(cwd) : "global";
|
|
419
449
|
const anchor = {
|
|
420
450
|
id,
|
|
421
|
-
title:
|
|
451
|
+
title: cleanTitle,
|
|
422
452
|
description: input.description?.trim(),
|
|
423
453
|
priority: input.priority || "p1",
|
|
424
454
|
status: "active",
|
|
@@ -428,7 +458,9 @@ var AnchorStore = class {
|
|
|
428
458
|
updatedAt: now,
|
|
429
459
|
lastTouchedAt: now,
|
|
430
460
|
files: inferredFiles,
|
|
431
|
-
tags: (input.tags || []).map((t) => t.trim()).filter(Boolean)
|
|
461
|
+
tags: (input.tags || []).map((t) => t.trim()).filter(Boolean),
|
|
462
|
+
...input.sessionFile ? { sessionFile: normalizePath(input.sessionFile) } : {},
|
|
463
|
+
...input.progress || parsedProgress ? { progress: input.progress || parsedProgress } : {}
|
|
432
464
|
};
|
|
433
465
|
state.anchors.push(anchor);
|
|
434
466
|
this.saveState(state);
|
|
@@ -477,7 +509,7 @@ var AnchorStore = class {
|
|
|
477
509
|
return updated;
|
|
478
510
|
});
|
|
479
511
|
}
|
|
480
|
-
touch(id, timestamp = Date.now()) {
|
|
512
|
+
touch(id, timestamp = Date.now(), sessionFile) {
|
|
481
513
|
return this.withLock(() => {
|
|
482
514
|
const state = this.loadState();
|
|
483
515
|
const idx = this.findAnchorIndex(state.anchors, id);
|
|
@@ -488,11 +520,44 @@ var AnchorStore = class {
|
|
|
488
520
|
if (current.status === "sleeping") {
|
|
489
521
|
current.status = "active";
|
|
490
522
|
}
|
|
523
|
+
if (sessionFile) {
|
|
524
|
+
current.sessionFile = normalizePath(sessionFile);
|
|
525
|
+
}
|
|
491
526
|
state.anchors[idx] = current;
|
|
492
527
|
this.saveState(state);
|
|
493
528
|
return current;
|
|
494
529
|
});
|
|
495
530
|
}
|
|
531
|
+
/**
|
|
532
|
+
* Advance (or set) progress on a sticky note:
|
|
533
|
+
* - If delta is provided (default +1), increments `done` up to `total`.
|
|
534
|
+
* - If note had no progress yet, initializes a 4-step bar (`1/4`).
|
|
535
|
+
*/
|
|
536
|
+
step(id, delta = 1, explicitTotal) {
|
|
537
|
+
return this.withLock(() => {
|
|
538
|
+
const state = this.loadState();
|
|
539
|
+
const idx = this.findAnchorIndex(state.anchors, id);
|
|
540
|
+
if (idx === -1) throw new Error(`Note not found: ${id}`);
|
|
541
|
+
const current = state.anchors[idx];
|
|
542
|
+
const now = Date.now();
|
|
543
|
+
const total = explicitTotal && explicitTotal >= 2 ? explicitTotal : current.progress?.total || 4;
|
|
544
|
+
const prevDone = current.progress?.done || 0;
|
|
545
|
+
const nextDone = Math.max(0, Math.min(total, prevDone + delta));
|
|
546
|
+
current.progress = {
|
|
547
|
+
...current.progress || {},
|
|
548
|
+
done: nextDone,
|
|
549
|
+
total
|
|
550
|
+
};
|
|
551
|
+
current.lastTouchedAt = now;
|
|
552
|
+
current.updatedAt = now;
|
|
553
|
+
if (current.status === "sleeping") {
|
|
554
|
+
current.status = "active";
|
|
555
|
+
}
|
|
556
|
+
state.anchors[idx] = current;
|
|
557
|
+
this.saveState(state);
|
|
558
|
+
return { anchor: current, completed: nextDone >= total };
|
|
559
|
+
});
|
|
560
|
+
}
|
|
496
561
|
/**
|
|
497
562
|
* Tear off (settle) a completed sticky note and archive it to archive.jsonl
|
|
498
563
|
*/
|
|
@@ -563,6 +628,7 @@ var AnchorStore = class {
|
|
|
563
628
|
};
|
|
564
629
|
|
|
565
630
|
// src/tui.ts
|
|
631
|
+
var import_node_fs2 = __toESM(require("fs"), 1);
|
|
566
632
|
var import_node_path2 = __toESM(require("path"), 1);
|
|
567
633
|
var ANSI_REGEX = /\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g;
|
|
568
634
|
function stripAnsi(str) {
|
|
@@ -629,6 +695,14 @@ function padToWidth(str, targetWidth) {
|
|
|
629
695
|
if (current >= targetWidth) return truncated;
|
|
630
696
|
return truncated + " ".repeat(targetWidth - current);
|
|
631
697
|
}
|
|
698
|
+
function formatProgressBar(progress, cells = 4) {
|
|
699
|
+
if (!progress || !progress.total || progress.total < 1) return "";
|
|
700
|
+
const done = Math.max(0, Math.min(progress.total, progress.done || 0));
|
|
701
|
+
const filled = Math.min(cells, Math.max(done > 0 ? 1 : 0, Math.round(done / progress.total * cells)));
|
|
702
|
+
const finalFilled = done >= progress.total ? cells : done === 0 ? 0 : filled;
|
|
703
|
+
const bar = "\u25A0".repeat(finalFilled) + "\u25A1".repeat(Math.max(0, cells - finalFilled));
|
|
704
|
+
return `[${bar} ${done}/${progress.total}]`;
|
|
705
|
+
}
|
|
632
706
|
function formatRelativeAge(timestamp, now = Date.now()) {
|
|
633
707
|
const diffSec = Math.max(0, Math.floor((now - timestamp) / 1e3));
|
|
634
708
|
if (diffSec < 60) return "just now";
|
|
@@ -682,6 +756,7 @@ function updateAnchorStatusBar(ctx, store) {
|
|
|
682
756
|
try {
|
|
683
757
|
const active = store.list({ status: "active", cwd: ctx.cwd });
|
|
684
758
|
ctx.ui.setStatus("anchor", active.length > 0 ? `\u2316 ${active.length}` : void 0);
|
|
759
|
+
updateStartupBanner(ctx, store);
|
|
685
760
|
} catch {
|
|
686
761
|
}
|
|
687
762
|
}
|
|
@@ -690,41 +765,41 @@ function updateStartupBanner(ctx, store) {
|
|
|
690
765
|
try {
|
|
691
766
|
const active = store.list({ status: "active", cwd: ctx.cwd });
|
|
692
767
|
if (active.length === 0) {
|
|
693
|
-
ctx.ui.setWidget("anchor-startup", void 0);
|
|
768
|
+
ctx.ui.setWidget("anchor-startup", void 0, { placement: "belowEditor" });
|
|
694
769
|
return;
|
|
695
770
|
}
|
|
696
771
|
const sorted = sortAnchorsForDisplay(active, ctx.cwd);
|
|
697
|
-
const topItems = sorted.slice(0, 3);
|
|
698
772
|
const factory = (_tui, theme) => ({
|
|
699
773
|
render: (width) => {
|
|
700
|
-
const maxTitleWidth = Math.max(14, width - 34);
|
|
701
774
|
const fg = theme?.fg ? theme.fg.bind(theme) : (_c, text) => text;
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
const num = a.id.replace(/^anc-/, "").padStart(2, "0");
|
|
775
|
+
if (sorted.length === 1) {
|
|
776
|
+
const a = sorted[0];
|
|
777
|
+
const num = a.id.replace(/^anc-/, "");
|
|
778
|
+
const bar = a.progress ? `${fg("accent", formatProgressBar(a.progress))} ` : "";
|
|
779
|
+
const nextStep = a.progress?.steps && a.progress.steps[a.progress.done] ? fg("muted", ` \u25B8 ${a.progress.steps[a.progress.done]}`) : "";
|
|
708
780
|
const origin = formatOrigin(a);
|
|
709
|
-
const
|
|
710
|
-
const
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
)
|
|
716
|
-
);
|
|
781
|
+
const sessionIcon = a.sessionFile ? " \u21AA" : "";
|
|
782
|
+
const prefix = `${fg("accent", "\u2316")} ${fg("dim", `#${num}`)} ${bar}`;
|
|
783
|
+
const suffix = ` ${fg("muted", `(${origin}${sessionIcon})`)}`;
|
|
784
|
+
const availTitle = Math.max(12, width - getDisplayWidth(prefix) - getDisplayWidth(nextStep) - getDisplayWidth(suffix) - 2);
|
|
785
|
+
const titleClipped = truncateToWidth(a.title, availTitle);
|
|
786
|
+
return [truncateToWidth(`${prefix}${titleClipped}${nextStep}${suffix}`, width)];
|
|
717
787
|
}
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
788
|
+
const countTag = fg("accent", `\u2316 ${sorted.length} notes: `);
|
|
789
|
+
const segments = sorted.slice(0, 3).map((a) => {
|
|
790
|
+
const num = a.id.replace(/^anc-/, "");
|
|
791
|
+
const bar = a.progress ? `${formatProgressBar(a.progress)} ` : "";
|
|
792
|
+
const shortTitle = truncateToWidth(a.title, Math.max(14, Math.floor((width - 20) / Math.min(3, sorted.length)) - 12));
|
|
793
|
+
return `${fg("dim", `#${num}`)} ${bar}${shortTitle}`;
|
|
794
|
+
});
|
|
795
|
+
const more = sorted.length > 3 ? fg("dim", ` +${sorted.length - 3}`) : "";
|
|
796
|
+
return [truncateToWidth(`${countTag}${segments.join(fg("dim", " \xB7 "))}${more}`, width)];
|
|
722
797
|
},
|
|
723
798
|
invalidate: () => {
|
|
724
799
|
}
|
|
725
800
|
});
|
|
726
801
|
const content = ctx.ui?.theme ? factory : factory().render(80);
|
|
727
|
-
ctx.ui.setWidget("anchor-startup", content, { placement: "
|
|
802
|
+
ctx.ui.setWidget("anchor-startup", content, { placement: "belowEditor" });
|
|
728
803
|
} catch {
|
|
729
804
|
}
|
|
730
805
|
}
|
|
@@ -732,7 +807,7 @@ async function openAnchorDashboard(ctx, store) {
|
|
|
732
807
|
if (!hasActiveUI(ctx)) return;
|
|
733
808
|
const allNotes = store.list({ cwd: ctx.cwd, all: true }).filter((a) => a.status === "active" || a.status === "sleeping");
|
|
734
809
|
if (allNotes.length === 0) {
|
|
735
|
-
ctx.ui.notify("\u2316 No sticky notes.
|
|
810
|
+
ctx.ui.notify("\u2316 No sticky notes. Type `pin: <note>` or `/pin <note>` to stick one.", "info");
|
|
736
811
|
return;
|
|
737
812
|
}
|
|
738
813
|
const sorted = sortAnchorsForDisplay(allNotes, ctx.cwd);
|
|
@@ -743,10 +818,11 @@ async function openAnchorDashboard(ctx, store) {
|
|
|
743
818
|
const titleWidth = isNarrow ? Math.max(20, termCols - 30) : 42;
|
|
744
819
|
for (const a of sorted) {
|
|
745
820
|
const num = a.id.replace(/^anc-/, "").padStart(2, "0");
|
|
746
|
-
const
|
|
821
|
+
const bar = a.progress ? `${formatProgressBar(a.progress)} ` : "";
|
|
822
|
+
const origin = formatOrigin(a) + (a.sessionFile ? " \u21AA" : "");
|
|
747
823
|
const age = a.status === "sleeping" ? `${formatRelativeAge(a.createdAt)} \xB7 quiet` : formatRelativeAge(a.createdAt);
|
|
748
824
|
const colNum = `${num} `;
|
|
749
|
-
const colTitle = padToWidth(a.title
|
|
825
|
+
const colTitle = padToWidth(`${bar}${a.title}`, titleWidth);
|
|
750
826
|
const colOrigin = padToWidth(origin, 22);
|
|
751
827
|
const label = isNarrow ? `${colNum}${colTitle} ${age}`.trimEnd() : `${colNum}${colTitle} ${colOrigin} ${age}`.trimEnd();
|
|
752
828
|
optionMap.set(label, a);
|
|
@@ -756,8 +832,15 @@ async function openAnchorDashboard(ctx, store) {
|
|
|
756
832
|
if (!selected) return;
|
|
757
833
|
const anchor = optionMap.get(selected);
|
|
758
834
|
if (!anchor) return;
|
|
835
|
+
const currentSessionFile = ctx.sessionManager?.getSessionFile?.();
|
|
836
|
+
const canJumpSession = Boolean(
|
|
837
|
+
anchor.sessionFile && import_node_fs2.default.existsSync(anchor.sessionFile) && typeof ctx.switchSession === "function" && (!currentSessionFile || import_node_path2.default.resolve(anchor.sessionFile) !== import_node_path2.default.resolve(currentSessionFile))
|
|
838
|
+
);
|
|
839
|
+
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)";
|
|
759
840
|
const actions = [
|
|
760
841
|
"\u2713 Done (tear off note)",
|
|
842
|
+
stepLabel,
|
|
843
|
+
...canJumpSession ? ["\u21AA Jump to session (resume linked conversation)"] : [],
|
|
761
844
|
...anchor.status === "sleeping" ? ["\u26A1 Wake up (mark active)"] : [],
|
|
762
845
|
"\u2139 Read full note",
|
|
763
846
|
"\u21A9 Back"
|
|
@@ -768,6 +851,23 @@ async function openAnchorDashboard(ctx, store) {
|
|
|
768
851
|
store.settle(anchor.id, { settledBy: "manual" });
|
|
769
852
|
ctx.ui.notify(`\u2316 Torn off #${anchor.id.replace(/^anc-/, "")}: "${anchor.title}" (/anchor undo to put back)`, "info");
|
|
770
853
|
updateAnchorStatusBar(ctx, store);
|
|
854
|
+
} else if (action.startsWith("\u25B6")) {
|
|
855
|
+
const { anchor: updated, completed } = store.step(anchor.id, 1);
|
|
856
|
+
updateAnchorStatusBar(ctx, store);
|
|
857
|
+
if (completed) {
|
|
858
|
+
ctx.ui.notify(
|
|
859
|
+
`\u2316 #${updated.id.replace(/^anc-/, "")} reached ${formatProgressBar(updated.progress)}! Run /done ${updated.id.replace(/^anc-/, "")} to tear off.`,
|
|
860
|
+
"info"
|
|
861
|
+
);
|
|
862
|
+
} else {
|
|
863
|
+
ctx.ui.notify(`\u2316 Advanced #${updated.id.replace(/^anc-/, "")} to ${formatProgressBar(updated.progress)}`, "info");
|
|
864
|
+
}
|
|
865
|
+
} else if (action.startsWith("\u21AA") && anchor.sessionFile) {
|
|
866
|
+
try {
|
|
867
|
+
await ctx.switchSession(anchor.sessionFile);
|
|
868
|
+
} catch (err) {
|
|
869
|
+
ctx.ui.notify(`Could not switch session: ${err.message}`, "error");
|
|
870
|
+
}
|
|
771
871
|
} else if (action.startsWith("\u26A1")) {
|
|
772
872
|
store.touch(anchor.id);
|
|
773
873
|
ctx.ui.notify(`\u2316 Woke up #${anchor.id.replace(/^anc-/, "")}: "${anchor.title}"`, "info");
|
|
@@ -775,8 +875,10 @@ async function openAnchorDashboard(ctx, store) {
|
|
|
775
875
|
} else if (action.startsWith("\u2139")) {
|
|
776
876
|
const details = [
|
|
777
877
|
`\u2316 Note #${anchor.id.replace(/^anc-/, "")}: ${anchor.title}`,
|
|
878
|
+
anchor.progress ? `Progress: ${formatProgressBar(anchor.progress)}${anchor.progress.steps ? ` (${anchor.progress.steps.join(" \u2192 ")})` : ""}` : null,
|
|
778
879
|
anchor.description ? `Details: ${anchor.description}` : null,
|
|
779
880
|
anchor.files.length > 0 ? `Files: ${anchor.files.join(", ")}` : null,
|
|
881
|
+
anchor.sessionFile ? `Session: ${import_node_path2.default.basename(anchor.sessionFile)}` : null,
|
|
780
882
|
`Workspace: ${anchor.project || "global"} \xB7 Pinned ${formatRelativeAge(anchor.createdAt)}`
|
|
781
883
|
].filter(Boolean).join("\n");
|
|
782
884
|
ctx.ui.notify(details, "info");
|
|
@@ -800,11 +902,30 @@ function parseAnchorCommandInput(raw) {
|
|
|
800
902
|
targetId: settleMatch[1].trim().replace(/^#/, "")
|
|
801
903
|
};
|
|
802
904
|
}
|
|
905
|
+
const stepMatch = trimmed.match(/^(?:step|\+)\s*(\S+)(?:\s+([+-]?\d+))?$/i);
|
|
906
|
+
if (stepMatch) {
|
|
907
|
+
return {
|
|
908
|
+
mode: "step",
|
|
909
|
+
targetId: stepMatch[1].trim().replace(/^#/, ""),
|
|
910
|
+
delta: stepMatch[2] ? parseInt(stepMatch[2], 10) : 1
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
const jumpMatch = trimmed.match(/^(?:jump|resume|session)\s+(.+)$/i);
|
|
914
|
+
if (jumpMatch) {
|
|
915
|
+
return {
|
|
916
|
+
mode: "jump",
|
|
917
|
+
targetId: jumpMatch[1].trim().replace(/^#/, "")
|
|
918
|
+
};
|
|
919
|
+
}
|
|
803
920
|
return {
|
|
804
921
|
mode: "pin",
|
|
805
922
|
title: trimmed
|
|
806
923
|
};
|
|
807
924
|
}
|
|
925
|
+
function parseQuickNoteInput(text) {
|
|
926
|
+
const m = (text || "").trim().match(/^(?:pin|note|便签|备忘)\s*[::]\s*(.+)$/i);
|
|
927
|
+
return m && m[1].trim() ? m[1].trim() : null;
|
|
928
|
+
}
|
|
808
929
|
function resolveAnchorTarget(store, idOrIndex, cwd) {
|
|
809
930
|
const clean = idOrIndex.trim().replace(/^#/, "");
|
|
810
931
|
if (/^\d+$/.test(clean)) {
|
|
@@ -817,33 +938,60 @@ function resolveAnchorTarget(store, idOrIndex, cwd) {
|
|
|
817
938
|
}
|
|
818
939
|
return store.get(clean)?.id;
|
|
819
940
|
}
|
|
941
|
+
function getSessionFileSafe(ctx) {
|
|
942
|
+
try {
|
|
943
|
+
const file = ctx?.sessionManager?.getSessionFile?.();
|
|
944
|
+
return typeof file === "string" && file.trim() ? file.trim() : void 0;
|
|
945
|
+
} catch {
|
|
946
|
+
return void 0;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
820
949
|
function extension_default(pi) {
|
|
821
950
|
const store = new AnchorStore();
|
|
822
951
|
const annotatedThisSession = /* @__PURE__ */ new Set();
|
|
823
|
-
|
|
952
|
+
const recentSessionFiles = [];
|
|
953
|
+
function rememberTouchedFile(filePath, cwd) {
|
|
954
|
+
const norm = normalizePath(filePath, cwd);
|
|
955
|
+
if (!norm) return;
|
|
956
|
+
const idx = recentSessionFiles.indexOf(norm);
|
|
957
|
+
if (idx !== -1) recentSessionFiles.splice(idx, 1);
|
|
958
|
+
recentSessionFiles.unshift(norm);
|
|
959
|
+
if (recentSessionFiles.length > 5) recentSessionFiles.pop();
|
|
960
|
+
}
|
|
961
|
+
function createStickyNoteFromUser(title, ctx) {
|
|
962
|
+
const sessionFile = getSessionFileSafe(ctx);
|
|
963
|
+
const anc = store.create({
|
|
964
|
+
title,
|
|
965
|
+
cwd: ctx.cwd,
|
|
966
|
+
sessionFile
|
|
967
|
+
});
|
|
968
|
+
if (anc.files.length === 0 && recentSessionFiles.length > 0) {
|
|
969
|
+
store.update(anc.id, { files: [recentSessionFiles[0]] });
|
|
970
|
+
}
|
|
971
|
+
updateAnchorStatusBar(ctx, store);
|
|
972
|
+
return store.get(anc.id) || anc;
|
|
973
|
+
}
|
|
974
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
824
975
|
annotatedThisSession.clear();
|
|
976
|
+
recentSessionFiles.length = 0;
|
|
825
977
|
sweepQuietNotes(store);
|
|
826
978
|
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));
|
|
827
979
|
for (const q of quietForHere) {
|
|
828
980
|
store.touch(q.id);
|
|
829
981
|
}
|
|
830
982
|
updateAnchorStatusBar(ctx, store);
|
|
831
|
-
|
|
832
|
-
updateStartupBanner(ctx, store);
|
|
833
|
-
} else if (hasActiveUI(ctx)) {
|
|
834
|
-
try {
|
|
835
|
-
ctx.ui.setWidget("anchor-startup", void 0);
|
|
836
|
-
} catch {
|
|
837
|
-
}
|
|
838
|
-
}
|
|
983
|
+
updateStartupBanner(ctx, store);
|
|
839
984
|
});
|
|
840
|
-
pi.on("
|
|
985
|
+
pi.on("input", async (event, ctx) => {
|
|
986
|
+
if (event?.source === "extension") return { action: "continue" };
|
|
987
|
+
const quickNote = parseQuickNoteInput(String(event?.text || ""));
|
|
988
|
+
if (!quickNote) return { action: "continue" };
|
|
989
|
+
const anc = createStickyNoteFromUser(quickNote, ctx);
|
|
990
|
+
const bar = anc.progress ? ` ${formatProgressBar(anc.progress)}` : "";
|
|
841
991
|
if (hasActiveUI(ctx)) {
|
|
842
|
-
|
|
843
|
-
ctx.ui.setWidget("anchor-startup", void 0);
|
|
844
|
-
} catch {
|
|
845
|
-
}
|
|
992
|
+
ctx.ui.notify(`\u2316 Pinned #${anc.id.replace(/^anc-/, "")}${bar}: "${anc.title}"`, "info");
|
|
846
993
|
}
|
|
994
|
+
return { action: "handled" };
|
|
847
995
|
});
|
|
848
996
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
849
997
|
const entries = ctx.sessionManager?.getEntries() || [];
|
|
@@ -860,6 +1008,7 @@ ${snippet}` };
|
|
|
860
1008
|
pi.on("tool_result", async (event, ctx) => {
|
|
861
1009
|
if (event.isError) return;
|
|
862
1010
|
const lowerTool = String(event.toolName || "").toLowerCase();
|
|
1011
|
+
const currentSessionFile = getSessionFileSafe(ctx);
|
|
863
1012
|
if (lowerTool === "bash" || lowerTool === "powershell") {
|
|
864
1013
|
const cmd = String(event.input?.command || "");
|
|
865
1014
|
if (cmd.includes("git commit")) {
|
|
@@ -884,12 +1033,18 @@ ${snippet}` };
|
|
|
884
1033
|
}
|
|
885
1034
|
const pathInput = event.input?.path;
|
|
886
1035
|
if (typeof pathInput !== "string") return;
|
|
1036
|
+
const isMutation = MUTATION_TOOLS.has(lowerTool);
|
|
1037
|
+
if (isMutation) {
|
|
1038
|
+
rememberTouchedFile(pathInput, ctx.cwd);
|
|
1039
|
+
}
|
|
887
1040
|
const touchedPath = normalizePath(pathInput, ctx.cwd);
|
|
888
1041
|
const notes = store.list({ cwd: ctx.cwd }).filter((a) => a.status === "active" || a.status === "sleeping");
|
|
889
1042
|
const matches = findMatchedAnchors(notes, [touchedPath]);
|
|
890
1043
|
if (matches.length === 0) return;
|
|
891
|
-
if (
|
|
892
|
-
for (const m of matches)
|
|
1044
|
+
if (isMutation) {
|
|
1045
|
+
for (const m of matches) {
|
|
1046
|
+
store.touch(m.anchor.id, Date.now(), currentSessionFile);
|
|
1047
|
+
}
|
|
893
1048
|
updateAnchorStatusBar(ctx, store);
|
|
894
1049
|
}
|
|
895
1050
|
const targetNote = matches[0].anchor;
|
|
@@ -911,10 +1066,10 @@ ${snippet}` };
|
|
|
911
1066
|
pi.registerTool({
|
|
912
1067
|
name: "anchor",
|
|
913
1068
|
label: "Anchor (Sticky Note)",
|
|
914
|
-
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).",
|
|
1069
|
+
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).",
|
|
915
1070
|
promptSnippet: "Pin or tear off cross-session sticky notes that survive terminal restarts",
|
|
916
1071
|
promptGuidelines: [
|
|
917
|
-
|
|
1072
|
+
"Use `anchor` when the user explicitly asks to remember something for later/tomorrow or across sessions.",
|
|
918
1073
|
"BOUNDARY WITH TODO: `todo` is for multi-step work right now in this session; `anchor` is a sticky note for future sessions.",
|
|
919
1074
|
'When a pinned note is completed, call `anchor` with action "settle" to tear it off.'
|
|
920
1075
|
],
|
|
@@ -922,22 +1077,23 @@ ${snippet}` };
|
|
|
922
1077
|
action: import_typebox.Type.Union([
|
|
923
1078
|
import_typebox.Type.Literal("pin"),
|
|
924
1079
|
import_typebox.Type.Literal("list"),
|
|
1080
|
+
import_typebox.Type.Literal("step"),
|
|
925
1081
|
import_typebox.Type.Literal("settle"),
|
|
926
1082
|
import_typebox.Type.Literal("touch"),
|
|
927
1083
|
import_typebox.Type.Literal("sweep")
|
|
928
1084
|
]),
|
|
929
|
-
title: import_typebox.Type.Optional(import_typebox.Type.String({ description: "Sticky note text (
|
|
1085
|
+
title: import_typebox.Type.Optional(import_typebox.Type.String({ description: "Sticky note text (supports [1/4] or 1. A 2. B progress steps)" })),
|
|
930
1086
|
description: import_typebox.Type.Optional(import_typebox.Type.String({ description: "Optional extra details" })),
|
|
931
1087
|
project: import_typebox.Type.Optional(import_typebox.Type.String({ description: "Optional project name or folder" })),
|
|
932
1088
|
files: import_typebox.Type.Optional(import_typebox.Type.Array(import_typebox.Type.String(), { description: "Optional associated file paths" })),
|
|
933
|
-
id: import_typebox.Type.Optional(import_typebox.Type.String({ description: "Note ID, e.g. anc-1 or 1 (for settle)" }))
|
|
1089
|
+
id: import_typebox.Type.Optional(import_typebox.Type.String({ description: "Note ID, e.g. anc-1 or 1 (for step/settle)" }))
|
|
934
1090
|
}),
|
|
935
1091
|
renderCall(args, theme) {
|
|
936
1092
|
const action = args.action || "list";
|
|
937
1093
|
let title = theme.fg("toolTitle", theme.bold("\u2316 anchor ")) + theme.fg("accent", action);
|
|
938
1094
|
if (action === "pin" && args.title) {
|
|
939
1095
|
title += ` "${args.title}"`;
|
|
940
|
-
} else if ((action === "settle" || action === "touch") && args.id) {
|
|
1096
|
+
} else if ((action === "settle" || action === "touch" || action === "step") && args.id) {
|
|
941
1097
|
title += ` ${theme.fg("dim", `#${String(args.id).replace(/^anc-/, "")}`)}`;
|
|
942
1098
|
}
|
|
943
1099
|
return {
|
|
@@ -967,7 +1123,8 @@ ${snippet}` };
|
|
|
967
1123
|
description: params.description,
|
|
968
1124
|
project: params.project,
|
|
969
1125
|
cwd: ctx.cwd,
|
|
970
|
-
files: params.files
|
|
1126
|
+
files: params.files?.length ? params.files : recentSessionFiles[0] ? [recentSessionFiles[0]] : [],
|
|
1127
|
+
sessionFile: getSessionFileSafe(ctx)
|
|
971
1128
|
});
|
|
972
1129
|
updateAnchorStatusBar(ctx, store);
|
|
973
1130
|
return {
|
|
@@ -982,6 +1139,20 @@ ${snippet}` };
|
|
|
982
1139
|
isError: false
|
|
983
1140
|
};
|
|
984
1141
|
}
|
|
1142
|
+
if (params.action === "step") {
|
|
1143
|
+
const rawId = params.id || "1";
|
|
1144
|
+
try {
|
|
1145
|
+
const targetId = resolveAnchorTarget(store, String(rawId), ctx.cwd) || String(rawId);
|
|
1146
|
+
const { anchor: updated } = store.step(targetId, 1);
|
|
1147
|
+
updateAnchorStatusBar(ctx, store);
|
|
1148
|
+
return {
|
|
1149
|
+
content: [{ type: "text", text: `Advanced #${updated.id.replace(/^anc-/, "")} to ${formatProgressBar(updated.progress)}` }],
|
|
1150
|
+
isError: false
|
|
1151
|
+
};
|
|
1152
|
+
} catch (err) {
|
|
1153
|
+
return { content: [{ type: "text", text: err.message }], isError: true };
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
985
1156
|
if (params.action === "settle") {
|
|
986
1157
|
if (!params.id) {
|
|
987
1158
|
return { content: [{ type: "text", text: "Error: id is required for settle" }], isError: true };
|
|
@@ -1000,7 +1171,7 @@ ${snippet}` };
|
|
|
1000
1171
|
}
|
|
1001
1172
|
if (params.action === "touch" && params.id) {
|
|
1002
1173
|
try {
|
|
1003
|
-
const touched = store.touch(String(params.id));
|
|
1174
|
+
const touched = store.touch(String(params.id), Date.now(), getSessionFileSafe(ctx));
|
|
1004
1175
|
updateAnchorStatusBar(ctx, store);
|
|
1005
1176
|
return { content: [{ type: "text", text: `Refreshed note #${touched.id}` }], isError: false };
|
|
1006
1177
|
} catch (err) {
|
|
@@ -1016,7 +1187,7 @@ ${snippet}` };
|
|
|
1016
1187
|
}
|
|
1017
1188
|
});
|
|
1018
1189
|
pi.registerCommand("anchor", {
|
|
1019
|
-
description: "View, pin, or tear off sticky notes (/anchor, /anchor
|
|
1190
|
+
description: "View, pin, step, jump to session, or tear off sticky notes (/anchor, /anchor step 1, /anchor jump 1, /anchor done 1)",
|
|
1020
1191
|
handler: async (args, ctx) => {
|
|
1021
1192
|
const parsed = parseAnchorCommandInput(args);
|
|
1022
1193
|
if (parsed.mode === "dashboard") {
|
|
@@ -1033,6 +1204,27 @@ ${snippet}` };
|
|
|
1033
1204
|
}
|
|
1034
1205
|
return;
|
|
1035
1206
|
}
|
|
1207
|
+
if (parsed.mode === "step" && parsed.targetId) {
|
|
1208
|
+
const resolvedId = resolveAnchorTarget(store, parsed.targetId, ctx.cwd);
|
|
1209
|
+
if (!resolvedId) {
|
|
1210
|
+
ctx.ui.notify(`\u2316 Note not found: ${parsed.targetId}`, "error");
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
const { anchor: updated } = store.step(resolvedId, parsed.delta || 1);
|
|
1214
|
+
ctx.ui.notify(`\u2316 Advanced #${updated.id.replace(/^anc-/, "")} to ${formatProgressBar(updated.progress)}`, "info");
|
|
1215
|
+
updateAnchorStatusBar(ctx, store);
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
if (parsed.mode === "jump" && parsed.targetId) {
|
|
1219
|
+
const resolvedId = resolveAnchorTarget(store, parsed.targetId, ctx.cwd);
|
|
1220
|
+
const note = resolvedId ? store.get(resolvedId) : void 0;
|
|
1221
|
+
if (!note || !note.sessionFile || !import_node_fs3.default.existsSync(note.sessionFile)) {
|
|
1222
|
+
ctx.ui.notify(`\u2316 No saved conversation session found for #${parsed.targetId}`, "error");
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
await ctx.switchSession(note.sessionFile);
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1036
1228
|
if (parsed.mode === "settle" && parsed.targetId) {
|
|
1037
1229
|
const resolvedId = resolveAnchorTarget(store, parsed.targetId, ctx.cwd);
|
|
1038
1230
|
if (!resolvedId) {
|
|
@@ -1054,21 +1246,37 @@ ${snippet}` };
|
|
|
1054
1246
|
return;
|
|
1055
1247
|
}
|
|
1056
1248
|
}
|
|
1057
|
-
const anc =
|
|
1058
|
-
|
|
1059
|
-
|
|
1249
|
+
const anc = createStickyNoteFromUser(parsed.title || rawTrimmed, ctx);
|
|
1250
|
+
const bar = anc.progress ? ` ${formatProgressBar(anc.progress)}` : "";
|
|
1251
|
+
ctx.ui.notify(`\u2316 Pinned #${anc.id.replace(/^anc-/, "")}${bar}: "${anc.title}"`, "info");
|
|
1060
1252
|
}
|
|
1061
1253
|
});
|
|
1062
1254
|
pi.registerCommand("pin", {
|
|
1063
|
-
description: "Stick a quick note across sessions (e.g. /pin
|
|
1255
|
+
description: "Stick a quick note across sessions (e.g. /pin Migrate auth [1/3])",
|
|
1064
1256
|
handler: async (args, ctx) => {
|
|
1065
1257
|
const text = (args || "").trim();
|
|
1066
1258
|
if (!text) {
|
|
1067
1259
|
await openAnchorDashboard(ctx, store);
|
|
1068
1260
|
return;
|
|
1069
1261
|
}
|
|
1070
|
-
const anc =
|
|
1071
|
-
|
|
1262
|
+
const anc = createStickyNoteFromUser(text, ctx);
|
|
1263
|
+
const bar = anc.progress ? ` ${formatProgressBar(anc.progress)}` : "";
|
|
1264
|
+
ctx.ui.notify(`\u2316 Pinned #${anc.id.replace(/^anc-/, "")}${bar}: "${anc.title}"`, "info");
|
|
1265
|
+
}
|
|
1266
|
+
});
|
|
1267
|
+
pi.registerCommand("step", {
|
|
1268
|
+
description: "Advance progress bar on a sticky note (e.g. /step 1)",
|
|
1269
|
+
handler: async (args, ctx) => {
|
|
1270
|
+
const tokens = (args || "").trim().split(/\s+/).filter(Boolean);
|
|
1271
|
+
const target = tokens[0] || "1";
|
|
1272
|
+
const delta = tokens[1] ? parseInt(tokens[1], 10) : 1;
|
|
1273
|
+
const resolvedId = resolveAnchorTarget(store, target, ctx.cwd);
|
|
1274
|
+
if (!resolvedId) {
|
|
1275
|
+
ctx.ui.notify(`\u2316 Note not found: ${target}`, "error");
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
const { anchor: updated } = store.step(resolvedId, isNaN(delta) ? 1 : delta);
|
|
1279
|
+
ctx.ui.notify(`\u2316 Advanced #${updated.id.replace(/^anc-/, "")} to ${formatProgressBar(updated.progress)}`, "info");
|
|
1072
1280
|
updateAnchorStatusBar(ctx, store);
|
|
1073
1281
|
}
|
|
1074
1282
|
});
|
|
@@ -1100,5 +1308,6 @@ ${snippet}` };
|
|
|
1100
1308
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1101
1309
|
0 && (module.exports = {
|
|
1102
1310
|
parseAnchorCommandInput,
|
|
1311
|
+
parseQuickNoteInput,
|
|
1103
1312
|
resolveAnchorTarget
|
|
1104
1313
|
});
|
package/dist/extension.d.cts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import '@earendil-works/pi-coding-agent';
|
|
2
|
-
export {
|
|
2
|
+
export { z as default, t as parseAnchorCommandInput, u as parseQuickNoteInput, x as resolveAnchorTarget } from './extension-Cq1NH1jT.cjs';
|
package/dist/extension.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import '@earendil-works/pi-coding-agent';
|
|
2
|
-
export {
|
|
2
|
+
export { z as default, t as parseAnchorCommandInput, u as parseQuickNoteInput, x as resolveAnchorTarget } from './extension-Cq1NH1jT.js';
|
package/dist/extension.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
extension_default,
|
|
3
3
|
parseAnchorCommandInput,
|
|
4
|
+
parseQuickNoteInput,
|
|
4
5
|
resolveAnchorTarget
|
|
5
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-VXEQDTK4.js";
|
|
6
7
|
export {
|
|
7
8
|
extension_default as default,
|
|
8
9
|
parseAnchorCommandInput,
|
|
10
|
+
parseQuickNoteInput,
|
|
9
11
|
resolveAnchorTarget
|
|
10
12
|
};
|