pi-anchor 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,14 +2,14 @@
2
2
 
3
3
  > **A digital sticky note docked below your Pi editor — 0ms instant stick, micro progress bars `[■■□□ 2/4]`, 1-click session teleport (`↪`), and automatic teardown on `git commit`.**
4
4
 
5
+ [![npm version](https://img.shields.io/npm/v/pi-anchor?color=blue)](https://www.npmjs.com/package/pi-anchor)
6
+ [![npm downloads](https://img.shields.io/npm/dm/pi-anchor?color=22c55e&label=downloads)](https://www.npmjs.com/package/pi-anchor)
7
+ [![GitHub stars](https://img.shields.io/github/stars/3ZEROS12/anchor?style=flat&color=f59e0b)](https://github.com/3ZEROS12/anchor/stargazers)
5
8
  [![CI Status](https://github.com/3ZEROS12/anchor/actions/workflows/ci.yml/badge.svg)](https://github.com/3ZEROS12/anchor/actions/workflows/ci.yml)
6
- [![Node v20+](https://img.shields.io/badge/Node-v20+-22c55e.svg)](package.json)
7
- [![TypeScript Strict](https://img.shields.io/badge/TypeScript-Strict-3b82f6.svg)](tsconfig.json)
8
9
  [![Tests: 10 Passed](https://img.shields.io/badge/Tests-10%20Passed-22c55e.svg)](tests/store.test.ts)
9
- [![Startup: < 45ms](https://img.shields.io/badge/Startup-%3C%2045ms-success.svg)](package.json)
10
10
  [![License: MIT](https://img.shields.io/badge/License-MIT-f97316.svg)](LICENSE)
11
11
 
12
- **English** | [简体中文](./README_zh.md)
12
+ **English** | [日本語](./README_ja.md) | [简体中文](./README_zh.md)
13
13
 
14
14
  ```bash
15
15
  pi install npm:pi-anchor
@@ -117,6 +117,19 @@ npm run build # Dual ESM/CJS compilation via tsup + declaration emit
117
117
 
118
118
  ---
119
119
 
120
+ ## Ecosystem: Pi Terminal Suite
121
+
122
+ Each extension occupies a distinct terminal slot (`aboveEditor`, `belowEditor`, `status-only` footer, and runtime tool hooks) so all four run together with zero widget overlap:
123
+
124
+ | Package | Terminal Slot & Mechanism | Install |
125
+ | :--- | :--- | :--- |
126
+ | **[`pi-lingual`](https://github.com/3ZEROS12/pi-lingual)** | `二 ⇄ two` `aboveEditor` HUD (≤9 lines, UAX #11 tree layout) & prompt-to-English transformer (0 chat history tokens) | `pi install npm:pi-lingual` |
127
+ | **[`toolflow`](https://github.com/3ZEROS12/toolflow)** | `⌬` Runtime tool hooks — stage-gated MCP mounting & >40-line log dehydration to disk (-95% log tokens) | `pi install npm:toolflow` |
128
+ | **[`pi-tui-status-beautifier`](https://github.com/3ZEROS12/pi-tui-status-beautifier)** | `status-only` footer hook — 6 badge presets, `<80`-col idle folding `(+N idle)`, 0 render-loop disk reads | `pi install npm:pi-tui-status-beautifier` |
129
+ | **[`pi-anchor`](https://github.com/3ZEROS12/anchor)** *(Current)* | `⌖` `belowEditor` 1-line sticky strip, 0ms `pin:` intercept, `[■■□□]` step bar & `↪` session jump | `pi install npm:pi-anchor` |
130
+
131
+ ---
132
+
120
133
  ## License
121
134
 
122
135
  MIT © [Jason Song](https://github.com/3ZEROS12)
package/README_ja.md ADDED
@@ -0,0 +1,90 @@
1
+ # ⌖ Anchor
2
+
3
+ > **Pi エディタ直下に常駐するデジタル付箋(Sticky Note)拡張機能。**
4
+ > `pin: ...` で **0ms 即時貼り付け**、エディタ直下(`belowEditor`)に 1 行で常駐表示(`aboveEditor` 拡張と干渉ゼロ)、長期タスク用の**ミニ進捗バー `[■■□□ 2/4]`**、**元の会話セッションへのワンタッチ復帰(`↪`)**、および **`git commit` 時の自動剥がし**に対応。
5
+
6
+ [![npm version](https://img.shields.io/npm/v/pi-anchor?color=blue)](https://www.npmjs.com/package/pi-anchor)
7
+ [![npm downloads](https://img.shields.io/npm/dm/pi-anchor?color=22c55e&label=downloads)](https://www.npmjs.com/package/pi-anchor)
8
+ [![GitHub stars](https://img.shields.io/github/stars/3ZEROS12/anchor?style=flat&color=f59e0b)](https://github.com/3ZEROS12/anchor/stargazers)
9
+ [![CI Status](https://github.com/3ZEROS12/anchor/actions/workflows/ci.yml/badge.svg)](https://github.com/3ZEROS12/anchor/actions/workflows/ci.yml)
10
+ [![Tests](https://img.shields.io/badge/Tests-10%2F10%20Pass%20(100%25)-brightgreen)](tests/store.test.ts)
11
+ [![License: MIT](https://img.shields.io/badge/License-MIT-f97316.svg)](LICENSE)
12
+
13
+ [English](./README.md) | **日本語** | [简体中文](./README_zh.md)
14
+
15
+ ```bash
16
+ pi install npm:pi-anchor
17
+ ```
18
+
19
+ <p align="center">
20
+ <img src="assets/hero-en.svg" alt="Anchor Sticky Notes" width="820">
21
+ </p>
22
+
23
+ ```text
24
+ ╭─ Pi Editor ──────────────────────────────────────────────────────────────────────╮
25
+ │ ❯ pin: Refactor auth flow: 1. Extract interface 2. HttpOnly cookie 3. Unit tests │
26
+ ╰──────────────────────────────────────────────────────────────────────────────────╯
27
+ ⌖ #1 [■■□□ 2/4] Refactor auth flow ▸ Unit tests (app · auth.ts ↪)
28
+ ```
29
+
30
+ ---
31
+
32
+ ## なぜ Anchor なのか?
33
+
34
+ コーディング中にふと *「この後 `src/gateway.ts` のレート制限フォールバックも確認しないと」* と思いついたときや、数日かかるタスクを夜に中断するとき:
35
+ - リポジトリ内に `TODO.md` を作ると `git status` が汚れ、毎ターンのプロンプトで無駄なトークンを消費します。
36
+ - 起動時だけのバナー表示は、最初のメッセージを送信した瞬間に消えてしまい、書いた内容を忘れてしまいます。
37
+
38
+ **Anchor は Pi の入力欄のすぐ下(`belowEditor`)に貼る本物のデジタル付箋です。** リポジトリ外の `~/.anchor/state.json` に保存され(全 4 ソースファイル・外部依存ゼロ)、人間の手になじむ 4 つの工夫を備えています:
39
+
40
+ ### 1. エディタ直下に 1 行常駐(`belowEditor`)& 0ms 即時貼り付け(`pin: ...`)
41
+ - **ウィジェット干渉ゼロ**:付箋バーはエディタ**直下(`placement: "belowEditor"`)**に 1 行で配置されるため、エディタ上部(`aboveEditor`)を `pi-lingual` などのツールに 100% 空けておけます。
42
+ - **0ms ダイレクト入力**:入力欄で直接 `pin: トークン有効期限を確認`(または `note: ...`、`/pin ...`)と打つだけ。LLM 呼び出しやトークン消費なしで **0.00ms** で即座に貼り付けられ、直前に編集していたファイルが自動リンクされます。
43
+
44
+ ### 2. 長期タスク用のミニ進捗バー(`[■■□□ 2/4]`)
45
+ - メモの中に比率(`[2/4]` や `5/30`)または番号付きステップ(`1. Extract interface 2. HttpOnly cookie 3. Unit tests`)を書くだけ:
46
+ ```text
47
+ /pin Refactor auth flow: 1. Extract interface 2. HttpOnly cookie 3. Unit tests
48
+ ```
49
+ - 付箋バーにミニ進捗バーと次のステップ(`[■■□□ 2/3] ▸ Unit tests`)が表示されます。`/step 1`(または `/anchor` メニューの `▶ Advance step`)で 1 ステップ進みます。単発のメモには進捗バーは表示されません。
50
+
51
+ ### 3. 会話セッションのブックマーク(`↪ Jump to session`)
52
+ - 各付箋は、作成時や関連ファイル編集時の Pi 会話セッション(`sessionFile`)を自動的にブックマークします。
53
+ - 数日後に別ウィンドウから作業を再開したいときも、`/anchor` を開いて **`↪ Jump to session`** を選ぶ(または `/anchor jump 1` を実行する)だけで、当時の会話履歴そのものに直接復帰できます。
54
+
55
+ ### 4. ファイル接触時の 1 行メモ注入 & `git commit` 自動剥がし
56
+ - **ファイル接触時の 1 行メモ(待機時 0 トークン)**:Turn 1 で一度だけ AI に概要を伝え、Turn 2 以降は **0 トークン**。AI が関連ファイルを読み書きした際、結果の末尾に構文安全な 1 行コメント(`// ⌖ Note #1: ...`)を添えます(`.json` や `.env` は厳格にスキップ)。
57
+ - **コミットで自動剥がし**:`git commit -m "..."` のメッセージが付箋内容と一致すると、自動的に付箋を剥がしてアーカイブします(`⌖ Auto-torn off #1`)。誤って剥がした場合は `/anchor undo` でいつでも戻せます。
58
+
59
+ ---
60
+
61
+ ## コマンド一覧
62
+
63
+ | 場所 | 入力 / コマンド | 動作 |
64
+ | :--- | :--- | :--- |
65
+ | **Pi エディタ** | `pin: <メモ>` または `note: <メモ>` | LLM を呼ばずに **0ms** で付箋を貼り付け |
66
+ | **スラッシュコマンド** | `/pin <メモ>` | 新しい付箋を貼る(`[1/4]` や `1. A 2. B` で進捗バー自動生成) |
67
+ | **スラッシュコマンド** | `/anchor` | 対話ボードを開く(剥がす、進捗 `+1`、`↪` セッション復帰) |
68
+ | **スラッシュコマンド** | `/step 1` | 付箋 `#1` の進捗バーを `+1` 進める |
69
+ | **スラッシュコマンド** | `/anchor jump 1` | 付箋 `#1` に紐づく元の会話セッションへ切り替え |
70
+ | **スラッシュコマンド** | `/done 1` / `/anchor undo` | 付箋 `#1` を剥がす / 直前に剥がした付箋を貼り直す |
71
+ | **ターミナル CLI** | `anchor` / `anchor step 1` / `anchor done 1` | Pi の外から付箋の一覧確認・進捗更新・完了を実行 |
72
+
73
+ ---
74
+
75
+ ## Ecosystem: Pi Terminal Suite
76
+
77
+ 4 つの拡張機能はそれぞれ異なるターミナルスロット(`aboveEditor`、`belowEditor`、`status-only` フッター、実行時ツールフック)にマウントされるため、単体でも全種併用でも UI が干渉しません:
78
+
79
+ | パッケージ | ターミナル配置スロットと機構 | インストール |
80
+ | :--- | :--- | :--- |
81
+ | **[`pi-lingual`](https://github.com/3ZEROS12/pi-lingual)** | `二 ⇄ two` `aboveEditor` 二言語 HUD(最大 9 行・UAX #11 ツリーレイアウト)&英語プロンプト変換(履歴トークン消費 0) | `pi install npm:pi-lingual` |
82
+ | **[`toolflow`](https://github.com/3ZEROS12/toolflow)** | `⌬` 実行時ツールフック —— フェーズ別 MCP マウント&40 行超のログ自動ディスク退避(ログトークン 95% 削減) | `pi install npm:toolflow` |
83
+ | **[`pi-tui-status-beautifier`](https://github.com/3ZEROS12/pi-tui-status-beautifier)** | `status-only` フッターフック —— 6 種のバッジプリセット、`<80` 列での待機バッジ折りたたみ `(+N idle)`、描画ループ内ディスク I/O ゼロ | `pi install npm:pi-tui-status-beautifier` |
84
+ | **[`pi-anchor`](https://github.com/3ZEROS12/anchor)** *(現在)* | `⌖` `belowEditor` 1 行常駐付箋バー、0ms `pin:` インターセプト、ミニ進捗バー `[■■□□]` & `↪` セッション復帰 | `pi install npm:pi-anchor` |
85
+
86
+ ---
87
+
88
+ ## ライセンス
89
+
90
+ MIT © [Jason Song](https://github.com/3ZEROS12)
package/README_zh.md CHANGED
@@ -2,14 +2,14 @@
2
2
 
3
3
  > **贴在 Pi 输入框正下方的电子便签 —— `便签: ...` 0ms 瞬贴上屏、长线任务迷你进度条 `[■■□□ 2/4]`、一键切回原对话上下文 (`↪`),`git commit` 提交代码自动撕便签。**
4
4
 
5
+ [![npm version](https://img.shields.io/npm/v/pi-anchor?color=blue)](https://www.npmjs.com/package/pi-anchor)
6
+ [![npm downloads](https://img.shields.io/npm/dm/pi-anchor?color=22c55e&label=downloads)](https://www.npmjs.com/package/pi-anchor)
7
+ [![GitHub stars](https://img.shields.io/github/stars/3ZEROS12/anchor?style=flat&color=f59e0b)](https://github.com/3ZEROS12/anchor/stargazers)
5
8
  [![CI 状态](https://github.com/3ZEROS12/anchor/actions/workflows/ci.yml/badge.svg)](https://github.com/3ZEROS12/anchor/actions/workflows/ci.yml)
6
- [![Node v20+](https://img.shields.io/badge/Node-v20+-22c55e.svg)](package.json)
7
- [![TypeScript Strict](https://img.shields.io/badge/TypeScript-Strict-3b82f6.svg)](tsconfig.json)
8
9
  [![Tests: 10 Passed](https://img.shields.io/badge/Tests-10%20通过-22c55e.svg)](tests/store.test.ts)
9
- [![启动耗时: < 45ms](https://img.shields.io/badge/启动耗时-%3C%2045ms-success.svg)](package.json)
10
10
  [![开源协议: MIT](https://img.shields.io/badge/开源协议-MIT-f97316.svg)](LICENSE)
11
11
 
12
- [English](./README.md) | **简体中文**
12
+ [English](./README.md) | [日本語](./README_ja.md) | **简体中文**
13
13
 
14
14
  ```bash
15
15
  pi install npm:pi-anchor
@@ -117,6 +117,19 @@ npm run build # tsup 双格式打包与类型声明生成
117
117
 
118
118
  ---
119
119
 
120
+ ## 配套生态套件 (Pi Terminal Suite)
121
+
122
+ 四款扩展分别挂载于终端的不同物理槽位(`aboveEditor`、`belowEditor`、`status-only` 底栏与运行时工具钩子),独立运行或组合安装均不产生小部件重叠:
123
+
124
+ | 插件仓库 | 终端挂载槽位与核心机制 | 一键安装 |
125
+ | :--- | :--- | :--- |
126
+ | **[`pi-lingual`](https://github.com/3ZEROS12/pi-lingual)** | `二 ⇄ two` `aboveEditor` 双语伴学视窗(≤9 行 UAX #11 树状排版)与母语提示词转写(0 会话历史 Token 注入) | `pi install npm:pi-lingual` |
127
+ | **[`toolflow`](https://github.com/3ZEROS12/toolflow)** | `⌬` 运行时工具钩子 —— 按阶段动态挂载重型 MCP,>40 行终端长日志自动落盘脱水(削减 95% 日志 Token) | `pi install npm:toolflow` |
128
+ | **[`pi-tui-status-beautifier`](https://github.com/3ZEROS12/pi-tui-status-beautifier)** | `status-only` 底栏微钩子 —— 6 套徽章预设、`<80` 列窄屏折叠 `(+N idle)`、重绘循环 0 磁盘读取 | `pi install npm:pi-tui-status-beautifier` |
129
+ | **[`pi-anchor`](https://github.com/3ZEROS12/anchor)** *(当前)* | `⌖` `belowEditor` 1 行常驻便签条、0ms `pin:` 输入拦截、迷你进度条 `[■■□□]` 与原会话跳转 `↪` | `pi install npm:pi-anchor` |
130
+
131
+ ---
132
+
120
133
  ## License
121
134
 
122
135
  MIT © [Jason Song](https://github.com/3ZEROS12)
@@ -258,10 +258,12 @@ function renderColdStartAnchorsContext(store, cwdOrNow, nowArg) {
258
258
  if (activeNotes.length === 0) return "";
259
259
  const lines = activeNotes.map((a) => {
260
260
  const num = a.id.replace(/^anc-/, "");
261
+ const tag = a.source === "todo-sync" ? `[Todo Checkpoint \xB7 ${a.project}]` : `[${a.project}]`;
261
262
  const files = a.files.length > 0 ? ` (${a.files.slice(0, 2).join(", ")})` : "";
262
- const prog = a.progress ? ` [${a.progress.done}/${a.progress.total}${a.progress.steps && a.progress.steps[a.progress.done] ? ` \xB7 next: ${a.progress.steps[a.progress.done]}` : ""}]` : "";
263
+ const nextStep = a.progress?.steps && a.progress.steps[a.progress.done] && a.progress.steps[a.progress.done] !== a.title ? ` \xB7 current/next: ${a.progress.steps[a.progress.done]}` : "";
264
+ const prog = a.progress ? ` [${a.progress.done}/${a.progress.total}${nextStep}]` : "";
263
265
  const desc = a.description ? ` \u2014 ${a.description}` : "";
264
- return `\u2022 #${num} [${a.project}] ${a.title}${prog}${files}${desc}`;
266
+ return `\u2022 #${num} ${tag} ${a.title}${prog}${files}${desc}`;
265
267
  });
266
268
  return [
267
269
  `<sticky-notes count="${activeNotes.length}">`,
@@ -453,6 +455,7 @@ var AnchorStore = class {
453
455
  description: input.description?.trim(),
454
456
  priority: input.priority || "p1",
455
457
  status: "active",
458
+ ...input.source ? { source: input.source } : {},
456
459
  project: projectName,
457
460
  cwd,
458
461
  createdAt: now,
@@ -560,6 +563,97 @@ var AnchorStore = class {
560
563
  return { anchor: current, completed: nextDone >= total };
561
564
  });
562
565
  }
566
+ /**
567
+ * Clever Touch 4 (Todo-Bridge): Automatically mirror intra-session `todo` tool lists
568
+ * into a cross-session checkpoint!
569
+ * - While tasks are in_progress/pending: updates the live `[■■□□ done/total]` checkpoint on disk;
570
+ * - When all tasks in the todo list reach `completed`: automatically tears off the checkpoint!
571
+ */
572
+ syncFromTodoTool(tasks, options = {}) {
573
+ if (!Array.isArray(tasks)) return { status: "noop" };
574
+ const visible = tasks.filter((t) => t && typeof t.subject === "string" && t.status !== "deleted");
575
+ const normCwd = options.cwd ? normalizePath(options.cwd) : "";
576
+ const normSession = options.sessionFile ? normalizePath(options.sessionFile) : void 0;
577
+ return this.withLock(() => {
578
+ const state = this.loadState();
579
+ const existingIdx = state.anchors.findIndex(
580
+ (a) => a.source === "todo-sync" && (normSession && a.sessionFile === normSession || normCwd && isSameOrSubPath(a.cwd, normCwd))
581
+ );
582
+ const completedTasks = visible.filter((t) => t.status === "completed");
583
+ const unfinishedTasks = visible.filter((t) => t.status !== "completed");
584
+ if (visible.length === 0 || unfinishedTasks.length === 0) {
585
+ if (existingIdx !== -1) {
586
+ const [settled] = state.anchors.splice(existingIdx, 1);
587
+ settled.status = "settled";
588
+ settled.updatedAt = Date.now();
589
+ settled.evidence = {
590
+ settledAt: Date.now(),
591
+ settledBy: "todo-complete",
592
+ summary: `All ${completedTasks.length} session todo(s) completed`
593
+ };
594
+ this.ensureDirs();
595
+ fs.appendFileSync(this.archivePath, JSON.stringify(settled) + "\n", "utf-8");
596
+ this.saveState(state);
597
+ return { status: "completed", anchor: settled };
598
+ }
599
+ return { status: "noop" };
600
+ }
601
+ const inProgress = unfinishedTasks.find((t) => t.status === "in_progress") || unfinishedTasks[0];
602
+ const orderedSteps = [
603
+ ...completedTasks.map((t) => t.subject.trim()),
604
+ ...unfinishedTasks.filter((t) => t.status === "in_progress").map((t) => t.subject.trim()),
605
+ ...unfinishedTasks.filter((t) => t.status === "pending").map((t) => t.subject.trim())
606
+ ];
607
+ const progress = {
608
+ done: completedTasks.length,
609
+ total: visible.length,
610
+ steps: orderedSteps
611
+ };
612
+ const remainingSummary = unfinishedTasks.slice(0, 3).map((t) => t.subject.trim()).join(" \u2192 ");
613
+ const headline = inProgress.subject.trim();
614
+ const now = Date.now();
615
+ if (existingIdx !== -1) {
616
+ const current = state.anchors[existingIdx];
617
+ current.title = headline;
618
+ current.description = `Remaining (${unfinishedTasks.length}/${visible.length}): ${remainingSummary}`;
619
+ current.progress = progress;
620
+ current.status = "active";
621
+ current.updatedAt = now;
622
+ current.lastTouchedAt = now;
623
+ if (normSession) current.sessionFile = normSession;
624
+ if (options.recentFiles && options.recentFiles.length > 0) {
625
+ const merged = Array.from(/* @__PURE__ */ new Set([...options.recentFiles.map((f) => normalizePath(f)), ...current.files])).slice(0, 5);
626
+ current.files = merged;
627
+ }
628
+ state.anchors[existingIdx] = current;
629
+ this.saveState(state);
630
+ return { status: "synced", anchor: current };
631
+ }
632
+ const id = this.generateId(state);
633
+ const inferredFiles = options.recentFiles && options.recentFiles.length > 0 ? options.recentFiles.map((f) => normalizePath(f)).filter(Boolean) : extractPathsFromText(orderedSteps.join(" "));
634
+ const projectName = normCwd ? path.basename(normCwd) : "global";
635
+ const created = {
636
+ id,
637
+ title: headline,
638
+ description: `Remaining (${unfinishedTasks.length}/${visible.length}): ${remainingSummary}`,
639
+ priority: "p1",
640
+ status: "active",
641
+ source: "todo-sync",
642
+ project: projectName,
643
+ cwd: normCwd,
644
+ createdAt: now,
645
+ updatedAt: now,
646
+ lastTouchedAt: now,
647
+ files: inferredFiles,
648
+ tags: ["todo-sync"],
649
+ ...normSession ? { sessionFile: normSession } : {},
650
+ progress
651
+ };
652
+ state.anchors.push(created);
653
+ this.saveState(state);
654
+ return { status: "synced", anchor: created };
655
+ });
656
+ }
563
657
  /**
564
658
  * Tear off (settle) a completed sticky note and archive it to archive.jsonl
565
659
  */
@@ -778,7 +872,8 @@ function updateStartupBanner(ctx, store) {
778
872
  const a = sorted[0];
779
873
  const num = a.id.replace(/^anc-/, "");
780
874
  const bar = a.progress ? `${fg("accent", formatProgressBar(a.progress))} ` : "";
781
- const nextStep = a.progress?.steps && a.progress.steps[a.progress.done] ? fg("muted", ` \u25B8 ${a.progress.steps[a.progress.done]}`) : "";
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}`) : "";
782
877
  const origin = formatOrigin(a);
783
878
  const sessionIcon = a.sessionFile ? " \u21AA" : "";
784
879
  const prefix = `${fg("accent", "\u2316")} ${fg("dim", `#${num}`)} ${bar}`;
@@ -1029,6 +1124,20 @@ ${snippet}` };
1029
1124
  if (event.isError) return;
1030
1125
  const lowerTool = String(event.toolName || "").toLowerCase();
1031
1126
  const currentSessionFile = getSessionFileSafe(ctx);
1127
+ if (lowerTool === "todo") {
1128
+ const tasks = event?.details?.tasks;
1129
+ if (Array.isArray(tasks)) {
1130
+ const syncRes = store.syncFromTodoTool(tasks, {
1131
+ cwd: ctx.cwd,
1132
+ sessionFile: currentSessionFile,
1133
+ recentFiles: recentSessionFiles
1134
+ });
1135
+ if (syncRes.status !== "noop") {
1136
+ updateAnchorStatusBar(ctx, store);
1137
+ }
1138
+ }
1139
+ return;
1140
+ }
1032
1141
  if (lowerTool === "bash" || lowerTool === "powershell") {
1033
1142
  const cmd = String(event.input?.command || "");
1034
1143
  if (cmd.includes("git commit")) {
@@ -1324,6 +1433,35 @@ ${snippet}` };
1324
1433
  await openAnchorDashboard(ctx, store);
1325
1434
  }
1326
1435
  });
1436
+ pi.registerCommand("back", {
1437
+ description: "Jump back to the conversation session of the latest unfinished note or todo checkpoint (/back [id])",
1438
+ handler: async (args, ctx) => {
1439
+ const targetArg = (args || "").trim();
1440
+ const currentSession = getSessionFileSafe(ctx);
1441
+ if (targetArg) {
1442
+ const resolvedId = resolveAnchorTarget(store, targetArg, ctx.cwd);
1443
+ const note = resolvedId ? store.get(resolvedId) : void 0;
1444
+ if (!note || !note.sessionFile || !fs3.existsSync(note.sessionFile)) {
1445
+ ctx.ui.notify(`\u2316 No saved session found for #${targetArg}`, "error");
1446
+ return;
1447
+ }
1448
+ await ctx.switchSession(note.sessionFile);
1449
+ return;
1450
+ }
1451
+ const candidates = sortAnchorsForDisplay(
1452
+ store.list({ cwd: ctx.cwd, all: true }).filter((a) => a.status === "active" || a.status === "sleeping"),
1453
+ ctx.cwd
1454
+ );
1455
+ const jumpTarget = candidates.find(
1456
+ (a) => a.sessionFile && fs3.existsSync(a.sessionFile) && (!currentSession || !isSameOrSubPath(a.sessionFile, currentSession))
1457
+ );
1458
+ if (!jumpTarget || !jumpTarget.sessionFile) {
1459
+ ctx.ui.notify("\u2316 No other linked conversation session found to jump back to.", "info");
1460
+ return;
1461
+ }
1462
+ await ctx.switchSession(jumpTarget.sessionFile);
1463
+ }
1464
+ });
1327
1465
  }
1328
1466
 
1329
1467
  export {
@@ -16,11 +16,18 @@ interface AnchorProgress {
16
16
  }
17
17
  interface AnchorEvidence {
18
18
  settledAt?: number;
19
- settledBy?: 'commit' | 'manual' | 'one-tap-settlement' | 'manual-command' | 'verification-test';
19
+ settledBy?: 'commit' | 'manual' | 'todo-complete' | 'one-tap-settlement' | 'manual-command' | 'verification-test';
20
20
  commitHash?: string;
21
21
  summary?: string;
22
22
  touchedFiles?: string[];
23
23
  }
24
+ interface TodoTaskItem {
25
+ id: number;
26
+ subject: string;
27
+ status: 'pending' | 'in_progress' | 'completed' | 'deleted';
28
+ activeForm?: string;
29
+ description?: string;
30
+ }
24
31
  interface Anchor {
25
32
  /** Sequential identifier, e.g. 'anc-1' */
26
33
  id: string;
@@ -30,6 +37,8 @@ interface Anchor {
30
37
  description?: string;
31
38
  /** Lifecycle status: 'active' (recent) or 'sleeping' (>7d untouched, 0 prompt tokens) */
32
39
  status: AnchorStatus;
40
+ /** Origin source: 'manual' (user/tool pinned) or 'todo-sync' (auto-bridged from unfinished session todos) */
41
+ source?: 'manual' | 'todo-sync';
33
42
  /** Short workspace label (e.g. 'anchor', 'College/优化', or 'global') */
34
43
  project: string;
35
44
  /** Workspace directory path where the note applies */
@@ -153,6 +162,7 @@ declare class AnchorStore {
153
162
  title: string;
154
163
  description?: string;
155
164
  priority?: AnchorPriority;
165
+ source?: 'manual' | 'todo-sync';
156
166
  cwd?: string;
157
167
  project?: string;
158
168
  files?: string[];
@@ -178,6 +188,20 @@ declare class AnchorStore {
178
188
  anchor: Anchor;
179
189
  completed: boolean;
180
190
  };
191
+ /**
192
+ * Clever Touch 4 (Todo-Bridge): Automatically mirror intra-session `todo` tool lists
193
+ * into a cross-session checkpoint!
194
+ * - While tasks are in_progress/pending: updates the live `[■■□□ done/total]` checkpoint on disk;
195
+ * - When all tasks in the todo list reach `completed`: automatically tears off the checkpoint!
196
+ */
197
+ syncFromTodoTool(tasks: TodoTaskItem[], options?: {
198
+ cwd?: string;
199
+ sessionFile?: string;
200
+ recentFiles?: string[];
201
+ }): {
202
+ status: 'synced' | 'completed' | 'noop';
203
+ anchor?: Anchor;
204
+ };
181
205
  /**
182
206
  * Tear off (settle) a completed sticky note and archive it to archive.jsonl
183
207
  */
@@ -207,4 +231,4 @@ declare function parseQuickNoteInput(text: string): string | null;
207
231
  declare function resolveAnchorTarget(store: AnchorStore, idOrIndex: string, cwd?: string): string | undefined;
208
232
  declare function export_default(pi: ExtensionAPI): void;
209
233
 
210
- export { type Anchor as A, COMMENT_FORMATS as C, MS_PER_DAY as M, QUIET_AFTER_DAYS as Q, type TouchMatchResult as T, type AnchorProgress as a, AnchorStore as b, type AnchorEvidence as c, type AnchorPriority as d, type AnchorStatus as e, type AnchorStoreState as f, acquireSyncLock as g, atomicRenameWithRetry as h, extractPathsFromText as i, extractProgressFromText as j, findMatchedAnchors as k, getDefaultStorageDir as l, globToRegExp as m, isAnchorTargetingWorkspace as n, isSameOrSubPath as o, makeSafeTaskAnnotation as p, matchAnchorAgainstTouchedFiles as q, matchesCommitMessage as r, normalizePath as s, parseAnchorCommandInput as t, parseQuickNoteInput as u, renderActiveAnchorsContext as v, renderColdStartAnchorsContext as w, resolveAnchorTarget as x, sweepQuietNotes as y, export_default as z };
234
+ export { type Anchor as A, export_default as B, COMMENT_FORMATS as C, MS_PER_DAY as M, QUIET_AFTER_DAYS as Q, type TodoTaskItem as T, type AnchorProgress as a, AnchorStore as b, type AnchorEvidence as c, type AnchorPriority as d, type AnchorStatus as e, type AnchorStoreState as f, type TouchMatchResult as g, acquireSyncLock as h, atomicRenameWithRetry as i, extractPathsFromText as j, extractProgressFromText as k, findMatchedAnchors as l, getDefaultStorageDir as m, globToRegExp as n, isAnchorTargetingWorkspace as o, isSameOrSubPath as p, makeSafeTaskAnnotation as q, matchAnchorAgainstTouchedFiles as r, matchesCommitMessage as s, normalizePath as t, parseAnchorCommandInput as u, parseQuickNoteInput as v, renderActiveAnchorsContext as w, renderColdStartAnchorsContext as x, resolveAnchorTarget as y, sweepQuietNotes as z };
@@ -16,11 +16,18 @@ interface AnchorProgress {
16
16
  }
17
17
  interface AnchorEvidence {
18
18
  settledAt?: number;
19
- settledBy?: 'commit' | 'manual' | 'one-tap-settlement' | 'manual-command' | 'verification-test';
19
+ settledBy?: 'commit' | 'manual' | 'todo-complete' | 'one-tap-settlement' | 'manual-command' | 'verification-test';
20
20
  commitHash?: string;
21
21
  summary?: string;
22
22
  touchedFiles?: string[];
23
23
  }
24
+ interface TodoTaskItem {
25
+ id: number;
26
+ subject: string;
27
+ status: 'pending' | 'in_progress' | 'completed' | 'deleted';
28
+ activeForm?: string;
29
+ description?: string;
30
+ }
24
31
  interface Anchor {
25
32
  /** Sequential identifier, e.g. 'anc-1' */
26
33
  id: string;
@@ -30,6 +37,8 @@ interface Anchor {
30
37
  description?: string;
31
38
  /** Lifecycle status: 'active' (recent) or 'sleeping' (>7d untouched, 0 prompt tokens) */
32
39
  status: AnchorStatus;
40
+ /** Origin source: 'manual' (user/tool pinned) or 'todo-sync' (auto-bridged from unfinished session todos) */
41
+ source?: 'manual' | 'todo-sync';
33
42
  /** Short workspace label (e.g. 'anchor', 'College/优化', or 'global') */
34
43
  project: string;
35
44
  /** Workspace directory path where the note applies */
@@ -153,6 +162,7 @@ declare class AnchorStore {
153
162
  title: string;
154
163
  description?: string;
155
164
  priority?: AnchorPriority;
165
+ source?: 'manual' | 'todo-sync';
156
166
  cwd?: string;
157
167
  project?: string;
158
168
  files?: string[];
@@ -178,6 +188,20 @@ declare class AnchorStore {
178
188
  anchor: Anchor;
179
189
  completed: boolean;
180
190
  };
191
+ /**
192
+ * Clever Touch 4 (Todo-Bridge): Automatically mirror intra-session `todo` tool lists
193
+ * into a cross-session checkpoint!
194
+ * - While tasks are in_progress/pending: updates the live `[■■□□ done/total]` checkpoint on disk;
195
+ * - When all tasks in the todo list reach `completed`: automatically tears off the checkpoint!
196
+ */
197
+ syncFromTodoTool(tasks: TodoTaskItem[], options?: {
198
+ cwd?: string;
199
+ sessionFile?: string;
200
+ recentFiles?: string[];
201
+ }): {
202
+ status: 'synced' | 'completed' | 'noop';
203
+ anchor?: Anchor;
204
+ };
181
205
  /**
182
206
  * Tear off (settle) a completed sticky note and archive it to archive.jsonl
183
207
  */
@@ -207,4 +231,4 @@ declare function parseQuickNoteInput(text: string): string | null;
207
231
  declare function resolveAnchorTarget(store: AnchorStore, idOrIndex: string, cwd?: string): string | undefined;
208
232
  declare function export_default(pi: ExtensionAPI): void;
209
233
 
210
- export { type Anchor as A, COMMENT_FORMATS as C, MS_PER_DAY as M, QUIET_AFTER_DAYS as Q, type TouchMatchResult as T, type AnchorProgress as a, AnchorStore as b, type AnchorEvidence as c, type AnchorPriority as d, type AnchorStatus as e, type AnchorStoreState as f, acquireSyncLock as g, atomicRenameWithRetry as h, extractPathsFromText as i, extractProgressFromText as j, findMatchedAnchors as k, getDefaultStorageDir as l, globToRegExp as m, isAnchorTargetingWorkspace as n, isSameOrSubPath as o, makeSafeTaskAnnotation as p, matchAnchorAgainstTouchedFiles as q, matchesCommitMessage as r, normalizePath as s, parseAnchorCommandInput as t, parseQuickNoteInput as u, renderActiveAnchorsContext as v, renderColdStartAnchorsContext as w, resolveAnchorTarget as x, sweepQuietNotes as y, export_default as z };
234
+ export { type Anchor as A, export_default as B, COMMENT_FORMATS as C, MS_PER_DAY as M, QUIET_AFTER_DAYS as Q, type TodoTaskItem as T, type AnchorProgress as a, AnchorStore as b, type AnchorEvidence as c, type AnchorPriority as d, type AnchorStatus as e, type AnchorStoreState as f, type TouchMatchResult as g, acquireSyncLock as h, atomicRenameWithRetry as i, extractPathsFromText as j, extractProgressFromText as k, findMatchedAnchors as l, getDefaultStorageDir as m, globToRegExp as n, isAnchorTargetingWorkspace as o, isSameOrSubPath as p, makeSafeTaskAnnotation as q, matchAnchorAgainstTouchedFiles as r, matchesCommitMessage as s, normalizePath as t, parseAnchorCommandInput as u, parseQuickNoteInput as v, renderActiveAnchorsContext as w, renderColdStartAnchorsContext as x, resolveAnchorTarget as y, sweepQuietNotes as z };
@@ -295,10 +295,12 @@ function renderColdStartAnchorsContext(store, cwdOrNow, nowArg) {
295
295
  if (activeNotes.length === 0) return "";
296
296
  const lines = activeNotes.map((a) => {
297
297
  const num = a.id.replace(/^anc-/, "");
298
+ const tag = a.source === "todo-sync" ? `[Todo Checkpoint \xB7 ${a.project}]` : `[${a.project}]`;
298
299
  const files = a.files.length > 0 ? ` (${a.files.slice(0, 2).join(", ")})` : "";
299
- const prog = a.progress ? ` [${a.progress.done}/${a.progress.total}${a.progress.steps && a.progress.steps[a.progress.done] ? ` \xB7 next: ${a.progress.steps[a.progress.done]}` : ""}]` : "";
300
+ const nextStep = a.progress?.steps && a.progress.steps[a.progress.done] && a.progress.steps[a.progress.done] !== a.title ? ` \xB7 current/next: ${a.progress.steps[a.progress.done]}` : "";
301
+ const prog = a.progress ? ` [${a.progress.done}/${a.progress.total}${nextStep}]` : "";
300
302
  const desc = a.description ? ` \u2014 ${a.description}` : "";
301
- return `\u2022 #${num} [${a.project}] ${a.title}${prog}${files}${desc}`;
303
+ return `\u2022 #${num} ${tag} ${a.title}${prog}${files}${desc}`;
302
304
  });
303
305
  return [
304
306
  `<sticky-notes count="${activeNotes.length}">`,
@@ -489,6 +491,7 @@ var AnchorStore = class {
489
491
  description: input.description?.trim(),
490
492
  priority: input.priority || "p1",
491
493
  status: "active",
494
+ ...input.source ? { source: input.source } : {},
492
495
  project: projectName,
493
496
  cwd,
494
497
  createdAt: now,
@@ -596,6 +599,97 @@ var AnchorStore = class {
596
599
  return { anchor: current, completed: nextDone >= total };
597
600
  });
598
601
  }
602
+ /**
603
+ * Clever Touch 4 (Todo-Bridge): Automatically mirror intra-session `todo` tool lists
604
+ * into a cross-session checkpoint!
605
+ * - While tasks are in_progress/pending: updates the live `[■■□□ done/total]` checkpoint on disk;
606
+ * - When all tasks in the todo list reach `completed`: automatically tears off the checkpoint!
607
+ */
608
+ syncFromTodoTool(tasks, options = {}) {
609
+ if (!Array.isArray(tasks)) return { status: "noop" };
610
+ const visible = tasks.filter((t) => t && typeof t.subject === "string" && t.status !== "deleted");
611
+ const normCwd = options.cwd ? normalizePath(options.cwd) : "";
612
+ const normSession = options.sessionFile ? normalizePath(options.sessionFile) : void 0;
613
+ return this.withLock(() => {
614
+ const state = this.loadState();
615
+ const existingIdx = state.anchors.findIndex(
616
+ (a) => a.source === "todo-sync" && (normSession && a.sessionFile === normSession || normCwd && isSameOrSubPath(a.cwd, normCwd))
617
+ );
618
+ const completedTasks = visible.filter((t) => t.status === "completed");
619
+ const unfinishedTasks = visible.filter((t) => t.status !== "completed");
620
+ if (visible.length === 0 || unfinishedTasks.length === 0) {
621
+ if (existingIdx !== -1) {
622
+ const [settled] = state.anchors.splice(existingIdx, 1);
623
+ settled.status = "settled";
624
+ settled.updatedAt = Date.now();
625
+ settled.evidence = {
626
+ settledAt: Date.now(),
627
+ settledBy: "todo-complete",
628
+ summary: `All ${completedTasks.length} session todo(s) completed`
629
+ };
630
+ this.ensureDirs();
631
+ import_node_fs.default.appendFileSync(this.archivePath, JSON.stringify(settled) + "\n", "utf-8");
632
+ this.saveState(state);
633
+ return { status: "completed", anchor: settled };
634
+ }
635
+ return { status: "noop" };
636
+ }
637
+ const inProgress = unfinishedTasks.find((t) => t.status === "in_progress") || unfinishedTasks[0];
638
+ const orderedSteps = [
639
+ ...completedTasks.map((t) => t.subject.trim()),
640
+ ...unfinishedTasks.filter((t) => t.status === "in_progress").map((t) => t.subject.trim()),
641
+ ...unfinishedTasks.filter((t) => t.status === "pending").map((t) => t.subject.trim())
642
+ ];
643
+ const progress = {
644
+ done: completedTasks.length,
645
+ total: visible.length,
646
+ steps: orderedSteps
647
+ };
648
+ const remainingSummary = unfinishedTasks.slice(0, 3).map((t) => t.subject.trim()).join(" \u2192 ");
649
+ const headline = inProgress.subject.trim();
650
+ const now = Date.now();
651
+ if (existingIdx !== -1) {
652
+ const current = state.anchors[existingIdx];
653
+ current.title = headline;
654
+ current.description = `Remaining (${unfinishedTasks.length}/${visible.length}): ${remainingSummary}`;
655
+ current.progress = progress;
656
+ current.status = "active";
657
+ current.updatedAt = now;
658
+ current.lastTouchedAt = now;
659
+ if (normSession) current.sessionFile = normSession;
660
+ if (options.recentFiles && options.recentFiles.length > 0) {
661
+ const merged = Array.from(/* @__PURE__ */ new Set([...options.recentFiles.map((f) => normalizePath(f)), ...current.files])).slice(0, 5);
662
+ current.files = merged;
663
+ }
664
+ state.anchors[existingIdx] = current;
665
+ this.saveState(state);
666
+ return { status: "synced", anchor: current };
667
+ }
668
+ const id = this.generateId(state);
669
+ const inferredFiles = options.recentFiles && options.recentFiles.length > 0 ? options.recentFiles.map((f) => normalizePath(f)).filter(Boolean) : extractPathsFromText(orderedSteps.join(" "));
670
+ const projectName = normCwd ? import_node_path.default.basename(normCwd) : "global";
671
+ const created = {
672
+ id,
673
+ title: headline,
674
+ description: `Remaining (${unfinishedTasks.length}/${visible.length}): ${remainingSummary}`,
675
+ priority: "p1",
676
+ status: "active",
677
+ source: "todo-sync",
678
+ project: projectName,
679
+ cwd: normCwd,
680
+ createdAt: now,
681
+ updatedAt: now,
682
+ lastTouchedAt: now,
683
+ files: inferredFiles,
684
+ tags: ["todo-sync"],
685
+ ...normSession ? { sessionFile: normSession } : {},
686
+ progress
687
+ };
688
+ state.anchors.push(created);
689
+ this.saveState(state);
690
+ return { status: "synced", anchor: created };
691
+ });
692
+ }
599
693
  /**
600
694
  * Tear off (settle) a completed sticky note and archive it to archive.jsonl
601
695
  */
@@ -814,7 +908,8 @@ function updateStartupBanner(ctx, store) {
814
908
  const a = sorted[0];
815
909
  const num = a.id.replace(/^anc-/, "");
816
910
  const bar = a.progress ? `${fg("accent", formatProgressBar(a.progress))} ` : "";
817
- const nextStep = a.progress?.steps && a.progress.steps[a.progress.done] ? fg("muted", ` \u25B8 ${a.progress.steps[a.progress.done]}`) : "";
911
+ 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;
912
+ const nextStep = candidateStep ? fg("muted", ` \u25B8 ${candidateStep}`) : "";
818
913
  const origin = formatOrigin(a);
819
914
  const sessionIcon = a.sessionFile ? " \u21AA" : "";
820
915
  const prefix = `${fg("accent", "\u2316")} ${fg("dim", `#${num}`)} ${bar}`;
@@ -1065,6 +1160,20 @@ ${snippet}` };
1065
1160
  if (event.isError) return;
1066
1161
  const lowerTool = String(event.toolName || "").toLowerCase();
1067
1162
  const currentSessionFile = getSessionFileSafe(ctx);
1163
+ if (lowerTool === "todo") {
1164
+ const tasks = event?.details?.tasks;
1165
+ if (Array.isArray(tasks)) {
1166
+ const syncRes = store.syncFromTodoTool(tasks, {
1167
+ cwd: ctx.cwd,
1168
+ sessionFile: currentSessionFile,
1169
+ recentFiles: recentSessionFiles
1170
+ });
1171
+ if (syncRes.status !== "noop") {
1172
+ updateAnchorStatusBar(ctx, store);
1173
+ }
1174
+ }
1175
+ return;
1176
+ }
1068
1177
  if (lowerTool === "bash" || lowerTool === "powershell") {
1069
1178
  const cmd = String(event.input?.command || "");
1070
1179
  if (cmd.includes("git commit")) {
@@ -1360,6 +1469,35 @@ ${snippet}` };
1360
1469
  await openAnchorDashboard(ctx, store);
1361
1470
  }
1362
1471
  });
1472
+ pi.registerCommand("back", {
1473
+ description: "Jump back to the conversation session of the latest unfinished note or todo checkpoint (/back [id])",
1474
+ handler: async (args, ctx) => {
1475
+ const targetArg = (args || "").trim();
1476
+ const currentSession = getSessionFileSafe(ctx);
1477
+ if (targetArg) {
1478
+ const resolvedId = resolveAnchorTarget(store, targetArg, ctx.cwd);
1479
+ const note = resolvedId ? store.get(resolvedId) : void 0;
1480
+ if (!note || !note.sessionFile || !import_node_fs3.default.existsSync(note.sessionFile)) {
1481
+ ctx.ui.notify(`\u2316 No saved session found for #${targetArg}`, "error");
1482
+ return;
1483
+ }
1484
+ await ctx.switchSession(note.sessionFile);
1485
+ return;
1486
+ }
1487
+ const candidates = sortAnchorsForDisplay(
1488
+ store.list({ cwd: ctx.cwd, all: true }).filter((a) => a.status === "active" || a.status === "sleeping"),
1489
+ ctx.cwd
1490
+ );
1491
+ const jumpTarget = candidates.find(
1492
+ (a) => a.sessionFile && import_node_fs3.default.existsSync(a.sessionFile) && (!currentSession || !isSameOrSubPath(a.sessionFile, currentSession))
1493
+ );
1494
+ if (!jumpTarget || !jumpTarget.sessionFile) {
1495
+ ctx.ui.notify("\u2316 No other linked conversation session found to jump back to.", "info");
1496
+ return;
1497
+ }
1498
+ await ctx.switchSession(jumpTarget.sessionFile);
1499
+ }
1500
+ });
1363
1501
  }
1364
1502
  // Annotate the CommonJS export names for ESM import in node:
1365
1503
  0 && (module.exports = {
@@ -1,2 +1,2 @@
1
1
  import '@earendil-works/pi-coding-agent';
2
- export { z as default, t as parseAnchorCommandInput, u as parseQuickNoteInput, x as resolveAnchorTarget } from './extension-CvTgSv1L.cjs';
2
+ export { B as default, u as parseAnchorCommandInput, v as parseQuickNoteInput, y as resolveAnchorTarget } from './extension-09AVvgl-.cjs';
@@ -1,2 +1,2 @@
1
1
  import '@earendil-works/pi-coding-agent';
2
- export { z as default, t as parseAnchorCommandInput, u as parseQuickNoteInput, x as resolveAnchorTarget } from './extension-CvTgSv1L.js';
2
+ export { B as default, u as parseAnchorCommandInput, v as parseQuickNoteInput, y as resolveAnchorTarget } from './extension-09AVvgl-.js';
package/dist/extension.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  parseAnchorCommandInput,
4
4
  parseQuickNoteInput,
5
5
  resolveAnchorTarget
6
- } from "./chunk-XL3YH2AF.js";
6
+ } from "./chunk-6NYXKR4F.js";
7
7
  export {
8
8
  extension_default as default,
9
9
  parseAnchorCommandInput,
package/dist/index.cjs CHANGED
@@ -322,10 +322,12 @@ function renderColdStartAnchorsContext(store, cwdOrNow, nowArg) {
322
322
  if (activeNotes.length === 0) return "";
323
323
  const lines = activeNotes.map((a) => {
324
324
  const num = a.id.replace(/^anc-/, "");
325
+ const tag = a.source === "todo-sync" ? `[Todo Checkpoint \xB7 ${a.project}]` : `[${a.project}]`;
325
326
  const files = a.files.length > 0 ? ` (${a.files.slice(0, 2).join(", ")})` : "";
326
- const prog = a.progress ? ` [${a.progress.done}/${a.progress.total}${a.progress.steps && a.progress.steps[a.progress.done] ? ` \xB7 next: ${a.progress.steps[a.progress.done]}` : ""}]` : "";
327
+ const nextStep = a.progress?.steps && a.progress.steps[a.progress.done] && a.progress.steps[a.progress.done] !== a.title ? ` \xB7 current/next: ${a.progress.steps[a.progress.done]}` : "";
328
+ const prog = a.progress ? ` [${a.progress.done}/${a.progress.total}${nextStep}]` : "";
327
329
  const desc = a.description ? ` \u2014 ${a.description}` : "";
328
- return `\u2022 #${num} [${a.project}] ${a.title}${prog}${files}${desc}`;
330
+ return `\u2022 #${num} ${tag} ${a.title}${prog}${files}${desc}`;
329
331
  });
330
332
  return [
331
333
  `<sticky-notes count="${activeNotes.length}">`,
@@ -517,6 +519,7 @@ var AnchorStore = class {
517
519
  description: input.description?.trim(),
518
520
  priority: input.priority || "p1",
519
521
  status: "active",
522
+ ...input.source ? { source: input.source } : {},
520
523
  project: projectName,
521
524
  cwd,
522
525
  createdAt: now,
@@ -624,6 +627,97 @@ var AnchorStore = class {
624
627
  return { anchor: current, completed: nextDone >= total };
625
628
  });
626
629
  }
630
+ /**
631
+ * Clever Touch 4 (Todo-Bridge): Automatically mirror intra-session `todo` tool lists
632
+ * into a cross-session checkpoint!
633
+ * - While tasks are in_progress/pending: updates the live `[■■□□ done/total]` checkpoint on disk;
634
+ * - When all tasks in the todo list reach `completed`: automatically tears off the checkpoint!
635
+ */
636
+ syncFromTodoTool(tasks, options = {}) {
637
+ if (!Array.isArray(tasks)) return { status: "noop" };
638
+ const visible = tasks.filter((t) => t && typeof t.subject === "string" && t.status !== "deleted");
639
+ const normCwd = options.cwd ? normalizePath(options.cwd) : "";
640
+ const normSession = options.sessionFile ? normalizePath(options.sessionFile) : void 0;
641
+ return this.withLock(() => {
642
+ const state = this.loadState();
643
+ const existingIdx = state.anchors.findIndex(
644
+ (a) => a.source === "todo-sync" && (normSession && a.sessionFile === normSession || normCwd && isSameOrSubPath(a.cwd, normCwd))
645
+ );
646
+ const completedTasks = visible.filter((t) => t.status === "completed");
647
+ const unfinishedTasks = visible.filter((t) => t.status !== "completed");
648
+ if (visible.length === 0 || unfinishedTasks.length === 0) {
649
+ if (existingIdx !== -1) {
650
+ const [settled] = state.anchors.splice(existingIdx, 1);
651
+ settled.status = "settled";
652
+ settled.updatedAt = Date.now();
653
+ settled.evidence = {
654
+ settledAt: Date.now(),
655
+ settledBy: "todo-complete",
656
+ summary: `All ${completedTasks.length} session todo(s) completed`
657
+ };
658
+ this.ensureDirs();
659
+ import_node_fs.default.appendFileSync(this.archivePath, JSON.stringify(settled) + "\n", "utf-8");
660
+ this.saveState(state);
661
+ return { status: "completed", anchor: settled };
662
+ }
663
+ return { status: "noop" };
664
+ }
665
+ const inProgress = unfinishedTasks.find((t) => t.status === "in_progress") || unfinishedTasks[0];
666
+ const orderedSteps = [
667
+ ...completedTasks.map((t) => t.subject.trim()),
668
+ ...unfinishedTasks.filter((t) => t.status === "in_progress").map((t) => t.subject.trim()),
669
+ ...unfinishedTasks.filter((t) => t.status === "pending").map((t) => t.subject.trim())
670
+ ];
671
+ const progress = {
672
+ done: completedTasks.length,
673
+ total: visible.length,
674
+ steps: orderedSteps
675
+ };
676
+ const remainingSummary = unfinishedTasks.slice(0, 3).map((t) => t.subject.trim()).join(" \u2192 ");
677
+ const headline = inProgress.subject.trim();
678
+ const now = Date.now();
679
+ if (existingIdx !== -1) {
680
+ const current = state.anchors[existingIdx];
681
+ current.title = headline;
682
+ current.description = `Remaining (${unfinishedTasks.length}/${visible.length}): ${remainingSummary}`;
683
+ current.progress = progress;
684
+ current.status = "active";
685
+ current.updatedAt = now;
686
+ current.lastTouchedAt = now;
687
+ if (normSession) current.sessionFile = normSession;
688
+ if (options.recentFiles && options.recentFiles.length > 0) {
689
+ const merged = Array.from(/* @__PURE__ */ new Set([...options.recentFiles.map((f) => normalizePath(f)), ...current.files])).slice(0, 5);
690
+ current.files = merged;
691
+ }
692
+ state.anchors[existingIdx] = current;
693
+ this.saveState(state);
694
+ return { status: "synced", anchor: current };
695
+ }
696
+ const id = this.generateId(state);
697
+ const inferredFiles = options.recentFiles && options.recentFiles.length > 0 ? options.recentFiles.map((f) => normalizePath(f)).filter(Boolean) : extractPathsFromText(orderedSteps.join(" "));
698
+ const projectName = normCwd ? import_node_path.default.basename(normCwd) : "global";
699
+ const created = {
700
+ id,
701
+ title: headline,
702
+ description: `Remaining (${unfinishedTasks.length}/${visible.length}): ${remainingSummary}`,
703
+ priority: "p1",
704
+ status: "active",
705
+ source: "todo-sync",
706
+ project: projectName,
707
+ cwd: normCwd,
708
+ createdAt: now,
709
+ updatedAt: now,
710
+ lastTouchedAt: now,
711
+ files: inferredFiles,
712
+ tags: ["todo-sync"],
713
+ ...normSession ? { sessionFile: normSession } : {},
714
+ progress
715
+ };
716
+ state.anchors.push(created);
717
+ this.saveState(state);
718
+ return { status: "synced", anchor: created };
719
+ });
720
+ }
627
721
  /**
628
722
  * Tear off (settle) a completed sticky note and archive it to archive.jsonl
629
723
  */
@@ -842,7 +936,8 @@ function updateStartupBanner(ctx, store) {
842
936
  const a = sorted[0];
843
937
  const num = a.id.replace(/^anc-/, "");
844
938
  const bar = a.progress ? `${fg("accent", formatProgressBar(a.progress))} ` : "";
845
- const nextStep = a.progress?.steps && a.progress.steps[a.progress.done] ? fg("muted", ` \u25B8 ${a.progress.steps[a.progress.done]}`) : "";
939
+ 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;
940
+ const nextStep = candidateStep ? fg("muted", ` \u25B8 ${candidateStep}`) : "";
846
941
  const origin = formatOrigin(a);
847
942
  const sessionIcon = a.sessionFile ? " \u21AA" : "";
848
943
  const prefix = `${fg("accent", "\u2316")} ${fg("dim", `#${num}`)} ${bar}`;
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { A as Anchor, a as AnchorProgress, b as AnchorStore } from './extension-CvTgSv1L.cjs';
2
- export { c as AnchorEvidence, d as AnchorPriority, e as AnchorStatus, f as AnchorStoreState, C as COMMENT_FORMATS, M as MS_PER_DAY, Q as QUIET_AFTER_DAYS, T as TouchMatchResult, g as acquireSyncLock, h as atomicRenameWithRetry, i as extractPathsFromText, j as extractProgressFromText, k as findMatchedAnchors, l as getDefaultStorageDir, m as globToRegExp, n as isAnchorTargetingWorkspace, o as isSameOrSubPath, p as makeSafeTaskAnnotation, q as matchAnchorAgainstTouchedFiles, r as matchesCommitMessage, s as normalizePath, t as parseAnchorCommandInput, u as parseQuickNoteInput, v as renderActiveAnchorsContext, w as renderColdStartAnchorsContext, x as resolveAnchorTarget, y as sweepQuietNotes } from './extension-CvTgSv1L.cjs';
1
+ import { A as Anchor, a as AnchorProgress, b as AnchorStore } from './extension-09AVvgl-.cjs';
2
+ export { c as AnchorEvidence, d as AnchorPriority, e as AnchorStatus, f as AnchorStoreState, C as COMMENT_FORMATS, M as MS_PER_DAY, Q as QUIET_AFTER_DAYS, T as TodoTaskItem, g as TouchMatchResult, h as acquireSyncLock, i as atomicRenameWithRetry, j as extractPathsFromText, k as extractProgressFromText, l as findMatchedAnchors, m as getDefaultStorageDir, n as globToRegExp, o as isAnchorTargetingWorkspace, p as isSameOrSubPath, q as makeSafeTaskAnnotation, r as matchAnchorAgainstTouchedFiles, s as matchesCommitMessage, t as normalizePath, u as parseAnchorCommandInput, v as parseQuickNoteInput, w as renderActiveAnchorsContext, x as renderColdStartAnchorsContext, y as resolveAnchorTarget, z as sweepQuietNotes } from './extension-09AVvgl-.cjs';
3
3
  import { ExtensionContext, ExtensionCommandContext } from '@earendil-works/pi-coding-agent';
4
4
 
5
5
  declare function stripAnsi(str: string): string;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { A as Anchor, a as AnchorProgress, b as AnchorStore } from './extension-CvTgSv1L.js';
2
- export { c as AnchorEvidence, d as AnchorPriority, e as AnchorStatus, f as AnchorStoreState, C as COMMENT_FORMATS, M as MS_PER_DAY, Q as QUIET_AFTER_DAYS, T as TouchMatchResult, g as acquireSyncLock, h as atomicRenameWithRetry, i as extractPathsFromText, j as extractProgressFromText, k as findMatchedAnchors, l as getDefaultStorageDir, m as globToRegExp, n as isAnchorTargetingWorkspace, o as isSameOrSubPath, p as makeSafeTaskAnnotation, q as matchAnchorAgainstTouchedFiles, r as matchesCommitMessage, s as normalizePath, t as parseAnchorCommandInput, u as parseQuickNoteInput, v as renderActiveAnchorsContext, w as renderColdStartAnchorsContext, x as resolveAnchorTarget, y as sweepQuietNotes } from './extension-CvTgSv1L.js';
1
+ import { A as Anchor, a as AnchorProgress, b as AnchorStore } from './extension-09AVvgl-.js';
2
+ export { c as AnchorEvidence, d as AnchorPriority, e as AnchorStatus, f as AnchorStoreState, C as COMMENT_FORMATS, M as MS_PER_DAY, Q as QUIET_AFTER_DAYS, T as TodoTaskItem, g as TouchMatchResult, h as acquireSyncLock, i as atomicRenameWithRetry, j as extractPathsFromText, k as extractProgressFromText, l as findMatchedAnchors, m as getDefaultStorageDir, n as globToRegExp, o as isAnchorTargetingWorkspace, p as isSameOrSubPath, q as makeSafeTaskAnnotation, r as matchAnchorAgainstTouchedFiles, s as matchesCommitMessage, t as normalizePath, u as parseAnchorCommandInput, v as parseQuickNoteInput, w as renderActiveAnchorsContext, x as renderColdStartAnchorsContext, y as resolveAnchorTarget, z as sweepQuietNotes } from './extension-09AVvgl-.js';
3
3
  import { ExtensionContext, ExtensionCommandContext } from '@earendil-works/pi-coding-agent';
4
4
 
5
5
  declare function stripAnsi(str: string): string;
package/dist/index.js CHANGED
@@ -34,7 +34,7 @@ import {
34
34
  truncateToWidth,
35
35
  updateAnchorStatusBar,
36
36
  updateStartupBanner
37
- } from "./chunk-XL3YH2AF.js";
37
+ } from "./chunk-6NYXKR4F.js";
38
38
  export {
39
39
  AnchorStore,
40
40
  COMMENT_FORMATS,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-anchor",
3
- "version": "0.5.1",
4
- "description": "A digital sticky note docked below your Pi editor — stick notes in 0ms (pin: ...), track long tasks with micro progress bars [■■□□], jump back to the original session (↪), and auto-tear off on git commit.",
3
+ "version": "0.6.0",
4
+ "description": "Cross-session sticky note & auto-todo checkpoint for Pi — automatically saves unfinished todos across terminal restarts, docks a 1-line progress bar [■■□□] below your editor, jumps back to the original session (/back), and auto-clears on git commit.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.js",
@@ -12,6 +12,7 @@
12
12
  "bin",
13
13
  "assets",
14
14
  "README.md",
15
+ "README_ja.md",
15
16
  "README_zh.md",
16
17
  "LICENSE"
17
18
  ],
package/src/extension.ts CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  AnchorStore,
6
6
  findMatchedAnchors,
7
7
  isAnchorTargetingWorkspace,
8
+ isSameOrSubPath,
8
9
  makeSafeTaskAnnotation,
9
10
  matchesCommitMessage,
10
11
  normalizePath,
@@ -193,15 +194,33 @@ export default function (pi: ExtensionAPI) {
193
194
  });
194
195
 
195
196
  // 4. Clever Touches on Tool Execution:
196
- // (a) Auto-tear off sticky notes on matching `git commit`
197
- // (b) Slip a 1-line comment whisper when reading/editing a matching file & bind current sessionFile
197
+ // (a) Auto-Bridge for `todo` tool (`@juicesharp/rpiv-todo` / built-in `todo`):
198
+ // Mirrors unfinished session todos into a persistent cross-session checkpoint, and auto-clears when 100% done!
199
+ // (b) Auto-tear off sticky notes on matching `git commit`
200
+ // (c) Slip a 1-line comment whisper when reading/editing a matching file & bind current sessionFile
198
201
  pi.on('tool_result', async (event: any, ctx: ExtensionContext) => {
199
202
  if (event.isError) return;
200
203
 
201
204
  const lowerTool = String(event.toolName || '').toLowerCase();
202
205
  const currentSessionFile = getSessionFileSafe(ctx);
203
206
 
204
- // (a) Real-time auto-tear on `git commit -m "..."`
207
+ // (a) Auto-Bridge for intra-session `todo` tool
208
+ if (lowerTool === 'todo') {
209
+ const tasks = event?.details?.tasks;
210
+ if (Array.isArray(tasks)) {
211
+ const syncRes = store.syncFromTodoTool(tasks, {
212
+ cwd: ctx.cwd,
213
+ sessionFile: currentSessionFile,
214
+ recentFiles: recentSessionFiles
215
+ });
216
+ if (syncRes.status !== 'noop') {
217
+ updateAnchorStatusBar(ctx, store);
218
+ }
219
+ }
220
+ return;
221
+ }
222
+
223
+ // (b) Real-time auto-tear on `git commit -m "..."`
205
224
  if (lowerTool === 'bash' || lowerTool === 'powershell') {
206
225
  const cmd = String((event.input as any)?.command || '');
207
226
  if (cmd.includes('git commit')) {
@@ -523,4 +542,41 @@ export default function (pi: ExtensionAPI) {
523
542
  await openAnchorDashboard(ctx, store);
524
543
  }
525
544
  });
545
+
546
+ // 1-click Session Time-Machine: `/back` (or `/back 1`) jumps straight to the saved session of an unfinished note/todo checkpoint
547
+ pi.registerCommand('back', {
548
+ description: 'Jump back to the conversation session of the latest unfinished note or todo checkpoint (/back [id])',
549
+ handler: async (args: string, ctx: ExtensionCommandContext) => {
550
+ const targetArg = (args || '').trim();
551
+ const currentSession = getSessionFileSafe(ctx);
552
+
553
+ if (targetArg) {
554
+ const resolvedId = resolveAnchorTarget(store, targetArg, ctx.cwd);
555
+ const note = resolvedId ? store.get(resolvedId) : undefined;
556
+ if (!note || !note.sessionFile || !fs.existsSync(note.sessionFile)) {
557
+ ctx.ui.notify(`⌖ No saved session found for #${targetArg}`, 'error');
558
+ return;
559
+ }
560
+ await ctx.switchSession(note.sessionFile);
561
+ return;
562
+ }
563
+
564
+ const candidates = sortAnchorsForDisplay(
565
+ store.list({ cwd: ctx.cwd, all: true }).filter(a => a.status === 'active' || a.status === 'sleeping'),
566
+ ctx.cwd
567
+ );
568
+ const jumpTarget = candidates.find(a =>
569
+ a.sessionFile &&
570
+ fs.existsSync(a.sessionFile) &&
571
+ (!currentSession || !isSameOrSubPath(a.sessionFile, currentSession))
572
+ );
573
+
574
+ if (!jumpTarget || !jumpTarget.sessionFile) {
575
+ ctx.ui.notify('⌖ No other linked conversation session found to jump back to.', 'info');
576
+ return;
577
+ }
578
+
579
+ await ctx.switchSession(jumpTarget.sessionFile);
580
+ }
581
+ });
526
582
  }
package/src/store.ts CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  type AnchorProgress,
9
9
  type AnchorStatus,
10
10
  type AnchorStoreState,
11
+ type TodoTaskItem,
11
12
  type TouchMatchResult,
12
13
  QUIET_AFTER_DAYS,
13
14
  MS_PER_DAY
@@ -291,12 +292,16 @@ export function renderColdStartAnchorsContext(
291
292
 
292
293
  const lines = activeNotes.map(a => {
293
294
  const num = a.id.replace(/^anc-/, '');
295
+ const tag = a.source === 'todo-sync' ? `[Todo Checkpoint · ${a.project}]` : `[${a.project}]`;
294
296
  const files = a.files.length > 0 ? ` (${a.files.slice(0, 2).join(', ')})` : '';
297
+ const nextStep = a.progress?.steps && a.progress.steps[a.progress.done] && a.progress.steps[a.progress.done] !== a.title
298
+ ? ` · current/next: ${a.progress.steps[a.progress.done]}`
299
+ : '';
295
300
  const prog = a.progress
296
- ? ` [${a.progress.done}/${a.progress.total}${a.progress.steps && a.progress.steps[a.progress.done] ? ` · next: ${a.progress.steps[a.progress.done]}` : ''}]`
301
+ ? ` [${a.progress.done}/${a.progress.total}${nextStep}]`
297
302
  : '';
298
303
  const desc = a.description ? ` — ${a.description}` : '';
299
- return `• #${num} [${a.project}] ${a.title}${prog}${files}${desc}`;
304
+ return `• #${num} ${tag} ${a.title}${prog}${files}${desc}`;
300
305
  });
301
306
 
302
307
  return [
@@ -484,6 +489,7 @@ export class AnchorStore {
484
489
  title: string;
485
490
  description?: string;
486
491
  priority?: AnchorPriority;
492
+ source?: 'manual' | 'todo-sync';
487
493
  cwd?: string;
488
494
  project?: string;
489
495
  files?: string[];
@@ -520,6 +526,7 @@ export class AnchorStore {
520
526
  description: input.description?.trim(),
521
527
  priority: input.priority || 'p1',
522
528
  status: 'active',
529
+ ...(input.source ? { source: input.source } : {}),
523
530
  project: projectName,
524
531
  cwd,
525
532
  createdAt: now,
@@ -649,6 +656,120 @@ export class AnchorStore {
649
656
  });
650
657
  }
651
658
 
659
+ /**
660
+ * Clever Touch 4 (Todo-Bridge): Automatically mirror intra-session `todo` tool lists
661
+ * into a cross-session checkpoint!
662
+ * - While tasks are in_progress/pending: updates the live `[■■□□ done/total]` checkpoint on disk;
663
+ * - When all tasks in the todo list reach `completed`: automatically tears off the checkpoint!
664
+ */
665
+ public syncFromTodoTool(
666
+ tasks: TodoTaskItem[],
667
+ options: { cwd?: string; sessionFile?: string; recentFiles?: string[] } = {}
668
+ ): { status: 'synced' | 'completed' | 'noop'; anchor?: Anchor } {
669
+ if (!Array.isArray(tasks)) return { status: 'noop' };
670
+
671
+ const visible = tasks.filter(t => t && typeof t.subject === 'string' && t.status !== 'deleted');
672
+ const normCwd = options.cwd ? normalizePath(options.cwd) : '';
673
+ const normSession = options.sessionFile ? normalizePath(options.sessionFile) : undefined;
674
+
675
+ return this.withLock(() => {
676
+ const state = this.loadState();
677
+ const existingIdx = state.anchors.findIndex(a =>
678
+ a.source === 'todo-sync' &&
679
+ ((normSession && a.sessionFile === normSession) || (normCwd && isSameOrSubPath(a.cwd, normCwd)))
680
+ );
681
+
682
+ // 1. If todo list was cleared or 100% completed, auto-tear off the synced checkpoint
683
+ const completedTasks = visible.filter(t => t.status === 'completed');
684
+ const unfinishedTasks = visible.filter(t => t.status !== 'completed');
685
+
686
+ if (visible.length === 0 || unfinishedTasks.length === 0) {
687
+ if (existingIdx !== -1) {
688
+ const [settled] = state.anchors.splice(existingIdx, 1);
689
+ settled.status = 'settled';
690
+ settled.updatedAt = Date.now();
691
+ settled.evidence = {
692
+ settledAt: Date.now(),
693
+ settledBy: 'todo-complete',
694
+ summary: `All ${completedTasks.length} session todo(s) completed`
695
+ };
696
+ this.ensureDirs();
697
+ fs.appendFileSync(this.archivePath, JSON.stringify(settled) + '\n', 'utf-8');
698
+ this.saveState(state);
699
+ return { status: 'completed', anchor: settled };
700
+ }
701
+ return { status: 'noop' };
702
+ }
703
+
704
+ // 2. Active unfinished todos exist: build live progress & next-step snapshot
705
+ const inProgress = unfinishedTasks.find(t => t.status === 'in_progress') || unfinishedTasks[0];
706
+ const orderedSteps = [
707
+ ...completedTasks.map(t => t.subject.trim()),
708
+ ...unfinishedTasks.filter(t => t.status === 'in_progress').map(t => t.subject.trim()),
709
+ ...unfinishedTasks.filter(t => t.status === 'pending').map(t => t.subject.trim())
710
+ ];
711
+ const progress: AnchorProgress = {
712
+ done: completedTasks.length,
713
+ total: visible.length,
714
+ steps: orderedSteps
715
+ };
716
+
717
+ const remainingSummary = unfinishedTasks
718
+ .slice(0, 3)
719
+ .map(t => t.subject.trim())
720
+ .join(' → ');
721
+ const headline = inProgress.subject.trim();
722
+ const now = Date.now();
723
+
724
+ if (existingIdx !== -1) {
725
+ const current = state.anchors[existingIdx];
726
+ current.title = headline;
727
+ current.description = `Remaining (${unfinishedTasks.length}/${visible.length}): ${remainingSummary}`;
728
+ current.progress = progress;
729
+ current.status = 'active';
730
+ current.updatedAt = now;
731
+ current.lastTouchedAt = now;
732
+ if (normSession) current.sessionFile = normSession;
733
+ if (options.recentFiles && options.recentFiles.length > 0) {
734
+ const merged = Array.from(new Set([...options.recentFiles.map(f => normalizePath(f)), ...current.files])).slice(0, 5);
735
+ current.files = merged;
736
+ }
737
+ state.anchors[existingIdx] = current;
738
+ this.saveState(state);
739
+ return { status: 'synced', anchor: current };
740
+ }
741
+
742
+ // Create new todo-synced checkpoint
743
+ const id = this.generateId(state);
744
+ const inferredFiles = (options.recentFiles && options.recentFiles.length > 0)
745
+ ? options.recentFiles.map(f => normalizePath(f)).filter(Boolean)
746
+ : extractPathsFromText(orderedSteps.join(' '));
747
+ const projectName = normCwd ? path.basename(normCwd) : 'global';
748
+
749
+ const created: Anchor = {
750
+ id,
751
+ title: headline,
752
+ description: `Remaining (${unfinishedTasks.length}/${visible.length}): ${remainingSummary}`,
753
+ priority: 'p1',
754
+ status: 'active',
755
+ source: 'todo-sync',
756
+ project: projectName,
757
+ cwd: normCwd,
758
+ createdAt: now,
759
+ updatedAt: now,
760
+ lastTouchedAt: now,
761
+ files: inferredFiles,
762
+ tags: ['todo-sync'],
763
+ ...(normSession ? { sessionFile: normSession } : {}),
764
+ progress
765
+ };
766
+
767
+ state.anchors.push(created);
768
+ this.saveState(state);
769
+ return { status: 'synced', anchor: created };
770
+ });
771
+ }
772
+
652
773
  /**
653
774
  * Tear off (settle) a completed sticky note and archive it to archive.jsonl
654
775
  */
package/src/tui.ts CHANGED
@@ -226,14 +226,17 @@ export function updateStartupBanner(ctx: ExtensionContext, store: AnchorStore):
226
226
  const a = sorted[0];
227
227
  const num = a.id.replace(/^anc-/, '');
228
228
  const bar = a.progress ? `${fg('accent', formatProgressBar(a.progress))} ` : '';
229
- const nextStep = a.progress?.steps && a.progress.steps[a.progress.done]
230
- ? fg('muted', ` ▸ ${a.progress.steps[a.progress.done]}`)
231
- : '';
229
+ const candidateStep = a.progress?.steps
230
+ ? (a.progress.steps[a.progress.done] !== a.title
231
+ ? a.progress.steps[a.progress.done]
232
+ : a.progress.steps[a.progress.done + 1])
233
+ : undefined;
234
+ const nextStep = candidateStep ? fg('muted', ` ▸ ${candidateStep}`) : '';
232
235
  const origin = formatOrigin(a);
233
236
  const sessionIcon = a.sessionFile ? ' ↪' : '';
234
237
  const prefix = `${fg('accent', '⌖')} ${fg('dim', `#${num}`)} ${bar}`;
235
238
  const suffix = ` ${fg('muted', `(${origin}${sessionIcon})`)}`;
236
- const availTitle = Math.max(12, width - getDisplayWidth(prefix) - getDisplayWidth( nextStep) - getDisplayWidth(suffix) - 2);
239
+ const availTitle = Math.max(12, width - getDisplayWidth(prefix) - getDisplayWidth(nextStep) - getDisplayWidth(suffix) - 2);
237
240
  const titleClipped = truncateToWidth(a.title, availTitle);
238
241
  return [truncateToWidth(`${prefix}${titleClipped}${nextStep}${suffix}`, width)];
239
242
  }
package/src/types.ts CHANGED
@@ -17,12 +17,20 @@ export interface AnchorProgress {
17
17
 
18
18
  export interface AnchorEvidence {
19
19
  settledAt?: number;
20
- settledBy?: 'commit' | 'manual' | 'one-tap-settlement' | 'manual-command' | 'verification-test';
20
+ settledBy?: 'commit' | 'manual' | 'todo-complete' | 'one-tap-settlement' | 'manual-command' | 'verification-test';
21
21
  commitHash?: string;
22
22
  summary?: string;
23
23
  touchedFiles?: string[];
24
24
  }
25
25
 
26
+ export interface TodoTaskItem {
27
+ id: number;
28
+ subject: string;
29
+ status: 'pending' | 'in_progress' | 'completed' | 'deleted';
30
+ activeForm?: string;
31
+ description?: string;
32
+ }
33
+
26
34
  export interface Anchor {
27
35
  /** Sequential identifier, e.g. 'anc-1' */
28
36
  id: string;
@@ -32,6 +40,8 @@ export interface Anchor {
32
40
  description?: string;
33
41
  /** Lifecycle status: 'active' (recent) or 'sleeping' (>7d untouched, 0 prompt tokens) */
34
42
  status: AnchorStatus;
43
+ /** Origin source: 'manual' (user/tool pinned) or 'todo-sync' (auto-bridged from unfinished session todos) */
44
+ source?: 'manual' | 'todo-sync';
35
45
  /** Short workspace label (e.g. 'anchor', 'College/优化', or 'global') */
36
46
  project: string;
37
47
  /** Workspace directory path where the note applies */