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.
@@ -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(`${input.title} ${input.description || ""}`);
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: input.title.trim(),
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
- const label = active.length === 1 ? "1 note" : `${active.length} notes`;
703
- const lines = [
704
- truncateToWidth(fg("accent", `\u2316 ${label}`), width)
705
- ];
706
- for (const a of topItems) {
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 age = formatRelativeAge(a.createdAt);
710
- const titleClipped = truncateToWidth(a.title, maxTitleWidth);
711
- lines.push(
712
- truncateToWidth(
713
- ` ${fg("dim", num)} ${titleClipped} ${fg("muted", `\xB7 ${origin}`)} ${fg("dim", `(${age})`)}`,
714
- width
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
- if (sorted.length > 3) {
719
- lines.push(truncateToWidth(fg("dim", ` +${sorted.length - 3} more \xB7 /anchor`), width));
720
- }
721
- return lines;
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: "aboveEditor" });
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. Use /pin <note> to stick one.", "info");
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 origin = formatOrigin(a);
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, titleWidth);
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
- pi.on("session_start", async (event, ctx) => {
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
- if (event.reason !== "resume") {
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("agent_start", async (_event, ctx) => {
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
- try {
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 (MUTATION_TOOLS.has(lowerTool)) {
892
- for (const m of matches) store.touch(m.anchor.id);
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
- 'Use `anchor` when the user asks to remember something for later/tomorrow or across sessions (e.g. "\u8BB0\u4E00\u4E0B\u660E\u5929\u91CD\u6784\u9274\u6743", "\u665A\u4E0A\u6E05\u7406\u7F13\u5B58").',
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 (for pin)" })),
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 done 1, /anchor undo)",
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 = store.create({ title: parsed.title || rawTrimmed, cwd: ctx.cwd });
1058
- ctx.ui.notify(`\u2316 Pinned #${anc.id.replace(/^anc-/, "")}: "${anc.title}"`, "info");
1059
- updateAnchorStatusBar(ctx, store);
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 Check auth token expiry)",
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 = store.create({ title: text, cwd: ctx.cwd });
1071
- ctx.ui.notify(`\u2316 Pinned #${anc.id.replace(/^anc-/, "")}: "${anc.title}"`, "info");
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
  });
@@ -1,2 +1,2 @@
1
1
  import '@earendil-works/pi-coding-agent';
2
- export { w as default, r as parseAnchorCommandInput, u as resolveAnchorTarget } from './extension-B1-fzCox.cjs';
2
+ export { z as default, t as parseAnchorCommandInput, u as parseQuickNoteInput, x as resolveAnchorTarget } from './extension-Cq1NH1jT.cjs';
@@ -1,2 +1,2 @@
1
1
  import '@earendil-works/pi-coding-agent';
2
- export { w as default, r as parseAnchorCommandInput, u as resolveAnchorTarget } from './extension-B1-fzCox.js';
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-LKY6WIKH.js";
6
+ } from "./chunk-VXEQDTK4.js";
6
7
  export {
7
8
  extension_default as default,
8
9
  parseAnchorCommandInput,
10
+ parseQuickNoteInput,
9
11
  resolveAnchorTarget
10
12
  };