pi-anchor 0.2.0 → 0.2.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 +46 -28
- package/README_zh.md +54 -32
- package/dist/{chunk-T62PHIQI.js → chunk-B652T7QR.js} +50 -14
- package/dist/extension.cjs +53 -17
- package/dist/extension.js +5 -4
- package/dist/index.cjs +50 -14
- package/dist/index.js +1 -1
- package/package.json +5 -4
- package/src/extension.ts +4 -4
- package/src/tui.ts +58 -16
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
</a>
|
|
22
22
|
<img src="https://img.shields.io/badge/Node-v20+-22c55e.svg" alt="Node v20+">
|
|
23
23
|
<img src="https://img.shields.io/badge/TypeScript-Strict-3b82f6.svg" alt="TypeScript Strict">
|
|
24
|
-
<img src="https://img.shields.io/badge/Tests-
|
|
24
|
+
<img src="https://img.shields.io/badge/Tests-26%20Passed-22c55e.svg" alt="Tests: 26 Passed">
|
|
25
25
|
<img src="https://img.shields.io/badge/Startup-%3C%2090ms-success.svg" alt="Startup: < 90ms">
|
|
26
26
|
<img src="https://img.shields.io/badge/Storage-Zero%20Repo%20Pollution-success.svg" alt="Zero Repo Pollution">
|
|
27
27
|
<img src="https://img.shields.io/badge/License-MIT-f97316.svg" alt="License: MIT">
|
|
@@ -234,40 +234,58 @@ Engineered with strict TypeScript and 100% standard library test coverage using
|
|
|
234
234
|
```bash
|
|
235
235
|
npm run build # Dual ESM/CJS compilation via tsup + declaration emit
|
|
236
236
|
npm run typecheck # Strict tsc --noEmit check (0 errors)
|
|
237
|
-
npm test # Native Node test runner (
|
|
237
|
+
npm test # Native Node test runner (26/26 passing)
|
|
238
238
|
```
|
|
239
239
|
|
|
240
240
|
```text
|
|
241
|
-
✔
|
|
242
|
-
✔
|
|
243
|
-
✔
|
|
244
|
-
✔
|
|
245
|
-
✔
|
|
246
|
-
✔
|
|
247
|
-
✔
|
|
248
|
-
✔
|
|
249
|
-
✔
|
|
250
|
-
✔
|
|
251
|
-
✔
|
|
252
|
-
✔
|
|
253
|
-
✔
|
|
254
|
-
✔
|
|
255
|
-
✔
|
|
256
|
-
✔ AnchorStore -
|
|
257
|
-
✔ AnchorStore -
|
|
258
|
-
✔ AnchorStore -
|
|
259
|
-
✔ AnchorStore -
|
|
260
|
-
✔
|
|
261
|
-
✔ AnchorTUI -
|
|
262
|
-
✔ AnchorTUI -
|
|
263
|
-
✔ AnchorTUI -
|
|
264
|
-
✔ AnchorTUI -
|
|
265
|
-
|
|
266
|
-
|
|
241
|
+
✔ ContextInjector - renders only active anchors, sleeping consume 0 tokens
|
|
242
|
+
✔ ContextInjector - renders targetDate and task aging in prompt
|
|
243
|
+
✔ ContextInjector - renders preflight failure hints into cold-start context
|
|
244
|
+
✔ AnchorDecay - status evaluation transitions
|
|
245
|
+
✔ AnchorDecay - sweepStore transitions and graveyard eviction
|
|
246
|
+
✔ AnchorMatcher - exact, prefix, and tag matching
|
|
247
|
+
✔ AnchorMatcher - glob pattern matching (*.ts, src/**/*.ts)
|
|
248
|
+
✔ AnchorMatcher - findMatchedAnchors prioritizes high-confidence & high-priority
|
|
249
|
+
✔ SessionTouchObserver - records read, edit, write and commits
|
|
250
|
+
✔ SessionTouchObserver - matches CJK commit messages with segmentation
|
|
251
|
+
✔ SessionTouchObserver - separates read inspection from edit mutation
|
|
252
|
+
✔ Lock - acquireSyncLock acquires, holds, and releases exclusive lockfile
|
|
253
|
+
✔ Lock - acquireSyncLock safely reclaims stale lock from dead PID
|
|
254
|
+
✔ Context - makeSafeTaskAnnotation produces language-accurate comments and skips JSON
|
|
255
|
+
✔ Protocol - AnchorProtocol lifecycle handles cold-start and safe JIT
|
|
256
|
+
✔ AnchorStore - basic CRUD & atomic writes
|
|
257
|
+
✔ AnchorStore - corrupt state recovery
|
|
258
|
+
✔ AnchorStore - daily recurring task completes for today and wakes tomorrow
|
|
259
|
+
✔ AnchorStore - temporal durability keywords and project detection
|
|
260
|
+
✔ AnchorStore - atomicRenameWithRetry successfully replaces files atomically
|
|
261
|
+
✔ AnchorTUI - status bar reflects active state cleanly with ⌖ N
|
|
262
|
+
✔ AnchorTUI - formatTargetDate and formatCreationTime render clear dual-timeline
|
|
263
|
+
✔ AnchorTUI - openAnchorDashboard renders Plan 2 Neovim/Geek layout with folder metadata
|
|
264
|
+
✔ AnchorTUI - getDisplayWidth and padToWidth properly align CJK full-width columns
|
|
265
|
+
✔ AnchorTUI - updateStartupBanner renders clean widget above editor
|
|
266
|
+
✔ AnchorTUI - truncateToWidth properly handles ANSI escape sequences and bounds width
|
|
267
|
+
|
|
268
|
+
ℹ pass 26, fail 0
|
|
267
269
|
```
|
|
268
270
|
|
|
269
271
|
---
|
|
270
272
|
|
|
273
|
+
## Author's Note
|
|
274
|
+
|
|
275
|
+
The motivation behind Anchor came from a recurring headache during pair-programming with AI coding agents: watching `TODO.md` and `AGENTS.md` rot inside my git trees.
|
|
276
|
+
|
|
277
|
+
Every time a task remained unfinished across sessions, writing it to a project markdown file felt harmless at first. But over weeks of work, two frictions became unbearable:
|
|
278
|
+
1. In multi-turn sessions, the AI dragged dozens of stagnant task lines into the prompt on every single turn, burning hundreds of tokens while diluting the model's focus on the actual code being edited.
|
|
279
|
+
2. Merge conflicts on task checklists in Git commits polluted the repository's history with irrelevant bookkeeping noise.
|
|
280
|
+
|
|
281
|
+
When I looked at existing solutions, the trade-offs felt disproportionate. Stuffing a 200MB Dolt SQL relational database into the repository root (`beads`), or running a background Go daemon with SQLite and continuous vector retrieval (`engram`), inverted the relationship between tool and developer. A developer task tracker should not demand more system resources than the application being built.
|
|
282
|
+
|
|
283
|
+
Anchor takes the opposite approach: store state outside the project tree in a lean global ledger (`~/.anchor/state.json`), inject tasks only on Turn 1 of a cold start, keep Turn 2+ at exactly 0 tokens, and auto-settle upon git commits or session exit.
|
|
284
|
+
|
|
285
|
+
I hope Anchor helps keep your project trees clean and your focus where it belongs: on the code.
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
271
289
|
## License
|
|
272
290
|
|
|
273
291
|
MIT License © 2025 [Jason Song (@3ZEROS12)](https://github.com/3ZEROS12)
|
package/README_zh.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
4
|
<strong>面向 AI 编程智能体的零污染、零闲置 Token 跨会话任务记忆。</strong><br>
|
|
5
|
-
<em>告别发霉的 TODO.md。零 Git 历史污染,0 闲置 Token
|
|
5
|
+
<em>告别发霉的 TODO.md。零 Git 历史污染,0 闲置 Token 损耗,代码提交自动核销结算。</em>
|
|
6
6
|
</p>
|
|
7
7
|
|
|
8
8
|
<p align="center">
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
</a>
|
|
22
22
|
<img src="https://img.shields.io/badge/Node-v20+-22c55e.svg" alt="Node v20+">
|
|
23
23
|
<img src="https://img.shields.io/badge/TypeScript-Strict-3b82f6.svg" alt="TypeScript Strict">
|
|
24
|
-
<img src="https://img.shields.io/badge/Tests-
|
|
24
|
+
<img src="https://img.shields.io/badge/Tests-26%20通过-22c55e.svg" alt="Tests: 26 Passed">
|
|
25
25
|
<img src="https://img.shields.io/badge/启动耗时-%3C%2090ms-success.svg" alt="启动耗时: < 90ms">
|
|
26
26
|
<img src="https://img.shields.io/badge/存储-零仓库污染-success.svg" alt="零仓库污染">
|
|
27
27
|
<img src="https://img.shields.io/badge/开源协议-MIT-f97316.svg" alt="License: MIT">
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
使用终端编程智能体(Claude Code、Pi、Aider 等)进行实际项目开发时,开发者普遍面临三个摩擦:
|
|
39
39
|
|
|
40
40
|
1. **终端退出即失忆**:按下 `Ctrl+C` 退出终端,Agent 内存中的上下文被全部清空。未完成的代码重构计划、临时交代的排期与多步约定瞬间丢失。
|
|
41
|
-
2. **TODO.md 污染与 Token 泥潭**:直接在工程根目录写 `TODO.md` 或 `AGENTS.md
|
|
41
|
+
2. **TODO.md 污染与 Token 泥潭**:直接在工程根目录写 `TODO.md` 或 `AGENTS.md`,会产生大量 Git 杂质提交,且**每一轮对话都要把几十行陈旧清单塞入 System Prompt**,白白消耗大量 Token 预算,并分散模型对当前核心代码的注意力。
|
|
42
42
|
3. **静态清单无限腐烂**:写完代码后,工程师极少会专程打开 Markdown 逐个打勾。未勾选的旧任务烂在仓库里,越积越多。
|
|
43
43
|
|
|
44
44
|
现有的解决方案大多走向了两个重型极端:
|
|
@@ -122,7 +122,7 @@ Anchor 从两个正交的时间维度管理任务,兼顾“何时要交”与
|
|
|
122
122
|
* **`[Habits]`(每日循环)**:日常例行循环任务。当天打勾后隐身,明日零点自动复苏。
|
|
123
123
|
* **`[Backlog]`(长期备忘)**:无指定日期的宏观架构愿景(`Someday`)。
|
|
124
124
|
|
|
125
|
-
### 5.
|
|
125
|
+
### 5. 零心智负担的自动核销与生命周期结算
|
|
126
126
|
* **Git Commit 自动语义匹配**:内置基于标准 `Intl.Segmenter` 的多语系分词引擎。代码提交信息(如 `git commit -m "fix(auth): migrate to HttpOnly cookies"`)命中任务描述时自动核销结案。
|
|
127
127
|
* **退出终端一键确认**:检测到本会话修改了任务关联的文件时,退出终端前弹出极简单键确认:
|
|
128
128
|
```text
|
|
@@ -170,7 +170,7 @@ Anchor 从两个正交的时间维度管理任务,兼顾“何时要交”与
|
|
|
170
170
|
| **Token 损耗** | 每轮中/高消耗 | 高(注入长文历史) | 极高(陈旧文本成倍累积) | **0 Token 闲置(仅文件触碰 JIT 唤醒)** |
|
|
171
171
|
| **JIT 语法安全** | 不支持文件注释 | 无语法感知 | 静态文本无防护 | **精确匹配 30+ 语言注释,跳过 JSON** |
|
|
172
172
|
| **任务时效** | 单层平铺 | 单层平铺 | 静态复选框 | **双时间维度(交付排期 + 立项时间)** |
|
|
173
|
-
|
|
|
173
|
+
| **任务结算与核销** | 手动命令 `close` | 无自动结算机制 | 人工修改文本打勾 | **Git Commit 自动识别 + 退出前一键核销** |
|
|
174
174
|
| **衰减清理** | 手动 prune | 无衰退机制 | 长期堆积发霉 | **静默衰退(临时任务 48h 自动清理)** |
|
|
175
175
|
| **启动耗时** | 重型 CLI 初始化 | Go 守护进程常驻 | 无 | **< 90ms 预打包瞬时秒开** |
|
|
176
176
|
| **环境依赖** | 外部 Dolt 二进制 | 外部 Go 编译产物 | 无 | **零原生外部依赖(纯 TypeScript)** |
|
|
@@ -233,40 +233,62 @@ pi install npm:pi-anchor
|
|
|
233
233
|
```bash
|
|
234
234
|
npm run build # tsup 双格式打包 (ESM/CJS) 与 .d.ts 类型生成
|
|
235
235
|
npm run typecheck # TypeScript 严格类型检查 (0 错误)
|
|
236
|
-
npm test # 全套自动化单元测试 (
|
|
236
|
+
npm test # 全套自动化单元测试 (26/26 全绿通过)
|
|
237
237
|
```
|
|
238
238
|
|
|
239
239
|
```text
|
|
240
|
-
✔
|
|
241
|
-
✔
|
|
242
|
-
✔
|
|
243
|
-
✔
|
|
244
|
-
✔
|
|
245
|
-
✔
|
|
246
|
-
✔
|
|
247
|
-
✔
|
|
248
|
-
✔
|
|
249
|
-
✔
|
|
250
|
-
✔
|
|
251
|
-
✔
|
|
252
|
-
✔
|
|
253
|
-
✔
|
|
254
|
-
✔
|
|
255
|
-
✔ AnchorStore -
|
|
256
|
-
✔ AnchorStore -
|
|
257
|
-
✔ AnchorStore -
|
|
258
|
-
✔ AnchorStore -
|
|
259
|
-
✔
|
|
260
|
-
✔ AnchorTUI -
|
|
261
|
-
✔ AnchorTUI -
|
|
262
|
-
✔ AnchorTUI -
|
|
263
|
-
✔ AnchorTUI -
|
|
264
|
-
|
|
265
|
-
|
|
240
|
+
✔ ContextInjector - renders only active anchors, sleeping consume 0 tokens
|
|
241
|
+
✔ ContextInjector - renders targetDate and task aging in prompt
|
|
242
|
+
✔ ContextInjector - renders preflight failure hints into cold-start context
|
|
243
|
+
✔ AnchorDecay - status evaluation transitions
|
|
244
|
+
✔ AnchorDecay - sweepStore transitions and graveyard eviction
|
|
245
|
+
✔ AnchorMatcher - exact, prefix, and tag matching
|
|
246
|
+
✔ AnchorMatcher - glob pattern matching (*.ts, src/**/*.ts)
|
|
247
|
+
✔ AnchorMatcher - findMatchedAnchors prioritizes high-confidence & high-priority
|
|
248
|
+
✔ SessionTouchObserver - records read, edit, write and commits
|
|
249
|
+
✔ SessionTouchObserver - matches CJK commit messages with segmentation
|
|
250
|
+
✔ SessionTouchObserver - separates read inspection from edit mutation
|
|
251
|
+
✔ Lock - acquireSyncLock acquires, holds, and releases exclusive lockfile
|
|
252
|
+
✔ Lock - acquireSyncLock safely reclaims stale lock from dead PID
|
|
253
|
+
✔ Context - makeSafeTaskAnnotation produces language-accurate comments and skips JSON
|
|
254
|
+
✔ Protocol - AnchorProtocol lifecycle handles cold-start and safe JIT
|
|
255
|
+
✔ AnchorStore - basic CRUD & atomic writes
|
|
256
|
+
✔ AnchorStore - corrupt state recovery
|
|
257
|
+
✔ AnchorStore - daily recurring task completes for today and wakes tomorrow
|
|
258
|
+
✔ AnchorStore - temporal durability keywords and project detection
|
|
259
|
+
✔ AnchorStore - atomicRenameWithRetry successfully replaces files atomically
|
|
260
|
+
✔ AnchorTUI - status bar reflects active state cleanly with ⌖ N
|
|
261
|
+
✔ AnchorTUI - formatTargetDate and formatCreationTime render clear dual-timeline
|
|
262
|
+
✔ AnchorTUI - openAnchorDashboard renders Plan 2 Neovim/Geek layout with folder metadata
|
|
263
|
+
✔ AnchorTUI - getDisplayWidth and padToWidth properly align CJK full-width columns
|
|
264
|
+
✔ AnchorTUI - updateStartupBanner renders clean widget above editor
|
|
265
|
+
✔ AnchorTUI - truncateToWidth properly handles ANSI escape sequences and bounds width
|
|
266
|
+
|
|
267
|
+
ℹ pass 26, fail 0
|
|
266
268
|
```
|
|
267
269
|
|
|
268
270
|
---
|
|
269
271
|
|
|
272
|
+
## 作者手记 (Author's Note)
|
|
273
|
+
|
|
274
|
+
写 Anchor 的最初冲动,来自于我用终端 AI 编程智能体结对开发时经历的一场持续阵痛:看着项目根目录下的 `TODO.md` 与 `AGENTS.md` 在 Git 历史里发霉。
|
|
275
|
+
|
|
276
|
+
很多时候,写完一段代码已经是深夜,跨会话未完成的任务随手记在项目的 Markdown 里,看起来省事。然而几个月下来,两点物理摩擦让我极其痛苦:
|
|
277
|
+
1. 每一轮对话交互,AI 都要把陈旧的任务清单整段塞进系统提示词,白白烧掉几十上百行 Token,还会分散模型对当前正在修改的代码的注意力;
|
|
278
|
+
2. 团队多人协作或分支合并时,这类任务清单文件频发 Git 合并冲突,无端污染了核心代码的提交历史。
|
|
279
|
+
|
|
280
|
+
调研现存工具时,我发现社区给出的方案走向了沉重的极端:为了管理几个简单的任务,有的方案直接在代码仓库根目录塞入一个超过 200MB 的 Dolt SQL 关系型数据库(`beads`),有的方案在后台常驻一个用 Go 编写的 SQLite 守护进程并做持续向量检索(`engram`)。一个任务追踪工具消耗的系统资源与认知负担,甚至超过了开发者正在编写的业务代码本身。
|
|
281
|
+
|
|
282
|
+
这颠倒了工具与生产者的关系。
|
|
283
|
+
|
|
284
|
+
Anchor 选择了一条极度克制的工程路径:把状态移出代码仓库,托管在用户主目录下的单文件轻量状态表(`~/.anchor/state.json`);首轮对话注入冷启动上下文,第 2 轮起保持 0 Token 损耗;通过语言对应的语法安全注释在触碰代码时精准唤醒,并在代码提交或会话退出时自动核销。
|
|
285
|
+
|
|
286
|
+
在这次更新中,我们还为 `/anchor` 增加了防误触详情面板,按回车可以从容查看关联文件与执行命令,彻底消灭误敲回车直接结算的焦虑。
|
|
287
|
+
|
|
288
|
+
希望 Anchor 能让你的代码仓库保持干净,把专注力还给代码本身。
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
270
292
|
## 开源协议
|
|
271
293
|
|
|
272
294
|
MIT License © 2025 [Jason Song (@3ZEROS12)](https://github.com/3ZEROS12)
|
|
@@ -1066,8 +1066,9 @@ function groupAnchorsByQuadrant(anchors, now = Date.now()) {
|
|
|
1066
1066
|
}
|
|
1067
1067
|
return groups;
|
|
1068
1068
|
}
|
|
1069
|
+
var ANSI_REGEX = /\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g;
|
|
1069
1070
|
function stripAnsi(str) {
|
|
1070
|
-
return str.replace(
|
|
1071
|
+
return str.replace(ANSI_REGEX, "");
|
|
1071
1072
|
}
|
|
1072
1073
|
function getDisplayWidth(str) {
|
|
1073
1074
|
const clean = stripAnsi(str);
|
|
@@ -1086,22 +1087,36 @@ function getDisplayWidth(str) {
|
|
|
1086
1087
|
return width;
|
|
1087
1088
|
}
|
|
1088
1089
|
function truncateToWidth(str, maxWidth, ellipsis = "\u2026") {
|
|
1089
|
-
|
|
1090
|
-
|
|
1090
|
+
if (maxWidth <= 0) return "";
|
|
1091
|
+
const totalWidth = getDisplayWidth(str);
|
|
1092
|
+
if (totalWidth <= maxWidth) return str;
|
|
1091
1093
|
const ellipsisWidth = getDisplayWidth(ellipsis);
|
|
1092
|
-
const target = maxWidth - ellipsisWidth;
|
|
1093
|
-
if (target
|
|
1094
|
+
const target = Math.max(0, maxWidth - ellipsisWidth);
|
|
1095
|
+
if (target === 0) return ellipsis.slice(0, maxWidth);
|
|
1094
1096
|
let accumulated = "";
|
|
1095
1097
|
let accumWidth = 0;
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
+
let i = 0;
|
|
1099
|
+
while (i < str.length) {
|
|
1100
|
+
ANSI_REGEX.lastIndex = i;
|
|
1101
|
+
const match = ANSI_REGEX.exec(str);
|
|
1102
|
+
if (match && match.index === i) {
|
|
1103
|
+
accumulated += match[0];
|
|
1104
|
+
i += match[0].length;
|
|
1105
|
+
continue;
|
|
1106
|
+
}
|
|
1107
|
+
const codePoint = str.codePointAt(i) || 0;
|
|
1108
|
+
const charLen = codePoint > 65535 ? 2 : 1;
|
|
1109
|
+
const fullChar = str.slice(i, i + charLen);
|
|
1110
|
+
const charWidth = getDisplayWidth(fullChar);
|
|
1098
1111
|
if (accumWidth + charWidth > target) {
|
|
1099
1112
|
break;
|
|
1100
1113
|
}
|
|
1101
|
-
accumulated +=
|
|
1114
|
+
accumulated += fullChar;
|
|
1102
1115
|
accumWidth += charWidth;
|
|
1116
|
+
i += charLen;
|
|
1103
1117
|
}
|
|
1104
|
-
|
|
1118
|
+
accumulated = accumulated.replace(/\s*[\(\[\{(【]$/, "");
|
|
1119
|
+
return accumulated + ellipsis + (str.includes("\x1B") ? "\x1B[0m" : "");
|
|
1105
1120
|
}
|
|
1106
1121
|
function padToWidth(str, targetWidth) {
|
|
1107
1122
|
const truncated = truncateToWidth(str, targetWidth);
|
|
@@ -1254,14 +1269,35 @@ async function openAnchorDashboard(ctx, store) {
|
|
|
1254
1269
|
optionMap.set(label, a);
|
|
1255
1270
|
displayOptions.push(label);
|
|
1256
1271
|
}
|
|
1257
|
-
const selected = await ctx.ui.select("\u2316 Anchors (
|
|
1272
|
+
const selected = await ctx.ui.select("\u2316 Anchors (select to inspect or settle):", displayOptions);
|
|
1258
1273
|
if (!selected) return;
|
|
1259
1274
|
const anchor = optionMap.get(selected);
|
|
1260
1275
|
if (!anchor) return;
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1276
|
+
const action = await ctx.ui.select(
|
|
1277
|
+
`\u2316 Anchor #${anchor.id}: "${anchor.title}"`,
|
|
1278
|
+
[
|
|
1279
|
+
"\u2713 Settle (Mark completed and archive)",
|
|
1280
|
+
"\u2139 View details & associated files",
|
|
1281
|
+
"\u21A9 Cancel"
|
|
1282
|
+
]
|
|
1283
|
+
);
|
|
1284
|
+
if (!action || action.startsWith("\u21A9")) return;
|
|
1285
|
+
if (action.startsWith("\u2713")) {
|
|
1286
|
+
store.settle(anchor.id, { settledBy: "manual-command" });
|
|
1287
|
+
const successMsg = anchor.recurrence === "daily" ? `\u2316 Completed for today: "${anchor.title}" (resets tomorrow)` : `\u2316 Settled #${anchor.id}: "${anchor.title}" (/anchor undo to revert)`;
|
|
1288
|
+
ctx.ui.notify(successMsg, "info");
|
|
1289
|
+
updateAnchorStatusBar(ctx, store);
|
|
1290
|
+
} else if (action.startsWith("\u2139")) {
|
|
1291
|
+
const details = [
|
|
1292
|
+
`#${anchor.id} [${anchor.priority.toUpperCase()}] ${anchor.title}`,
|
|
1293
|
+
anchor.description ? `Description: ${anchor.description}` : null,
|
|
1294
|
+
anchor.targetDate ? `Target Date: ${anchor.targetDate} (${formatTargetDate(anchor)})` : null,
|
|
1295
|
+
anchor.files && anchor.files.length > 0 ? `Files: ${anchor.files.join(", ")}` : null,
|
|
1296
|
+
anchor.verifyCommand ? `Verify Command: ${anchor.verifyCommand}` : null,
|
|
1297
|
+
`Created: ${formatCreationTime(anchor.createdAt)} (in ${anchor.project || "global"})`
|
|
1298
|
+
].filter(Boolean).join("\n");
|
|
1299
|
+
ctx.ui.notify(details, "info");
|
|
1300
|
+
}
|
|
1265
1301
|
}
|
|
1266
1302
|
|
|
1267
1303
|
export {
|
package/dist/extension.cjs
CHANGED
|
@@ -1086,8 +1086,9 @@ function groupAnchorsByQuadrant(anchors, now = Date.now()) {
|
|
|
1086
1086
|
}
|
|
1087
1087
|
return groups;
|
|
1088
1088
|
}
|
|
1089
|
+
var ANSI_REGEX = /\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g;
|
|
1089
1090
|
function stripAnsi(str) {
|
|
1090
|
-
return str.replace(
|
|
1091
|
+
return str.replace(ANSI_REGEX, "");
|
|
1091
1092
|
}
|
|
1092
1093
|
function getDisplayWidth(str) {
|
|
1093
1094
|
const clean = stripAnsi(str);
|
|
@@ -1106,22 +1107,36 @@ function getDisplayWidth(str) {
|
|
|
1106
1107
|
return width;
|
|
1107
1108
|
}
|
|
1108
1109
|
function truncateToWidth(str, maxWidth, ellipsis = "\u2026") {
|
|
1109
|
-
|
|
1110
|
-
|
|
1110
|
+
if (maxWidth <= 0) return "";
|
|
1111
|
+
const totalWidth = getDisplayWidth(str);
|
|
1112
|
+
if (totalWidth <= maxWidth) return str;
|
|
1111
1113
|
const ellipsisWidth = getDisplayWidth(ellipsis);
|
|
1112
|
-
const target = maxWidth - ellipsisWidth;
|
|
1113
|
-
if (target
|
|
1114
|
+
const target = Math.max(0, maxWidth - ellipsisWidth);
|
|
1115
|
+
if (target === 0) return ellipsis.slice(0, maxWidth);
|
|
1114
1116
|
let accumulated = "";
|
|
1115
1117
|
let accumWidth = 0;
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
+
let i = 0;
|
|
1119
|
+
while (i < str.length) {
|
|
1120
|
+
ANSI_REGEX.lastIndex = i;
|
|
1121
|
+
const match = ANSI_REGEX.exec(str);
|
|
1122
|
+
if (match && match.index === i) {
|
|
1123
|
+
accumulated += match[0];
|
|
1124
|
+
i += match[0].length;
|
|
1125
|
+
continue;
|
|
1126
|
+
}
|
|
1127
|
+
const codePoint = str.codePointAt(i) || 0;
|
|
1128
|
+
const charLen = codePoint > 65535 ? 2 : 1;
|
|
1129
|
+
const fullChar = str.slice(i, i + charLen);
|
|
1130
|
+
const charWidth = getDisplayWidth(fullChar);
|
|
1118
1131
|
if (accumWidth + charWidth > target) {
|
|
1119
1132
|
break;
|
|
1120
1133
|
}
|
|
1121
|
-
accumulated +=
|
|
1134
|
+
accumulated += fullChar;
|
|
1122
1135
|
accumWidth += charWidth;
|
|
1136
|
+
i += charLen;
|
|
1123
1137
|
}
|
|
1124
|
-
|
|
1138
|
+
accumulated = accumulated.replace(/\s*[\(\[\{(【]$/, "");
|
|
1139
|
+
return accumulated + ellipsis + (str.includes("\x1B") ? "\x1B[0m" : "");
|
|
1125
1140
|
}
|
|
1126
1141
|
function padToWidth(str, targetWidth) {
|
|
1127
1142
|
const truncated = truncateToWidth(str, targetWidth);
|
|
@@ -1268,14 +1283,35 @@ async function openAnchorDashboard(ctx, store) {
|
|
|
1268
1283
|
optionMap.set(label, a);
|
|
1269
1284
|
displayOptions.push(label);
|
|
1270
1285
|
}
|
|
1271
|
-
const selected = await ctx.ui.select("\u2316 Anchors (
|
|
1286
|
+
const selected = await ctx.ui.select("\u2316 Anchors (select to inspect or settle):", displayOptions);
|
|
1272
1287
|
if (!selected) return;
|
|
1273
1288
|
const anchor = optionMap.get(selected);
|
|
1274
1289
|
if (!anchor) return;
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1290
|
+
const action = await ctx.ui.select(
|
|
1291
|
+
`\u2316 Anchor #${anchor.id}: "${anchor.title}"`,
|
|
1292
|
+
[
|
|
1293
|
+
"\u2713 Settle (Mark completed and archive)",
|
|
1294
|
+
"\u2139 View details & associated files",
|
|
1295
|
+
"\u21A9 Cancel"
|
|
1296
|
+
]
|
|
1297
|
+
);
|
|
1298
|
+
if (!action || action.startsWith("\u21A9")) return;
|
|
1299
|
+
if (action.startsWith("\u2713")) {
|
|
1300
|
+
store.settle(anchor.id, { settledBy: "manual-command" });
|
|
1301
|
+
const successMsg = anchor.recurrence === "daily" ? `\u2316 Completed for today: "${anchor.title}" (resets tomorrow)` : `\u2316 Settled #${anchor.id}: "${anchor.title}" (/anchor undo to revert)`;
|
|
1302
|
+
ctx.ui.notify(successMsg, "info");
|
|
1303
|
+
updateAnchorStatusBar(ctx, store);
|
|
1304
|
+
} else if (action.startsWith("\u2139")) {
|
|
1305
|
+
const details = [
|
|
1306
|
+
`#${anchor.id} [${anchor.priority.toUpperCase()}] ${anchor.title}`,
|
|
1307
|
+
anchor.description ? `Description: ${anchor.description}` : null,
|
|
1308
|
+
anchor.targetDate ? `Target Date: ${anchor.targetDate} (${formatTargetDate(anchor)})` : null,
|
|
1309
|
+
anchor.files && anchor.files.length > 0 ? `Files: ${anchor.files.join(", ")}` : null,
|
|
1310
|
+
anchor.verifyCommand ? `Verify Command: ${anchor.verifyCommand}` : null,
|
|
1311
|
+
`Created: ${formatCreationTime(anchor.createdAt)} (in ${anchor.project || "global"})`
|
|
1312
|
+
].filter(Boolean).join("\n");
|
|
1313
|
+
ctx.ui.notify(details, "info");
|
|
1314
|
+
}
|
|
1279
1315
|
}
|
|
1280
1316
|
|
|
1281
1317
|
// src/extension.ts
|
|
@@ -1488,7 +1524,7 @@ Mark as completed and archive?`
|
|
|
1488
1524
|
if (args.priority) title += ` ${theme.fg("muted", `[${args.priority}]`)}`;
|
|
1489
1525
|
}
|
|
1490
1526
|
return {
|
|
1491
|
-
render: () => [title],
|
|
1527
|
+
render: (width) => [truncateToWidth(title, width)],
|
|
1492
1528
|
invalidate: () => {
|
|
1493
1529
|
}
|
|
1494
1530
|
};
|
|
@@ -1497,14 +1533,14 @@ Mark as completed and archive?`
|
|
|
1497
1533
|
if (result.isError) {
|
|
1498
1534
|
const errText = result.content?.[0]?.text || "Execution failed";
|
|
1499
1535
|
return {
|
|
1500
|
-
render: () => [theme.fg("error", `\u2717 ${errText}`)],
|
|
1536
|
+
render: (width) => [truncateToWidth(theme.fg("error", `\u2717 ${errText}`), width)],
|
|
1501
1537
|
invalidate: () => {
|
|
1502
1538
|
}
|
|
1503
1539
|
};
|
|
1504
1540
|
}
|
|
1505
1541
|
const text = result.content?.[0]?.text || "OK";
|
|
1506
1542
|
return {
|
|
1507
|
-
render: () => [theme.fg("success", `\u2714 ${text}`)],
|
|
1543
|
+
render: (width) => [truncateToWidth(theme.fg("success", `\u2714 ${text}`), width)],
|
|
1508
1544
|
invalidate: () => {
|
|
1509
1545
|
}
|
|
1510
1546
|
};
|
package/dist/extension.js
CHANGED
|
@@ -9,9 +9,10 @@ import {
|
|
|
9
9
|
renderColdStartAnchorsContext,
|
|
10
10
|
runPhysicalVerification,
|
|
11
11
|
sweepStore,
|
|
12
|
+
truncateToWidth,
|
|
12
13
|
updateAnchorStatusBar,
|
|
13
14
|
updateStartupBanner
|
|
14
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-B652T7QR.js";
|
|
15
16
|
|
|
16
17
|
// src/extension.ts
|
|
17
18
|
import { Type } from "@sinclair/typebox";
|
|
@@ -224,7 +225,7 @@ Mark as completed and archive?`
|
|
|
224
225
|
if (args.priority) title += ` ${theme.fg("muted", `[${args.priority}]`)}`;
|
|
225
226
|
}
|
|
226
227
|
return {
|
|
227
|
-
render: () => [title],
|
|
228
|
+
render: (width) => [truncateToWidth(title, width)],
|
|
228
229
|
invalidate: () => {
|
|
229
230
|
}
|
|
230
231
|
};
|
|
@@ -233,14 +234,14 @@ Mark as completed and archive?`
|
|
|
233
234
|
if (result.isError) {
|
|
234
235
|
const errText = result.content?.[0]?.text || "Execution failed";
|
|
235
236
|
return {
|
|
236
|
-
render: () => [theme.fg("error", `\u2717 ${errText}`)],
|
|
237
|
+
render: (width) => [truncateToWidth(theme.fg("error", `\u2717 ${errText}`), width)],
|
|
237
238
|
invalidate: () => {
|
|
238
239
|
}
|
|
239
240
|
};
|
|
240
241
|
}
|
|
241
242
|
const text = result.content?.[0]?.text || "OK";
|
|
242
243
|
return {
|
|
243
|
-
render: () => [theme.fg("success", `\u2714 ${text}`)],
|
|
244
|
+
render: (width) => [truncateToWidth(theme.fg("success", `\u2714 ${text}`), width)],
|
|
244
245
|
invalidate: () => {
|
|
245
246
|
}
|
|
246
247
|
};
|
package/dist/index.cjs
CHANGED
|
@@ -910,8 +910,9 @@ function groupAnchorsByQuadrant(anchors, now = Date.now()) {
|
|
|
910
910
|
}
|
|
911
911
|
return groups;
|
|
912
912
|
}
|
|
913
|
+
var ANSI_REGEX = /\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g;
|
|
913
914
|
function stripAnsi(str) {
|
|
914
|
-
return str.replace(
|
|
915
|
+
return str.replace(ANSI_REGEX, "");
|
|
915
916
|
}
|
|
916
917
|
function getDisplayWidth(str) {
|
|
917
918
|
const clean = stripAnsi(str);
|
|
@@ -930,22 +931,36 @@ function getDisplayWidth(str) {
|
|
|
930
931
|
return width;
|
|
931
932
|
}
|
|
932
933
|
function truncateToWidth(str, maxWidth, ellipsis = "\u2026") {
|
|
933
|
-
|
|
934
|
-
|
|
934
|
+
if (maxWidth <= 0) return "";
|
|
935
|
+
const totalWidth = getDisplayWidth(str);
|
|
936
|
+
if (totalWidth <= maxWidth) return str;
|
|
935
937
|
const ellipsisWidth = getDisplayWidth(ellipsis);
|
|
936
|
-
const target = maxWidth - ellipsisWidth;
|
|
937
|
-
if (target
|
|
938
|
+
const target = Math.max(0, maxWidth - ellipsisWidth);
|
|
939
|
+
if (target === 0) return ellipsis.slice(0, maxWidth);
|
|
938
940
|
let accumulated = "";
|
|
939
941
|
let accumWidth = 0;
|
|
940
|
-
|
|
941
|
-
|
|
942
|
+
let i = 0;
|
|
943
|
+
while (i < str.length) {
|
|
944
|
+
ANSI_REGEX.lastIndex = i;
|
|
945
|
+
const match = ANSI_REGEX.exec(str);
|
|
946
|
+
if (match && match.index === i) {
|
|
947
|
+
accumulated += match[0];
|
|
948
|
+
i += match[0].length;
|
|
949
|
+
continue;
|
|
950
|
+
}
|
|
951
|
+
const codePoint = str.codePointAt(i) || 0;
|
|
952
|
+
const charLen = codePoint > 65535 ? 2 : 1;
|
|
953
|
+
const fullChar = str.slice(i, i + charLen);
|
|
954
|
+
const charWidth = getDisplayWidth(fullChar);
|
|
942
955
|
if (accumWidth + charWidth > target) {
|
|
943
956
|
break;
|
|
944
957
|
}
|
|
945
|
-
accumulated +=
|
|
958
|
+
accumulated += fullChar;
|
|
946
959
|
accumWidth += charWidth;
|
|
960
|
+
i += charLen;
|
|
947
961
|
}
|
|
948
|
-
|
|
962
|
+
accumulated = accumulated.replace(/\s*[\(\[\{(【]$/, "");
|
|
963
|
+
return accumulated + ellipsis + (str.includes("\x1B") ? "\x1B[0m" : "");
|
|
949
964
|
}
|
|
950
965
|
function padToWidth(str, targetWidth) {
|
|
951
966
|
const truncated = truncateToWidth(str, targetWidth);
|
|
@@ -1098,14 +1113,35 @@ async function openAnchorDashboard(ctx, store) {
|
|
|
1098
1113
|
optionMap.set(label, a);
|
|
1099
1114
|
displayOptions.push(label);
|
|
1100
1115
|
}
|
|
1101
|
-
const selected = await ctx.ui.select("\u2316 Anchors (
|
|
1116
|
+
const selected = await ctx.ui.select("\u2316 Anchors (select to inspect or settle):", displayOptions);
|
|
1102
1117
|
if (!selected) return;
|
|
1103
1118
|
const anchor = optionMap.get(selected);
|
|
1104
1119
|
if (!anchor) return;
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1120
|
+
const action = await ctx.ui.select(
|
|
1121
|
+
`\u2316 Anchor #${anchor.id}: "${anchor.title}"`,
|
|
1122
|
+
[
|
|
1123
|
+
"\u2713 Settle (Mark completed and archive)",
|
|
1124
|
+
"\u2139 View details & associated files",
|
|
1125
|
+
"\u21A9 Cancel"
|
|
1126
|
+
]
|
|
1127
|
+
);
|
|
1128
|
+
if (!action || action.startsWith("\u21A9")) return;
|
|
1129
|
+
if (action.startsWith("\u2713")) {
|
|
1130
|
+
store.settle(anchor.id, { settledBy: "manual-command" });
|
|
1131
|
+
const successMsg = anchor.recurrence === "daily" ? `\u2316 Completed for today: "${anchor.title}" (resets tomorrow)` : `\u2316 Settled #${anchor.id}: "${anchor.title}" (/anchor undo to revert)`;
|
|
1132
|
+
ctx.ui.notify(successMsg, "info");
|
|
1133
|
+
updateAnchorStatusBar(ctx, store);
|
|
1134
|
+
} else if (action.startsWith("\u2139")) {
|
|
1135
|
+
const details = [
|
|
1136
|
+
`#${anchor.id} [${anchor.priority.toUpperCase()}] ${anchor.title}`,
|
|
1137
|
+
anchor.description ? `Description: ${anchor.description}` : null,
|
|
1138
|
+
anchor.targetDate ? `Target Date: ${anchor.targetDate} (${formatTargetDate(anchor)})` : null,
|
|
1139
|
+
anchor.files && anchor.files.length > 0 ? `Files: ${anchor.files.join(", ")}` : null,
|
|
1140
|
+
anchor.verifyCommand ? `Verify Command: ${anchor.verifyCommand}` : null,
|
|
1141
|
+
`Created: ${formatCreationTime(anchor.createdAt)} (in ${anchor.project || "global"})`
|
|
1142
|
+
].filter(Boolean).join("\n");
|
|
1143
|
+
ctx.ui.notify(details, "info");
|
|
1144
|
+
}
|
|
1109
1145
|
}
|
|
1110
1146
|
|
|
1111
1147
|
// src/observer.ts
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-anchor",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Zero-pollution, 0-idle-token task memory for AI coding agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -52,13 +52,14 @@
|
|
|
52
52
|
"publishConfig": {
|
|
53
53
|
"access": "public"
|
|
54
54
|
},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"@sinclair/typebox": "*"
|
|
57
|
+
},
|
|
55
58
|
"devDependencies": {
|
|
59
|
+
"@sinclair/typebox": "^0.34.52",
|
|
56
60
|
"@types/node": "^26.6.2",
|
|
57
61
|
"tsup": "^8.5.1",
|
|
58
62
|
"tsx": "^4.23.15",
|
|
59
63
|
"typescript": "^5.8.2"
|
|
60
|
-
},
|
|
61
|
-
"dependencies": {
|
|
62
|
-
"@sinclair/typebox": "^0.34.52"
|
|
63
64
|
}
|
|
64
65
|
}
|
package/src/extension.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { renderColdStartAnchorsContext, renderActiveAnchorsContext, makeSafeTask
|
|
|
6
6
|
import { generateSettlementProposals, runPhysicalVerification } from './settlement.ts';
|
|
7
7
|
import { normalizePath, findMatchedAnchors } from './matcher.ts';
|
|
8
8
|
import { sweepStore } from './decay.ts';
|
|
9
|
-
import { updateAnchorStatusBar, openAnchorDashboard, updateStartupBanner } from './tui.ts';
|
|
9
|
+
import { updateAnchorStatusBar, openAnchorDashboard, updateStartupBanner, truncateToWidth } from './tui.ts';
|
|
10
10
|
import { execSync } from 'node:child_process';
|
|
11
11
|
import fs from 'node:fs';
|
|
12
12
|
import path from 'node:path';
|
|
@@ -267,7 +267,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
267
267
|
}
|
|
268
268
|
|
|
269
269
|
return {
|
|
270
|
-
render: () => [title],
|
|
270
|
+
render: (width: number) => [truncateToWidth(title, width)],
|
|
271
271
|
invalidate: () => {}
|
|
272
272
|
};
|
|
273
273
|
},
|
|
@@ -275,14 +275,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
275
275
|
if (result.isError) {
|
|
276
276
|
const errText = result.content?.[0]?.text || 'Execution failed';
|
|
277
277
|
return {
|
|
278
|
-
render: () => [theme.fg('error', `✗ ${errText}`)],
|
|
278
|
+
render: (width: number) => [truncateToWidth(theme.fg('error', `✗ ${errText}`), width)],
|
|
279
279
|
invalidate: () => {}
|
|
280
280
|
};
|
|
281
281
|
}
|
|
282
282
|
|
|
283
283
|
const text = result.content?.[0]?.text || 'OK';
|
|
284
284
|
return {
|
|
285
|
-
render: () => [theme.fg('success', `✔ ${text}`)],
|
|
285
|
+
render: (width: number) => [truncateToWidth(theme.fg('success', `✔ ${text}`), width)],
|
|
286
286
|
invalidate: () => {}
|
|
287
287
|
};
|
|
288
288
|
},
|
package/src/tui.ts
CHANGED
|
@@ -57,11 +57,13 @@ export function groupAnchorsByQuadrant(anchors: Anchor[], now: number = Date.now
|
|
|
57
57
|
return groups;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
const ANSI_REGEX = /\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g;
|
|
61
|
+
|
|
60
62
|
/**
|
|
61
63
|
* Strip ANSI escape codes from string
|
|
62
64
|
*/
|
|
63
65
|
export function stripAnsi(str: string): string {
|
|
64
|
-
return str.replace(
|
|
66
|
+
return str.replace(ANSI_REGEX, '');
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
/**
|
|
@@ -124,26 +126,44 @@ export function getDisplayWidth(str: string): number {
|
|
|
124
126
|
* Truncate string to target terminal visual width with ellipsis
|
|
125
127
|
*/
|
|
126
128
|
export function truncateToWidth(str: string, maxWidth: number, ellipsis = '…'): string {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
+
if (maxWidth <= 0) return '';
|
|
130
|
+
const totalWidth = getDisplayWidth(str);
|
|
131
|
+
if (totalWidth <= maxWidth) return str;
|
|
129
132
|
|
|
130
133
|
const ellipsisWidth = getDisplayWidth(ellipsis);
|
|
131
|
-
const target = maxWidth - ellipsisWidth;
|
|
132
|
-
if (target
|
|
134
|
+
const target = Math.max(0, maxWidth - ellipsisWidth);
|
|
135
|
+
if (target === 0) return ellipsis.slice(0, maxWidth);
|
|
133
136
|
|
|
134
137
|
let accumulated = '';
|
|
135
138
|
let accumWidth = 0;
|
|
139
|
+
let i = 0;
|
|
140
|
+
|
|
141
|
+
while (i < str.length) {
|
|
142
|
+
ANSI_REGEX.lastIndex = i;
|
|
143
|
+
const match = ANSI_REGEX.exec(str);
|
|
144
|
+
if (match && match.index === i) {
|
|
145
|
+
accumulated += match[0];
|
|
146
|
+
i += match[0].length;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const codePoint = str.codePointAt(i) || 0;
|
|
151
|
+
const charLen = codePoint > 0xffff ? 2 : 1;
|
|
152
|
+
const fullChar = str.slice(i, i + charLen);
|
|
153
|
+
const charWidth = getDisplayWidth(fullChar);
|
|
136
154
|
|
|
137
|
-
for (const char of str) {
|
|
138
|
-
const charWidth = getDisplayWidth(char);
|
|
139
155
|
if (accumWidth + charWidth > target) {
|
|
140
156
|
break;
|
|
141
157
|
}
|
|
142
|
-
accumulated +=
|
|
158
|
+
accumulated += fullChar;
|
|
143
159
|
accumWidth += charWidth;
|
|
160
|
+
i += charLen;
|
|
144
161
|
}
|
|
145
162
|
|
|
146
|
-
|
|
163
|
+
// Invariant 11: Guard against trailing open brackets right before ellipsis (e.g. "Fix auth (…" -> "Fix auth…")
|
|
164
|
+
accumulated = accumulated.replace(/\s*[\(\[\{(【]$/, '');
|
|
165
|
+
|
|
166
|
+
return accumulated + ellipsis + (str.includes('\x1b') ? '\x1b[0m' : '');
|
|
147
167
|
}
|
|
148
168
|
|
|
149
169
|
/**
|
|
@@ -384,16 +404,38 @@ export async function openAnchorDashboard(
|
|
|
384
404
|
displayOptions.push(label);
|
|
385
405
|
}
|
|
386
406
|
|
|
387
|
-
const selected = await ctx.ui.select('⌖ Anchors (
|
|
407
|
+
const selected = await ctx.ui.select('⌖ Anchors (select to inspect or settle):', displayOptions);
|
|
388
408
|
if (!selected) return;
|
|
389
409
|
|
|
390
410
|
const anchor = optionMap.get(selected);
|
|
391
411
|
if (!anchor) return;
|
|
392
412
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
413
|
+
const action = await ctx.ui.select(
|
|
414
|
+
`⌖ Anchor #${anchor.id}: "${anchor.title}"`,
|
|
415
|
+
[
|
|
416
|
+
'✓ Settle (Mark completed and archive)',
|
|
417
|
+
'ℹ View details & associated files',
|
|
418
|
+
'↩ Cancel'
|
|
419
|
+
]
|
|
420
|
+
);
|
|
421
|
+
if (!action || action.startsWith('↩')) return;
|
|
422
|
+
|
|
423
|
+
if (action.startsWith('✓')) {
|
|
424
|
+
store.settle(anchor.id, { settledBy: 'manual-command' });
|
|
425
|
+
const successMsg = anchor.recurrence === 'daily'
|
|
426
|
+
? `⌖ Completed for today: "${anchor.title}" (resets tomorrow)`
|
|
427
|
+
: `⌖ Settled #${anchor.id}: "${anchor.title}" (/anchor undo to revert)`;
|
|
428
|
+
ctx.ui.notify(successMsg, 'info');
|
|
429
|
+
updateAnchorStatusBar(ctx, store);
|
|
430
|
+
} else if (action.startsWith('ℹ')) {
|
|
431
|
+
const details = [
|
|
432
|
+
`#${anchor.id} [${anchor.priority.toUpperCase()}] ${anchor.title}`,
|
|
433
|
+
anchor.description ? `Description: ${anchor.description}` : null,
|
|
434
|
+
anchor.targetDate ? `Target Date: ${anchor.targetDate} (${formatTargetDate(anchor)})` : null,
|
|
435
|
+
anchor.files && anchor.files.length > 0 ? `Files: ${anchor.files.join(', ')}` : null,
|
|
436
|
+
anchor.verifyCommand ? `Verify Command: ${anchor.verifyCommand}` : null,
|
|
437
|
+
`Created: ${formatCreationTime(anchor.createdAt)} (in ${anchor.project || 'global'})`
|
|
438
|
+
].filter(Boolean).join('\n');
|
|
439
|
+
ctx.ui.notify(details, 'info');
|
|
440
|
+
}
|
|
399
441
|
}
|