pi-anchor 0.2.1 → 0.2.3

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.
@@ -22,20 +22,34 @@ var DEFAULT_DECAY_POLICY = {
22
22
  // src/matcher.ts
23
23
  import path from "path";
24
24
  function globToRegExp(pattern) {
25
- const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\/\*\*\//g, "___GLOB_DIR_SLASH___").replace(/\*\*/g, "___GLOB_STAR_STAR___").replace(/\*/g, "[^/]*").replace(/___GLOB_DIR_SLASH___/g, "(?:/|/.+/)").replace(/___GLOB_STAR_STAR___/g, ".*");
25
+ const collapsed = pattern.slice(0, 512).replace(/\/+(\*\*\/+)+/g, "/**/").replace(/\*{3,}/g, "**");
26
+ const escaped = collapsed.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\/\*\*\//g, "___GLOB_DIR_SLASH___").replace(/\*\*/g, "___GLOB_STAR_STAR___").replace(/\*/g, "[^/]*").replace(/___GLOB_DIR_SLASH___/g, "(?:/|/(?:[^/]+/)+)").replace(/___GLOB_STAR_STAR___/g, ".*");
26
27
  return new RegExp(`^${escaped}$`);
27
28
  }
28
29
  function normalizePath(filePath, baseCwd) {
29
30
  if (typeof filePath !== "string") return "";
30
31
  let norm = filePath.trim().replace(/\\/g, "/").replace(/^\.\//, "");
31
- if (baseCwd && (path.isAbsolute(filePath) || norm.startsWith("/"))) {
32
- const normBase = baseCwd.trim().replace(/\\/g, "/");
33
- if (norm.toLowerCase().startsWith(normBase.toLowerCase())) {
34
- norm = norm.slice(normBase.length).replace(/^\/+/, "");
32
+ if (baseCwd && (path.isAbsolute(filePath) || norm.startsWith("/") || /^[a-zA-Z]:\//.test(norm))) {
33
+ const normBase = baseCwd.trim().replace(/\\/g, "/").replace(/\/+$/, "");
34
+ const lowerNorm = norm.toLowerCase();
35
+ const lowerBase = normBase.toLowerCase();
36
+ if (lowerNorm === lowerBase) {
37
+ norm = "";
38
+ } else if (lowerNorm.startsWith(lowerBase + "/")) {
39
+ norm = norm.slice(normBase.length + 1).replace(/^\/+/, "");
35
40
  }
36
41
  }
37
42
  return norm;
38
43
  }
44
+ function isSameOrSubPath(anchorCwd, targetCwd) {
45
+ if (!anchorCwd || !targetCwd) return false;
46
+ const normA = anchorCwd.trim().replace(/\\/g, "/").replace(/\/+$/, "");
47
+ const normT = targetCwd.trim().replace(/\\/g, "/").replace(/\/+$/, "");
48
+ const isWinStyle = process.platform === "win32" || /^[a-zA-Z]:\//.test(normA) || /^[a-zA-Z]:\//.test(normT);
49
+ const cmpA = isWinStyle ? normA.toLowerCase() : normA;
50
+ const cmpT = isWinStyle ? normT.toLowerCase() : normT;
51
+ return cmpA === cmpT || cmpT.startsWith(cmpA + "/") || cmpA.startsWith(cmpT + "/");
52
+ }
39
53
  function matchAnchorAgainstTouchedFiles(anchor, touchedFiles) {
40
54
  if (!anchor.files || anchor.files.length === 0) return null;
41
55
  const normTouched = touchedFiles.map((f) => normalizePath(f, anchor.cwd)).filter(Boolean);
@@ -400,9 +414,19 @@ var AnchorStore = class {
400
414
  }
401
415
  try {
402
416
  const data = JSON.parse(raw);
403
- if (!Array.isArray(data.anchors)) {
417
+ if (!data || typeof data !== "object" || !Array.isArray(data.anchors)) {
404
418
  throw new Error("Invalid state: anchors must be array");
405
419
  }
420
+ data.anchors = data.anchors.filter((a) => a && typeof a === "object" && typeof a.id === "string" && typeof a.title === "string").map((a) => ({
421
+ ...a,
422
+ files: Array.isArray(a.files) ? a.files.filter((f) => typeof f === "string") : [],
423
+ tags: Array.isArray(a.tags) ? a.tags.filter((t) => typeof t === "string") : [],
424
+ decay: {
425
+ activeDays: Number.isFinite(a.decay?.activeDays) && a.decay.activeDays > 0 ? a.decay.activeDays : DURABLE_DECAY_POLICY.activeDays,
426
+ sleepDays: Number.isFinite(a.decay?.sleepDays) && a.decay.sleepDays > 0 ? a.decay.sleepDays : DURABLE_DECAY_POLICY.sleepDays,
427
+ graveyardDays: Number.isFinite(a.decay?.graveyardDays) && a.decay.graveyardDays > 0 ? a.decay.graveyardDays : DURABLE_DECAY_POLICY.graveyardDays
428
+ }
429
+ }));
406
430
  return data;
407
431
  } catch {
408
432
  const backupPath = path2.join(this.storageDir, `state.corrupt.${Date.now()}.json`);
@@ -422,16 +446,19 @@ var AnchorStore = class {
422
446
  */
423
447
  saveState(state) {
424
448
  this.ensureDirs();
425
- const tempPath = path2.join(this.storageDir, `state.tmp.${process.pid}.${Date.now()}`);
449
+ const rand = Math.random().toString(36).slice(2, 7);
450
+ const tempPath = path2.join(this.storageDir, `state.tmp.${process.pid}.${Date.now()}.${rand}`);
426
451
  fs.writeFileSync(tempPath, JSON.stringify(state, null, 2), "utf-8");
427
452
  atomicRenameWithRetry(tempPath, this.statePath);
428
453
  }
429
- generateId(existingAnchors) {
430
- const numbers = existingAnchors.map((a) => {
454
+ generateId(state) {
455
+ const numbers = state.anchors.map((a) => {
431
456
  const match = a.id.match(/^anc-(\d+)$/);
432
457
  return match ? parseInt(match[1], 10) : 0;
433
458
  }).filter((n) => n > 0);
434
- const nextNum = numbers.length > 0 ? Math.max(...numbers) + 1 : 1;
459
+ const maxExisting = numbers.length > 0 ? Math.max(...numbers) : 0;
460
+ const nextNum = Math.max(maxExisting + 1, state.nextId || 1);
461
+ state.nextId = nextNum + 1;
435
462
  return `anc-${nextNum}`;
436
463
  }
437
464
  /**
@@ -441,7 +468,7 @@ var AnchorStore = class {
441
468
  return this.withLock(() => {
442
469
  const state = this.loadState();
443
470
  const now = Date.now();
444
- const id = this.generateId(state.anchors);
471
+ const id = this.generateId(state);
445
472
  const cwd = input.cwd ? normalizePath(input.cwd) : "";
446
473
  const projectName = input.project ? input.project.trim() : cwd ? path2.basename(cwd) : "global";
447
474
  const recurrence = input.recurrence || detectRecurrence(input.title);
@@ -501,7 +528,7 @@ var AnchorStore = class {
501
528
  if (filter?.status && a.status !== filter.status) return false;
502
529
  if (filter?.priority && a.priority !== filter.priority) return false;
503
530
  if (!filter?.all && targetCwd) {
504
- if (a.files.length > 0 && a.cwd && a.cwd !== targetCwd) {
531
+ if (a.files.length > 0 && a.cwd && !isSameOrSubPath(a.cwd, targetCwd)) {
505
532
  return false;
506
533
  }
507
534
  }
@@ -602,18 +629,46 @@ var AnchorStore = class {
602
629
  throw new Error("No archived anchors to undo");
603
630
  }
604
631
  const raw = fs.readFileSync(this.archivePath, "utf-8");
605
- const lines = raw.split("\n").filter(Boolean);
606
- if (lines.length === 0) {
632
+ const lines = raw.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
633
+ let anchor = null;
634
+ while (lines.length > 0 && !anchor) {
635
+ const candidate = lines.pop();
636
+ try {
637
+ const parsed = JSON.parse(candidate);
638
+ if (parsed && typeof parsed === "object" && typeof parsed.id === "string" && typeof parsed.title === "string") {
639
+ anchor = parsed;
640
+ }
641
+ } catch {
642
+ }
643
+ }
644
+ if (!anchor) {
607
645
  throw new Error("Archive is empty, nothing to undo");
608
646
  }
609
- const lastLine = lines.pop();
610
- const anchor = JSON.parse(lastLine);
611
647
  fs.writeFileSync(this.archivePath, lines.length > 0 ? lines.join("\n") + "\n" : "", "utf-8");
612
648
  anchor.status = "active";
613
649
  anchor.updatedAt = Date.now();
614
650
  delete anchor.evidence;
651
+ delete anchor.settledForDate;
652
+ if (anchor.recurrence === "daily") {
653
+ delete anchor.lastCompletedDate;
654
+ }
615
655
  const state = this.loadState();
616
- state.anchors.push(anchor);
656
+ const existingIdx = state.anchors.findIndex((a) => a.id === anchor.id);
657
+ if (existingIdx !== -1) {
658
+ if (state.anchors[existingIdx].title === anchor.title) {
659
+ state.anchors[existingIdx] = anchor;
660
+ } else {
661
+ anchor.id = this.generateId(state);
662
+ state.anchors.push(anchor);
663
+ }
664
+ } else {
665
+ const match = anchor.id.match(/^anc-(\d+)$/);
666
+ if (match) {
667
+ const restoredNum = parseInt(match[1], 10);
668
+ state.nextId = Math.max(state.nextId || 1, restoredNum + 1);
669
+ }
670
+ state.anchors.push(anchor);
671
+ }
617
672
  this.saveState(state);
618
673
  return anchor;
619
674
  });
@@ -645,10 +700,19 @@ var AnchorStore = class {
645
700
  if (!fs.existsSync(this.archivePath)) return [];
646
701
  try {
647
702
  const raw = fs.readFileSync(this.archivePath, "utf-8");
648
- const all = raw.split("\n").filter(Boolean).map((l) => JSON.parse(l));
703
+ const all = [];
704
+ for (const l of raw.split(/\r?\n/)) {
705
+ const trimmed = l.trim();
706
+ if (!trimmed) continue;
707
+ try {
708
+ const parsed = JSON.parse(trimmed);
709
+ if (parsed && typeof parsed === "object" && typeof parsed.id === "string") all.push(parsed);
710
+ } catch {
711
+ }
712
+ }
649
713
  if (filter?.all || !filter?.cwd) return all;
650
714
  const targetCwd = normalizePath(filter.cwd);
651
- return all.filter((a) => !a.cwd || a.cwd === targetCwd);
715
+ return all.filter((a) => !a.cwd || isSameOrSubPath(a.cwd, targetCwd));
652
716
  } catch {
653
717
  return [];
654
718
  }
@@ -657,10 +721,19 @@ var AnchorStore = class {
657
721
  if (!fs.existsSync(this.graveyardPath)) return [];
658
722
  try {
659
723
  const raw = fs.readFileSync(this.graveyardPath, "utf-8");
660
- const all = raw.split("\n").filter(Boolean).map((l) => JSON.parse(l));
724
+ const all = [];
725
+ for (const l of raw.split(/\r?\n/)) {
726
+ const trimmed = l.trim();
727
+ if (!trimmed) continue;
728
+ try {
729
+ const parsed = JSON.parse(trimmed);
730
+ if (parsed && typeof parsed === "object" && typeof parsed.id === "string") all.push(parsed);
731
+ } catch {
732
+ }
733
+ }
661
734
  if (filter?.all || !filter?.cwd) return all;
662
735
  const targetCwd = normalizePath(filter.cwd);
663
- return all.filter((a) => !a.cwd || a.cwd === targetCwd);
736
+ return all.filter((a) => !a.cwd || isSameOrSubPath(a.cwd, targetCwd));
664
737
  } catch {
665
738
  return [];
666
739
  }
@@ -816,56 +889,58 @@ function evaluateAnchorDecay(anchor, now = Date.now()) {
816
889
  };
817
890
  }
818
891
  function sweepStore(store, now = Date.now()) {
819
- const state = store.loadState();
820
- const result = {
821
- transitionedToSleeping: [],
822
- wokenToActive: [],
823
- evictedToGraveyard: []
824
- };
825
- const toEvict = [];
826
- let stateModified = false;
827
- for (const anchor of state.anchors) {
828
- const evalResult = evaluateAnchorDecay(anchor, now);
829
- if (evalResult.nextStatus === "graveyard") {
830
- toEvict.push(anchor);
831
- } else if (evalResult.nextStatus !== anchor.status) {
832
- if (evalResult.nextStatus === "sleeping" && anchor.status === "active") {
833
- anchor.status = "sleeping";
834
- anchor.updatedAt = now;
835
- result.transitionedToSleeping.push(anchor.id);
836
- stateModified = true;
837
- } else if (evalResult.nextStatus === "active" && anchor.status === "sleeping") {
838
- anchor.status = "active";
839
- anchor.updatedAt = now;
840
- result.wokenToActive.push(anchor.id);
841
- stateModified = true;
892
+ return store.withLock(() => {
893
+ const state = store.loadState();
894
+ const result = {
895
+ transitionedToSleeping: [],
896
+ wokenToActive: [],
897
+ evictedToGraveyard: []
898
+ };
899
+ const toEvict = [];
900
+ let stateModified = false;
901
+ for (const anchor of state.anchors) {
902
+ const evalResult = evaluateAnchorDecay(anchor, now);
903
+ if (evalResult.nextStatus === "graveyard") {
904
+ toEvict.push(anchor);
905
+ } else if (evalResult.nextStatus !== anchor.status) {
906
+ if (evalResult.nextStatus === "sleeping" && anchor.status === "active") {
907
+ anchor.status = "sleeping";
908
+ anchor.updatedAt = now;
909
+ result.transitionedToSleeping.push(anchor.id);
910
+ stateModified = true;
911
+ } else if (evalResult.nextStatus === "active" && anchor.status === "sleeping") {
912
+ anchor.status = "active";
913
+ anchor.updatedAt = now;
914
+ result.wokenToActive.push(anchor.id);
915
+ stateModified = true;
916
+ }
842
917
  }
843
918
  }
844
- }
845
- if (toEvict.length > 0) {
846
- const evictIds = new Set(toEvict.map((e) => e.id));
847
- state.anchors = state.anchors.filter((a) => !evictIds.has(a.id));
848
- stateModified = true;
849
- const graveyardLines = toEvict.map((exp) => {
850
- const rec = {
851
- ...exp,
852
- status: "graveyard",
853
- updatedAt: now,
854
- evictedAt: now,
855
- evictionReason: `Exceeded decay threshold (${exp.decay.graveyardDays} days untouched)`
856
- };
857
- return JSON.stringify(rec);
858
- }).join("\n") + "\n";
859
- fs2.appendFileSync(store.graveyardPath, graveyardLines, "utf-8");
860
- for (const exp of toEvict) {
861
- result.evictedToGraveyard.push(exp.id);
919
+ if (toEvict.length > 0) {
920
+ const evictIds = new Set(toEvict.map((e) => e.id));
921
+ state.anchors = state.anchors.filter((a) => !evictIds.has(a.id));
922
+ stateModified = true;
923
+ const graveyardLines = toEvict.map((exp) => {
924
+ const rec = {
925
+ ...exp,
926
+ status: "graveyard",
927
+ updatedAt: now,
928
+ evictedAt: now,
929
+ evictionReason: `Exceeded decay threshold (${exp.decay.graveyardDays} days untouched)`
930
+ };
931
+ return JSON.stringify(rec);
932
+ }).join("\n") + "\n";
933
+ fs2.appendFileSync(store.graveyardPath, graveyardLines, "utf-8");
934
+ for (const exp of toEvict) {
935
+ result.evictedToGraveyard.push(exp.id);
936
+ }
862
937
  }
863
- }
864
- state.lastSweepAt = now;
865
- if (stateModified) {
866
- store.saveState(state);
867
- }
868
- return result;
938
+ state.lastSweepAt = now;
939
+ if (stateModified) {
940
+ store.saveState(state);
941
+ }
942
+ return result;
943
+ });
869
944
  }
870
945
 
871
946
  // src/context_injector.ts
@@ -990,14 +1065,16 @@ var renderActiveAnchorsContext = renderColdStartAnchorsContext;
990
1065
  // src/settlement.ts
991
1066
  import { execSync } from "child_process";
992
1067
  function runPhysicalVerification(anchor, cwd) {
993
- if (!anchor.verifyCommand) {
1068
+ if (!anchor.verifyCommand?.trim()) {
994
1069
  return { success: false, output: "No verification command specified" };
995
1070
  }
1071
+ const targetCwd = anchor.cwd || cwd;
996
1072
  try {
997
1073
  const stdout = execSync(anchor.verifyCommand, {
998
- cwd,
1074
+ cwd: targetCwd,
999
1075
  encoding: "utf-8",
1000
- timeout: 3e4,
1076
+ timeout: 8e3,
1077
+ maxBuffer: 4 * 1024 * 1024,
1001
1078
  stdio: ["ignore", "pipe", "pipe"]
1002
1079
  });
1003
1080
  return { success: true, output: stdout.trim() };
@@ -1086,27 +1163,36 @@ function getDisplayWidth(str) {
1086
1163
  }
1087
1164
  return width;
1088
1165
  }
1166
+ function hasActiveUI(ctx) {
1167
+ if (!ctx) return false;
1168
+ try {
1169
+ return Boolean(ctx.hasUI && ctx.ui);
1170
+ } catch {
1171
+ return false;
1172
+ }
1173
+ }
1089
1174
  function truncateToWidth(str, maxWidth, ellipsis = "\u2026") {
1090
1175
  if (maxWidth <= 0) return "";
1091
- const totalWidth = getDisplayWidth(str);
1092
- if (totalWidth <= maxWidth) return str;
1176
+ const singleLine = str.replace(/[\r\n\t]+/g, " ");
1177
+ const totalWidth = getDisplayWidth(singleLine);
1178
+ if (totalWidth <= maxWidth) return singleLine;
1093
1179
  const ellipsisWidth = getDisplayWidth(ellipsis);
1094
1180
  const target = Math.max(0, maxWidth - ellipsisWidth);
1095
1181
  if (target === 0) return ellipsis.slice(0, maxWidth);
1096
1182
  let accumulated = "";
1097
1183
  let accumWidth = 0;
1098
1184
  let i = 0;
1099
- while (i < str.length) {
1185
+ while (i < singleLine.length) {
1100
1186
  ANSI_REGEX.lastIndex = i;
1101
- const match = ANSI_REGEX.exec(str);
1187
+ const match = ANSI_REGEX.exec(singleLine);
1102
1188
  if (match && match.index === i) {
1103
1189
  accumulated += match[0];
1104
1190
  i += match[0].length;
1105
1191
  continue;
1106
1192
  }
1107
- const codePoint = str.codePointAt(i) || 0;
1193
+ const codePoint = singleLine.codePointAt(i) || 0;
1108
1194
  const charLen = codePoint > 65535 ? 2 : 1;
1109
- const fullChar = str.slice(i, i + charLen);
1195
+ const fullChar = singleLine.slice(i, i + charLen);
1110
1196
  const charWidth = getDisplayWidth(fullChar);
1111
1197
  if (accumWidth + charWidth > target) {
1112
1198
  break;
@@ -1116,7 +1202,7 @@ function truncateToWidth(str, maxWidth, ellipsis = "\u2026") {
1116
1202
  i += charLen;
1117
1203
  }
1118
1204
  accumulated = accumulated.replace(/\s*[\(\[\{(【]$/, "");
1119
- return accumulated + ellipsis + (str.includes("\x1B") ? "\x1B[0m" : "");
1205
+ return accumulated + ellipsis + (singleLine.includes("\x1B") ? "\x1B[0m" : "");
1120
1206
  }
1121
1207
  function padToWidth(str, targetWidth) {
1122
1208
  const truncated = truncateToWidth(str, targetWidth);
@@ -1192,52 +1278,58 @@ function formatOrigin(anchor) {
1192
1278
  return path4.basename(anchor) || "global";
1193
1279
  }
1194
1280
  function updateAnchorStatusBar(ctx, store) {
1195
- if (!ctx.hasUI || !ctx.ui) return;
1196
- const active = store.list({ status: "active", cwd: ctx.cwd });
1197
- if (active.length === 0) {
1198
- ctx.ui.setStatus("anchor", void 0);
1199
- return;
1281
+ if (!hasActiveUI(ctx)) return;
1282
+ try {
1283
+ const active = store.list({ status: "active", cwd: ctx.cwd });
1284
+ if (active.length === 0) {
1285
+ ctx.ui.setStatus("anchor", void 0);
1286
+ return;
1287
+ }
1288
+ ctx.ui.setStatus("anchor", `\u2316 ${active.length}`);
1289
+ } catch {
1200
1290
  }
1201
- ctx.ui.setStatus("anchor", `\u2316 ${active.length}`);
1202
1291
  }
1203
1292
  function updateStartupBanner(ctx, store) {
1204
- if (!ctx.hasUI || !ctx.ui) return;
1205
- const active = store.list({ status: "active", cwd: ctx.cwd });
1206
- if (active.length === 0) {
1207
- ctx.ui.setWidget("anchor-startup", void 0);
1208
- return;
1209
- }
1210
- const groups = groupAnchorsByQuadrant(active);
1211
- const sorted = [...groups.today, ...groups.upcoming, ...groups.habits, ...groups.backlog];
1212
- const topItems = sorted.slice(0, 3);
1213
- const factory = (_tui, theme) => ({
1214
- render: (width) => {
1215
- const maxTitleWidth = Math.max(16, width - 36);
1216
- const fg = theme?.fg ? theme.fg.bind(theme) : (_c, text) => text;
1217
- const lines = [
1218
- fg("accent", `\u2316 Anchor \xB7 ${active.length} active commitments:`)
1219
- ];
1220
- for (let i = 0; i < topItems.length; i++) {
1221
- const a = topItems[i];
1222
- const num = (i + 1).toString().padStart(2, "0");
1223
- const q = classifyAnchor(a);
1224
- const tgt = formatTargetDate(a);
1225
- const titlePadded = truncateToWidth(a.title, maxTitleWidth);
1226
- lines.push(` \u2022 ${fg("muted", `[${q}]`)} ${fg("dim", num)} ${titlePadded} ${fg("dim", `(${tgt})`)}`);
1227
- }
1228
- if (sorted.length > 3) {
1229
- lines.push(fg("dim", ` (+${sorted.length - 3} more \xB7 run /anchor to inspect)`));
1230
- }
1231
- return lines;
1232
- },
1233
- invalidate: () => {
1293
+ if (!hasActiveUI(ctx)) return;
1294
+ try {
1295
+ const active = store.list({ status: "active", cwd: ctx.cwd });
1296
+ if (active.length === 0) {
1297
+ ctx.ui.setWidget("anchor-startup", void 0);
1298
+ return;
1234
1299
  }
1235
- });
1236
- const content = ctx.ui?.theme ? factory : factory().render(80);
1237
- ctx.ui.setWidget("anchor-startup", content, { placement: "aboveEditor" });
1300
+ const groups = groupAnchorsByQuadrant(active);
1301
+ const sorted = [...groups.today, ...groups.upcoming, ...groups.habits, ...groups.backlog];
1302
+ const topItems = sorted.slice(0, 3);
1303
+ const factory = (_tui, theme) => ({
1304
+ render: (width) => {
1305
+ const maxTitleWidth = Math.max(12, width - 36);
1306
+ const fg = theme?.fg ? theme.fg.bind(theme) : (_c, text) => text;
1307
+ const lines = [
1308
+ truncateToWidth(fg("accent", `\u2316 Anchor \xB7 ${active.length} active commitments:`), width)
1309
+ ];
1310
+ for (let i = 0; i < topItems.length; i++) {
1311
+ const a = topItems[i];
1312
+ const num = a.id.replace(/^anc-/, "").padStart(2, "0");
1313
+ const q = classifyAnchor(a);
1314
+ const tgt = formatTargetDate(a);
1315
+ const titlePadded = truncateToWidth(a.title, maxTitleWidth);
1316
+ lines.push(truncateToWidth(` \u2022 ${fg("muted", `[${q}]`)} ${fg("dim", num)} ${titlePadded} ${fg("dim", `(${tgt})`)}`, width));
1317
+ }
1318
+ if (sorted.length > 3) {
1319
+ lines.push(truncateToWidth(fg("dim", ` (+${sorted.length - 3} more \xB7 run /anchor to inspect)`), width));
1320
+ }
1321
+ return lines;
1322
+ },
1323
+ invalidate: () => {
1324
+ }
1325
+ });
1326
+ const content = ctx.ui?.theme ? factory : factory().render(80);
1327
+ ctx.ui.setWidget("anchor-startup", content, { placement: "aboveEditor" });
1328
+ } catch {
1329
+ }
1238
1330
  }
1239
1331
  async function openAnchorDashboard(ctx, store) {
1240
- if (!ctx.hasUI || !ctx.ui) return;
1332
+ if (!hasActiveUI(ctx)) return;
1241
1333
  const list = store.list({ status: "active", cwd: ctx.cwd });
1242
1334
  if (list.length === 0) {
1243
1335
  ctx.ui.notify("\u2316 No active anchors. Use /pin <task> to record.", "info");
@@ -1252,20 +1344,23 @@ async function openAnchorDashboard(ctx, store) {
1252
1344
  ];
1253
1345
  const optionMap = /* @__PURE__ */ new Map();
1254
1346
  const displayOptions = [];
1347
+ const termCols = process.stdout?.columns || 96;
1348
+ const isNarrow = termCols < 88;
1349
+ const titleWidth = isNarrow ? Math.max(18, termCols - 36) : 34;
1255
1350
  for (let i = 0; i < sortedList.length; i++) {
1256
1351
  const a = sortedList[i];
1257
- const num = (i + 1).toString().padStart(2, "0");
1352
+ const num = a.id.replace(/^anc-/, "").padStart(2, "0");
1258
1353
  const category = padToWidth(`[${classifyAnchor(a)}]`, 12);
1259
1354
  const origin = formatOrigin(a);
1260
1355
  const target = formatTargetDate(a);
1261
1356
  const created = formatCreationTime(a.createdAt);
1262
1357
  const titleWithFiles = a.files && a.files.length > 0 ? `${a.title} [${a.files.slice(0, 1).join(", ")}]` : a.title;
1263
1358
  const colNum = `${num} `;
1264
- const colTitle = padToWidth(titleWithFiles, 34);
1359
+ const colTitle = padToWidth(titleWithFiles, titleWidth);
1265
1360
  const colOrigin = padToWidth(origin, 10);
1266
1361
  const colTarget = padToWidth(target, 12);
1267
1362
  const colCreated = created;
1268
- const label = `${colNum}${category}${colTitle} ${colOrigin} ${colTarget} ${colCreated}`.trimEnd();
1363
+ const label = isNarrow ? `${colNum}${category}${colTitle} ${colTarget}`.trimEnd() : `${colNum}${category}${colTitle} ${colOrigin} ${colTarget} ${colCreated}`.trimEnd();
1269
1364
  optionMap.set(label, a);
1270
1365
  displayOptions.push(label);
1271
1366
  }
@@ -1306,6 +1401,7 @@ export {
1306
1401
  DEFAULT_DECAY_POLICY,
1307
1402
  globToRegExp,
1308
1403
  normalizePath,
1404
+ isSameOrSubPath,
1309
1405
  matchAnchorAgainstTouchedFiles,
1310
1406
  findMatchedAnchors,
1311
1407
  detectDurability,
@@ -1331,6 +1427,7 @@ export {
1331
1427
  groupAnchorsByQuadrant,
1332
1428
  stripAnsi,
1333
1429
  getDisplayWidth,
1430
+ hasActiveUI,
1334
1431
  truncateToWidth,
1335
1432
  padToWidth,
1336
1433
  formatTargetDate,