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/dist/index.cjs CHANGED
@@ -37,8 +37,10 @@ __export(index_exports, {
37
37
  acquireSyncLock: () => acquireSyncLock,
38
38
  atomicRenameWithRetry: () => atomicRenameWithRetry,
39
39
  extractPathsFromText: () => extractPathsFromText,
40
+ extractProgressFromText: () => extractProgressFromText,
40
41
  findMatchedAnchors: () => findMatchedAnchors,
41
42
  formatOrigin: () => formatOrigin,
43
+ formatProgressBar: () => formatProgressBar,
42
44
  formatRelativeAge: () => formatRelativeAge,
43
45
  getDefaultStorageDir: () => getDefaultStorageDir,
44
46
  getDisplayWidth: () => getDisplayWidth,
@@ -53,6 +55,7 @@ __export(index_exports, {
53
55
  openAnchorDashboard: () => openAnchorDashboard,
54
56
  padToWidth: () => padToWidth,
55
57
  parseAnchorCommandInput: () => parseAnchorCommandInput,
58
+ parseQuickNoteInput: () => parseQuickNoteInput,
56
59
  renderActiveAnchorsContext: () => renderActiveAnchorsContext,
57
60
  renderColdStartAnchorsContext: () => renderColdStartAnchorsContext,
58
61
  resolveAnchorTarget: () => resolveAnchorTarget,
@@ -115,6 +118,32 @@ function extractPathsFromText(text) {
115
118
  }
116
119
  return Array.from(results);
117
120
  }
121
+ function extractProgressFromText(rawTitle) {
122
+ if (!rawTitle) return { cleanTitle: "" };
123
+ const stepParts = Array.from(rawTitle.matchAll(/(?:^|\s)(\d+)\.\s*([^\d]+?)(?=\s+\d+\.\s|$)/g));
124
+ if (stepParts.length >= 2) {
125
+ const steps = stepParts.map((m) => m[2].trim().replace(/[;;,,]+$/, "")).filter(Boolean);
126
+ const firstMatchIdx = rawTitle.indexOf(stepParts[0][0]);
127
+ const prefix = rawTitle.slice(0, firstMatchIdx).replace(/[::\-—\s]+$/, "").trim();
128
+ return {
129
+ cleanTitle: prefix || steps.join(" \u2192 "),
130
+ progress: { done: 0, total: steps.length, steps }
131
+ };
132
+ }
133
+ const ratioMatch = rawTitle.match(/(?:\[(\d{1,3})\/(\d{1,3})\]|\((\d{1,3})\/(\d{1,3})\)|(?:^|\s)(\d{1,3})\/(\d{1,3})(?=\s|$))/);
134
+ if (ratioMatch) {
135
+ const done = parseInt(ratioMatch[1] ?? ratioMatch[3] ?? ratioMatch[5], 10);
136
+ const total = parseInt(ratioMatch[2] ?? ratioMatch[4] ?? ratioMatch[6], 10);
137
+ if (!isNaN(done) && !isNaN(total) && total >= 2 && total <= 500 && done >= 0 && done <= total) {
138
+ const stripped = rawTitle.replace(ratioMatch[0], " ").replace(/\s+/g, " ").trim();
139
+ return {
140
+ cleanTitle: stripped || rawTitle.trim(),
141
+ progress: { done, total }
142
+ };
143
+ }
144
+ }
145
+ return { cleanTitle: rawTitle.trim() };
146
+ }
118
147
  function isAnchorTargetingWorkspace(anchor, targetCwd) {
119
148
  if (!targetCwd) return true;
120
149
  const normTarget = targetCwd.trim().replace(/\\/g, "/").replace(/\/+$/, "");
@@ -257,8 +286,9 @@ function renderColdStartAnchorsContext(store, cwdOrNow, nowArg) {
257
286
  const lines = activeNotes.map((a) => {
258
287
  const num = a.id.replace(/^anc-/, "");
259
288
  const files = a.files.length > 0 ? ` (${a.files.slice(0, 2).join(", ")})` : "";
289
+ 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]}` : ""}]` : "";
260
290
  const desc = a.description ? ` \u2014 ${a.description}` : "";
261
- return `\u2022 #${num} [${a.project}] ${a.title}${files}${desc}`;
291
+ return `\u2022 #${num} [${a.project}] ${a.title}${prog}${files}${desc}`;
262
292
  });
263
293
  return [
264
294
  `<sticky-notes count="${activeNotes.length}">`,
@@ -433,8 +463,9 @@ var AnchorStore = class {
433
463
  const state = this.loadState();
434
464
  const now = Date.now();
435
465
  const id = this.generateId(state);
466
+ const { cleanTitle, progress: parsedProgress } = extractProgressFromText(input.title);
436
467
  const explicitFiles = (input.files || []).map((f) => normalizePath(f)).filter(Boolean);
437
- const inferredFiles = explicitFiles.length > 0 ? explicitFiles : extractPathsFromText(`${input.title} ${input.description || ""}`);
468
+ const inferredFiles = explicitFiles.length > 0 ? explicitFiles : extractPathsFromText(`${cleanTitle} ${input.description || ""}`);
438
469
  let cwd = input.cwd ? normalizePath(input.cwd) : "";
439
470
  if (input.project && (/^[a-zA-Z]:[\\/]/.test(input.project.trim()) || input.project.trim().startsWith("/"))) {
440
471
  cwd = normalizePath(input.project.trim());
@@ -445,7 +476,7 @@ var AnchorStore = class {
445
476
  const projectName = input.project ? input.project.trim() : cwd ? import_node_path.default.basename(cwd) : "global";
446
477
  const anchor = {
447
478
  id,
448
- title: input.title.trim(),
479
+ title: cleanTitle,
449
480
  description: input.description?.trim(),
450
481
  priority: input.priority || "p1",
451
482
  status: "active",
@@ -455,7 +486,9 @@ var AnchorStore = class {
455
486
  updatedAt: now,
456
487
  lastTouchedAt: now,
457
488
  files: inferredFiles,
458
- tags: (input.tags || []).map((t) => t.trim()).filter(Boolean)
489
+ tags: (input.tags || []).map((t) => t.trim()).filter(Boolean),
490
+ ...input.sessionFile ? { sessionFile: normalizePath(input.sessionFile) } : {},
491
+ ...input.progress || parsedProgress ? { progress: input.progress || parsedProgress } : {}
459
492
  };
460
493
  state.anchors.push(anchor);
461
494
  this.saveState(state);
@@ -504,7 +537,7 @@ var AnchorStore = class {
504
537
  return updated;
505
538
  });
506
539
  }
507
- touch(id, timestamp = Date.now()) {
540
+ touch(id, timestamp = Date.now(), sessionFile) {
508
541
  return this.withLock(() => {
509
542
  const state = this.loadState();
510
543
  const idx = this.findAnchorIndex(state.anchors, id);
@@ -515,11 +548,44 @@ var AnchorStore = class {
515
548
  if (current.status === "sleeping") {
516
549
  current.status = "active";
517
550
  }
551
+ if (sessionFile) {
552
+ current.sessionFile = normalizePath(sessionFile);
553
+ }
518
554
  state.anchors[idx] = current;
519
555
  this.saveState(state);
520
556
  return current;
521
557
  });
522
558
  }
559
+ /**
560
+ * Advance (or set) progress on a sticky note:
561
+ * - If delta is provided (default +1), increments `done` up to `total`.
562
+ * - If note had no progress yet, initializes a 4-step bar (`1/4`).
563
+ */
564
+ step(id, delta = 1, explicitTotal) {
565
+ return this.withLock(() => {
566
+ const state = this.loadState();
567
+ const idx = this.findAnchorIndex(state.anchors, id);
568
+ if (idx === -1) throw new Error(`Note not found: ${id}`);
569
+ const current = state.anchors[idx];
570
+ const now = Date.now();
571
+ const total = explicitTotal && explicitTotal >= 2 ? explicitTotal : current.progress?.total || 4;
572
+ const prevDone = current.progress?.done || 0;
573
+ const nextDone = Math.max(0, Math.min(total, prevDone + delta));
574
+ current.progress = {
575
+ ...current.progress || {},
576
+ done: nextDone,
577
+ total
578
+ };
579
+ current.lastTouchedAt = now;
580
+ current.updatedAt = now;
581
+ if (current.status === "sleeping") {
582
+ current.status = "active";
583
+ }
584
+ state.anchors[idx] = current;
585
+ this.saveState(state);
586
+ return { anchor: current, completed: nextDone >= total };
587
+ });
588
+ }
523
589
  /**
524
590
  * Tear off (settle) a completed sticky note and archive it to archive.jsonl
525
591
  */
@@ -590,6 +656,7 @@ var AnchorStore = class {
590
656
  };
591
657
 
592
658
  // src/tui.ts
659
+ var import_node_fs2 = __toESM(require("fs"), 1);
593
660
  var import_node_path2 = __toESM(require("path"), 1);
594
661
  var ANSI_REGEX = /\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g;
595
662
  function stripAnsi(str) {
@@ -656,6 +723,14 @@ function padToWidth(str, targetWidth) {
656
723
  if (current >= targetWidth) return truncated;
657
724
  return truncated + " ".repeat(targetWidth - current);
658
725
  }
726
+ function formatProgressBar(progress, cells = 4) {
727
+ if (!progress || !progress.total || progress.total < 1) return "";
728
+ const done = Math.max(0, Math.min(progress.total, progress.done || 0));
729
+ const filled = Math.min(cells, Math.max(done > 0 ? 1 : 0, Math.round(done / progress.total * cells)));
730
+ const finalFilled = done >= progress.total ? cells : done === 0 ? 0 : filled;
731
+ const bar = "\u25A0".repeat(finalFilled) + "\u25A1".repeat(Math.max(0, cells - finalFilled));
732
+ return `[${bar} ${done}/${progress.total}]`;
733
+ }
659
734
  function formatRelativeAge(timestamp, now = Date.now()) {
660
735
  const diffSec = Math.max(0, Math.floor((now - timestamp) / 1e3));
661
736
  if (diffSec < 60) return "just now";
@@ -709,6 +784,7 @@ function updateAnchorStatusBar(ctx, store) {
709
784
  try {
710
785
  const active = store.list({ status: "active", cwd: ctx.cwd });
711
786
  ctx.ui.setStatus("anchor", active.length > 0 ? `\u2316 ${active.length}` : void 0);
787
+ updateStartupBanner(ctx, store);
712
788
  } catch {
713
789
  }
714
790
  }
@@ -717,41 +793,41 @@ function updateStartupBanner(ctx, store) {
717
793
  try {
718
794
  const active = store.list({ status: "active", cwd: ctx.cwd });
719
795
  if (active.length === 0) {
720
- ctx.ui.setWidget("anchor-startup", void 0);
796
+ ctx.ui.setWidget("anchor-startup", void 0, { placement: "belowEditor" });
721
797
  return;
722
798
  }
723
799
  const sorted = sortAnchorsForDisplay(active, ctx.cwd);
724
- const topItems = sorted.slice(0, 3);
725
800
  const factory = (_tui, theme) => ({
726
801
  render: (width) => {
727
- const maxTitleWidth = Math.max(14, width - 34);
728
802
  const fg = theme?.fg ? theme.fg.bind(theme) : (_c, text) => text;
729
- const label = active.length === 1 ? "1 note" : `${active.length} notes`;
730
- const lines = [
731
- truncateToWidth(fg("accent", `\u2316 ${label}`), width)
732
- ];
733
- for (const a of topItems) {
734
- const num = a.id.replace(/^anc-/, "").padStart(2, "0");
803
+ if (sorted.length === 1) {
804
+ const a = sorted[0];
805
+ const num = a.id.replace(/^anc-/, "");
806
+ const bar = a.progress ? `${fg("accent", formatProgressBar(a.progress))} ` : "";
807
+ const nextStep = a.progress?.steps && a.progress.steps[a.progress.done] ? fg("muted", ` \u25B8 ${a.progress.steps[a.progress.done]}`) : "";
735
808
  const origin = formatOrigin(a);
736
- const age = formatRelativeAge(a.createdAt);
737
- const titleClipped = truncateToWidth(a.title, maxTitleWidth);
738
- lines.push(
739
- truncateToWidth(
740
- ` ${fg("dim", num)} ${titleClipped} ${fg("muted", `\xB7 ${origin}`)} ${fg("dim", `(${age})`)}`,
741
- width
742
- )
743
- );
809
+ const sessionIcon = a.sessionFile ? " \u21AA" : "";
810
+ const prefix = `${fg("accent", "\u2316")} ${fg("dim", `#${num}`)} ${bar}`;
811
+ const suffix = ` ${fg("muted", `(${origin}${sessionIcon})`)}`;
812
+ const availTitle = Math.max(12, width - getDisplayWidth(prefix) - getDisplayWidth(nextStep) - getDisplayWidth(suffix) - 2);
813
+ const titleClipped = truncateToWidth(a.title, availTitle);
814
+ return [truncateToWidth(`${prefix}${titleClipped}${nextStep}${suffix}`, width)];
744
815
  }
745
- if (sorted.length > 3) {
746
- lines.push(truncateToWidth(fg("dim", ` +${sorted.length - 3} more \xB7 /anchor`), width));
747
- }
748
- return lines;
816
+ const countTag = fg("accent", `\u2316 ${sorted.length} notes: `);
817
+ const segments = sorted.slice(0, 3).map((a) => {
818
+ const num = a.id.replace(/^anc-/, "");
819
+ const bar = a.progress ? `${formatProgressBar(a.progress)} ` : "";
820
+ const shortTitle = truncateToWidth(a.title, Math.max(14, Math.floor((width - 20) / Math.min(3, sorted.length)) - 12));
821
+ return `${fg("dim", `#${num}`)} ${bar}${shortTitle}`;
822
+ });
823
+ const more = sorted.length > 3 ? fg("dim", ` +${sorted.length - 3}`) : "";
824
+ return [truncateToWidth(`${countTag}${segments.join(fg("dim", " \xB7 "))}${more}`, width)];
749
825
  },
750
826
  invalidate: () => {
751
827
  }
752
828
  });
753
829
  const content = ctx.ui?.theme ? factory : factory().render(80);
754
- ctx.ui.setWidget("anchor-startup", content, { placement: "aboveEditor" });
830
+ ctx.ui.setWidget("anchor-startup", content, { placement: "belowEditor" });
755
831
  } catch {
756
832
  }
757
833
  }
@@ -759,7 +835,7 @@ async function openAnchorDashboard(ctx, store) {
759
835
  if (!hasActiveUI(ctx)) return;
760
836
  const allNotes = store.list({ cwd: ctx.cwd, all: true }).filter((a) => a.status === "active" || a.status === "sleeping");
761
837
  if (allNotes.length === 0) {
762
- ctx.ui.notify("\u2316 No sticky notes. Use /pin <note> to stick one.", "info");
838
+ ctx.ui.notify("\u2316 No sticky notes. Type `pin: <note>` or `/pin <note>` to stick one.", "info");
763
839
  return;
764
840
  }
765
841
  const sorted = sortAnchorsForDisplay(allNotes, ctx.cwd);
@@ -770,10 +846,11 @@ async function openAnchorDashboard(ctx, store) {
770
846
  const titleWidth = isNarrow ? Math.max(20, termCols - 30) : 42;
771
847
  for (const a of sorted) {
772
848
  const num = a.id.replace(/^anc-/, "").padStart(2, "0");
773
- const origin = formatOrigin(a);
849
+ const bar = a.progress ? `${formatProgressBar(a.progress)} ` : "";
850
+ const origin = formatOrigin(a) + (a.sessionFile ? " \u21AA" : "");
774
851
  const age = a.status === "sleeping" ? `${formatRelativeAge(a.createdAt)} \xB7 quiet` : formatRelativeAge(a.createdAt);
775
852
  const colNum = `${num} `;
776
- const colTitle = padToWidth(a.title, titleWidth);
853
+ const colTitle = padToWidth(`${bar}${a.title}`, titleWidth);
777
854
  const colOrigin = padToWidth(origin, 22);
778
855
  const label = isNarrow ? `${colNum}${colTitle} ${age}`.trimEnd() : `${colNum}${colTitle} ${colOrigin} ${age}`.trimEnd();
779
856
  optionMap.set(label, a);
@@ -783,8 +860,15 @@ async function openAnchorDashboard(ctx, store) {
783
860
  if (!selected) return;
784
861
  const anchor = optionMap.get(selected);
785
862
  if (!anchor) return;
863
+ const currentSessionFile = ctx.sessionManager?.getSessionFile?.();
864
+ const canJumpSession = Boolean(
865
+ 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))
866
+ );
867
+ 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)";
786
868
  const actions = [
787
869
  "\u2713 Done (tear off note)",
870
+ stepLabel,
871
+ ...canJumpSession ? ["\u21AA Jump to session (resume linked conversation)"] : [],
788
872
  ...anchor.status === "sleeping" ? ["\u26A1 Wake up (mark active)"] : [],
789
873
  "\u2139 Read full note",
790
874
  "\u21A9 Back"
@@ -795,6 +879,23 @@ async function openAnchorDashboard(ctx, store) {
795
879
  store.settle(anchor.id, { settledBy: "manual" });
796
880
  ctx.ui.notify(`\u2316 Torn off #${anchor.id.replace(/^anc-/, "")}: "${anchor.title}" (/anchor undo to put back)`, "info");
797
881
  updateAnchorStatusBar(ctx, store);
882
+ } else if (action.startsWith("\u25B6")) {
883
+ const { anchor: updated, completed } = store.step(anchor.id, 1);
884
+ updateAnchorStatusBar(ctx, store);
885
+ if (completed) {
886
+ ctx.ui.notify(
887
+ `\u2316 #${updated.id.replace(/^anc-/, "")} reached ${formatProgressBar(updated.progress)}! Run /done ${updated.id.replace(/^anc-/, "")} to tear off.`,
888
+ "info"
889
+ );
890
+ } else {
891
+ ctx.ui.notify(`\u2316 Advanced #${updated.id.replace(/^anc-/, "")} to ${formatProgressBar(updated.progress)}`, "info");
892
+ }
893
+ } else if (action.startsWith("\u21AA") && anchor.sessionFile) {
894
+ try {
895
+ await ctx.switchSession(anchor.sessionFile);
896
+ } catch (err) {
897
+ ctx.ui.notify(`Could not switch session: ${err.message}`, "error");
898
+ }
798
899
  } else if (action.startsWith("\u26A1")) {
799
900
  store.touch(anchor.id);
800
901
  ctx.ui.notify(`\u2316 Woke up #${anchor.id.replace(/^anc-/, "")}: "${anchor.title}"`, "info");
@@ -802,8 +903,10 @@ async function openAnchorDashboard(ctx, store) {
802
903
  } else if (action.startsWith("\u2139")) {
803
904
  const details = [
804
905
  `\u2316 Note #${anchor.id.replace(/^anc-/, "")}: ${anchor.title}`,
906
+ anchor.progress ? `Progress: ${formatProgressBar(anchor.progress)}${anchor.progress.steps ? ` (${anchor.progress.steps.join(" \u2192 ")})` : ""}` : null,
805
907
  anchor.description ? `Details: ${anchor.description}` : null,
806
908
  anchor.files.length > 0 ? `Files: ${anchor.files.join(", ")}` : null,
909
+ anchor.sessionFile ? `Session: ${import_node_path2.default.basename(anchor.sessionFile)}` : null,
807
910
  `Workspace: ${anchor.project || "global"} \xB7 Pinned ${formatRelativeAge(anchor.createdAt)}`
808
911
  ].filter(Boolean).join("\n");
809
912
  ctx.ui.notify(details, "info");
@@ -812,6 +915,7 @@ async function openAnchorDashboard(ctx, store) {
812
915
 
813
916
  // src/extension.ts
814
917
  var import_typebox = require("@sinclair/typebox");
918
+ var import_node_fs3 = __toESM(require("fs"), 1);
815
919
  function parseAnchorCommandInput(raw) {
816
920
  const trimmed = (raw || "").trim();
817
921
  if (!trimmed || trimmed === "list" || trimmed === "ls") {
@@ -827,11 +931,30 @@ function parseAnchorCommandInput(raw) {
827
931
  targetId: settleMatch[1].trim().replace(/^#/, "")
828
932
  };
829
933
  }
934
+ const stepMatch = trimmed.match(/^(?:step|\+)\s*(\S+)(?:\s+([+-]?\d+))?$/i);
935
+ if (stepMatch) {
936
+ return {
937
+ mode: "step",
938
+ targetId: stepMatch[1].trim().replace(/^#/, ""),
939
+ delta: stepMatch[2] ? parseInt(stepMatch[2], 10) : 1
940
+ };
941
+ }
942
+ const jumpMatch = trimmed.match(/^(?:jump|resume|session)\s+(.+)$/i);
943
+ if (jumpMatch) {
944
+ return {
945
+ mode: "jump",
946
+ targetId: jumpMatch[1].trim().replace(/^#/, "")
947
+ };
948
+ }
830
949
  return {
831
950
  mode: "pin",
832
951
  title: trimmed
833
952
  };
834
953
  }
954
+ function parseQuickNoteInput(text) {
955
+ const m = (text || "").trim().match(/^(?:pin|note|便签|备忘)\s*[::]\s*(.+)$/i);
956
+ return m && m[1].trim() ? m[1].trim() : null;
957
+ }
835
958
  function resolveAnchorTarget(store, idOrIndex, cwd) {
836
959
  const clean = idOrIndex.trim().replace(/^#/, "");
837
960
  if (/^\d+$/.test(clean)) {
@@ -853,8 +976,10 @@ function resolveAnchorTarget(store, idOrIndex, cwd) {
853
976
  acquireSyncLock,
854
977
  atomicRenameWithRetry,
855
978
  extractPathsFromText,
979
+ extractProgressFromText,
856
980
  findMatchedAnchors,
857
981
  formatOrigin,
982
+ formatProgressBar,
858
983
  formatRelativeAge,
859
984
  getDefaultStorageDir,
860
985
  getDisplayWidth,
@@ -869,6 +994,7 @@ function resolveAnchorTarget(store, idOrIndex, cwd) {
869
994
  openAnchorDashboard,
870
995
  padToWidth,
871
996
  parseAnchorCommandInput,
997
+ parseQuickNoteInput,
872
998
  renderActiveAnchorsContext,
873
999
  renderColdStartAnchorsContext,
874
1000
  resolveAnchorTarget,
package/dist/index.d.cts CHANGED
@@ -1,6 +1,6 @@
1
- import { A as Anchor, a as AnchorStore } from './extension-B1-fzCox.cjs';
2
- export { b as AnchorEvidence, c as AnchorPriority, d as AnchorStatus, e as AnchorStoreState, C as COMMENT_FORMATS, M as MS_PER_DAY, Q as QUIET_AFTER_DAYS, T as TouchMatchResult, f as acquireSyncLock, g as atomicRenameWithRetry, h as extractPathsFromText, i as findMatchedAnchors, j as getDefaultStorageDir, k as globToRegExp, l as isAnchorTargetingWorkspace, m as isSameOrSubPath, n as makeSafeTaskAnnotation, o as matchAnchorAgainstTouchedFiles, p as matchesCommitMessage, q as normalizePath, r as parseAnchorCommandInput, s as renderActiveAnchorsContext, t as renderColdStartAnchorsContext, u as resolveAnchorTarget, v as sweepQuietNotes } from './extension-B1-fzCox.cjs';
3
- import { ExtensionContext } from '@earendil-works/pi-coding-agent';
1
+ import { A as Anchor, a as AnchorProgress, b as AnchorStore } from './extension-Cq1NH1jT.cjs';
2
+ export { c as AnchorEvidence, d as AnchorPriority, e as AnchorStatus, f as AnchorStoreState, C as COMMENT_FORMATS, M as MS_PER_DAY, Q as QUIET_AFTER_DAYS, T as TouchMatchResult, g as acquireSyncLock, h as atomicRenameWithRetry, i as extractPathsFromText, j as extractProgressFromText, k as findMatchedAnchors, l as getDefaultStorageDir, m as globToRegExp, n as isAnchorTargetingWorkspace, o as isSameOrSubPath, p as makeSafeTaskAnnotation, q as matchAnchorAgainstTouchedFiles, r as matchesCommitMessage, s as normalizePath, t as parseAnchorCommandInput, u as parseQuickNoteInput, v as renderActiveAnchorsContext, w as renderColdStartAnchorsContext, x as resolveAnchorTarget, y as sweepQuietNotes } from './extension-Cq1NH1jT.cjs';
3
+ import { ExtensionContext, ExtensionCommandContext } from '@earendil-works/pi-coding-agent';
4
4
 
5
5
  declare function stripAnsi(str: string): string;
6
6
  /**
@@ -10,12 +10,16 @@ declare function getDisplayWidth(str: string): number;
10
10
  declare function hasActiveUI(ctx: ExtensionContext | null | undefined): ctx is ExtensionContext;
11
11
  declare function truncateToWidth(str: string, maxWidth: number, ellipsis?: string): string;
12
12
  declare function padToWidth(str: string, targetWidth: number): string;
13
+ /**
14
+ * Render a 4-cell micro progress bar: e.g. `[■■□□ 2/4]` or `[■□□□ 5/30]`
15
+ */
16
+ declare function formatProgressBar(progress?: AnchorProgress, cells?: number): string;
13
17
  /**
14
18
  * Format relative sticky-note age cleanly: 'just now', '15m ago', '3h ago', '2d ago'
15
19
  */
16
20
  declare function formatRelativeAge(timestamp: number, now?: number): string;
17
21
  /**
18
- * Format workspace & first file context concisely (e.g. 'anchor · src/auth.ts' or 'College/优化')
22
+ * Format workspace & first file context concisely (e.g. 'anchor · auth.ts' or 'College/优化')
19
23
  */
20
24
  declare function formatOrigin(anchor: Anchor | string): string;
21
25
  /**
@@ -26,17 +30,18 @@ declare function formatOrigin(anchor: Anchor | string): string;
26
30
  */
27
31
  declare function sortAnchorsForDisplay(anchors: Anchor[], cwd?: string): Anchor[];
28
32
  /**
29
- * Update status bar badge: `⌖ 2` (invisible when 0 active notes)
33
+ * Update status bar badge (`⌖ 2`) AND the persistent below-editor sticky strip!
30
34
  */
31
35
  declare function updateAnchorStatusBar(ctx: ExtensionContext, store: AnchorStore): void;
32
36
  /**
33
- * Render clean sticky-note card above editor on session startup.
34
- * Automatically disappears as soon as the user sends their first message.
37
+ * Render persistent 1-line sticky-note strip BELOW the editor (`placement: 'belowEditor'`).
38
+ * Never collides with `pi-lingual` (`aboveEditor`), stays visible while you work,
39
+ * and takes 0 lines when there are no active notes.
35
40
  */
36
41
  declare function updateStartupBanner(ctx: ExtensionContext, store: AnchorStore): void;
37
42
  /**
38
- * Interactive `/anchor` sticky-note board
43
+ * Interactive `/anchor` sticky-note board with 1-click progress stepping and session teleport (`switchSession`)
39
44
  */
40
- declare function openAnchorDashboard(ctx: ExtensionContext, store: AnchorStore): Promise<void>;
45
+ declare function openAnchorDashboard(ctx: ExtensionContext | ExtensionCommandContext, store: AnchorStore): Promise<void>;
41
46
 
42
- export { Anchor, AnchorStore, formatOrigin, formatRelativeAge, getDisplayWidth, hasActiveUI, openAnchorDashboard, padToWidth, sortAnchorsForDisplay, stripAnsi, truncateToWidth, updateAnchorStatusBar, updateStartupBanner };
47
+ export { Anchor, AnchorProgress, AnchorStore, formatOrigin, formatProgressBar, formatRelativeAge, getDisplayWidth, hasActiveUI, openAnchorDashboard, padToWidth, sortAnchorsForDisplay, stripAnsi, truncateToWidth, updateAnchorStatusBar, updateStartupBanner };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { A as Anchor, a as AnchorStore } from './extension-B1-fzCox.js';
2
- export { b as AnchorEvidence, c as AnchorPriority, d as AnchorStatus, e as AnchorStoreState, C as COMMENT_FORMATS, M as MS_PER_DAY, Q as QUIET_AFTER_DAYS, T as TouchMatchResult, f as acquireSyncLock, g as atomicRenameWithRetry, h as extractPathsFromText, i as findMatchedAnchors, j as getDefaultStorageDir, k as globToRegExp, l as isAnchorTargetingWorkspace, m as isSameOrSubPath, n as makeSafeTaskAnnotation, o as matchAnchorAgainstTouchedFiles, p as matchesCommitMessage, q as normalizePath, r as parseAnchorCommandInput, s as renderActiveAnchorsContext, t as renderColdStartAnchorsContext, u as resolveAnchorTarget, v as sweepQuietNotes } from './extension-B1-fzCox.js';
3
- import { ExtensionContext } from '@earendil-works/pi-coding-agent';
1
+ import { A as Anchor, a as AnchorProgress, b as AnchorStore } from './extension-Cq1NH1jT.js';
2
+ export { c as AnchorEvidence, d as AnchorPriority, e as AnchorStatus, f as AnchorStoreState, C as COMMENT_FORMATS, M as MS_PER_DAY, Q as QUIET_AFTER_DAYS, T as TouchMatchResult, g as acquireSyncLock, h as atomicRenameWithRetry, i as extractPathsFromText, j as extractProgressFromText, k as findMatchedAnchors, l as getDefaultStorageDir, m as globToRegExp, n as isAnchorTargetingWorkspace, o as isSameOrSubPath, p as makeSafeTaskAnnotation, q as matchAnchorAgainstTouchedFiles, r as matchesCommitMessage, s as normalizePath, t as parseAnchorCommandInput, u as parseQuickNoteInput, v as renderActiveAnchorsContext, w as renderColdStartAnchorsContext, x as resolveAnchorTarget, y as sweepQuietNotes } from './extension-Cq1NH1jT.js';
3
+ import { ExtensionContext, ExtensionCommandContext } from '@earendil-works/pi-coding-agent';
4
4
 
5
5
  declare function stripAnsi(str: string): string;
6
6
  /**
@@ -10,12 +10,16 @@ declare function getDisplayWidth(str: string): number;
10
10
  declare function hasActiveUI(ctx: ExtensionContext | null | undefined): ctx is ExtensionContext;
11
11
  declare function truncateToWidth(str: string, maxWidth: number, ellipsis?: string): string;
12
12
  declare function padToWidth(str: string, targetWidth: number): string;
13
+ /**
14
+ * Render a 4-cell micro progress bar: e.g. `[■■□□ 2/4]` or `[■□□□ 5/30]`
15
+ */
16
+ declare function formatProgressBar(progress?: AnchorProgress, cells?: number): string;
13
17
  /**
14
18
  * Format relative sticky-note age cleanly: 'just now', '15m ago', '3h ago', '2d ago'
15
19
  */
16
20
  declare function formatRelativeAge(timestamp: number, now?: number): string;
17
21
  /**
18
- * Format workspace & first file context concisely (e.g. 'anchor · src/auth.ts' or 'College/优化')
22
+ * Format workspace & first file context concisely (e.g. 'anchor · auth.ts' or 'College/优化')
19
23
  */
20
24
  declare function formatOrigin(anchor: Anchor | string): string;
21
25
  /**
@@ -26,17 +30,18 @@ declare function formatOrigin(anchor: Anchor | string): string;
26
30
  */
27
31
  declare function sortAnchorsForDisplay(anchors: Anchor[], cwd?: string): Anchor[];
28
32
  /**
29
- * Update status bar badge: `⌖ 2` (invisible when 0 active notes)
33
+ * Update status bar badge (`⌖ 2`) AND the persistent below-editor sticky strip!
30
34
  */
31
35
  declare function updateAnchorStatusBar(ctx: ExtensionContext, store: AnchorStore): void;
32
36
  /**
33
- * Render clean sticky-note card above editor on session startup.
34
- * Automatically disappears as soon as the user sends their first message.
37
+ * Render persistent 1-line sticky-note strip BELOW the editor (`placement: 'belowEditor'`).
38
+ * Never collides with `pi-lingual` (`aboveEditor`), stays visible while you work,
39
+ * and takes 0 lines when there are no active notes.
35
40
  */
36
41
  declare function updateStartupBanner(ctx: ExtensionContext, store: AnchorStore): void;
37
42
  /**
38
- * Interactive `/anchor` sticky-note board
43
+ * Interactive `/anchor` sticky-note board with 1-click progress stepping and session teleport (`switchSession`)
39
44
  */
40
- declare function openAnchorDashboard(ctx: ExtensionContext, store: AnchorStore): Promise<void>;
45
+ declare function openAnchorDashboard(ctx: ExtensionContext | ExtensionCommandContext, store: AnchorStore): Promise<void>;
41
46
 
42
- export { Anchor, AnchorStore, formatOrigin, formatRelativeAge, getDisplayWidth, hasActiveUI, openAnchorDashboard, padToWidth, sortAnchorsForDisplay, stripAnsi, truncateToWidth, updateAnchorStatusBar, updateStartupBanner };
47
+ export { Anchor, AnchorProgress, AnchorStore, formatOrigin, formatProgressBar, formatRelativeAge, getDisplayWidth, hasActiveUI, openAnchorDashboard, padToWidth, sortAnchorsForDisplay, stripAnsi, truncateToWidth, updateAnchorStatusBar, updateStartupBanner };
package/dist/index.js CHANGED
@@ -6,8 +6,10 @@ import {
6
6
  acquireSyncLock,
7
7
  atomicRenameWithRetry,
8
8
  extractPathsFromText,
9
+ extractProgressFromText,
9
10
  findMatchedAnchors,
10
11
  formatOrigin,
12
+ formatProgressBar,
11
13
  formatRelativeAge,
12
14
  getDefaultStorageDir,
13
15
  getDisplayWidth,
@@ -22,6 +24,7 @@ import {
22
24
  openAnchorDashboard,
23
25
  padToWidth,
24
26
  parseAnchorCommandInput,
27
+ parseQuickNoteInput,
25
28
  renderActiveAnchorsContext,
26
29
  renderColdStartAnchorsContext,
27
30
  resolveAnchorTarget,
@@ -31,7 +34,7 @@ import {
31
34
  truncateToWidth,
32
35
  updateAnchorStatusBar,
33
36
  updateStartupBanner
34
- } from "./chunk-LKY6WIKH.js";
37
+ } from "./chunk-VXEQDTK4.js";
35
38
  export {
36
39
  AnchorStore,
37
40
  COMMENT_FORMATS,
@@ -40,8 +43,10 @@ export {
40
43
  acquireSyncLock,
41
44
  atomicRenameWithRetry,
42
45
  extractPathsFromText,
46
+ extractProgressFromText,
43
47
  findMatchedAnchors,
44
48
  formatOrigin,
49
+ formatProgressBar,
45
50
  formatRelativeAge,
46
51
  getDefaultStorageDir,
47
52
  getDisplayWidth,
@@ -56,6 +61,7 @@ export {
56
61
  openAnchorDashboard,
57
62
  padToWidth,
58
63
  parseAnchorCommandInput,
64
+ parseQuickNoteInput,
59
65
  renderActiveAnchorsContext,
60
66
  renderColdStartAnchorsContext,
61
67
  resolveAnchorTarget,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-anchor",
3
- "version": "0.4.0",
4
- "description": "A digital sticky note for Pi — stick a quick note across sessions (/pin), get a 1-line whisper when touching matching files, and auto-tear off on git commit with 0 TODO.md clutter.",
3
+ "version": "0.5.0",
4
+ "description": "A digital sticky note docked below your Pi editor — stick notes in 0ms (pin: ...), track long tasks with micro progress bars [■■□□], jump back to the original session (↪), and auto-tear off on git commit.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.js",