pi-anchor 0.5.0 → 0.6.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/README.md +93 -34
- package/README_ja.md +90 -0
- package/README_zh.md +100 -36
- package/bin/anchor.js +16 -16
- package/dist/{chunk-VXEQDTK4.js → chunk-6NYXKR4F.js} +219 -25
- package/dist/{extension-Cq1NH1jT.d.cts → extension-09AVvgl-.d.cts} +31 -2
- package/dist/{extension-Cq1NH1jT.d.ts → extension-09AVvgl-.d.ts} +31 -2
- package/dist/extension.cjs +219 -25
- package/dist/extension.d.cts +1 -1
- package/dist/extension.d.ts +1 -1
- package/dist/extension.js +1 -1
- package/dist/index.cjs +168 -23
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/package.json +3 -2
- package/src/extension.ts +82 -8
- package/src/store.ts +146 -14
- package/src/tui.ts +25 -12
- package/src/types.ts +11 -1
|
@@ -66,10 +66,10 @@ function extractProgressFromText(rawTitle) {
|
|
|
66
66
|
progress: { done: 0, total: steps.length, steps }
|
|
67
67
|
};
|
|
68
68
|
}
|
|
69
|
-
const ratioMatch = rawTitle.match(/(?:\[(\d{1,3})\/(\d{1,3})\]|\((\d{1,3})\/(\d{1,3})\)
|
|
69
|
+
const ratioMatch = rawTitle.match(/(?:\[(\d{1,3})\/(\d{1,3})\]|\((\d{1,3})\/(\d{1,3})\))/);
|
|
70
70
|
if (ratioMatch) {
|
|
71
|
-
const done = parseInt(ratioMatch[1] ?? ratioMatch[3]
|
|
72
|
-
const total = parseInt(ratioMatch[2] ?? ratioMatch[4]
|
|
71
|
+
const done = parseInt(ratioMatch[1] ?? ratioMatch[3], 10);
|
|
72
|
+
const total = parseInt(ratioMatch[2] ?? ratioMatch[4], 10);
|
|
73
73
|
if (!isNaN(done) && !isNaN(total) && total >= 2 && total <= 500 && done >= 0 && done <= total) {
|
|
74
74
|
const stripped = rawTitle.replace(ratioMatch[0], " ").replace(/\s+/g, " ").trim();
|
|
75
75
|
return {
|
|
@@ -91,7 +91,8 @@ function isAnchorTargetingWorkspace(anchor, targetCwd) {
|
|
|
91
91
|
}
|
|
92
92
|
if (anchor.files && anchor.files.length > 0) {
|
|
93
93
|
for (const f of anchor.files) {
|
|
94
|
-
|
|
94
|
+
const resolvedFile = !path.isAbsolute(f) && !/^[a-zA-Z]:\//.test(f) && anchor.cwd ? `${anchor.cwd.replace(/\\/g, "/").replace(/\/+$/, "")}/${f.replace(/^\/+/, "")}` : f;
|
|
95
|
+
if (isSameOrSubPath(resolvedFile, normTarget)) return true;
|
|
95
96
|
}
|
|
96
97
|
}
|
|
97
98
|
if (anchor.cwd && isSameOrSubPath(anchor.cwd, normTarget)) {
|
|
@@ -181,6 +182,41 @@ function makeSafeTaskAnnotation(filePath, anchor) {
|
|
|
181
182
|
const num = anchor.id.replace(/^anc-/, "");
|
|
182
183
|
return "\n\n" + fmt(`\u2316 Note #${num}: ${anchor.title}`);
|
|
183
184
|
}
|
|
185
|
+
var COMMIT_STOP_WORDS = /* @__PURE__ */ new Set([
|
|
186
|
+
"fix",
|
|
187
|
+
"feat",
|
|
188
|
+
"chore",
|
|
189
|
+
"docs",
|
|
190
|
+
"test",
|
|
191
|
+
"tests",
|
|
192
|
+
"refactor",
|
|
193
|
+
"style",
|
|
194
|
+
"perf",
|
|
195
|
+
"build",
|
|
196
|
+
"ci",
|
|
197
|
+
"bug",
|
|
198
|
+
"issue",
|
|
199
|
+
"update",
|
|
200
|
+
"add",
|
|
201
|
+
"remove",
|
|
202
|
+
"todo",
|
|
203
|
+
"note",
|
|
204
|
+
"task",
|
|
205
|
+
"the",
|
|
206
|
+
"and",
|
|
207
|
+
"for",
|
|
208
|
+
"with",
|
|
209
|
+
"\u4FEE\u590D",
|
|
210
|
+
"\u4F18\u5316",
|
|
211
|
+
"\u66F4\u65B0",
|
|
212
|
+
"\u6DFB\u52A0",
|
|
213
|
+
"\u4FEE\u6539",
|
|
214
|
+
"\u5B8C\u6210",
|
|
215
|
+
"\u95EE\u9898",
|
|
216
|
+
"\u4EFB\u52A1",
|
|
217
|
+
"\u6A21\u5757",
|
|
218
|
+
"\u529F\u80FD"
|
|
219
|
+
]);
|
|
184
220
|
function matchesCommitMessage(anchor, commitMessages) {
|
|
185
221
|
const idLower = anchor.id.toLowerCase();
|
|
186
222
|
const numId = `#${anchor.id.replace(/^anc-/, "")}`;
|
|
@@ -189,21 +225,22 @@ function matchesCommitMessage(anchor, commitMessages) {
|
|
|
189
225
|
if (typeof Intl !== "undefined" && Intl.Segmenter) {
|
|
190
226
|
try {
|
|
191
227
|
const seg = new Intl.Segmenter("und", { granularity: "word" });
|
|
192
|
-
titleTokens = Array.from(seg.segment(title)).filter((s) => s.isWordLike).map((s) => s.segment.trim().toLowerCase()).filter((t) => t.length >= 2);
|
|
228
|
+
titleTokens = Array.from(seg.segment(title)).filter((s) => s.isWordLike).map((s) => s.segment.trim().toLowerCase()).filter((t) => t.length >= 2 && !COMMIT_STOP_WORDS.has(t));
|
|
193
229
|
} catch {
|
|
194
|
-
titleTokens = title.split(/[\s,._\-\/]+/).filter((t) => t.length >= 2);
|
|
230
|
+
titleTokens = title.split(/[\s,._\-\/]+/).filter((t) => t.length >= 2 && !COMMIT_STOP_WORDS.has(t));
|
|
195
231
|
}
|
|
196
232
|
} else {
|
|
197
|
-
titleTokens = title.split(/[\s,._\-\/]+/).filter((t) => t.length >= 2);
|
|
233
|
+
titleTokens = title.split(/[\s,._\-\/]+/).filter((t) => t.length >= 2 && !COMMIT_STOP_WORDS.has(t));
|
|
198
234
|
}
|
|
199
235
|
for (const msg of commitMessages) {
|
|
200
236
|
const msgLower = msg.toLowerCase();
|
|
201
|
-
if (msgLower.includes(idLower) ||
|
|
237
|
+
if (msgLower.includes(idLower) || new RegExp(`(?:^|\\s|\\()${numId}(?:\\b|\\)|$)`).test(msgLower)) {
|
|
202
238
|
return { matched: true, message: msg };
|
|
203
239
|
}
|
|
204
240
|
if (titleTokens.length > 0) {
|
|
205
241
|
const hits = titleTokens.filter((tok) => msgLower.includes(tok)).length;
|
|
206
|
-
|
|
242
|
+
const requiredHits = titleTokens.length === 1 ? 1 : 2;
|
|
243
|
+
if (hits >= requiredHits) {
|
|
207
244
|
return { matched: true, message: msg };
|
|
208
245
|
}
|
|
209
246
|
}
|
|
@@ -221,10 +258,12 @@ function renderColdStartAnchorsContext(store, cwdOrNow, nowArg) {
|
|
|
221
258
|
if (activeNotes.length === 0) return "";
|
|
222
259
|
const lines = activeNotes.map((a) => {
|
|
223
260
|
const num = a.id.replace(/^anc-/, "");
|
|
261
|
+
const tag = a.source === "todo-sync" ? `[Todo Checkpoint \xB7 ${a.project}]` : `[${a.project}]`;
|
|
224
262
|
const files = a.files.length > 0 ? ` (${a.files.slice(0, 2).join(", ")})` : "";
|
|
225
|
-
const
|
|
263
|
+
const nextStep = a.progress?.steps && a.progress.steps[a.progress.done] && a.progress.steps[a.progress.done] !== a.title ? ` \xB7 current/next: ${a.progress.steps[a.progress.done]}` : "";
|
|
264
|
+
const prog = a.progress ? ` [${a.progress.done}/${a.progress.total}${nextStep}]` : "";
|
|
226
265
|
const desc = a.description ? ` \u2014 ${a.description}` : "";
|
|
227
|
-
return `\u2022 #${num}
|
|
266
|
+
return `\u2022 #${num} ${tag} ${a.title}${prog}${files}${desc}`;
|
|
228
267
|
});
|
|
229
268
|
return [
|
|
230
269
|
`<sticky-notes count="${activeNotes.length}">`,
|
|
@@ -416,6 +455,7 @@ var AnchorStore = class {
|
|
|
416
455
|
description: input.description?.trim(),
|
|
417
456
|
priority: input.priority || "p1",
|
|
418
457
|
status: "active",
|
|
458
|
+
...input.source ? { source: input.source } : {},
|
|
419
459
|
project: projectName,
|
|
420
460
|
cwd,
|
|
421
461
|
createdAt: now,
|
|
@@ -504,9 +544,10 @@ var AnchorStore = class {
|
|
|
504
544
|
if (idx === -1) throw new Error(`Note not found: ${id}`);
|
|
505
545
|
const current = state.anchors[idx];
|
|
506
546
|
const now = Date.now();
|
|
507
|
-
const
|
|
547
|
+
const safeDelta = Number.isFinite(delta) ? delta : 1;
|
|
548
|
+
const total = explicitTotal && Number.isFinite(explicitTotal) && explicitTotal >= 1 ? explicitTotal : current.progress?.total || 4;
|
|
508
549
|
const prevDone = current.progress?.done || 0;
|
|
509
|
-
const nextDone = Math.max(0, Math.min(total, prevDone +
|
|
550
|
+
const nextDone = Math.max(0, Math.min(total, prevDone + safeDelta));
|
|
510
551
|
current.progress = {
|
|
511
552
|
...current.progress || {},
|
|
512
553
|
done: nextDone,
|
|
@@ -522,6 +563,97 @@ var AnchorStore = class {
|
|
|
522
563
|
return { anchor: current, completed: nextDone >= total };
|
|
523
564
|
});
|
|
524
565
|
}
|
|
566
|
+
/**
|
|
567
|
+
* Clever Touch 4 (Todo-Bridge): Automatically mirror intra-session `todo` tool lists
|
|
568
|
+
* into a cross-session checkpoint!
|
|
569
|
+
* - While tasks are in_progress/pending: updates the live `[■■□□ done/total]` checkpoint on disk;
|
|
570
|
+
* - When all tasks in the todo list reach `completed`: automatically tears off the checkpoint!
|
|
571
|
+
*/
|
|
572
|
+
syncFromTodoTool(tasks, options = {}) {
|
|
573
|
+
if (!Array.isArray(tasks)) return { status: "noop" };
|
|
574
|
+
const visible = tasks.filter((t) => t && typeof t.subject === "string" && t.status !== "deleted");
|
|
575
|
+
const normCwd = options.cwd ? normalizePath(options.cwd) : "";
|
|
576
|
+
const normSession = options.sessionFile ? normalizePath(options.sessionFile) : void 0;
|
|
577
|
+
return this.withLock(() => {
|
|
578
|
+
const state = this.loadState();
|
|
579
|
+
const existingIdx = state.anchors.findIndex(
|
|
580
|
+
(a) => a.source === "todo-sync" && (normSession && a.sessionFile === normSession || normCwd && isSameOrSubPath(a.cwd, normCwd))
|
|
581
|
+
);
|
|
582
|
+
const completedTasks = visible.filter((t) => t.status === "completed");
|
|
583
|
+
const unfinishedTasks = visible.filter((t) => t.status !== "completed");
|
|
584
|
+
if (visible.length === 0 || unfinishedTasks.length === 0) {
|
|
585
|
+
if (existingIdx !== -1) {
|
|
586
|
+
const [settled] = state.anchors.splice(existingIdx, 1);
|
|
587
|
+
settled.status = "settled";
|
|
588
|
+
settled.updatedAt = Date.now();
|
|
589
|
+
settled.evidence = {
|
|
590
|
+
settledAt: Date.now(),
|
|
591
|
+
settledBy: "todo-complete",
|
|
592
|
+
summary: `All ${completedTasks.length} session todo(s) completed`
|
|
593
|
+
};
|
|
594
|
+
this.ensureDirs();
|
|
595
|
+
fs.appendFileSync(this.archivePath, JSON.stringify(settled) + "\n", "utf-8");
|
|
596
|
+
this.saveState(state);
|
|
597
|
+
return { status: "completed", anchor: settled };
|
|
598
|
+
}
|
|
599
|
+
return { status: "noop" };
|
|
600
|
+
}
|
|
601
|
+
const inProgress = unfinishedTasks.find((t) => t.status === "in_progress") || unfinishedTasks[0];
|
|
602
|
+
const orderedSteps = [
|
|
603
|
+
...completedTasks.map((t) => t.subject.trim()),
|
|
604
|
+
...unfinishedTasks.filter((t) => t.status === "in_progress").map((t) => t.subject.trim()),
|
|
605
|
+
...unfinishedTasks.filter((t) => t.status === "pending").map((t) => t.subject.trim())
|
|
606
|
+
];
|
|
607
|
+
const progress = {
|
|
608
|
+
done: completedTasks.length,
|
|
609
|
+
total: visible.length,
|
|
610
|
+
steps: orderedSteps
|
|
611
|
+
};
|
|
612
|
+
const remainingSummary = unfinishedTasks.slice(0, 3).map((t) => t.subject.trim()).join(" \u2192 ");
|
|
613
|
+
const headline = inProgress.subject.trim();
|
|
614
|
+
const now = Date.now();
|
|
615
|
+
if (existingIdx !== -1) {
|
|
616
|
+
const current = state.anchors[existingIdx];
|
|
617
|
+
current.title = headline;
|
|
618
|
+
current.description = `Remaining (${unfinishedTasks.length}/${visible.length}): ${remainingSummary}`;
|
|
619
|
+
current.progress = progress;
|
|
620
|
+
current.status = "active";
|
|
621
|
+
current.updatedAt = now;
|
|
622
|
+
current.lastTouchedAt = now;
|
|
623
|
+
if (normSession) current.sessionFile = normSession;
|
|
624
|
+
if (options.recentFiles && options.recentFiles.length > 0) {
|
|
625
|
+
const merged = Array.from(/* @__PURE__ */ new Set([...options.recentFiles.map((f) => normalizePath(f)), ...current.files])).slice(0, 5);
|
|
626
|
+
current.files = merged;
|
|
627
|
+
}
|
|
628
|
+
state.anchors[existingIdx] = current;
|
|
629
|
+
this.saveState(state);
|
|
630
|
+
return { status: "synced", anchor: current };
|
|
631
|
+
}
|
|
632
|
+
const id = this.generateId(state);
|
|
633
|
+
const inferredFiles = options.recentFiles && options.recentFiles.length > 0 ? options.recentFiles.map((f) => normalizePath(f)).filter(Boolean) : extractPathsFromText(orderedSteps.join(" "));
|
|
634
|
+
const projectName = normCwd ? path.basename(normCwd) : "global";
|
|
635
|
+
const created = {
|
|
636
|
+
id,
|
|
637
|
+
title: headline,
|
|
638
|
+
description: `Remaining (${unfinishedTasks.length}/${visible.length}): ${remainingSummary}`,
|
|
639
|
+
priority: "p1",
|
|
640
|
+
status: "active",
|
|
641
|
+
source: "todo-sync",
|
|
642
|
+
project: projectName,
|
|
643
|
+
cwd: normCwd,
|
|
644
|
+
createdAt: now,
|
|
645
|
+
updatedAt: now,
|
|
646
|
+
lastTouchedAt: now,
|
|
647
|
+
files: inferredFiles,
|
|
648
|
+
tags: ["todo-sync"],
|
|
649
|
+
...normSession ? { sessionFile: normSession } : {},
|
|
650
|
+
progress
|
|
651
|
+
};
|
|
652
|
+
state.anchors.push(created);
|
|
653
|
+
this.saveState(state);
|
|
654
|
+
return { status: "synced", anchor: created };
|
|
655
|
+
});
|
|
656
|
+
}
|
|
525
657
|
/**
|
|
526
658
|
* Tear off (settle) a completed sticky note and archive it to archive.jsonl
|
|
527
659
|
*/
|
|
@@ -740,7 +872,8 @@ function updateStartupBanner(ctx, store) {
|
|
|
740
872
|
const a = sorted[0];
|
|
741
873
|
const num = a.id.replace(/^anc-/, "");
|
|
742
874
|
const bar = a.progress ? `${fg("accent", formatProgressBar(a.progress))} ` : "";
|
|
743
|
-
const
|
|
875
|
+
const candidateStep = a.progress?.steps ? a.progress.steps[a.progress.done] !== a.title ? a.progress.steps[a.progress.done] : a.progress.steps[a.progress.done + 1] : void 0;
|
|
876
|
+
const nextStep = candidateStep ? fg("muted", ` \u25B8 ${candidateStep}`) : "";
|
|
744
877
|
const origin = formatOrigin(a);
|
|
745
878
|
const sessionIcon = a.sessionFile ? " \u21AA" : "";
|
|
746
879
|
const prefix = `${fg("accent", "\u2316")} ${fg("dim", `#${num}`)} ${bar}`;
|
|
@@ -749,15 +882,24 @@ function updateStartupBanner(ctx, store) {
|
|
|
749
882
|
const titleClipped = truncateToWidth(a.title, availTitle);
|
|
750
883
|
return [truncateToWidth(`${prefix}${titleClipped}${nextStep}${suffix}`, width)];
|
|
751
884
|
}
|
|
752
|
-
const
|
|
753
|
-
const
|
|
885
|
+
const maxItems = width < 68 ? 1 : width < 100 ? 2 : 3;
|
|
886
|
+
const visibleSlice = sorted.slice(0, maxItems);
|
|
887
|
+
const countTag = fg("accent", `\u2316 ${sorted.length}: `);
|
|
888
|
+
const moreCount = sorted.length - visibleSlice.length;
|
|
889
|
+
const moreSuffix = moreCount > 0 ? fg("dim", ` +${moreCount}`) : "";
|
|
890
|
+
const perSlotWidth = Math.max(
|
|
891
|
+
10,
|
|
892
|
+
Math.floor((width - getDisplayWidth(countTag) - getDisplayWidth(moreSuffix) - (visibleSlice.length - 1) * 5) / visibleSlice.length)
|
|
893
|
+
);
|
|
894
|
+
const segments = visibleSlice.map((a) => {
|
|
754
895
|
const num = a.id.replace(/^anc-/, "");
|
|
755
896
|
const bar = a.progress ? `${formatProgressBar(a.progress)} ` : "";
|
|
756
|
-
const
|
|
897
|
+
const prefix = `#${num} ${bar}`;
|
|
898
|
+
const titleBudget = Math.max(6, perSlotWidth - getDisplayWidth(prefix));
|
|
899
|
+
const shortTitle = truncateToWidth(a.title, titleBudget);
|
|
757
900
|
return `${fg("dim", `#${num}`)} ${bar}${shortTitle}`;
|
|
758
901
|
});
|
|
759
|
-
|
|
760
|
-
return [truncateToWidth(`${countTag}${segments.join(fg("dim", " \xB7 "))}${more}`, width)];
|
|
902
|
+
return [truncateToWidth(`${countTag}${segments.join(fg("dim", " \xB7 "))}${moreSuffix}`, width)];
|
|
761
903
|
},
|
|
762
904
|
invalidate: () => {
|
|
763
905
|
}
|
|
@@ -798,7 +940,7 @@ async function openAnchorDashboard(ctx, store) {
|
|
|
798
940
|
if (!anchor) return;
|
|
799
941
|
const currentSessionFile = ctx.sessionManager?.getSessionFile?.();
|
|
800
942
|
const canJumpSession = Boolean(
|
|
801
|
-
anchor.sessionFile && fs2.existsSync(anchor.sessionFile) && typeof ctx.switchSession === "function" && (!currentSessionFile ||
|
|
943
|
+
anchor.sessionFile && fs2.existsSync(anchor.sessionFile) && typeof ctx.switchSession === "function" && (!currentSessionFile || !isSameOrSubPath(anchor.sessionFile, currentSessionFile))
|
|
802
944
|
);
|
|
803
945
|
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)";
|
|
804
946
|
const actions = [
|
|
@@ -887,11 +1029,14 @@ function parseAnchorCommandInput(raw) {
|
|
|
887
1029
|
};
|
|
888
1030
|
}
|
|
889
1031
|
function parseQuickNoteInput(text) {
|
|
890
|
-
const m = (text || "").trim().match(/^(?:pin|note|便签|备忘)\s*[::]\s*(
|
|
891
|
-
|
|
1032
|
+
const m = (text || "").trim().match(/^(?:pin|note|便签|备忘)\s*[::]\s*(.*)$/i);
|
|
1033
|
+
if (!m) return null;
|
|
1034
|
+
return m[1].trim();
|
|
892
1035
|
}
|
|
893
1036
|
function resolveAnchorTarget(store, idOrIndex, cwd) {
|
|
894
1037
|
const clean = idOrIndex.trim().replace(/^#/, "");
|
|
1038
|
+
const direct = store.get(clean);
|
|
1039
|
+
if (direct) return direct.id;
|
|
895
1040
|
if (/^\d+$/.test(clean)) {
|
|
896
1041
|
const seqIndex = parseInt(clean, 10);
|
|
897
1042
|
const visible = store.list({ cwd, all: true }).filter((a) => a.status === "active" || a.status === "sleeping");
|
|
@@ -900,7 +1045,7 @@ function resolveAnchorTarget(store, idOrIndex, cwd) {
|
|
|
900
1045
|
return sorted[seqIndex - 1].id;
|
|
901
1046
|
}
|
|
902
1047
|
}
|
|
903
|
-
return
|
|
1048
|
+
return void 0;
|
|
904
1049
|
}
|
|
905
1050
|
function getSessionFileSafe(ctx) {
|
|
906
1051
|
try {
|
|
@@ -949,7 +1094,13 @@ function extension_default(pi) {
|
|
|
949
1094
|
pi.on("input", async (event, ctx) => {
|
|
950
1095
|
if (event?.source === "extension") return { action: "continue" };
|
|
951
1096
|
const quickNote = parseQuickNoteInput(String(event?.text || ""));
|
|
952
|
-
if (
|
|
1097
|
+
if (quickNote === null) return { action: "continue" };
|
|
1098
|
+
if (!quickNote) {
|
|
1099
|
+
if (hasActiveUI(ctx)) {
|
|
1100
|
+
await openAnchorDashboard(ctx, store);
|
|
1101
|
+
}
|
|
1102
|
+
return { action: "handled" };
|
|
1103
|
+
}
|
|
953
1104
|
const anc = createStickyNoteFromUser(quickNote, ctx);
|
|
954
1105
|
const bar = anc.progress ? ` ${formatProgressBar(anc.progress)}` : "";
|
|
955
1106
|
if (hasActiveUI(ctx)) {
|
|
@@ -973,6 +1124,20 @@ ${snippet}` };
|
|
|
973
1124
|
if (event.isError) return;
|
|
974
1125
|
const lowerTool = String(event.toolName || "").toLowerCase();
|
|
975
1126
|
const currentSessionFile = getSessionFileSafe(ctx);
|
|
1127
|
+
if (lowerTool === "todo") {
|
|
1128
|
+
const tasks = event?.details?.tasks;
|
|
1129
|
+
if (Array.isArray(tasks)) {
|
|
1130
|
+
const syncRes = store.syncFromTodoTool(tasks, {
|
|
1131
|
+
cwd: ctx.cwd,
|
|
1132
|
+
sessionFile: currentSessionFile,
|
|
1133
|
+
recentFiles: recentSessionFiles
|
|
1134
|
+
});
|
|
1135
|
+
if (syncRes.status !== "noop") {
|
|
1136
|
+
updateAnchorStatusBar(ctx, store);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
976
1141
|
if (lowerTool === "bash" || lowerTool === "powershell") {
|
|
977
1142
|
const cmd = String(event.input?.command || "");
|
|
978
1143
|
if (cmd.includes("git commit")) {
|
|
@@ -1201,7 +1366,7 @@ ${snippet}` };
|
|
|
1201
1366
|
return;
|
|
1202
1367
|
}
|
|
1203
1368
|
const rawTrimmed = (args || "").trim();
|
|
1204
|
-
if (/^(?:#?anc-\d
|
|
1369
|
+
if (/^(?:#?anc-\d+|#?\d+)$/i.test(rawTrimmed)) {
|
|
1205
1370
|
const resolvedId = resolveAnchorTarget(store, rawTrimmed, ctx.cwd);
|
|
1206
1371
|
if (resolvedId) {
|
|
1207
1372
|
const settled = store.settle(resolvedId, { settledBy: "manual" });
|
|
@@ -1268,6 +1433,35 @@ ${snippet}` };
|
|
|
1268
1433
|
await openAnchorDashboard(ctx, store);
|
|
1269
1434
|
}
|
|
1270
1435
|
});
|
|
1436
|
+
pi.registerCommand("back", {
|
|
1437
|
+
description: "Jump back to the conversation session of the latest unfinished note or todo checkpoint (/back [id])",
|
|
1438
|
+
handler: async (args, ctx) => {
|
|
1439
|
+
const targetArg = (args || "").trim();
|
|
1440
|
+
const currentSession = getSessionFileSafe(ctx);
|
|
1441
|
+
if (targetArg) {
|
|
1442
|
+
const resolvedId = resolveAnchorTarget(store, targetArg, ctx.cwd);
|
|
1443
|
+
const note = resolvedId ? store.get(resolvedId) : void 0;
|
|
1444
|
+
if (!note || !note.sessionFile || !fs3.existsSync(note.sessionFile)) {
|
|
1445
|
+
ctx.ui.notify(`\u2316 No saved session found for #${targetArg}`, "error");
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
await ctx.switchSession(note.sessionFile);
|
|
1449
|
+
return;
|
|
1450
|
+
}
|
|
1451
|
+
const candidates = sortAnchorsForDisplay(
|
|
1452
|
+
store.list({ cwd: ctx.cwd, all: true }).filter((a) => a.status === "active" || a.status === "sleeping"),
|
|
1453
|
+
ctx.cwd
|
|
1454
|
+
);
|
|
1455
|
+
const jumpTarget = candidates.find(
|
|
1456
|
+
(a) => a.sessionFile && fs3.existsSync(a.sessionFile) && (!currentSession || !isSameOrSubPath(a.sessionFile, currentSession))
|
|
1457
|
+
);
|
|
1458
|
+
if (!jumpTarget || !jumpTarget.sessionFile) {
|
|
1459
|
+
ctx.ui.notify("\u2316 No other linked conversation session found to jump back to.", "info");
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
await ctx.switchSession(jumpTarget.sessionFile);
|
|
1463
|
+
}
|
|
1464
|
+
});
|
|
1271
1465
|
}
|
|
1272
1466
|
|
|
1273
1467
|
export {
|
|
@@ -16,11 +16,18 @@ interface AnchorProgress {
|
|
|
16
16
|
}
|
|
17
17
|
interface AnchorEvidence {
|
|
18
18
|
settledAt?: number;
|
|
19
|
-
settledBy?: 'commit' | 'manual' | 'one-tap-settlement' | 'manual-command' | 'verification-test';
|
|
19
|
+
settledBy?: 'commit' | 'manual' | 'todo-complete' | 'one-tap-settlement' | 'manual-command' | 'verification-test';
|
|
20
20
|
commitHash?: string;
|
|
21
21
|
summary?: string;
|
|
22
22
|
touchedFiles?: string[];
|
|
23
23
|
}
|
|
24
|
+
interface TodoTaskItem {
|
|
25
|
+
id: number;
|
|
26
|
+
subject: string;
|
|
27
|
+
status: 'pending' | 'in_progress' | 'completed' | 'deleted';
|
|
28
|
+
activeForm?: string;
|
|
29
|
+
description?: string;
|
|
30
|
+
}
|
|
24
31
|
interface Anchor {
|
|
25
32
|
/** Sequential identifier, e.g. 'anc-1' */
|
|
26
33
|
id: string;
|
|
@@ -30,6 +37,8 @@ interface Anchor {
|
|
|
30
37
|
description?: string;
|
|
31
38
|
/** Lifecycle status: 'active' (recent) or 'sleeping' (>7d untouched, 0 prompt tokens) */
|
|
32
39
|
status: AnchorStatus;
|
|
40
|
+
/** Origin source: 'manual' (user/tool pinned) or 'todo-sync' (auto-bridged from unfinished session todos) */
|
|
41
|
+
source?: 'manual' | 'todo-sync';
|
|
33
42
|
/** Short workspace label (e.g. 'anchor', 'College/优化', or 'global') */
|
|
34
43
|
project: string;
|
|
35
44
|
/** Workspace directory path where the note applies */
|
|
@@ -153,6 +162,7 @@ declare class AnchorStore {
|
|
|
153
162
|
title: string;
|
|
154
163
|
description?: string;
|
|
155
164
|
priority?: AnchorPriority;
|
|
165
|
+
source?: 'manual' | 'todo-sync';
|
|
156
166
|
cwd?: string;
|
|
157
167
|
project?: string;
|
|
158
168
|
files?: string[];
|
|
@@ -178,6 +188,20 @@ declare class AnchorStore {
|
|
|
178
188
|
anchor: Anchor;
|
|
179
189
|
completed: boolean;
|
|
180
190
|
};
|
|
191
|
+
/**
|
|
192
|
+
* Clever Touch 4 (Todo-Bridge): Automatically mirror intra-session `todo` tool lists
|
|
193
|
+
* into a cross-session checkpoint!
|
|
194
|
+
* - While tasks are in_progress/pending: updates the live `[■■□□ done/total]` checkpoint on disk;
|
|
195
|
+
* - When all tasks in the todo list reach `completed`: automatically tears off the checkpoint!
|
|
196
|
+
*/
|
|
197
|
+
syncFromTodoTool(tasks: TodoTaskItem[], options?: {
|
|
198
|
+
cwd?: string;
|
|
199
|
+
sessionFile?: string;
|
|
200
|
+
recentFiles?: string[];
|
|
201
|
+
}): {
|
|
202
|
+
status: 'synced' | 'completed' | 'noop';
|
|
203
|
+
anchor?: Anchor;
|
|
204
|
+
};
|
|
181
205
|
/**
|
|
182
206
|
* Tear off (settle) a completed sticky note and archive it to archive.jsonl
|
|
183
207
|
*/
|
|
@@ -197,9 +221,14 @@ declare function parseAnchorCommandInput(raw: string): {
|
|
|
197
221
|
/**
|
|
198
222
|
* Detect 0ms human-first quick note prefix in the input box:
|
|
199
223
|
* e.g. `pin: 明天把Token改Cookie` or `便签: 检查网关重试 [1/4]`
|
|
224
|
+
* Returns `{ matched: true, note: '...' }` (or `note: ''` if user typed bare `pin:`)
|
|
200
225
|
*/
|
|
201
226
|
declare function parseQuickNoteInput(text: string): string | null;
|
|
227
|
+
/**
|
|
228
|
+
* Resolve note target by exact ID first (`#2` -> `anc-2`), falling back to 1-based display row index
|
|
229
|
+
* only when no note with that ID exists. Prevents settling the wrong note when sort order differs from ID order!
|
|
230
|
+
*/
|
|
202
231
|
declare function resolveAnchorTarget(store: AnchorStore, idOrIndex: string, cwd?: string): string | undefined;
|
|
203
232
|
declare function export_default(pi: ExtensionAPI): void;
|
|
204
233
|
|
|
205
|
-
export { type Anchor as A, COMMENT_FORMATS as C, MS_PER_DAY as M, QUIET_AFTER_DAYS as Q, type
|
|
234
|
+
export { type Anchor as A, export_default as B, COMMENT_FORMATS as C, MS_PER_DAY as M, QUIET_AFTER_DAYS as Q, type TodoTaskItem as T, type AnchorProgress as a, AnchorStore as b, type AnchorEvidence as c, type AnchorPriority as d, type AnchorStatus as e, type AnchorStoreState as f, type TouchMatchResult as g, acquireSyncLock as h, atomicRenameWithRetry as i, extractPathsFromText as j, extractProgressFromText as k, findMatchedAnchors as l, getDefaultStorageDir as m, globToRegExp as n, isAnchorTargetingWorkspace as o, isSameOrSubPath as p, makeSafeTaskAnnotation as q, matchAnchorAgainstTouchedFiles as r, matchesCommitMessage as s, normalizePath as t, parseAnchorCommandInput as u, parseQuickNoteInput as v, renderActiveAnchorsContext as w, renderColdStartAnchorsContext as x, resolveAnchorTarget as y, sweepQuietNotes as z };
|
|
@@ -16,11 +16,18 @@ interface AnchorProgress {
|
|
|
16
16
|
}
|
|
17
17
|
interface AnchorEvidence {
|
|
18
18
|
settledAt?: number;
|
|
19
|
-
settledBy?: 'commit' | 'manual' | 'one-tap-settlement' | 'manual-command' | 'verification-test';
|
|
19
|
+
settledBy?: 'commit' | 'manual' | 'todo-complete' | 'one-tap-settlement' | 'manual-command' | 'verification-test';
|
|
20
20
|
commitHash?: string;
|
|
21
21
|
summary?: string;
|
|
22
22
|
touchedFiles?: string[];
|
|
23
23
|
}
|
|
24
|
+
interface TodoTaskItem {
|
|
25
|
+
id: number;
|
|
26
|
+
subject: string;
|
|
27
|
+
status: 'pending' | 'in_progress' | 'completed' | 'deleted';
|
|
28
|
+
activeForm?: string;
|
|
29
|
+
description?: string;
|
|
30
|
+
}
|
|
24
31
|
interface Anchor {
|
|
25
32
|
/** Sequential identifier, e.g. 'anc-1' */
|
|
26
33
|
id: string;
|
|
@@ -30,6 +37,8 @@ interface Anchor {
|
|
|
30
37
|
description?: string;
|
|
31
38
|
/** Lifecycle status: 'active' (recent) or 'sleeping' (>7d untouched, 0 prompt tokens) */
|
|
32
39
|
status: AnchorStatus;
|
|
40
|
+
/** Origin source: 'manual' (user/tool pinned) or 'todo-sync' (auto-bridged from unfinished session todos) */
|
|
41
|
+
source?: 'manual' | 'todo-sync';
|
|
33
42
|
/** Short workspace label (e.g. 'anchor', 'College/优化', or 'global') */
|
|
34
43
|
project: string;
|
|
35
44
|
/** Workspace directory path where the note applies */
|
|
@@ -153,6 +162,7 @@ declare class AnchorStore {
|
|
|
153
162
|
title: string;
|
|
154
163
|
description?: string;
|
|
155
164
|
priority?: AnchorPriority;
|
|
165
|
+
source?: 'manual' | 'todo-sync';
|
|
156
166
|
cwd?: string;
|
|
157
167
|
project?: string;
|
|
158
168
|
files?: string[];
|
|
@@ -178,6 +188,20 @@ declare class AnchorStore {
|
|
|
178
188
|
anchor: Anchor;
|
|
179
189
|
completed: boolean;
|
|
180
190
|
};
|
|
191
|
+
/**
|
|
192
|
+
* Clever Touch 4 (Todo-Bridge): Automatically mirror intra-session `todo` tool lists
|
|
193
|
+
* into a cross-session checkpoint!
|
|
194
|
+
* - While tasks are in_progress/pending: updates the live `[■■□□ done/total]` checkpoint on disk;
|
|
195
|
+
* - When all tasks in the todo list reach `completed`: automatically tears off the checkpoint!
|
|
196
|
+
*/
|
|
197
|
+
syncFromTodoTool(tasks: TodoTaskItem[], options?: {
|
|
198
|
+
cwd?: string;
|
|
199
|
+
sessionFile?: string;
|
|
200
|
+
recentFiles?: string[];
|
|
201
|
+
}): {
|
|
202
|
+
status: 'synced' | 'completed' | 'noop';
|
|
203
|
+
anchor?: Anchor;
|
|
204
|
+
};
|
|
181
205
|
/**
|
|
182
206
|
* Tear off (settle) a completed sticky note and archive it to archive.jsonl
|
|
183
207
|
*/
|
|
@@ -197,9 +221,14 @@ declare function parseAnchorCommandInput(raw: string): {
|
|
|
197
221
|
/**
|
|
198
222
|
* Detect 0ms human-first quick note prefix in the input box:
|
|
199
223
|
* e.g. `pin: 明天把Token改Cookie` or `便签: 检查网关重试 [1/4]`
|
|
224
|
+
* Returns `{ matched: true, note: '...' }` (or `note: ''` if user typed bare `pin:`)
|
|
200
225
|
*/
|
|
201
226
|
declare function parseQuickNoteInput(text: string): string | null;
|
|
227
|
+
/**
|
|
228
|
+
* Resolve note target by exact ID first (`#2` -> `anc-2`), falling back to 1-based display row index
|
|
229
|
+
* only when no note with that ID exists. Prevents settling the wrong note when sort order differs from ID order!
|
|
230
|
+
*/
|
|
202
231
|
declare function resolveAnchorTarget(store: AnchorStore, idOrIndex: string, cwd?: string): string | undefined;
|
|
203
232
|
declare function export_default(pi: ExtensionAPI): void;
|
|
204
233
|
|
|
205
|
-
export { type Anchor as A, COMMENT_FORMATS as C, MS_PER_DAY as M, QUIET_AFTER_DAYS as Q, type
|
|
234
|
+
export { type Anchor as A, export_default as B, COMMENT_FORMATS as C, MS_PER_DAY as M, QUIET_AFTER_DAYS as Q, type TodoTaskItem as T, type AnchorProgress as a, AnchorStore as b, type AnchorEvidence as c, type AnchorPriority as d, type AnchorStatus as e, type AnchorStoreState as f, type TouchMatchResult as g, acquireSyncLock as h, atomicRenameWithRetry as i, extractPathsFromText as j, extractProgressFromText as k, findMatchedAnchors as l, getDefaultStorageDir as m, globToRegExp as n, isAnchorTargetingWorkspace as o, isSameOrSubPath as p, makeSafeTaskAnnotation as q, matchAnchorAgainstTouchedFiles as r, matchesCommitMessage as s, normalizePath as t, parseAnchorCommandInput as u, parseQuickNoteInput as v, renderActiveAnchorsContext as w, renderColdStartAnchorsContext as x, resolveAnchorTarget as y, sweepQuietNotes as z };
|