pi-anchor 0.5.1 → 0.6.1
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 +26 -12
- package/README_zh.md +27 -13
- package/dist/{chunk-XL3YH2AF.js → chunk-6NYXKR4F.js} +141 -3
- package/dist/{extension-CvTgSv1L.d.cts → extension-09AVvgl-.d.cts} +26 -2
- package/dist/{extension-CvTgSv1L.d.ts → extension-09AVvgl-.d.ts} +26 -2
- package/dist/extension.cjs +141 -3
- package/dist/extension.d.cts +1 -1
- package/dist/extension.d.ts +1 -1
- package/dist/extension.js +1 -1
- package/dist/index.cjs +98 -3
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/package.json +3 -2
- package/src/extension.ts +59 -3
- package/src/store.ts +123 -2
- package/src/tui.ts +7 -4
- package/src/types.ts +11 -1
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# ⌖ Anchor
|
|
2
2
|
|
|
3
|
-
> **A digital sticky note docked below your Pi editor —
|
|
3
|
+
> **A digital sticky note & cross-session `todo` checkpoint docked below your Pi editor — automatically catches unfinished `todo` lists when you close the terminal, sticks quick notes in 0ms (`pin: ...`), and teleports back to the original session (`/back`).**
|
|
4
4
|
|
|
5
|
+
[](https://www.npmjs.com/package/pi-anchor)
|
|
6
|
+
[](https://www.npmjs.com/package/pi-anchor)
|
|
7
|
+
[](https://github.com/3ZEROS12/anchor/stargazers)
|
|
5
8
|
[](https://github.com/3ZEROS12/anchor/actions/workflows/ci.yml)
|
|
6
|
-
[](tsconfig.json)
|
|
8
|
-
[](tests/store.test.ts)
|
|
9
|
-
[](package.json)
|
|
9
|
+
[](tests/store.test.ts)
|
|
10
10
|
[](LICENSE)
|
|
11
11
|
|
|
12
12
|
**English** | [简体中文](./README_zh.md)
|
|
@@ -37,11 +37,11 @@ Late at night, mid-way through fixing a bug, you spot a second issue in `src/aut
|
|
|
37
37
|
2. **`TODO.md` Pollutes Git & Burns Context Tokens**: Tracking personal reminders inside repository markdown files dirties `git status` and re-injects stale checklists into the system prompt on every turn.
|
|
38
38
|
3. **Splash Banners Vanish When You Start Typing**: Traditional startup widgets disappear on the first message. For the rest of a two-hour session, your notes are hidden unless you manually run a slash command.
|
|
39
39
|
|
|
40
|
-
**Anchor acts like a physical sticky note stuck to the bottom bezel of your Pi editor**:
|
|
41
|
-
- **
|
|
40
|
+
**Anchor acts like a physical sticky note and auto-save checkpoint stuck to the bottom bezel of your Pi editor**:
|
|
41
|
+
- **Auto-Todo Bridge (`todo` ➔ Cross-Session Checkpoint)**: Standard `todo` tools (`@juicesharp/rpiv-todo` and built-in `todo`) live only inside a single conversation with zero disk writes — closing the terminal or running `/new` wipes unfinished tasks. Anchor automatically mirrors unfinished `todo` snapshots to disk (`[■■□□ 2/5]`) and auto-clears them the moment all steps reach `completed`.
|
|
42
|
+
- **Persistent Below-Editor Strip (`belowEditor`)**: Docks a single-line note strip directly beneath your input box. It stays in view while you code, leaves `aboveEditor` untouched for extensions like `pi-lingual` and `rpiv-todo`, and collapses to 0 lines when empty.
|
|
42
43
|
- **0ms Human-First Capture (`pin: ...`)**: Type `pin: Check token expiry` (or `/pin ...`) straight into the editor. An input hook intercepts the text in `0.00ms` without calling the language model or spending tokens.
|
|
43
|
-
- **
|
|
44
|
-
- **Session Teleport (`↪ Jump to session`)**: Every note bookmarks the exact `.jsonl` session file where it was created or last touched. Select `↪ Jump to session` in `/anchor` to switch Pi straight back to that conversation.
|
|
44
|
+
- **Session Teleport (`/back` & `↪ Jump to session`)**: Every note and auto-checkpoint bookmarks the exact `.jsonl` session file where work happened. Run `/back` (or select `↪ Jump to session` in `/anchor`) to switch Pi straight back to that conversation.
|
|
45
45
|
|
|
46
46
|
---
|
|
47
47
|
|
|
@@ -49,9 +49,10 @@ Late at night, mid-way through fixing a bug, you spot a second issue in `src/aut
|
|
|
49
49
|
|
|
50
50
|
| Mode / Action | What You Input | What the Engine Does | What You See | Best For |
|
|
51
51
|
| :--- | :--- | :--- | :--- | :--- |
|
|
52
|
+
| **Auto-Todo Checkpoint** | Agent calls `todo` (`2/5` done, then you quit or `/new`) | Mirrors unfinished `todo` snapshot + modified files + `sessionFile`; auto-clears at `5/5` | Next session shows: `⌖ #1 [■■□□ 2/5] Switch token ▸ Add tests (↪)` | Never losing unfinished `todo` lists across terminal restarts |
|
|
52
53
|
| **Stick a Note (0ms)** | Type `pin: Fix auth in src/auth.ts` or `/pin ...` | Intercepts via input hook in `0ms`, extracts file paths, and links current `sessionFile` | 1-line strip appears below editor: `⌖ #1 Fix auth (app · auth.ts ↪)` | Capturing a tangent without derailing the current prompt |
|
|
53
54
|
| **Track Multi-Step Work** | Type `/pin Refactor: 1. API 2. Cookie 3. Tests` then `/step 1` | Parses step list and increments `done / total` counter in `~/.anchor/state.json` | Strip updates in place: `⌖ #1 [■■□□ 2/3] Refactor ▸ Tests` | Multi-session features and long-running study goals |
|
|
54
|
-
| **Resume Context (
|
|
55
|
+
| **Resume Context (`/back`)** | Run `/back` (or `/anchor` ➔ `↪ Jump to session`) | Invokes Pi's native `ctx.switchSession(note.sessionFile)` | Terminal switches directly back to the original conversation transcript | Picking up a complex task days later with full history |
|
|
55
56
|
| **Auto-Tear on Commit** | Commit code (`git commit -m "feat: fix auth"`) or run `/done 1` | Matches commit message against active notes (filtering generic stop-words) | Note archives to `archive.jsonl`; strip clears (`⌖ Auto-torn off #1`) | Keeping the sticky strip clean without manual bookkeeping |
|
|
56
57
|
|
|
57
58
|
---
|
|
@@ -90,7 +91,7 @@ All state lives in `~/.anchor/` outside your git repository:
|
|
|
90
91
|
| **Pi Slash** | `/pin <note>` | Stick a note (supports `[1/4]` or `1. A 2. B 3. C` steps) |
|
|
91
92
|
| **Pi Slash** | `/anchor` | Open interactive sticky note board (tear off, `+1` step, `↪` jump to session, or read details) |
|
|
92
93
|
| **Pi Slash** | `/step 1` | Advance progress bar on note `#1` by `+1` |
|
|
93
|
-
| **Pi Slash** | `/anchor jump 1` |
|
|
94
|
+
| **Pi Slash** | `/back` (or `/anchor jump 1`) | Teleport Pi back to the conversation session of the latest unfinished note/todo checkpoint |
|
|
94
95
|
| **Pi Slash** | `/done 1` / `/anchor 1` | Tear off note `#1` and archive it |
|
|
95
96
|
| **Pi Slash** | `/anchor undo` | Put back the most recently torn-off note |
|
|
96
97
|
| **Terminal CLI** | `anchor` / `anchor "<note>"` | View or stick notes from any shell outside Pi |
|
|
@@ -111,12 +112,25 @@ In `v0.4` and `v0.5`, I threw away 60% of the codebase and rebuilt Anchor around
|
|
|
111
112
|
|
|
112
113
|
```bash
|
|
113
114
|
npm run typecheck # Strict tsc --noEmit check (0 errors)
|
|
114
|
-
npm test # Native Node test runner (
|
|
115
|
+
npm test # Native Node test runner (11/11 passing)
|
|
115
116
|
npm run build # Dual ESM/CJS compilation via tsup + declaration emit
|
|
116
117
|
```
|
|
117
118
|
|
|
118
119
|
---
|
|
119
120
|
|
|
121
|
+
## Ecosystem: Pi Terminal Suite
|
|
122
|
+
|
|
123
|
+
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:
|
|
124
|
+
|
|
125
|
+
| Package | Terminal Slot & Mechanism | Install |
|
|
126
|
+
| :--- | :--- | :--- |
|
|
127
|
+
| **[`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` |
|
|
128
|
+
| **[`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` |
|
|
129
|
+
| **[`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` |
|
|
130
|
+
| **[`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` |
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
120
134
|
## License
|
|
121
135
|
|
|
122
136
|
MIT © [Jason Song](https://github.com/3ZEROS12)
|
package/README_zh.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# ⌖ Anchor
|
|
2
2
|
|
|
3
|
-
> **贴在 Pi
|
|
3
|
+
> **贴在 Pi 输入框正下方的跨会话电子便签与 `todo` 自动存档点 —— 关终端自动接管未完 `todo` 进度 `[■■□□ 2/5]`,敲 `便签: ...` 0ms 瞬贴支线灵感,敲 `/back` 一键穿越回原对话,`git commit` 自动撕便签。**
|
|
4
4
|
|
|
5
|
+
[](https://www.npmjs.com/package/pi-anchor)
|
|
6
|
+
[](https://www.npmjs.com/package/pi-anchor)
|
|
7
|
+
[](https://github.com/3ZEROS12/anchor/stargazers)
|
|
5
8
|
[](https://github.com/3ZEROS12/anchor/actions/workflows/ci.yml)
|
|
6
|
-
[](tsconfig.json)
|
|
8
|
-
[](tests/store.test.ts)
|
|
9
|
-
[](package.json)
|
|
9
|
+
[](tests/store.test.ts)
|
|
10
10
|
[](LICENSE)
|
|
11
11
|
|
|
12
12
|
[English](./README.md) | **简体中文**
|
|
@@ -37,11 +37,11 @@ pi install npm:pi-anchor
|
|
|
37
37
|
2. **`TODO.md` 弄脏 Git 且每轮白烧 Token**:在项目根目录维护待办文件,既制造杂乱的 Git 修改记录,又在每一轮对话里反复把旧清单塞进系统提示词。
|
|
38
38
|
3. **开局闪现横幅一打字就消失**:普通插件只在打开终端时闪一下提示,你发完第一句话它就自我隐藏。接下来两个小时写代码,除非专门敲命令查询,否则根本看不见自己记了什么。
|
|
39
39
|
|
|
40
|
-
**Anchor
|
|
41
|
-
-
|
|
42
|
-
-
|
|
43
|
-
-
|
|
44
|
-
- **原对话一键穿越 (`↪ Jump to session`)
|
|
40
|
+
**Anchor 与单次对话内的 `todo` 形成了天然互补:它是贴在输入框下沿的「跨会话自动存档点 + 随手便签」**:
|
|
41
|
+
- **自动接管未完 `todo`(Auto-Todo Bridge)**:现有的 `todo` 工具(如 `@juicesharp/rpiv-todo` 与内置 `todo`)只存在于单次对话分支中、不写磁盘,一旦关掉终端或敲 `/new` 开启新会话,没跑完的 `todo` 清单就会全部清空。Anchor 会自动监听 `todo` 进度,把未走完的步骤(如 `[■■□□ 2/5]`)与当前修改的文件自动存为跨会话断点;当同一会话内 `todo` 全部完成(`5/5`)时又会自动清理,全程零手动操作。
|
|
42
|
+
- **输入框正下方常驻 1 行 (`belowEditor`)**:便签条固定在输入框下沿,写代码时抬眼可见;输入框上方空间完全留给 `pi-lingual` 与 `rpiv-todo`;没有便签时自动收起为 0 行。
|
|
43
|
+
- **自己随手敲,0ms 瞬贴 (`便签: ...`)**:直接在输入框敲 `便签: 检查网关超时重试`(或 `pin: ...`、`/pin ...`),输入钩子在 `0.00ms` 内直接拦截上屏,不调用大模型、不耗费 1 个 Token。
|
|
44
|
+
- **原对话一键穿越 (`/back` & `↪ Jump to session`)**:每张便签与自动断点都会记录当时的会话文件路径(`sessionFile`)。敲一句 `/back`(或在 `/anchor` 面板中点击 `↪ Jump to session`),直接切回当时那场对话。
|
|
45
45
|
|
|
46
46
|
---
|
|
47
47
|
|
|
@@ -49,9 +49,10 @@ pi install npm:pi-anchor
|
|
|
49
49
|
|
|
50
50
|
| 操作场景 | 你的输入 | 后台执行逻辑 | 界面即时反馈 | 适用时刻 |
|
|
51
51
|
| :--- | :--- | :--- | :--- | :--- |
|
|
52
|
+
| **未完 `todo` 自动存档** | AI 调用 `todo` 做到 `2/5` 时你关掉终端或敲 `/new` | 自动同步未完 `todo` 步骤、已改文件与 `sessionFile`;若做到 `5/5` 则自动销毁 | 下次打开终端直接显示:`⌖ #1 [■■□□ 2/5] 替换Cookie ▸ 补单测 (↪)` | 弥补单会话 `todo` 关窗即失忆的短板,实现零操作跨会话续传 |
|
|
52
53
|
| **0ms 随手贴便签** | 输入框敲 `便签: 把 src/auth.ts 改用 Cookie` | `input` 钩子 0ms 拦截,提取文件路径并记录当前 `sessionFile` | 输入框下方立即浮现:`⌖ #1 把 src/auth.ts 改用 Cookie (app · auth.ts ↪)` | 改代码中途冒出支线念头,不想打断当前对话 |
|
|
53
54
|
| **分步推进长任务** | 敲 `/pin 重构: 1. 接口 2. Cookie 3. 单测` 后敲 `/step 1` | 解析分步列表并在 `~/.anchor/state.json` 推进完成步数 | 下方便签条实时变为:`⌖ #1 [■■□□ 2/3] 重构 ▸ 单测` | 跨天重构、分步排障或刷课程章节 |
|
|
54
|
-
| **切回原对话 (
|
|
55
|
+
| **切回原对话 (`/back`)** | 敲 `/back`(或 `/anchor` 选中便签点 `↪ Jump to session`) | 调用 Pi 原生 `ctx.switchSession(note.sessionFile)` | 终端直接切回贴这张便签时的完整历史对话上下文 | 隔了几天回来继续施工,需要找回当时的讨论细节 |
|
|
55
56
|
| **提交代码自动撕** | 执行 `git commit -m "feat: 登录改用 Cookie"` 或敲 `/done 1` | 过滤通用停用词后比对提交信息与便签关键词 | 便签自动归档至 `archive.jsonl`,下方便签条同步消失 | 写完代码顺手提交,免去手动打勾销账 |
|
|
56
57
|
|
|
57
58
|
---
|
|
@@ -90,7 +91,7 @@ Anchor 显式指定 `{ placement: "belowEditor" }` 挂载在输入框下方,
|
|
|
90
91
|
| **Pi 斜杠命令** | `/pin <内容>` | 贴一张新便签(支持写 `[1/4]` 或 `1. A 2. B 3. C` 自动生成进度条) |
|
|
91
92
|
| **Pi 斜杠命令** | `/anchor` | 打开交互面板(回车可撕便签、推进度 `+1`、`↪` 切回原对话 Session、看详情) |
|
|
92
93
|
| **Pi 斜杠命令** | `/step 1` | 将 `#1` 便签的进度条往前推进 `+1` 步 |
|
|
93
|
-
| **Pi 斜杠命令** | `/anchor jump 1
|
|
94
|
+
| **Pi 斜杠命令** | `/back`(或 `/anchor jump 1`) | 一键切换回最近一条未完成便签/`todo` 断点对应的历史对话 (`switchSession`) |
|
|
94
95
|
| **Pi 斜杠命令** | `/done 1` / `/anchor 1` | 按编号 `#1` 直接撕下便签并归档 |
|
|
95
96
|
| **Pi 斜杠命令** | `/anchor undo` | 把刚刚撕下的便签重新贴回原位 |
|
|
96
97
|
| **系统命令行** | `anchor` / `anchor "<内容>"` | 不进 Pi 也能在外部终端查看或贴便签 |
|
|
@@ -111,12 +112,25 @@ Anchor 显式指定 `{ placement: "belowEditor" }` 挂载在输入框下方,
|
|
|
111
112
|
|
|
112
113
|
```bash
|
|
113
114
|
npm run typecheck # TypeScript 严格类型检查 (0 错误)
|
|
114
|
-
npm test # 原生 Node 测试运行器 (
|
|
115
|
+
npm test # 原生 Node 测试运行器 (11/11 全绿通过)
|
|
115
116
|
npm run build # tsup 双格式打包与类型声明生成
|
|
116
117
|
```
|
|
117
118
|
|
|
118
119
|
---
|
|
119
120
|
|
|
121
|
+
## 配套生态套件 (Pi Terminal Suite)
|
|
122
|
+
|
|
123
|
+
四款扩展分别挂载于终端的不同物理槽位(`aboveEditor`、`belowEditor`、`status-only` 底栏与运行时工具钩子),独立运行或组合安装均不产生小部件重叠:
|
|
124
|
+
|
|
125
|
+
| 插件仓库 | 终端挂载槽位与核心机制 | 一键安装 |
|
|
126
|
+
| :--- | :--- | :--- |
|
|
127
|
+
| **[`pi-lingual`](https://github.com/3ZEROS12/pi-lingual)** | `二 ⇄ two` `aboveEditor` 双语伴学视窗(≤9 行 UAX #11 树状排版)与母语提示词转写(0 会话历史 Token 注入) | `pi install npm:pi-lingual` |
|
|
128
|
+
| **[`toolflow`](https://github.com/3ZEROS12/toolflow)** | `⌬` 运行时工具钩子 —— 按阶段动态挂载重型 MCP,>40 行终端长日志自动落盘脱水(削减 95% 日志 Token) | `pi install npm:toolflow` |
|
|
129
|
+
| **[`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` |
|
|
130
|
+
| **[`pi-anchor`](https://github.com/3ZEROS12/anchor)** *(当前)* | `⌖` `belowEditor` 1 行常驻便签条、0ms `pin:` 输入拦截、迷你进度条 `[■■□□]` 与原会话跳转 `↪` | `pi install npm:pi-anchor` |
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
120
134
|
## License
|
|
121
135
|
|
|
122
136
|
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
|
|
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}
|
|
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
|
|
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
|
|
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
|
|
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 };
|
package/dist/extension.cjs
CHANGED
|
@@ -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
|
|
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}
|
|
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
|
|
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 = {
|
package/dist/extension.d.cts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import '@earendil-works/pi-coding-agent';
|
|
2
|
-
export {
|
|
2
|
+
export { B as default, u as parseAnchorCommandInput, v as parseQuickNoteInput, y as resolveAnchorTarget } from './extension-09AVvgl-.cjs';
|
package/dist/extension.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import '@earendil-works/pi-coding-agent';
|
|
2
|
-
export {
|
|
2
|
+
export { B as default, u as parseAnchorCommandInput, v as parseQuickNoteInput, y as resolveAnchorTarget } from './extension-09AVvgl-.js';
|
package/dist/extension.js
CHANGED
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
|
|
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}
|
|
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
|
|
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-
|
|
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
|
|
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-
|
|
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
|
|
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
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-anchor",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.6.1",
|
|
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-
|
|
197
|
-
//
|
|
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)
|
|
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}${
|
|
301
|
+
? ` [${a.progress.done}/${a.progress.total}${nextStep}]`
|
|
297
302
|
: '';
|
|
298
303
|
const desc = a.description ? ` — ${a.description}` : '';
|
|
299
|
-
return `• #${num}
|
|
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
|
|
230
|
-
?
|
|
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(
|
|
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 */
|