pi-anchor 0.2.0 → 0.2.2

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() };
@@ -1066,8 +1143,9 @@ function groupAnchorsByQuadrant(anchors, now = Date.now()) {
1066
1143
  }
1067
1144
  return groups;
1068
1145
  }
1146
+ var ANSI_REGEX = /\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g;
1069
1147
  function stripAnsi(str) {
1070
- return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
1148
+ return str.replace(ANSI_REGEX, "");
1071
1149
  }
1072
1150
  function getDisplayWidth(str) {
1073
1151
  const clean = stripAnsi(str);
@@ -1085,23 +1163,46 @@ function getDisplayWidth(str) {
1085
1163
  }
1086
1164
  return width;
1087
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
+ }
1088
1174
  function truncateToWidth(str, maxWidth, ellipsis = "\u2026") {
1089
- const current = getDisplayWidth(str);
1090
- if (current <= maxWidth) return str;
1175
+ if (maxWidth <= 0) return "";
1176
+ const singleLine = str.replace(/[\r\n\t]+/g, " ");
1177
+ const totalWidth = getDisplayWidth(singleLine);
1178
+ if (totalWidth <= maxWidth) return singleLine;
1091
1179
  const ellipsisWidth = getDisplayWidth(ellipsis);
1092
- const target = maxWidth - ellipsisWidth;
1093
- if (target <= 0) return ellipsis.slice(0, maxWidth);
1180
+ const target = Math.max(0, maxWidth - ellipsisWidth);
1181
+ if (target === 0) return ellipsis.slice(0, maxWidth);
1094
1182
  let accumulated = "";
1095
1183
  let accumWidth = 0;
1096
- for (const char of str) {
1097
- const charWidth = getDisplayWidth(char);
1184
+ let i = 0;
1185
+ while (i < singleLine.length) {
1186
+ ANSI_REGEX.lastIndex = i;
1187
+ const match = ANSI_REGEX.exec(singleLine);
1188
+ if (match && match.index === i) {
1189
+ accumulated += match[0];
1190
+ i += match[0].length;
1191
+ continue;
1192
+ }
1193
+ const codePoint = singleLine.codePointAt(i) || 0;
1194
+ const charLen = codePoint > 65535 ? 2 : 1;
1195
+ const fullChar = singleLine.slice(i, i + charLen);
1196
+ const charWidth = getDisplayWidth(fullChar);
1098
1197
  if (accumWidth + charWidth > target) {
1099
1198
  break;
1100
1199
  }
1101
- accumulated += char;
1200
+ accumulated += fullChar;
1102
1201
  accumWidth += charWidth;
1202
+ i += charLen;
1103
1203
  }
1104
- return accumulated + ellipsis;
1204
+ accumulated = accumulated.replace(/\s*[\(\[\{(【]$/, "");
1205
+ return accumulated + ellipsis + (singleLine.includes("\x1B") ? "\x1B[0m" : "");
1105
1206
  }
1106
1207
  function padToWidth(str, targetWidth) {
1107
1208
  const truncated = truncateToWidth(str, targetWidth);
@@ -1177,52 +1278,58 @@ function formatOrigin(anchor) {
1177
1278
  return path4.basename(anchor) || "global";
1178
1279
  }
1179
1280
  function updateAnchorStatusBar(ctx, store) {
1180
- if (!ctx.hasUI || !ctx.ui) return;
1181
- const active = store.list({ status: "active", cwd: ctx.cwd });
1182
- if (active.length === 0) {
1183
- ctx.ui.setStatus("anchor", void 0);
1184
- 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 {
1185
1290
  }
1186
- ctx.ui.setStatus("anchor", `\u2316 ${active.length}`);
1187
1291
  }
1188
1292
  function updateStartupBanner(ctx, store) {
1189
- if (!ctx.hasUI || !ctx.ui) return;
1190
- const active = store.list({ status: "active", cwd: ctx.cwd });
1191
- if (active.length === 0) {
1192
- ctx.ui.setWidget("anchor-startup", void 0);
1193
- return;
1194
- }
1195
- const groups = groupAnchorsByQuadrant(active);
1196
- const sorted = [...groups.today, ...groups.upcoming, ...groups.habits, ...groups.backlog];
1197
- const topItems = sorted.slice(0, 3);
1198
- const factory = (_tui, theme) => ({
1199
- render: (width) => {
1200
- const maxTitleWidth = Math.max(16, width - 36);
1201
- const fg = theme?.fg ? theme.fg.bind(theme) : (_c, text) => text;
1202
- const lines = [
1203
- fg("accent", `\u2316 Anchor \xB7 ${active.length} active commitments:`)
1204
- ];
1205
- for (let i = 0; i < topItems.length; i++) {
1206
- const a = topItems[i];
1207
- const num = (i + 1).toString().padStart(2, "0");
1208
- const q = classifyAnchor(a);
1209
- const tgt = formatTargetDate(a);
1210
- const titlePadded = truncateToWidth(a.title, maxTitleWidth);
1211
- lines.push(` \u2022 ${fg("muted", `[${q}]`)} ${fg("dim", num)} ${titlePadded} ${fg("dim", `(${tgt})`)}`);
1212
- }
1213
- if (sorted.length > 3) {
1214
- lines.push(fg("dim", ` (+${sorted.length - 3} more \xB7 run /anchor to inspect)`));
1215
- }
1216
- return lines;
1217
- },
1218
- 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;
1219
1299
  }
1220
- });
1221
- const content = ctx.ui?.theme ? factory : factory().render(80);
1222
- 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
+ }
1223
1330
  }
1224
1331
  async function openAnchorDashboard(ctx, store) {
1225
- if (!ctx.hasUI || !ctx.ui) return;
1332
+ if (!hasActiveUI(ctx)) return;
1226
1333
  const list = store.list({ status: "active", cwd: ctx.cwd });
1227
1334
  if (list.length === 0) {
1228
1335
  ctx.ui.notify("\u2316 No active anchors. Use /pin <task> to record.", "info");
@@ -1237,31 +1344,55 @@ async function openAnchorDashboard(ctx, store) {
1237
1344
  ];
1238
1345
  const optionMap = /* @__PURE__ */ new Map();
1239
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;
1240
1350
  for (let i = 0; i < sortedList.length; i++) {
1241
1351
  const a = sortedList[i];
1242
- const num = (i + 1).toString().padStart(2, "0");
1352
+ const num = a.id.replace(/^anc-/, "").padStart(2, "0");
1243
1353
  const category = padToWidth(`[${classifyAnchor(a)}]`, 12);
1244
1354
  const origin = formatOrigin(a);
1245
1355
  const target = formatTargetDate(a);
1246
1356
  const created = formatCreationTime(a.createdAt);
1247
1357
  const titleWithFiles = a.files && a.files.length > 0 ? `${a.title} [${a.files.slice(0, 1).join(", ")}]` : a.title;
1248
1358
  const colNum = `${num} `;
1249
- const colTitle = padToWidth(titleWithFiles, 34);
1359
+ const colTitle = padToWidth(titleWithFiles, titleWidth);
1250
1360
  const colOrigin = padToWidth(origin, 10);
1251
1361
  const colTarget = padToWidth(target, 12);
1252
1362
  const colCreated = created;
1253
- 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();
1254
1364
  optionMap.set(label, a);
1255
1365
  displayOptions.push(label);
1256
1366
  }
1257
- const selected = await ctx.ui.select("\u2316 Anchors (enter to complete):", displayOptions);
1367
+ const selected = await ctx.ui.select("\u2316 Anchors (select to inspect or settle):", displayOptions);
1258
1368
  if (!selected) return;
1259
1369
  const anchor = optionMap.get(selected);
1260
1370
  if (!anchor) return;
1261
- store.settle(anchor.id, { settledBy: "manual-command" });
1262
- const successMsg = anchor.recurrence === "daily" ? `\u2316 Completed for today: "${anchor.title}" (resets tomorrow)` : `\u2316 Settled: "${anchor.title}"`;
1263
- ctx.ui.notify(successMsg, "info");
1264
- updateAnchorStatusBar(ctx, store);
1371
+ const action = await ctx.ui.select(
1372
+ `\u2316 Anchor #${anchor.id}: "${anchor.title}"`,
1373
+ [
1374
+ "\u2713 Settle (Mark completed and archive)",
1375
+ "\u2139 View details & associated files",
1376
+ "\u21A9 Cancel"
1377
+ ]
1378
+ );
1379
+ if (!action || action.startsWith("\u21A9")) return;
1380
+ if (action.startsWith("\u2713")) {
1381
+ store.settle(anchor.id, { settledBy: "manual-command" });
1382
+ const successMsg = anchor.recurrence === "daily" ? `\u2316 Completed for today: "${anchor.title}" (resets tomorrow)` : `\u2316 Settled #${anchor.id}: "${anchor.title}" (/anchor undo to revert)`;
1383
+ ctx.ui.notify(successMsg, "info");
1384
+ updateAnchorStatusBar(ctx, store);
1385
+ } else if (action.startsWith("\u2139")) {
1386
+ const details = [
1387
+ `#${anchor.id} [${anchor.priority.toUpperCase()}] ${anchor.title}`,
1388
+ anchor.description ? `Description: ${anchor.description}` : null,
1389
+ anchor.targetDate ? `Target Date: ${anchor.targetDate} (${formatTargetDate(anchor)})` : null,
1390
+ anchor.files && anchor.files.length > 0 ? `Files: ${anchor.files.join(", ")}` : null,
1391
+ anchor.verifyCommand ? `Verify Command: ${anchor.verifyCommand}` : null,
1392
+ `Created: ${formatCreationTime(anchor.createdAt)} (in ${anchor.project || "global"})`
1393
+ ].filter(Boolean).join("\n");
1394
+ ctx.ui.notify(details, "info");
1395
+ }
1265
1396
  }
1266
1397
 
1267
1398
  export {
@@ -1270,6 +1401,7 @@ export {
1270
1401
  DEFAULT_DECAY_POLICY,
1271
1402
  globToRegExp,
1272
1403
  normalizePath,
1404
+ isSameOrSubPath,
1273
1405
  matchAnchorAgainstTouchedFiles,
1274
1406
  findMatchedAnchors,
1275
1407
  detectDurability,
@@ -1295,6 +1427,7 @@ export {
1295
1427
  groupAnchorsByQuadrant,
1296
1428
  stripAnsi,
1297
1429
  getDisplayWidth,
1430
+ hasActiveUI,
1298
1431
  truncateToWidth,
1299
1432
  padToWidth,
1300
1433
  formatTargetDate,