pi-anchor 0.6.0 → 1.0.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 +18 -12
- package/README_zh.md +19 -13
- package/assets/hero-en.svg +55 -52
- package/assets/hero-zh.svg +56 -53
- package/dist/{chunk-6NYXKR4F.js → chunk-IOJQYC4Y.js} +717 -65
- package/dist/{extension-09AVvgl-.d.cts → extension-B4JOU06c.d.cts} +71 -14
- package/dist/{extension-09AVvgl-.d.ts → extension-B4JOU06c.d.ts} +71 -14
- package/dist/extension.cjs +670 -65
- package/dist/extension.d.cts +1 -1
- package/dist/extension.d.ts +1 -1
- package/dist/extension.js +1 -1
- package/dist/index.cjs +683 -63
- package/dist/index.d.cts +109 -4
- package/dist/index.d.ts +109 -4
- package/dist/index.js +37 -3
- package/package.json +2 -2
- package/src/bridge.ts +101 -0
- package/src/dock.ts +297 -0
- package/src/extension.ts +66 -7
- package/src/graph.ts +256 -0
- package/src/index.ts +3 -0
- package/src/store.ts +165 -4
- package/src/tui.ts +33 -48
- package/src/types.ts +48 -12
- package/README_ja.md +0 -90
|
@@ -4,18 +4,277 @@ import fs3 from "fs";
|
|
|
4
4
|
|
|
5
5
|
// src/store.ts
|
|
6
6
|
import fs from "fs";
|
|
7
|
-
import
|
|
7
|
+
import path2 from "path";
|
|
8
8
|
import os from "os";
|
|
9
9
|
|
|
10
10
|
// src/types.ts
|
|
11
11
|
var QUIET_AFTER_DAYS = 7;
|
|
12
12
|
var MS_PER_DAY = 864e5;
|
|
13
|
+
var DEFAULT_MAX_DOCK_LINES = 8;
|
|
14
|
+
|
|
15
|
+
// src/graph.ts
|
|
16
|
+
function validateStepDependencies(steps, stepId, newBlockedBy) {
|
|
17
|
+
const visibleMap = /* @__PURE__ */ new Map();
|
|
18
|
+
for (const s of steps) {
|
|
19
|
+
if (s.status !== "deleted") {
|
|
20
|
+
visibleMap.set(s.id, s);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
for (const depId of newBlockedBy) {
|
|
24
|
+
if (depId === stepId) {
|
|
25
|
+
return { valid: false, error: `Step #${stepId} cannot be blocked by itself` };
|
|
26
|
+
}
|
|
27
|
+
if (!visibleMap.has(depId)) {
|
|
28
|
+
return { valid: false, error: `Prerequisite step #${depId} does not exist or is deleted` };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const edges = /* @__PURE__ */ new Map();
|
|
32
|
+
for (const s of visibleMap.values()) {
|
|
33
|
+
if (s.id === stepId) {
|
|
34
|
+
const merged = /* @__PURE__ */ new Set([...s.blockedBy ?? [], ...newBlockedBy]);
|
|
35
|
+
edges.set(s.id, [...merged]);
|
|
36
|
+
} else {
|
|
37
|
+
edges.set(s.id, s.blockedBy ? [...s.blockedBy] : []);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
41
|
+
const visited = /* @__PURE__ */ new Set();
|
|
42
|
+
const hasCycle = (node) => {
|
|
43
|
+
if (visiting.has(node)) return true;
|
|
44
|
+
if (visited.has(node)) return false;
|
|
45
|
+
visiting.add(node);
|
|
46
|
+
for (const nb of edges.get(node) ?? []) {
|
|
47
|
+
if (hasCycle(nb)) return true;
|
|
48
|
+
}
|
|
49
|
+
visiting.delete(node);
|
|
50
|
+
visited.add(node);
|
|
51
|
+
return false;
|
|
52
|
+
};
|
|
53
|
+
for (const node of edges.keys()) {
|
|
54
|
+
if (hasCycle(node)) {
|
|
55
|
+
return { valid: false, error: `Dependency cycle detected involving step #${stepId}` };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return { valid: true };
|
|
59
|
+
}
|
|
60
|
+
function validateAnchorDependencies(anchors, anchorId, newBlockedBy) {
|
|
61
|
+
const normId = (raw) => {
|
|
62
|
+
const clean = raw.trim().replace(/^#/, "");
|
|
63
|
+
return /^\d+$/.test(clean) ? `anc-${parseInt(clean, 10)}` : clean;
|
|
64
|
+
};
|
|
65
|
+
const targetId = normId(anchorId);
|
|
66
|
+
const activeMap = /* @__PURE__ */ new Map();
|
|
67
|
+
for (const a of anchors) {
|
|
68
|
+
activeMap.set(a.id, a);
|
|
69
|
+
}
|
|
70
|
+
const normalizedDeps = newBlockedBy.map(normId);
|
|
71
|
+
for (const depId of normalizedDeps) {
|
|
72
|
+
if (depId === targetId) {
|
|
73
|
+
return { valid: false, error: `Note #${targetId} cannot depend on itself` };
|
|
74
|
+
}
|
|
75
|
+
if (!activeMap.has(depId)) {
|
|
76
|
+
return { valid: false, error: `Prerequisite note #${depId} does not exist` };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const edges = /* @__PURE__ */ new Map();
|
|
80
|
+
for (const a of activeMap.values()) {
|
|
81
|
+
if (a.id === targetId) {
|
|
82
|
+
edges.set(a.id, Array.from(/* @__PURE__ */ new Set([...a.blockedBy ?? [], ...normalizedDeps])));
|
|
83
|
+
} else {
|
|
84
|
+
edges.set(a.id, a.blockedBy ? [...a.blockedBy] : []);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
88
|
+
const visited = /* @__PURE__ */ new Set();
|
|
89
|
+
const hasCycle = (node) => {
|
|
90
|
+
if (visiting.has(node)) return true;
|
|
91
|
+
if (visited.has(node)) return false;
|
|
92
|
+
visiting.add(node);
|
|
93
|
+
for (const nb of edges.get(node) ?? []) {
|
|
94
|
+
if (hasCycle(nb)) return true;
|
|
95
|
+
}
|
|
96
|
+
visiting.delete(node);
|
|
97
|
+
visited.add(node);
|
|
98
|
+
return false;
|
|
99
|
+
};
|
|
100
|
+
for (const node of edges.keys()) {
|
|
101
|
+
if (hasCycle(node)) {
|
|
102
|
+
return { valid: false, error: `Dependency cycle detected involving #${targetId}` };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return { valid: true };
|
|
106
|
+
}
|
|
107
|
+
function deriveStepBlocks(steps) {
|
|
108
|
+
const blocks = /* @__PURE__ */ new Map();
|
|
109
|
+
for (const s of steps) {
|
|
110
|
+
if (s.status === "deleted") continue;
|
|
111
|
+
for (const dep of s.blockedBy ?? []) {
|
|
112
|
+
const arr = blocks.get(dep) ?? [];
|
|
113
|
+
arr.push(s.id);
|
|
114
|
+
blocks.set(dep, arr);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return blocks;
|
|
118
|
+
}
|
|
119
|
+
function materializeSteps(progress) {
|
|
120
|
+
if (!progress) return [];
|
|
121
|
+
if (Array.isArray(progress.items) && progress.items.length > 0) {
|
|
122
|
+
return progress.items.filter((s) => s.status !== "deleted").map((s) => ({ ...s }));
|
|
123
|
+
}
|
|
124
|
+
if (Array.isArray(progress.steps) && progress.steps.length > 0) {
|
|
125
|
+
const done = Math.max(0, progress.done || 0);
|
|
126
|
+
return progress.steps.map((subject, idx) => {
|
|
127
|
+
let status = "pending";
|
|
128
|
+
if (idx < done) status = "completed";
|
|
129
|
+
else if (idx === done) status = "in_progress";
|
|
130
|
+
return {
|
|
131
|
+
id: idx + 1,
|
|
132
|
+
subject,
|
|
133
|
+
status
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
function syncProgressFromItems(items, existing) {
|
|
140
|
+
const visible = items.filter((i) => i.status !== "deleted");
|
|
141
|
+
const completed = visible.filter((i) => i.status === "completed").length;
|
|
142
|
+
return {
|
|
143
|
+
...existing || {},
|
|
144
|
+
done: completed,
|
|
145
|
+
total: visible.length,
|
|
146
|
+
steps: visible.map((i) => i.subject),
|
|
147
|
+
items: items.map((i) => ({ ...i }))
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function selectStepCounts(steps) {
|
|
151
|
+
const visible = steps.filter((s) => s.status !== "deleted");
|
|
152
|
+
let pending = 0;
|
|
153
|
+
let inProgress = 0;
|
|
154
|
+
let completed = 0;
|
|
155
|
+
for (const s of visible) {
|
|
156
|
+
if (s.status === "completed") completed++;
|
|
157
|
+
else if (s.status === "in_progress") inProgress++;
|
|
158
|
+
else pending++;
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
total: visible.length,
|
|
162
|
+
pending,
|
|
163
|
+
inProgress,
|
|
164
|
+
completed
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function selectDockStepLayout(steps, budget, hiddenCompletedIds = /* @__PURE__ */ new Set()) {
|
|
168
|
+
const allVisible = steps.filter(
|
|
169
|
+
(s) => s.status !== "deleted" && !(s.status === "completed" && hiddenCompletedIds.has(s.id))
|
|
170
|
+
);
|
|
171
|
+
const preHiddenCompleted = steps.filter(
|
|
172
|
+
(s) => s.status === "completed" && hiddenCompletedIds.has(s.id)
|
|
173
|
+
).length;
|
|
174
|
+
if (budget <= 0) {
|
|
175
|
+
return { visible: [], hiddenCompleted: preHiddenCompleted, truncatedTail: allVisible.length };
|
|
176
|
+
}
|
|
177
|
+
if (allVisible.length <= budget && preHiddenCompleted === 0) {
|
|
178
|
+
return { visible: allVisible, hiddenCompleted: 0, truncatedTail: 0 };
|
|
179
|
+
}
|
|
180
|
+
const innerBudget = preHiddenCompleted > 0 ? Math.max(1, budget - 1) : allVisible.length <= budget ? budget : Math.max(1, budget - 1);
|
|
181
|
+
if (allVisible.length <= innerBudget && preHiddenCompleted === 0) {
|
|
182
|
+
return { visible: allVisible, hiddenCompleted: 0, truncatedTail: 0 };
|
|
183
|
+
}
|
|
184
|
+
const nonCompleted = allVisible.filter((s) => s.status !== "completed");
|
|
185
|
+
const newlyCompletedCount = allVisible.length - nonCompleted.length;
|
|
186
|
+
if (nonCompleted.length <= innerBudget) {
|
|
187
|
+
const kept = new Set(nonCompleted);
|
|
188
|
+
for (const s of allVisible) {
|
|
189
|
+
if (kept.size >= innerBudget) break;
|
|
190
|
+
if (s.status === "completed") kept.add(s);
|
|
191
|
+
}
|
|
192
|
+
const visible2 = allVisible.filter((s) => kept.has(s));
|
|
193
|
+
const shownCompleted = visible2.filter((s) => s.status === "completed").length;
|
|
194
|
+
return {
|
|
195
|
+
visible: visible2,
|
|
196
|
+
hiddenCompleted: preHiddenCompleted + (newlyCompletedCount - shownCompleted),
|
|
197
|
+
truncatedTail: 0
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
const visible = nonCompleted.slice(0, innerBudget);
|
|
201
|
+
return {
|
|
202
|
+
visible,
|
|
203
|
+
hiddenCompleted: preHiddenCompleted + newlyCompletedCount,
|
|
204
|
+
truncatedTail: nonCompleted.length - innerBudget
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// src/bridge.ts
|
|
209
|
+
import path from "path";
|
|
210
|
+
function replayTodoSnapshotFromBranch(branch) {
|
|
211
|
+
if (!branch || typeof branch[Symbol.iterator] !== "function") return null;
|
|
212
|
+
let latestTasks = null;
|
|
213
|
+
for (const entry of branch) {
|
|
214
|
+
const e = entry;
|
|
215
|
+
if (e?.type !== "message") continue;
|
|
216
|
+
const msg = e.message;
|
|
217
|
+
if (msg?.role !== "toolResult" || String(msg.toolName || "").toLowerCase() !== "todo") continue;
|
|
218
|
+
const rawTasks = msg.details?.tasks;
|
|
219
|
+
if (Array.isArray(rawTasks)) {
|
|
220
|
+
latestTasks = rawTasks.filter((t) => t && typeof t === "object" && typeof t.subject === "string").map((t, idx) => ({
|
|
221
|
+
id: typeof t.id === "number" ? t.id : idx + 1,
|
|
222
|
+
subject: String(t.subject),
|
|
223
|
+
status: t.status === "in_progress" || t.status === "completed" || t.status === "deleted" ? t.status : "pending",
|
|
224
|
+
...typeof t.activeForm === "string" && t.activeForm.trim() ? { activeForm: t.activeForm.trim() } : {},
|
|
225
|
+
...Array.isArray(t.blockedBy) ? { blockedBy: t.blockedBy.filter((n) => typeof n === "number") } : {}
|
|
226
|
+
}));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return latestTasks;
|
|
230
|
+
}
|
|
231
|
+
function convertTodoTasksToSteps(tasks) {
|
|
232
|
+
return tasks.filter((t) => t && typeof t.subject === "string" && t.status !== "deleted").map((t, idx) => ({
|
|
233
|
+
id: typeof t.id === "number" ? t.id : idx + 1,
|
|
234
|
+
subject: t.subject.trim(),
|
|
235
|
+
status: t.status,
|
|
236
|
+
...t.activeForm ? { activeForm: t.activeForm.trim() } : {},
|
|
237
|
+
...t.blockedBy && t.blockedBy.length > 0 ? { blockedBy: [...t.blockedBy] } : {}
|
|
238
|
+
}));
|
|
239
|
+
}
|
|
240
|
+
function inferLiveActivityLabel(toolName, input, _cwd) {
|
|
241
|
+
if (!input || typeof input !== "object") return void 0;
|
|
242
|
+
const lower = (toolName || "").toLowerCase();
|
|
243
|
+
const rawPath = input.path || input.filePath || input.file;
|
|
244
|
+
if (typeof rawPath === "string" && rawPath.trim()) {
|
|
245
|
+
const base = path.basename(rawPath.trim().replace(/\\/g, "/"));
|
|
246
|
+
if (!base) return void 0;
|
|
247
|
+
if (lower === "edit" || lower === "patch" || lower === "apply_diff" || lower === "modify") {
|
|
248
|
+
return `editing ${base}`;
|
|
249
|
+
}
|
|
250
|
+
if (lower === "write" || lower === "create_file") {
|
|
251
|
+
return `writing ${base}`;
|
|
252
|
+
}
|
|
253
|
+
if (lower === "read") {
|
|
254
|
+
return `reading ${base}`;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (lower === "bash" || lower === "powershell") {
|
|
258
|
+
const cmd = String(input.command || "").trim();
|
|
259
|
+
if (!cmd) return void 0;
|
|
260
|
+
if (/\b(?:npm\s+test|vitest|pytest|cargo\s+test|go\s+test|tsx\s+--test)\b/i.test(cmd)) {
|
|
261
|
+
return "running tests";
|
|
262
|
+
}
|
|
263
|
+
if (/\b(?:npm\s+run\s+build|tsc|tsup|cargo\s+build)\b/i.test(cmd)) {
|
|
264
|
+
return "building project";
|
|
265
|
+
}
|
|
266
|
+
if (/\bgit\s+commit\b/i.test(cmd)) {
|
|
267
|
+
return "committing changes";
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return void 0;
|
|
271
|
+
}
|
|
13
272
|
|
|
14
273
|
// src/store.ts
|
|
15
274
|
function normalizePath(filePath, baseCwd) {
|
|
16
275
|
if (typeof filePath !== "string") return "";
|
|
17
276
|
let norm = filePath.trim().replace(/\\/g, "/").replace(/^\.\//, "");
|
|
18
|
-
if (baseCwd && (
|
|
277
|
+
if (baseCwd && (path2.isAbsolute(filePath) || norm.startsWith("/") || /^[a-zA-Z]:\//.test(norm))) {
|
|
19
278
|
const normBase = baseCwd.trim().replace(/\\/g, "/").replace(/\/+$/, "");
|
|
20
279
|
const lowerNorm = norm.toLowerCase();
|
|
21
280
|
const lowerBase = normBase.toLowerCase();
|
|
@@ -59,11 +318,16 @@ function extractProgressFromText(rawTitle) {
|
|
|
59
318
|
const stepParts = Array.from(rawTitle.matchAll(/(?:^|\s)(\d+)\.\s*([^\d]+?)(?=\s+\d+\.\s|$)/g));
|
|
60
319
|
if (stepParts.length >= 2) {
|
|
61
320
|
const steps = stepParts.map((m) => m[2].trim().replace(/[;;,,]+$/, "")).filter(Boolean);
|
|
321
|
+
const items = steps.map((subject, idx) => ({
|
|
322
|
+
id: idx + 1,
|
|
323
|
+
subject,
|
|
324
|
+
status: idx === 0 ? "in_progress" : "pending"
|
|
325
|
+
}));
|
|
62
326
|
const firstMatchIdx = rawTitle.indexOf(stepParts[0][0]);
|
|
63
327
|
const prefix = rawTitle.slice(0, firstMatchIdx).replace(/[::\-—\s]+$/, "").trim();
|
|
64
328
|
return {
|
|
65
329
|
cleanTitle: prefix || steps.join(" \u2192 "),
|
|
66
|
-
progress: { done: 0, total: steps.length, steps }
|
|
330
|
+
progress: { done: 0, total: steps.length, steps, items }
|
|
67
331
|
};
|
|
68
332
|
}
|
|
69
333
|
const ratioMatch = rawTitle.match(/(?:\[(\d{1,3})\/(\d{1,3})\]|\((\d{1,3})\/(\d{1,3})\))/);
|
|
@@ -83,15 +347,15 @@ function extractProgressFromText(rawTitle) {
|
|
|
83
347
|
function isAnchorTargetingWorkspace(anchor, targetCwd) {
|
|
84
348
|
if (!targetCwd) return true;
|
|
85
349
|
const normTarget = targetCwd.trim().replace(/\\/g, "/").replace(/\/+$/, "");
|
|
86
|
-
const targetBase =
|
|
350
|
+
const targetBase = path2.basename(normTarget).toLowerCase();
|
|
87
351
|
if (anchor.project && anchor.project !== "global") {
|
|
88
352
|
const normProj = anchor.project.trim().replace(/\\/g, "/").replace(/\/+$/, "");
|
|
89
353
|
if (isSameOrSubPath(normProj, normTarget)) return true;
|
|
90
|
-
if (
|
|
354
|
+
if (path2.basename(normProj).toLowerCase() === targetBase) return true;
|
|
91
355
|
}
|
|
92
356
|
if (anchor.files && anchor.files.length > 0) {
|
|
93
357
|
for (const f of anchor.files) {
|
|
94
|
-
const resolvedFile = !
|
|
358
|
+
const resolvedFile = !path2.isAbsolute(f) && !/^[a-zA-Z]:\//.test(f) && anchor.cwd ? `${anchor.cwd.replace(/\\/g, "/").replace(/\/+$/, "")}/${f.replace(/^\/+/, "")}` : f;
|
|
95
359
|
if (isSameOrSubPath(resolvedFile, normTarget)) return true;
|
|
96
360
|
}
|
|
97
361
|
}
|
|
@@ -176,7 +440,7 @@ var COMMENT_FORMATS = {
|
|
|
176
440
|
".lua": (m) => `-- ${m}`
|
|
177
441
|
};
|
|
178
442
|
function makeSafeTaskAnnotation(filePath, anchor) {
|
|
179
|
-
const ext =
|
|
443
|
+
const ext = path2.extname(filePath).toLowerCase();
|
|
180
444
|
const fmt = COMMENT_FORMATS[ext];
|
|
181
445
|
if (!fmt) return null;
|
|
182
446
|
const num = anchor.id.replace(/^anc-/, "");
|
|
@@ -293,8 +557,8 @@ function sweepQuietNotes(store, now = Date.now()) {
|
|
|
293
557
|
});
|
|
294
558
|
}
|
|
295
559
|
function getDefaultStorageDir() {
|
|
296
|
-
if (process.env.ANCHOR_DIR) return
|
|
297
|
-
return
|
|
560
|
+
if (process.env.ANCHOR_DIR) return path2.resolve(process.env.ANCHOR_DIR);
|
|
561
|
+
return path2.join(os.homedir(), ".anchor");
|
|
298
562
|
}
|
|
299
563
|
function sleepSync(ms) {
|
|
300
564
|
const end = Date.now() + ms;
|
|
@@ -376,10 +640,10 @@ var AnchorStore = class {
|
|
|
376
640
|
lockPath;
|
|
377
641
|
archivePath;
|
|
378
642
|
constructor(storageDir) {
|
|
379
|
-
this.storageDir = storageDir ?
|
|
380
|
-
this.statePath =
|
|
381
|
-
this.lockPath =
|
|
382
|
-
this.archivePath =
|
|
643
|
+
this.storageDir = storageDir ? path2.resolve(storageDir) : getDefaultStorageDir();
|
|
644
|
+
this.statePath = path2.join(this.storageDir, "state.json");
|
|
645
|
+
this.lockPath = path2.join(this.storageDir, "state.lock");
|
|
646
|
+
this.archivePath = path2.join(this.storageDir, "archive.jsonl");
|
|
383
647
|
}
|
|
384
648
|
ensureDirs() {
|
|
385
649
|
if (!fs.existsSync(this.storageDir)) {
|
|
@@ -408,7 +672,7 @@ var AnchorStore = class {
|
|
|
408
672
|
return parsed;
|
|
409
673
|
} catch {
|
|
410
674
|
try {
|
|
411
|
-
fs.copyFileSync(this.statePath,
|
|
675
|
+
fs.copyFileSync(this.statePath, path2.join(this.storageDir, `state.corrupt.${Date.now()}.json`));
|
|
412
676
|
} catch {
|
|
413
677
|
}
|
|
414
678
|
return { version: 1, anchors: [], lastSweepAt: Date.now(), nextId: 1 };
|
|
@@ -438,6 +702,10 @@ var AnchorStore = class {
|
|
|
438
702
|
const state = this.loadState();
|
|
439
703
|
const now = Date.now();
|
|
440
704
|
const id = this.generateId(state);
|
|
705
|
+
if (input.blockedBy && input.blockedBy.length > 0) {
|
|
706
|
+
const check = validateAnchorDependencies(state.anchors, id, input.blockedBy);
|
|
707
|
+
if (!check.valid) throw new Error(check.error);
|
|
708
|
+
}
|
|
441
709
|
const { cleanTitle, progress: parsedProgress } = extractProgressFromText(input.title);
|
|
442
710
|
const explicitFiles = (input.files || []).map((f) => normalizePath(f)).filter(Boolean);
|
|
443
711
|
const inferredFiles = explicitFiles.length > 0 ? explicitFiles : extractPathsFromText(`${cleanTitle} ${input.description || ""}`);
|
|
@@ -445,10 +713,14 @@ var AnchorStore = class {
|
|
|
445
713
|
if (input.project && (/^[a-zA-Z]:[\\/]/.test(input.project.trim()) || input.project.trim().startsWith("/"))) {
|
|
446
714
|
cwd = normalizePath(input.project.trim());
|
|
447
715
|
} else if (inferredFiles.length > 0 && (/^[a-zA-Z]:\//.test(inferredFiles[0]) || inferredFiles[0].startsWith("/"))) {
|
|
448
|
-
const firstDir =
|
|
716
|
+
const firstDir = path2.dirname(inferredFiles[0]);
|
|
449
717
|
if (firstDir && firstDir !== ".") cwd = normalizePath(firstDir);
|
|
450
718
|
}
|
|
451
|
-
const projectName = input.project ? input.project.trim() : cwd ?
|
|
719
|
+
const projectName = input.project ? input.project.trim() : cwd ? path2.basename(cwd) : "global";
|
|
720
|
+
const normBlockedBy = input.blockedBy ? input.blockedBy.map((b) => {
|
|
721
|
+
const c = b.trim().replace(/^#/, "");
|
|
722
|
+
return /^\d+$/.test(c) ? `anc-${parseInt(c, 10)}` : c;
|
|
723
|
+
}) : void 0;
|
|
452
724
|
const anchor = {
|
|
453
725
|
id,
|
|
454
726
|
title: cleanTitle,
|
|
@@ -463,6 +735,8 @@ var AnchorStore = class {
|
|
|
463
735
|
lastTouchedAt: now,
|
|
464
736
|
files: inferredFiles,
|
|
465
737
|
tags: (input.tags || []).map((t) => t.trim()).filter(Boolean),
|
|
738
|
+
...input.activeForm ? { activeForm: input.activeForm.trim() } : {},
|
|
739
|
+
...normBlockedBy && normBlockedBy.length > 0 ? { blockedBy: normBlockedBy } : {},
|
|
466
740
|
...input.sessionFile ? { sessionFile: normalizePath(input.sessionFile) } : {},
|
|
467
741
|
...input.progress || parsedProgress ? { progress: input.progress || parsedProgress } : {}
|
|
468
742
|
};
|
|
@@ -548,10 +822,21 @@ var AnchorStore = class {
|
|
|
548
822
|
const total = explicitTotal && Number.isFinite(explicitTotal) && explicitTotal >= 1 ? explicitTotal : current.progress?.total || 4;
|
|
549
823
|
const prevDone = current.progress?.done || 0;
|
|
550
824
|
const nextDone = Math.max(0, Math.min(total, prevDone + safeDelta));
|
|
825
|
+
const existingItems = materializeSteps(current.progress);
|
|
826
|
+
let updatedItems;
|
|
827
|
+
if (existingItems.length > 0) {
|
|
828
|
+
updatedItems = existingItems.map((item, idx2) => {
|
|
829
|
+
let status = "pending";
|
|
830
|
+
if (idx2 < nextDone) status = "completed";
|
|
831
|
+
else if (idx2 === nextDone) status = "in_progress";
|
|
832
|
+
return { ...item, status };
|
|
833
|
+
});
|
|
834
|
+
}
|
|
551
835
|
current.progress = {
|
|
552
836
|
...current.progress || {},
|
|
553
837
|
done: nextDone,
|
|
554
|
-
total
|
|
838
|
+
total,
|
|
839
|
+
...updatedItems ? { items: updatedItems, steps: updatedItems.map((i) => i.subject) } : {}
|
|
555
840
|
};
|
|
556
841
|
current.lastTouchedAt = now;
|
|
557
842
|
current.updatedAt = now;
|
|
@@ -563,6 +848,98 @@ var AnchorStore = class {
|
|
|
563
848
|
return { anchor: current, completed: nextDone >= total };
|
|
564
849
|
});
|
|
565
850
|
}
|
|
851
|
+
/**
|
|
852
|
+
* Add a structured sub-step to an anchor (with DAG dependency validation)
|
|
853
|
+
*/
|
|
854
|
+
addStep(id, subject, options = {}) {
|
|
855
|
+
return this.withLock(() => {
|
|
856
|
+
const state = this.loadState();
|
|
857
|
+
const idx = this.findAnchorIndex(state.anchors, id);
|
|
858
|
+
if (idx === -1) throw new Error(`Note not found: ${id}`);
|
|
859
|
+
const current = state.anchors[idx];
|
|
860
|
+
const items = materializeSteps(current.progress);
|
|
861
|
+
const nextStepId = items.length > 0 ? Math.max(...items.map((s) => s.id)) + 1 : 1;
|
|
862
|
+
if (options.blockedBy && options.blockedBy.length > 0) {
|
|
863
|
+
const check = validateStepDependencies(items, nextStepId, options.blockedBy);
|
|
864
|
+
if (!check.valid) throw new Error(check.error);
|
|
865
|
+
}
|
|
866
|
+
const newStep = {
|
|
867
|
+
id: nextStepId,
|
|
868
|
+
subject: subject.trim(),
|
|
869
|
+
status: options.status || (items.some((s) => s.status === "in_progress") ? "pending" : "in_progress"),
|
|
870
|
+
...options.activeForm ? { activeForm: options.activeForm.trim() } : {},
|
|
871
|
+
...options.blockedBy && options.blockedBy.length > 0 ? { blockedBy: [...options.blockedBy] } : {}
|
|
872
|
+
};
|
|
873
|
+
const nextItems = [...items, newStep];
|
|
874
|
+
current.progress = syncProgressFromItems(nextItems, current.progress);
|
|
875
|
+
current.updatedAt = Date.now();
|
|
876
|
+
current.lastTouchedAt = Date.now();
|
|
877
|
+
state.anchors[idx] = current;
|
|
878
|
+
this.saveState(state);
|
|
879
|
+
return current;
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Update a specific sub-step within an anchor (status, subject, activeForm, or blockedBy DAG edges)
|
|
884
|
+
*/
|
|
885
|
+
updateStep(id, stepId, patch) {
|
|
886
|
+
return this.withLock(() => {
|
|
887
|
+
const state = this.loadState();
|
|
888
|
+
const idx = this.findAnchorIndex(state.anchors, id);
|
|
889
|
+
if (idx === -1) throw new Error(`Note not found: ${id}`);
|
|
890
|
+
const current = state.anchors[idx];
|
|
891
|
+
const items = materializeSteps(current.progress);
|
|
892
|
+
const stepIdx = items.findIndex((s) => s.id === stepId);
|
|
893
|
+
if (stepIdx === -1) throw new Error(`Step #${stepId} not found in #${current.id}`);
|
|
894
|
+
if (patch.addBlockedBy && patch.addBlockedBy.length > 0) {
|
|
895
|
+
const check = validateStepDependencies(items, stepId, patch.addBlockedBy);
|
|
896
|
+
if (!check.valid) throw new Error(check.error);
|
|
897
|
+
}
|
|
898
|
+
const target = { ...items[stepIdx] };
|
|
899
|
+
if (patch.status) target.status = patch.status;
|
|
900
|
+
if (patch.subject) target.subject = patch.subject.trim();
|
|
901
|
+
if (patch.activeForm !== void 0) {
|
|
902
|
+
if (patch.activeForm) target.activeForm = patch.activeForm.trim();
|
|
903
|
+
else delete target.activeForm;
|
|
904
|
+
}
|
|
905
|
+
if (patch.addBlockedBy || patch.removeBlockedBy) {
|
|
906
|
+
const set = new Set(target.blockedBy ?? []);
|
|
907
|
+
for (const addId of patch.addBlockedBy ?? []) set.add(addId);
|
|
908
|
+
for (const rmId of patch.removeBlockedBy ?? []) set.delete(rmId);
|
|
909
|
+
if (set.size > 0) target.blockedBy = [...set];
|
|
910
|
+
else delete target.blockedBy;
|
|
911
|
+
}
|
|
912
|
+
items[stepIdx] = target;
|
|
913
|
+
current.progress = syncProgressFromItems(items, current.progress);
|
|
914
|
+
current.updatedAt = Date.now();
|
|
915
|
+
current.lastTouchedAt = Date.now();
|
|
916
|
+
state.anchors[idx] = current;
|
|
917
|
+
this.saveState(state);
|
|
918
|
+
return current;
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* Read recent settled/archived notes from `archive.jsonl` (newest first)
|
|
923
|
+
*/
|
|
924
|
+
listArchive(limit = 20) {
|
|
925
|
+
if (!fs.existsSync(this.archivePath)) return [];
|
|
926
|
+
try {
|
|
927
|
+
const lines = fs.readFileSync(this.archivePath, "utf-8").split(/\r?\n/).filter(Boolean);
|
|
928
|
+
const parsed = [];
|
|
929
|
+
for (let i = lines.length - 1; i >= 0 && parsed.length < limit; i--) {
|
|
930
|
+
try {
|
|
931
|
+
const item = JSON.parse(lines[i]);
|
|
932
|
+
if (item && typeof item.id === "string" && typeof item.title === "string") {
|
|
933
|
+
parsed.push(item);
|
|
934
|
+
}
|
|
935
|
+
} catch {
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
return parsed;
|
|
939
|
+
} catch {
|
|
940
|
+
return [];
|
|
941
|
+
}
|
|
942
|
+
}
|
|
566
943
|
/**
|
|
567
944
|
* Clever Touch 4 (Todo-Bridge): Automatically mirror intra-session `todo` tool lists
|
|
568
945
|
* into a cross-session checkpoint!
|
|
@@ -599,6 +976,7 @@ var AnchorStore = class {
|
|
|
599
976
|
return { status: "noop" };
|
|
600
977
|
}
|
|
601
978
|
const inProgress = unfinishedTasks.find((t) => t.status === "in_progress") || unfinishedTasks[0];
|
|
979
|
+
const structuredItems = convertTodoTasksToSteps(visible);
|
|
602
980
|
const orderedSteps = [
|
|
603
981
|
...completedTasks.map((t) => t.subject.trim()),
|
|
604
982
|
...unfinishedTasks.filter((t) => t.status === "in_progress").map((t) => t.subject.trim()),
|
|
@@ -607,7 +985,8 @@ var AnchorStore = class {
|
|
|
607
985
|
const progress = {
|
|
608
986
|
done: completedTasks.length,
|
|
609
987
|
total: visible.length,
|
|
610
|
-
steps: orderedSteps
|
|
988
|
+
steps: orderedSteps,
|
|
989
|
+
items: structuredItems
|
|
611
990
|
};
|
|
612
991
|
const remainingSummary = unfinishedTasks.slice(0, 3).map((t) => t.subject.trim()).join(" \u2192 ");
|
|
613
992
|
const headline = inProgress.subject.trim();
|
|
@@ -631,7 +1010,7 @@ var AnchorStore = class {
|
|
|
631
1010
|
}
|
|
632
1011
|
const id = this.generateId(state);
|
|
633
1012
|
const inferredFiles = options.recentFiles && options.recentFiles.length > 0 ? options.recentFiles.map((f) => normalizePath(f)).filter(Boolean) : extractPathsFromText(orderedSteps.join(" "));
|
|
634
|
-
const projectName = normCwd ?
|
|
1013
|
+
const projectName = normCwd ? path2.basename(normCwd) : "global";
|
|
635
1014
|
const created = {
|
|
636
1015
|
id,
|
|
637
1016
|
title: headline,
|
|
@@ -723,9 +1102,222 @@ var AnchorStore = class {
|
|
|
723
1102
|
}
|
|
724
1103
|
};
|
|
725
1104
|
|
|
1105
|
+
// src/dock.ts
|
|
1106
|
+
var STATUS_GLYPH = {
|
|
1107
|
+
pending: "\u25CB",
|
|
1108
|
+
in_progress: "\u25D0",
|
|
1109
|
+
completed: "\u2713",
|
|
1110
|
+
deleted: "\u2717"
|
|
1111
|
+
};
|
|
1112
|
+
function formatStepGlyph(status, fg) {
|
|
1113
|
+
switch (status) {
|
|
1114
|
+
case "pending":
|
|
1115
|
+
return fg("dim", "\u25CB");
|
|
1116
|
+
case "in_progress":
|
|
1117
|
+
return fg("warning", "\u25D0");
|
|
1118
|
+
case "completed":
|
|
1119
|
+
return fg("success", "\u2713");
|
|
1120
|
+
case "deleted":
|
|
1121
|
+
return fg("error", "\u2717");
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
function applyStrikethrough(text, theme) {
|
|
1125
|
+
if (theme && typeof theme.strikethrough === "function") {
|
|
1126
|
+
return theme.strikethrough(text);
|
|
1127
|
+
}
|
|
1128
|
+
return `\x1B[9m${text}\x1B[29m`;
|
|
1129
|
+
}
|
|
1130
|
+
function formatDockStepRow(step, options) {
|
|
1131
|
+
const { fg, theme, showId = true, fallbackActiveForm } = options;
|
|
1132
|
+
const glyph = formatStepGlyph(step.status, fg);
|
|
1133
|
+
const subjectColor = step.status === "in_progress" ? "accent" : step.status === "completed" || step.status === "deleted" ? "muted" : "text";
|
|
1134
|
+
let subject = fg(subjectColor, step.subject);
|
|
1135
|
+
if (step.status === "completed" || step.status === "deleted") {
|
|
1136
|
+
subject = applyStrikethrough(subject, theme);
|
|
1137
|
+
}
|
|
1138
|
+
let row = glyph;
|
|
1139
|
+
if (showId) {
|
|
1140
|
+
row += ` ${fg("dim", `${step.id}.`)}`;
|
|
1141
|
+
}
|
|
1142
|
+
row += ` ${subject}`;
|
|
1143
|
+
const activeForm = step.status === "in_progress" ? step.activeForm || fallbackActiveForm : void 0;
|
|
1144
|
+
if (activeForm) {
|
|
1145
|
+
row += ` ${fg("muted", `(${activeForm})`)}`;
|
|
1146
|
+
}
|
|
1147
|
+
if (step.blockedBy && step.blockedBy.length > 0) {
|
|
1148
|
+
row += ` ${fg("muted", `\u26D3 #${step.blockedBy.join(",#")}`)}`;
|
|
1149
|
+
}
|
|
1150
|
+
return row;
|
|
1151
|
+
}
|
|
1152
|
+
var AnchorDockController = class {
|
|
1153
|
+
collapsed = false;
|
|
1154
|
+
pendingHideByAnchor = /* @__PURE__ */ new Map();
|
|
1155
|
+
hiddenByAnchor = /* @__PURE__ */ new Map();
|
|
1156
|
+
isCollapsed() {
|
|
1157
|
+
return this.collapsed;
|
|
1158
|
+
}
|
|
1159
|
+
setCollapsed(val) {
|
|
1160
|
+
this.collapsed = val;
|
|
1161
|
+
}
|
|
1162
|
+
toggleCollapsed() {
|
|
1163
|
+
this.collapsed = !this.collapsed;
|
|
1164
|
+
return this.collapsed;
|
|
1165
|
+
}
|
|
1166
|
+
resetGraceState(anchorId) {
|
|
1167
|
+
if (anchorId) {
|
|
1168
|
+
this.pendingHideByAnchor.delete(anchorId);
|
|
1169
|
+
this.hiddenByAnchor.delete(anchorId);
|
|
1170
|
+
} else {
|
|
1171
|
+
this.pendingHideByAnchor.clear();
|
|
1172
|
+
this.hiddenByAnchor.clear();
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
/**
|
|
1176
|
+
* Called on `agent_start`: folds away completed steps that were shown with `✓` strikethrough
|
|
1177
|
+
* during the previous turn so the dock stays compact while coding.
|
|
1178
|
+
*/
|
|
1179
|
+
foldCompletedStepsFromPreviousTurn() {
|
|
1180
|
+
let changed = false;
|
|
1181
|
+
for (const [anchorId, pendingSet] of this.pendingHideByAnchor.entries()) {
|
|
1182
|
+
if (pendingSet.size === 0) continue;
|
|
1183
|
+
let hiddenSet = this.hiddenByAnchor.get(anchorId);
|
|
1184
|
+
if (!hiddenSet) {
|
|
1185
|
+
hiddenSet = /* @__PURE__ */ new Set();
|
|
1186
|
+
this.hiddenByAnchor.set(anchorId, hiddenSet);
|
|
1187
|
+
}
|
|
1188
|
+
for (const id of pendingSet) {
|
|
1189
|
+
hiddenSet.add(id);
|
|
1190
|
+
changed = true;
|
|
1191
|
+
}
|
|
1192
|
+
pendingSet.clear();
|
|
1193
|
+
}
|
|
1194
|
+
return changed;
|
|
1195
|
+
}
|
|
1196
|
+
/**
|
|
1197
|
+
* Render the `belowEditor` widget lines for active anchors.
|
|
1198
|
+
* - If `collapsed === true` OR no anchor has multi-step items, renders the 1-line Sticky Strip.
|
|
1199
|
+
* - If `collapsed === false` AND the primary anchor has structured steps (`total >= 2`),
|
|
1200
|
+
* renders the multi-row Tree Dock (up to `maxStepRows = 4` step rows + optional side note summary).
|
|
1201
|
+
*/
|
|
1202
|
+
renderDockLines(activeAnchors, options) {
|
|
1203
|
+
const { width, cwd, theme, maxStepRows = 4 } = options;
|
|
1204
|
+
if (activeAnchors.length === 0) return [];
|
|
1205
|
+
const sorted = sortAnchorsForDisplay([...activeAnchors], cwd);
|
|
1206
|
+
const fg = theme?.fg ? theme.fg.bind(theme) : (_c, text) => text;
|
|
1207
|
+
const primary = sorted[0];
|
|
1208
|
+
const steps = materializeSteps(primary.progress);
|
|
1209
|
+
if (this.collapsed || width < 60 || steps.length < 2) {
|
|
1210
|
+
return this.renderSingleLineStrip(sorted, width, fg);
|
|
1211
|
+
}
|
|
1212
|
+
const completedIds = new Set(steps.filter((s) => s.status === "completed").map((s) => s.id));
|
|
1213
|
+
const pendingSet = this.pendingHideByAnchor.get(primary.id) ?? /* @__PURE__ */ new Set();
|
|
1214
|
+
const hiddenSet = this.hiddenByAnchor.get(primary.id) ?? /* @__PURE__ */ new Set();
|
|
1215
|
+
for (const id of [...pendingSet]) {
|
|
1216
|
+
if (!completedIds.has(id)) pendingSet.delete(id);
|
|
1217
|
+
}
|
|
1218
|
+
for (const id of [...hiddenSet]) {
|
|
1219
|
+
if (!completedIds.has(id)) hiddenSet.delete(id);
|
|
1220
|
+
}
|
|
1221
|
+
this.pendingHideByAnchor.set(primary.id, pendingSet);
|
|
1222
|
+
this.hiddenByAnchor.set(primary.id, hiddenSet);
|
|
1223
|
+
const layout = selectDockStepLayout(steps, maxStepRows, hiddenSet);
|
|
1224
|
+
for (const s of layout.visible) {
|
|
1225
|
+
if (s.status === "completed" && !pendingSet.has(s.id) && !hiddenSet.has(s.id)) {
|
|
1226
|
+
pendingSet.add(s.id);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
if (layout.visible.length === 0) {
|
|
1230
|
+
return this.renderSingleLineStrip(sorted, width, fg);
|
|
1231
|
+
}
|
|
1232
|
+
const num = primary.id.replace(/^anc-/, "");
|
|
1233
|
+
const bar = primary.progress ? `${fg("accent", formatProgressBar(primary.progress))} ` : "";
|
|
1234
|
+
const origin = formatOrigin(primary);
|
|
1235
|
+
const sessionIcon = primary.sessionFile ? " \u21AA" : "";
|
|
1236
|
+
const blockedTag = primary.blockedBy && primary.blockedBy.length > 0 ? ` ${fg("muted", `\u26D3 #${primary.blockedBy.map((b) => b.replace(/^anc-/, "")).join(",#")}`)}` : "";
|
|
1237
|
+
const foldHint = width >= 76 ? fg("dim", " [Alt+A fold]") : "";
|
|
1238
|
+
const headerPrefix = `${fg("accent", "\u2316")} ${fg("dim", `#${num}`)} ${bar}`;
|
|
1239
|
+
const headerSuffix = `${blockedTag} ${fg("muted", `(${origin}${sessionIcon})`)}${foldHint}`;
|
|
1240
|
+
const titleBudget = Math.max(12, width - getDisplayWidth(headerPrefix) - getDisplayWidth(headerSuffix) - 2);
|
|
1241
|
+
const headerLine = truncateToWidth(
|
|
1242
|
+
`${headerPrefix}${truncateToWidth(primary.title, titleBudget)}${headerSuffix}`,
|
|
1243
|
+
width
|
|
1244
|
+
);
|
|
1245
|
+
const lines = [headerLine];
|
|
1246
|
+
const otherNotes = sorted.slice(1);
|
|
1247
|
+
const hasFooterRow = layout.hiddenCompleted > 0 || layout.truncatedTail > 0 || otherNotes.length > 0;
|
|
1248
|
+
for (let i = 0; i < layout.visible.length; i++) {
|
|
1249
|
+
const step = layout.visible[i];
|
|
1250
|
+
const isLastRow = i === layout.visible.length - 1 && !hasFooterRow;
|
|
1251
|
+
const branch = fg("dim", isLastRow ? "\u2514\u2500 " : "\u251C\u2500 ");
|
|
1252
|
+
const stepText = formatDockStepRow(step, {
|
|
1253
|
+
fg,
|
|
1254
|
+
theme,
|
|
1255
|
+
showId: true,
|
|
1256
|
+
fallbackActiveForm: primary.activeForm
|
|
1257
|
+
});
|
|
1258
|
+
lines.push(truncateToWidth(`${branch}${stepText}`, width));
|
|
1259
|
+
}
|
|
1260
|
+
if (hasFooterRow) {
|
|
1261
|
+
const parts = [];
|
|
1262
|
+
if (layout.hiddenCompleted > 0 || layout.truncatedTail > 0) {
|
|
1263
|
+
const overflowDetails = [];
|
|
1264
|
+
if (layout.hiddenCompleted > 0) overflowDetails.push(`${layout.hiddenCompleted} done`);
|
|
1265
|
+
if (layout.truncatedTail > 0) overflowDetails.push(`${layout.truncatedTail} pending`);
|
|
1266
|
+
parts.push(fg("dim", `+${layout.hiddenCompleted + layout.truncatedTail} steps (${overflowDetails.join(", ")})`));
|
|
1267
|
+
}
|
|
1268
|
+
if (otherNotes.length > 0) {
|
|
1269
|
+
const preview = otherNotes.slice(0, 2).map((a) => `#${a.id.replace(/^anc-/, "")} ${truncateToWidth(a.title, 20)}`).join(" \xB7 ");
|
|
1270
|
+
const extra = otherNotes.length > 2 ? ` +${otherNotes.length - 2}` : "";
|
|
1271
|
+
parts.push(fg("muted", `\u{1F4CC} ${preview}${extra}`));
|
|
1272
|
+
}
|
|
1273
|
+
lines.push(truncateToWidth(`${fg("dim", "\u2514\u2500 ")}${parts.join(fg("dim", " \u2502 "))}`, width));
|
|
1274
|
+
}
|
|
1275
|
+
return lines;
|
|
1276
|
+
}
|
|
1277
|
+
renderSingleLineStrip(sorted, width, fg) {
|
|
1278
|
+
if (sorted.length === 1) {
|
|
1279
|
+
const a = sorted[0];
|
|
1280
|
+
const num = a.id.replace(/^anc-/, "");
|
|
1281
|
+
const bar = a.progress ? `${fg("accent", formatProgressBar(a.progress))} ` : "";
|
|
1282
|
+
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;
|
|
1283
|
+
const nextStep = candidateStep ? fg("muted", ` \u25B8 ${candidateStep}`) : "";
|
|
1284
|
+
const activeHint = a.activeForm ? fg("muted", ` (${a.activeForm})`) : "";
|
|
1285
|
+
const origin = formatOrigin(a);
|
|
1286
|
+
const sessionIcon = a.sessionFile ? " \u21AA" : "";
|
|
1287
|
+
const prefix = `${fg("accent", "\u2316")} ${fg("dim", `#${num}`)} ${bar}`;
|
|
1288
|
+
const suffix = `${activeHint} ${fg("muted", `(${origin}${sessionIcon})`)}`;
|
|
1289
|
+
const availTitle = Math.max(
|
|
1290
|
+
12,
|
|
1291
|
+
width - getDisplayWidth(prefix) - getDisplayWidth(nextStep) - getDisplayWidth(suffix) - 2
|
|
1292
|
+
);
|
|
1293
|
+
const titleClipped = truncateToWidth(a.title, availTitle);
|
|
1294
|
+
return [truncateToWidth(`${prefix}${titleClipped}${nextStep}${suffix}`, width)];
|
|
1295
|
+
}
|
|
1296
|
+
const maxItems = width < 68 ? 1 : width < 100 ? 2 : 3;
|
|
1297
|
+
const visibleSlice = sorted.slice(0, maxItems);
|
|
1298
|
+
const countTag = fg("accent", `\u2316 ${sorted.length}: `);
|
|
1299
|
+
const moreCount = sorted.length - visibleSlice.length;
|
|
1300
|
+
const moreSuffix = moreCount > 0 ? fg("dim", ` +${moreCount}`) : "";
|
|
1301
|
+
const perSlotWidth = Math.max(
|
|
1302
|
+
10,
|
|
1303
|
+
Math.floor((width - getDisplayWidth(countTag) - getDisplayWidth(moreSuffix) - (visibleSlice.length - 1) * 5) / visibleSlice.length)
|
|
1304
|
+
);
|
|
1305
|
+
const segments = visibleSlice.map((a) => {
|
|
1306
|
+
const num = a.id.replace(/^anc-/, "");
|
|
1307
|
+
const bar = a.progress ? `${formatProgressBar(a.progress)} ` : "";
|
|
1308
|
+
const prefix = `#${num} ${bar}`;
|
|
1309
|
+
const titleBudget = Math.max(6, perSlotWidth - getDisplayWidth(prefix));
|
|
1310
|
+
const shortTitle = truncateToWidth(a.title, titleBudget);
|
|
1311
|
+
return `${fg("dim", `#${num}`)} ${bar}${shortTitle}`;
|
|
1312
|
+
});
|
|
1313
|
+
return [truncateToWidth(`${countTag}${segments.join(fg("dim", " \xB7 "))}${moreSuffix}`, width)];
|
|
1314
|
+
}
|
|
1315
|
+
};
|
|
1316
|
+
var defaultDockController = new AnchorDockController();
|
|
1317
|
+
|
|
726
1318
|
// src/tui.ts
|
|
727
1319
|
import fs2 from "fs";
|
|
728
|
-
import
|
|
1320
|
+
import path3 from "path";
|
|
729
1321
|
var ANSI_REGEX = /\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g;
|
|
730
1322
|
function stripAnsi(str) {
|
|
731
1323
|
return str.replace(ANSI_REGEX, "");
|
|
@@ -812,7 +1404,7 @@ function formatRelativeAge(timestamp, now = Date.now()) {
|
|
|
812
1404
|
function formatOrigin(anchor) {
|
|
813
1405
|
if (typeof anchor === "string") {
|
|
814
1406
|
if (!anchor) return "global";
|
|
815
|
-
return
|
|
1407
|
+
return path3.basename(anchor.replace(/\\/g, "/")) || "global";
|
|
816
1408
|
}
|
|
817
1409
|
let proj = "global";
|
|
818
1410
|
if (anchor.project && anchor.project !== "global") {
|
|
@@ -824,10 +1416,10 @@ function formatOrigin(anchor) {
|
|
|
824
1416
|
proj = normProj;
|
|
825
1417
|
}
|
|
826
1418
|
} else if (anchor.cwd) {
|
|
827
|
-
proj =
|
|
1419
|
+
proj = path3.basename(anchor.cwd.replace(/\\/g, "/")) || "global";
|
|
828
1420
|
}
|
|
829
1421
|
if (anchor.files && anchor.files.length > 0) {
|
|
830
|
-
const shortFile =
|
|
1422
|
+
const shortFile = path3.basename(anchor.files[0].replace(/\\/g, "/"));
|
|
831
1423
|
if (shortFile && !proj.endsWith(shortFile)) {
|
|
832
1424
|
return `${proj} \xB7 ${shortFile}`;
|
|
833
1425
|
}
|
|
@@ -864,43 +1456,12 @@ function updateStartupBanner(ctx, store) {
|
|
|
864
1456
|
ctx.ui.setWidget("anchor-startup", void 0, { placement: "belowEditor" });
|
|
865
1457
|
return;
|
|
866
1458
|
}
|
|
867
|
-
const sorted = sortAnchorsForDisplay(active, ctx.cwd);
|
|
868
1459
|
const factory = (_tui, theme) => ({
|
|
869
|
-
render: (width) => {
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
const bar = a.progress ? `${fg("accent", formatProgressBar(a.progress))} ` : "";
|
|
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}`) : "";
|
|
877
|
-
const origin = formatOrigin(a);
|
|
878
|
-
const sessionIcon = a.sessionFile ? " \u21AA" : "";
|
|
879
|
-
const prefix = `${fg("accent", "\u2316")} ${fg("dim", `#${num}`)} ${bar}`;
|
|
880
|
-
const suffix = ` ${fg("muted", `(${origin}${sessionIcon})`)}`;
|
|
881
|
-
const availTitle = Math.max(12, width - getDisplayWidth(prefix) - getDisplayWidth(nextStep) - getDisplayWidth(suffix) - 2);
|
|
882
|
-
const titleClipped = truncateToWidth(a.title, availTitle);
|
|
883
|
-
return [truncateToWidth(`${prefix}${titleClipped}${nextStep}${suffix}`, width)];
|
|
884
|
-
}
|
|
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) => {
|
|
895
|
-
const num = a.id.replace(/^anc-/, "");
|
|
896
|
-
const bar = a.progress ? `${formatProgressBar(a.progress)} ` : "";
|
|
897
|
-
const prefix = `#${num} ${bar}`;
|
|
898
|
-
const titleBudget = Math.max(6, perSlotWidth - getDisplayWidth(prefix));
|
|
899
|
-
const shortTitle = truncateToWidth(a.title, titleBudget);
|
|
900
|
-
return `${fg("dim", `#${num}`)} ${bar}${shortTitle}`;
|
|
901
|
-
});
|
|
902
|
-
return [truncateToWidth(`${countTag}${segments.join(fg("dim", " \xB7 "))}${moreSuffix}`, width)];
|
|
903
|
-
},
|
|
1460
|
+
render: (width) => defaultDockController.renderDockLines(active, {
|
|
1461
|
+
width,
|
|
1462
|
+
cwd: ctx.cwd,
|
|
1463
|
+
theme: ctx.ui?.theme ?? theme
|
|
1464
|
+
}),
|
|
904
1465
|
invalidate: () => {
|
|
905
1466
|
}
|
|
906
1467
|
});
|
|
@@ -943,9 +1504,16 @@ async function openAnchorDashboard(ctx, store) {
|
|
|
943
1504
|
anchor.sessionFile && fs2.existsSync(anchor.sessionFile) && typeof ctx.switchSession === "function" && (!currentSessionFile || !isSameOrSubPath(anchor.sessionFile, currentSessionFile))
|
|
944
1505
|
);
|
|
945
1506
|
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)";
|
|
1507
|
+
const structuredSteps = materializeSteps(anchor.progress);
|
|
1508
|
+
const stepOptions = structuredSteps.map((s) => {
|
|
1509
|
+
const g = STATUS_GLYPH[s.status] || "\u25CB";
|
|
1510
|
+
const nextAction = s.status === "completed" ? "reopen" : "complete";
|
|
1511
|
+
return `${g} Step ${s.id}: ${s.subject} (${nextAction})`;
|
|
1512
|
+
});
|
|
946
1513
|
const actions = [
|
|
947
1514
|
"\u2713 Done (tear off note)",
|
|
948
1515
|
stepLabel,
|
|
1516
|
+
...stepOptions,
|
|
949
1517
|
...canJumpSession ? ["\u21AA Jump to session (resume linked conversation)"] : [],
|
|
950
1518
|
...anchor.status === "sleeping" ? ["\u26A1 Wake up (mark active)"] : [],
|
|
951
1519
|
"\u2139 Read full note",
|
|
@@ -953,7 +1521,7 @@ async function openAnchorDashboard(ctx, store) {
|
|
|
953
1521
|
];
|
|
954
1522
|
const action = await ctx.ui.select(`\u2316 #${anchor.id.replace(/^anc-/, "")}: "${anchor.title}"`, actions);
|
|
955
1523
|
if (!action || action.startsWith("\u21A9")) return;
|
|
956
|
-
if (action.startsWith("\u2713")) {
|
|
1524
|
+
if (action.startsWith("\u2713 Done")) {
|
|
957
1525
|
store.settle(anchor.id, { settledBy: "manual" });
|
|
958
1526
|
ctx.ui.notify(`\u2316 Torn off #${anchor.id.replace(/^anc-/, "")}: "${anchor.title}" (/anchor undo to put back)`, "info");
|
|
959
1527
|
updateAnchorStatusBar(ctx, store);
|
|
@@ -968,6 +1536,21 @@ async function openAnchorDashboard(ctx, store) {
|
|
|
968
1536
|
} else {
|
|
969
1537
|
ctx.ui.notify(`\u2316 Advanced #${updated.id.replace(/^anc-/, "")} to ${formatProgressBar(updated.progress)}`, "info");
|
|
970
1538
|
}
|
|
1539
|
+
} else if (/^[○◐✓✗]\s+Step\s+(\d+):/.test(action)) {
|
|
1540
|
+
const m = action.match(/Step\s+(\d+):/);
|
|
1541
|
+
if (m) {
|
|
1542
|
+
const stepId = parseInt(m[1], 10);
|
|
1543
|
+
const targetStep = structuredSteps.find((s) => s.id === stepId);
|
|
1544
|
+
if (targetStep) {
|
|
1545
|
+
const nextStatus = targetStep.status === "completed" ? "pending" : "completed";
|
|
1546
|
+
const updated = store.updateStep(anchor.id, stepId, { status: nextStatus });
|
|
1547
|
+
ctx.ui.notify(
|
|
1548
|
+
`\u2316 Step ${stepId} ${nextStatus === "completed" ? "completed \u2713" : "reopened \u25CB"} (${formatProgressBar(updated.progress)})`,
|
|
1549
|
+
"info"
|
|
1550
|
+
);
|
|
1551
|
+
updateAnchorStatusBar(ctx, store);
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
971
1554
|
} else if (action.startsWith("\u21AA") && anchor.sessionFile) {
|
|
972
1555
|
try {
|
|
973
1556
|
await ctx.switchSession(anchor.sessionFile);
|
|
@@ -984,7 +1567,7 @@ async function openAnchorDashboard(ctx, store) {
|
|
|
984
1567
|
anchor.progress ? `Progress: ${formatProgressBar(anchor.progress)}${anchor.progress.steps ? ` (${anchor.progress.steps.join(" \u2192 ")})` : ""}` : null,
|
|
985
1568
|
anchor.description ? `Details: ${anchor.description}` : null,
|
|
986
1569
|
anchor.files.length > 0 ? `Files: ${anchor.files.join(", ")}` : null,
|
|
987
|
-
anchor.sessionFile ? `Session: ${
|
|
1570
|
+
anchor.sessionFile ? `Session: ${path3.basename(anchor.sessionFile)}` : null,
|
|
988
1571
|
`Workspace: ${anchor.project || "global"} \xB7 Pinned ${formatRelativeAge(anchor.createdAt)}`
|
|
989
1572
|
].filter(Boolean).join("\n");
|
|
990
1573
|
ctx.ui.notify(details, "info");
|
|
@@ -1001,6 +1584,9 @@ function parseAnchorCommandInput(raw) {
|
|
|
1001
1584
|
if (trimmed === "undo") {
|
|
1002
1585
|
return { mode: "undo" };
|
|
1003
1586
|
}
|
|
1587
|
+
if (trimmed === "fold" || trimmed === "compact" || trimmed === "toggle" || trimmed === "expand") {
|
|
1588
|
+
return { mode: "fold" };
|
|
1589
|
+
}
|
|
1004
1590
|
const settleMatch = trimmed.match(/^(?:done|rm|close|settle)\s+(.+)$/i);
|
|
1005
1591
|
if (settleMatch) {
|
|
1006
1592
|
return {
|
|
@@ -1083,14 +1669,42 @@ function extension_default(pi) {
|
|
|
1083
1669
|
pi.on("session_start", async (_event, ctx) => {
|
|
1084
1670
|
annotatedThisSession.clear();
|
|
1085
1671
|
recentSessionFiles.length = 0;
|
|
1672
|
+
defaultDockController.resetGraceState();
|
|
1086
1673
|
sweepQuietNotes(store);
|
|
1087
1674
|
const quietForHere = store.list({ status: "sleeping", all: true }).filter((a) => (a.files.length > 0 || a.project && a.project !== "global" || a.cwd) && isAnchorTargetingWorkspace(a, ctx.cwd));
|
|
1088
1675
|
for (const q of quietForHere) {
|
|
1089
1676
|
store.touch(q.id);
|
|
1090
1677
|
}
|
|
1678
|
+
try {
|
|
1679
|
+
const branch = ctx.sessionManager?.getBranch?.();
|
|
1680
|
+
const tasks = replayTodoSnapshotFromBranch(branch);
|
|
1681
|
+
if (tasks) {
|
|
1682
|
+
store.syncFromTodoTool(tasks, {
|
|
1683
|
+
cwd: ctx.cwd,
|
|
1684
|
+
sessionFile: getSessionFileSafe(ctx)
|
|
1685
|
+
});
|
|
1686
|
+
}
|
|
1687
|
+
} catch {
|
|
1688
|
+
}
|
|
1091
1689
|
updateAnchorStatusBar(ctx, store);
|
|
1092
|
-
updateStartupBanner(ctx, store);
|
|
1093
1690
|
});
|
|
1691
|
+
pi.on("agent_start", async (_event, ctx) => {
|
|
1692
|
+
if (defaultDockController.foldCompletedStepsFromPreviousTurn()) {
|
|
1693
|
+
updateStartupBanner(ctx, store);
|
|
1694
|
+
}
|
|
1695
|
+
});
|
|
1696
|
+
if (typeof pi.registerShortcut === "function") {
|
|
1697
|
+
try {
|
|
1698
|
+
pi.registerShortcut("alt+a", {
|
|
1699
|
+
description: "Collapse or expand the Anchor belowEditor task dock",
|
|
1700
|
+
handler: (ctx) => {
|
|
1701
|
+
defaultDockController.toggleCollapsed();
|
|
1702
|
+
updateStartupBanner(ctx, store);
|
|
1703
|
+
}
|
|
1704
|
+
});
|
|
1705
|
+
} catch {
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1094
1708
|
pi.on("input", async (event, ctx) => {
|
|
1095
1709
|
if (event?.source === "extension") return { action: "continue" };
|
|
1096
1710
|
const quickNote = parseQuickNoteInput(String(event?.text || ""));
|
|
@@ -1160,8 +1774,18 @@ ${snippet}` };
|
|
|
1160
1774
|
}
|
|
1161
1775
|
}
|
|
1162
1776
|
}
|
|
1777
|
+
const liveActivity = inferLiveActivityLabel(lowerTool, event.input, ctx.cwd);
|
|
1163
1778
|
const pathInput = event.input?.path;
|
|
1164
|
-
if (typeof pathInput !== "string")
|
|
1779
|
+
if (typeof pathInput !== "string") {
|
|
1780
|
+
if (liveActivity) {
|
|
1781
|
+
const activeHere = store.list({ status: "active", cwd: ctx.cwd });
|
|
1782
|
+
if (activeHere.length > 0) {
|
|
1783
|
+
store.update(activeHere[0].id, { activeForm: liveActivity });
|
|
1784
|
+
updateStartupBanner(ctx, store);
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1165
1789
|
const isMutation = MUTATION_TOOLS.has(lowerTool);
|
|
1166
1790
|
if (isMutation) {
|
|
1167
1791
|
rememberTouchedFile(pathInput, ctx.cwd);
|
|
@@ -1170,9 +1794,14 @@ ${snippet}` };
|
|
|
1170
1794
|
const notes = store.list({ cwd: ctx.cwd }).filter((a) => a.status === "active" || a.status === "sleeping");
|
|
1171
1795
|
const matches = findMatchedAnchors(notes, [touchedPath]);
|
|
1172
1796
|
if (matches.length === 0) return;
|
|
1173
|
-
if (isMutation) {
|
|
1797
|
+
if (isMutation || liveActivity) {
|
|
1174
1798
|
for (const m of matches) {
|
|
1175
|
-
|
|
1799
|
+
if (isMutation) {
|
|
1800
|
+
store.touch(m.anchor.id, Date.now(), currentSessionFile);
|
|
1801
|
+
}
|
|
1802
|
+
if (liveActivity) {
|
|
1803
|
+
store.update(m.anchor.id, { activeForm: liveActivity });
|
|
1804
|
+
}
|
|
1176
1805
|
}
|
|
1177
1806
|
updateAnchorStatusBar(ctx, store);
|
|
1178
1807
|
}
|
|
@@ -1333,6 +1962,12 @@ ${snippet}` };
|
|
|
1333
1962
|
}
|
|
1334
1963
|
return;
|
|
1335
1964
|
}
|
|
1965
|
+
if (parsed.mode === "fold") {
|
|
1966
|
+
const collapsed = defaultDockController.toggleCollapsed();
|
|
1967
|
+
updateStartupBanner(ctx, store);
|
|
1968
|
+
ctx.ui.notify(`\u2316 Anchor Dock ${collapsed ? "folded to 1-line strip" : "expanded to tree view"} (Alt+A)`, "info");
|
|
1969
|
+
return;
|
|
1970
|
+
}
|
|
1336
1971
|
if (parsed.mode === "step" && parsed.targetId) {
|
|
1337
1972
|
const resolvedId = resolveAnchorTarget(store, parsed.targetId, ctx.cwd);
|
|
1338
1973
|
if (!resolvedId) {
|
|
@@ -1467,6 +2102,17 @@ ${snippet}` };
|
|
|
1467
2102
|
export {
|
|
1468
2103
|
QUIET_AFTER_DAYS,
|
|
1469
2104
|
MS_PER_DAY,
|
|
2105
|
+
DEFAULT_MAX_DOCK_LINES,
|
|
2106
|
+
validateStepDependencies,
|
|
2107
|
+
validateAnchorDependencies,
|
|
2108
|
+
deriveStepBlocks,
|
|
2109
|
+
materializeSteps,
|
|
2110
|
+
syncProgressFromItems,
|
|
2111
|
+
selectStepCounts,
|
|
2112
|
+
selectDockStepLayout,
|
|
2113
|
+
replayTodoSnapshotFromBranch,
|
|
2114
|
+
convertTodoTasksToSteps,
|
|
2115
|
+
inferLiveActivityLabel,
|
|
1470
2116
|
normalizePath,
|
|
1471
2117
|
isSameOrSubPath,
|
|
1472
2118
|
globToRegExp,
|
|
@@ -1485,6 +2131,12 @@ export {
|
|
|
1485
2131
|
acquireSyncLock,
|
|
1486
2132
|
atomicRenameWithRetry,
|
|
1487
2133
|
AnchorStore,
|
|
2134
|
+
STATUS_GLYPH,
|
|
2135
|
+
formatStepGlyph,
|
|
2136
|
+
applyStrikethrough,
|
|
2137
|
+
formatDockStepRow,
|
|
2138
|
+
AnchorDockController,
|
|
2139
|
+
defaultDockController,
|
|
1488
2140
|
stripAnsi,
|
|
1489
2141
|
getDisplayWidth,
|
|
1490
2142
|
hasActiveUI,
|