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.
- package/README.md +101 -177
- package/README_zh.md +119 -187
- package/dist/{chunk-T62PHIQI.js → chunk-G72UHPBN.js} +261 -128
- package/dist/extension.cjs +332 -178
- package/dist/extension.js +77 -51
- package/dist/index.cjs +263 -128
- package/dist/index.d.cts +7 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +5 -1
- package/package.json +5 -4
- package/src/decay.ts +54 -52
- package/src/extension.ts +77 -58
- package/src/matcher.ts +27 -6
- package/src/settlement.ts +5 -3
- package/src/store.ts +75 -18
- package/src/tui.ts +133 -68
- package/src/types.ts +1 -0
|
@@ -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
|
|
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
|
-
|
|
34
|
-
|
|
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
|
|
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(
|
|
430
|
-
const numbers =
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
606
|
-
|
|
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.
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
const
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
if (evalResult.nextStatus
|
|
833
|
-
anchor.status
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
anchor.status
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
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
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
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
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
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:
|
|
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(
|
|
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
|
-
|
|
1090
|
-
|
|
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
|
|
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
|
-
|
|
1097
|
-
|
|
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 +=
|
|
1200
|
+
accumulated += fullChar;
|
|
1102
1201
|
accumWidth += charWidth;
|
|
1202
|
+
i += charLen;
|
|
1103
1203
|
}
|
|
1104
|
-
|
|
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
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
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
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
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
|
-
|
|
1222
|
-
|
|
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
|
|
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 = (
|
|
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,
|
|
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 (
|
|
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
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
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,
|