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.
@@ -4,18 +4,277 @@ import fs3 from "fs";
4
4
 
5
5
  // src/store.ts
6
6
  import fs from "fs";
7
- import path from "path";
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 && (path.isAbsolute(filePath) || norm.startsWith("/") || /^[a-zA-Z]:\//.test(norm))) {
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 = path.basename(normTarget).toLowerCase();
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 (path.basename(normProj).toLowerCase() === targetBase) return true;
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 = !path.isAbsolute(f) && !/^[a-zA-Z]:\//.test(f) && anchor.cwd ? `${anchor.cwd.replace(/\\/g, "/").replace(/\/+$/, "")}/${f.replace(/^\/+/, "")}` : f;
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 = path.extname(filePath).toLowerCase();
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 path.resolve(process.env.ANCHOR_DIR);
297
- return path.join(os.homedir(), ".anchor");
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 ? path.resolve(storageDir) : getDefaultStorageDir();
380
- this.statePath = path.join(this.storageDir, "state.json");
381
- this.lockPath = path.join(this.storageDir, "state.lock");
382
- this.archivePath = path.join(this.storageDir, "archive.jsonl");
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, path.join(this.storageDir, `state.corrupt.${Date.now()}.json`));
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 = path.dirname(inferredFiles[0]);
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 ? path.basename(cwd) : "global";
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 ? path.basename(normCwd) : "global";
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 path2 from "path";
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 path2.basename(anchor.replace(/\\/g, "/")) || "global";
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 = path2.basename(anchor.cwd.replace(/\\/g, "/")) || "global";
1419
+ proj = path3.basename(anchor.cwd.replace(/\\/g, "/")) || "global";
828
1420
  }
829
1421
  if (anchor.files && anchor.files.length > 0) {
830
- const shortFile = path2.basename(anchor.files[0].replace(/\\/g, "/"));
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
- const fg = theme?.fg ? theme.fg.bind(theme) : (_c, text) => text;
871
- if (sorted.length === 1) {
872
- const a = sorted[0];
873
- const num = a.id.replace(/^anc-/, "");
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: ${path2.basename(anchor.sessionFile)}` : null,
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") return;
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
- store.touch(m.anchor.id, Date.now(), currentSessionFile);
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,