@fanchao8609/agent_brain_sync 1.26.1 → 1.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/bin/abs.js +104 -29
- package/hooks/abs-rounded-editor/format.ts +84 -8
- package/hooks/abs-rounded-editor/index.ts +93 -3
- package/hooks/abs.omp.ts +60 -19
- package/hooks/abs.pi.ts +106 -33
- package/package.json +1 -1
- package/skill/abs-agent-brain-sync/SKILL.md +64 -260
- package/src/brain/db-cmd.js +5 -3
package/README.md
CHANGED
|
@@ -122,11 +122,11 @@ abs log "完成 X" # 记一行流水;abs log 无参 =
|
|
|
122
122
|
abs query <词> # 检索图谱(多词 OR);已被推翻的经验默认隐藏(--all 可看)
|
|
123
123
|
abs capture [--layer 1-6] [--kind …] "一句话" # 按七层判层后落到位(L7 用 abs note;--kind: gotcha|decision|tradeoff|howto)
|
|
124
124
|
abs db rebuild # 扫所有登记项目 → 建图数据库(~/.abs/graph.db)
|
|
125
|
-
abs
|
|
125
|
+
abs search <词> [--full] [--rules] # 检索:亮度排序+跨项目;--full 附正文,--rules 带铁律
|
|
126
126
|
abs db status # 库体检:七层分布 / 悬挂链接 / 已入库项目
|
|
127
127
|
abs db add # 把当前项目登记进 ~/.abs/projects.json
|
|
128
128
|
abs db evolve # 知识进化体检:被推翻/断链/主题堆积/僵化页
|
|
129
|
-
abs
|
|
129
|
+
abs ingest --list # 列出还没落页的会话(只读,不写盘)
|
|
130
130
|
abs ingest <会话文件或 id> # 把一场 pi 会话落成会话页(session-id 双向回溯)
|
|
131
131
|
# 已填 → 亮度×0.5;未填 → 整页不进检索
|
|
132
132
|
abs resolve <页名或id> # 反查页面路径(页改名后 id 不变)
|
package/bin/abs.js
CHANGED
|
@@ -151,6 +151,8 @@ const FLAG_SPEC = {
|
|
|
151
151
|
'force': { type: 'boolean' },
|
|
152
152
|
// ingest 用:--last 取最近一场会话(hook 在 session_shutdown 调,不用给 id);
|
|
153
153
|
// --auto 只落真改过文件的会话(纯聊天不落页)。
|
|
154
|
+
// ingest 用:--list 只看「哪些会话没落页」不写入(原 abs summon 的职责)
|
|
155
|
+
'list': { type: 'boolean' },
|
|
154
156
|
'last': { type: 'boolean' },
|
|
155
157
|
'auto': { type: 'boolean' },
|
|
156
158
|
// db search 用:不输出 L1 铁律(查主题时它们是噪音;开工看铁律是 load 的职责)。
|
|
@@ -247,36 +249,51 @@ function parseArgv(args) {
|
|
|
247
249
|
}
|
|
248
250
|
return o;
|
|
249
251
|
}
|
|
250
|
-
const usage = `abs —
|
|
252
|
+
const usage = `abs — 跨会话记忆(.brain/ 图谱)
|
|
251
253
|
|
|
252
|
-
|
|
253
|
-
abs load 开工先跑: 读状态、接上次的活
|
|
254
|
-
abs todo add X --note "做什么" 登记任务 (done/state/note 管后续)
|
|
255
|
-
abs note "经验一句话" 随手记经验; abs log "完成 X" 记成果
|
|
256
|
-
abs query <词…> 检索 .brain/; abs todo 看任务板
|
|
257
|
-
abs recall [词…] 卡住时主动取相关页(不给词就按当前处境推)
|
|
254
|
+
怎么用:按你要做的事找,不用记命令名。旧名都还能用,会自动提示新名。
|
|
258
255
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
256
|
+
【开工】读状态,接上次的活
|
|
257
|
+
abs load 必跑。Rules + 图谱计数 + 任务板 + 最近流水
|
|
258
|
+
abs recall [词…] 卡住时用:按当前处境推相关页(不给词也行)
|
|
259
|
+
|
|
260
|
+
【找东西】
|
|
261
|
+
abs search <词> 推荐。亮度排序 + 邻居扩散 + 跨项目
|
|
262
|
+
abs query <词…> [--all] 要字面精确时用:多词 OR,输出更短
|
|
263
|
+
abs resolve <页名…> 有了页名,反查文件路径
|
|
264
|
+
|
|
265
|
+
【写知识】(三者落点不同,别混)
|
|
266
|
+
abs note "一句话" 随手一句经验 → sources/ 待提炼
|
|
267
|
+
abs concept <slug> --title "…" 要成页 → concepts/(自动带四段骨架)
|
|
268
|
+
abs rule add "一句话" 要约束 → index 的 Rules(≤42 字)
|
|
269
|
+
|
|
270
|
+
【写任务】
|
|
271
|
+
abs todo 看板
|
|
272
|
+
abs todo add <id> --note "做什么"
|
|
273
|
+
abs todo note <id> --note "进度" 追加子项
|
|
274
|
+
abs todo done <id> --as 落地|否决|仅方案 [结语]
|
|
275
|
+
abs todo state <id> --note 进行中|讨论中|滞留中|搁置
|
|
276
|
+
abs todo remove <id> 删除(连子项,不可恢复)
|
|
277
|
+
|
|
278
|
+
【会话】
|
|
279
|
+
abs ingest [--list] 把一场 pi 会话落成结构化页;--list 看还有哪些没落
|
|
280
|
+
|
|
281
|
+
【维护】
|
|
282
|
+
abs lint 体检:死链 / 超限 / 待提炼
|
|
283
|
+
abs review 待确认的 draft 页(升 active 或否决)
|
|
284
|
+
abs supersede <页> --by <新页> 标经验已失效(不删文件)
|
|
285
|
+
abs show index|log|status 看完整索引 / 流水 / 概要
|
|
286
|
+
abs db rebuild|status|evolve 重建库 / 库体检 / 进化体检
|
|
287
|
+
abs init [--repair] 建图谱
|
|
288
|
+
|
|
289
|
+
【其它】
|
|
290
|
+
abs install|uninstall [--agent <宿主>] [--no-mcp] [--no-skill]
|
|
291
|
+
abs update 升级并刷新 hook/skill
|
|
292
|
+
abs config set user <名字> 使用者姓名(写操作需先设置)
|
|
293
|
+
|
|
294
|
+
旧名(仍可用,会提示新名):
|
|
295
|
+
index/log/status → show | capture → concept | db search → search
|
|
296
|
+
summon → ingest --list | db add → init
|
|
280
297
|
|
|
281
298
|
注: wrapup / teardown-check 是 hook 内部命令。
|
|
282
299
|
`;
|
|
@@ -493,26 +510,70 @@ async function main() {
|
|
|
493
510
|
rejectExtra(opts._, 'abs load');
|
|
494
511
|
console.log(await cmdLoad({ dir: opts.dir }));
|
|
495
512
|
break;
|
|
513
|
+
// 顶层 search(2026-11-06):MCP 侧一直叫 abs_search,CLI 却要敲 `db search` —— 名实不符。
|
|
514
|
+
// 现在 CLI 与 MCP 同名,`db search` 保留为别名(转指向这里并提示一次)。
|
|
515
|
+
case 'search': {
|
|
516
|
+
if (opts.help) {
|
|
517
|
+
console.log('abs search — 按亮度检索知识图谱\n\n'
|
|
518
|
+
+ '用法: abs search <词> [--limit N] [--full] [--no-rules] [--rules]\n\n'
|
|
519
|
+
+ ' 亮度排序 + 邻居扩散 + 跨项目(标 [跨项目])。\n'
|
|
520
|
+
+ ' 默认省略 L1 铁律(开工时 abs load 会给);要看加 --rules。\n'
|
|
521
|
+
+ ' --full 附正文;--limit 最多几页(默认 8)。\n\n'
|
|
522
|
+
+ '与 abs query 的区别:这是详略两档不是重复 ——\n'
|
|
523
|
+
+ ' query 给字面 OR 命中的页名+摘要,最省 token;\n'
|
|
524
|
+
+ ' search 给亮度与扩散,找「我上次怎么解决 X」更准。');
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
527
|
+
if (!opts._.length) throw new Error('✗ 缺检索词\n 用法: abs search <词> [--limit N] [--full] [--no-rules]');
|
|
528
|
+
// 顶层 search 默认省略铁律(2026-11-06):它是「查一个主题」的工具,
|
|
529
|
+
// 18 条铁律占输出近一半、每次都是噪音。要看铁律显式 --rules,或跑 abs load。
|
|
530
|
+
// db search 那个别名仍保持旧默认(带铁律),不改变老脚本的行为。
|
|
531
|
+
console.log(await cmdDbSearch({
|
|
532
|
+
query: opts._.join(' '), limit: Number(opts.limit) || 8,
|
|
533
|
+
full: !!opts.full, noRules: !opts.rules, cmdName: 'search',
|
|
534
|
+
}));
|
|
535
|
+
break;
|
|
536
|
+
}
|
|
496
537
|
case 'recall': {
|
|
497
538
|
const limit = Number(opts.limit) > 0 ? Number(opts.limit) : 5;
|
|
498
539
|
console.log(await cmdRecall({ dir: opts.dir, terms: opts._, limit }));
|
|
499
540
|
break;
|
|
500
541
|
}
|
|
542
|
+
// ── 只读视图:统一到 abs show <视图>,旧名保留为别名并提示 ──
|
|
543
|
+
// (2026-11-06 命令面整理:index/log/status 都是「看某个文件的现状」,
|
|
544
|
+
// 三个顶层命令占三个名额却只有一个心智动作 —— 看。合成 show 一个入口。)
|
|
545
|
+
case 'show': {
|
|
546
|
+
const v = opts._[0];
|
|
547
|
+
if (!v) {
|
|
548
|
+
console.log('用法: abs show index|log|status\n index=完整索引 log=操作流水 status=图谱概要');
|
|
549
|
+
break;
|
|
550
|
+
}
|
|
551
|
+
rejectExtra(opts._.slice(1), 'abs show index|log|status');
|
|
552
|
+
if (v === 'status') console.log(await cmdStatus({ dir: opts.dir }));
|
|
553
|
+
else if (v === 'index' || v === 'log') console.log(await cmdShow({ dir: opts.dir, view: v }));
|
|
554
|
+
else console.log(`✗ 未知视图 "${v}"。可用: index | log | status`);
|
|
555
|
+
break;
|
|
556
|
+
}
|
|
501
557
|
case 'index':
|
|
502
558
|
rejectExtra(opts._, 'abs index');
|
|
559
|
+
console.log('(abs index 已改名 → abs show index)');
|
|
503
560
|
console.log(await cmdShow({ dir: opts.dir, view: 'index' }));
|
|
504
561
|
break;
|
|
505
562
|
case 'log': {
|
|
506
563
|
// 无参=查看 log.md;带参=追加一行 (用户/AI 主动记; hook 生命周期事件走技术日志 hooks.log, 不经这里)
|
|
564
|
+
// ★ log 有双职责,不能并进 show:带参时它是**写**操作。
|
|
565
|
+
// 所以只把「无参查看」这一半指向 show,写的那半保持不变、也不提示改名。
|
|
507
566
|
if (opts._.length) {
|
|
508
567
|
console.log(await cmdLog({ dir: opts.dir, title: opts._.join(' '), kind: opts.kind }));
|
|
509
568
|
} else {
|
|
569
|
+
console.log('(查看流水已改名 → abs show log;abs log "完成 X" 仍是记成果)');
|
|
510
570
|
console.log(await cmdShow({ dir: opts.dir, view: 'log' }));
|
|
511
571
|
}
|
|
512
572
|
break;
|
|
513
573
|
}
|
|
514
574
|
case 'status':
|
|
515
575
|
rejectExtra(opts._, 'abs status');
|
|
576
|
+
console.log('(abs status 已改名 → abs show status)');
|
|
516
577
|
console.log(await cmdStatus({ dir: opts.dir }));
|
|
517
578
|
break;
|
|
518
579
|
// abs todo —— 无子命令=看板;带子命令=任务写操作
|
|
@@ -572,10 +633,18 @@ async function main() {
|
|
|
572
633
|
break;
|
|
573
634
|
}
|
|
574
635
|
case 'summon': {
|
|
636
|
+
// 别名(2026-11-06):并进 ingest --list —— 两者是同一件事的两半
|
|
637
|
+
// (summon 说「哪些没落页」,ingest 负责落)。分两个顶层命令只是多记一个词。
|
|
638
|
+
console.log('(abs summon 已改名 → abs ingest --list)');
|
|
575
639
|
console.log(await cmdSummon({ dir: opts.dir, mark: !!opts.mark, full: !!opts.full, status: !!opts.status, limit: Number(opts.limit) || 15 }));
|
|
576
640
|
break;
|
|
577
641
|
}
|
|
578
642
|
case 'ingest': {
|
|
643
|
+
// --list:只看「还有哪些会话没落页」,不写入(原来的 abs summon)
|
|
644
|
+
if (opts.list) {
|
|
645
|
+
console.log(await cmdSummon({ dir: opts.dir, mark: false, full: !!opts.full, status: false, limit: Number(opts.limit) || 15 }));
|
|
646
|
+
break;
|
|
647
|
+
}
|
|
579
648
|
console.log(await cmdIngest({
|
|
580
649
|
dir: opts.dir, file: opts._.join(' '), force: !!opts.force, mark: !!opts.mark,
|
|
581
650
|
last: !!opts.last, auto: !!opts.auto, session: opts.session,
|
|
@@ -628,7 +697,13 @@ async function main() {
|
|
|
628
697
|
const sub = opts._[0];
|
|
629
698
|
const rest = opts._.slice(1);
|
|
630
699
|
if (sub === 'rebuild') { console.log(await cmdDbRebuild({ dir: opts.dir })); break; }
|
|
631
|
-
if (sub === 'search') {
|
|
700
|
+
if (sub === 'search') {
|
|
701
|
+
// 别名(2026-11-06):顶层已改叫 abs search。留着是为了不打断现有习惯与脚本,
|
|
702
|
+
// 但每次提示一次 —— 不提示的话旧名会永远活着,命令面又变宽。
|
|
703
|
+
console.log('(abs db search 已改名 → abs search)');
|
|
704
|
+
console.log(await cmdDbSearch({ query: rest.join(' '), limit: Number(opts.limit) || 8, full: !!opts.full, noRules: !!opts['no-rules'] }));
|
|
705
|
+
break;
|
|
706
|
+
}
|
|
632
707
|
if (sub === 'status') { console.log(await cmdDbStatus()); break; }
|
|
633
708
|
if (sub === 'evolve') { console.log(await cmdDbEvolve({ staleDays: Number(opts.staleDays) || 90 })); break; }
|
|
634
709
|
if (sub === 'add') {
|
|
@@ -33,11 +33,71 @@ export function stripAnsi(s: string): string {
|
|
|
33
33
|
return s.replace(/\x1b\[[0-9;]*m/g, '')
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/** 终端显示宽度:CJK/全角记 2 列,其余记 1 列。
|
|
37
|
+
*
|
|
38
|
+
* ★ 为何必需(2026-11-07 用户实报「输入框右下角底下的线冒出来了」):
|
|
39
|
+
* 之前 roundBorder 按**字符数**算宽度,而左下角标签里有中文昵称
|
|
40
|
+
* (如「转发锦鲤求暴富」7 个汉字实际占 14 列)—— 少算了 7 列,
|
|
41
|
+
* 多出的宽度就把右下角圆角挤出了屏幕。
|
|
42
|
+
* 右侧状态栏一直没出问题是因为它只有 ASCII。
|
|
43
|
+
*
|
|
44
|
+
* 与 abs.pi.ts 的 dispWidth 同规则,但不共享(两个扩展各自独立安装)。 */
|
|
45
|
+
export function textWidth(s: string): number {
|
|
46
|
+
const plain = stripAnsi(s)
|
|
47
|
+
let w = 0
|
|
48
|
+
for (const ch of plain) {
|
|
49
|
+
const cp = ch.codePointAt(0) || 0
|
|
50
|
+
const wide =
|
|
51
|
+
(cp >= 0x1100 && cp <= 0x115f) ||
|
|
52
|
+
(cp >= 0x2e80 && cp <= 0xa4cf) ||
|
|
53
|
+
(cp >= 0xac00 && cp <= 0xd7a3) ||
|
|
54
|
+
(cp >= 0xf900 && cp <= 0xfaff) ||
|
|
55
|
+
(cp >= 0xfe30 && cp <= 0xfe6f) ||
|
|
56
|
+
(cp >= 0xff00 && cp <= 0xff60) ||
|
|
57
|
+
(cp >= 0xffe0 && cp <= 0xffe6) ||
|
|
58
|
+
(cp >= 0x1f300 && cp <= 0x1f64f) ||
|
|
59
|
+
(cp >= 0x1f900 && cp <= 0x1f9ff)
|
|
60
|
+
w += wide ? 2 : 1
|
|
61
|
+
}
|
|
62
|
+
return w
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 从尾部去掉 n 列。边框行只由 ASCII `─`(U+2500,各 1 列)与空格组成,
|
|
66
|
+
* 每字符 1 列,故直接按字符截。
|
|
67
|
+
* ★ 不要在这里用宽字符判断 —— 之前写 `cp >= 0x1100` 把 U+2500 当成了 2 列,
|
|
68
|
+
* 导致削掉一半、边框变长(2026-11-07 踩过)。 */
|
|
69
|
+
function dropCols(s: string, n: number): string {
|
|
70
|
+
if (n <= 0) return s
|
|
71
|
+
return s.slice(0, Math.max(0, s.length - n))
|
|
72
|
+
}
|
|
73
|
+
|
|
36
74
|
/**
|
|
37
|
-
*
|
|
75
|
+
* 推理强度 → 主题色 token。空串表示「不上色,沿用默认色」。
|
|
76
|
+
*
|
|
77
|
+
* 规则:`thinking` + 首字母大写,正好对上主题里的 thinkingMinimal…thinkingMax 七档。
|
|
78
|
+
* 为什么 off 返回空串而不是 `thinkingOff`:主题给 thinkingOff 的是接近背景的暗灰
|
|
79
|
+
* (dark.json: `okhsl(229 8% 49%)`),拿它去染文字会看不清。用户 2026-11-06 定的是
|
|
80
|
+
* 「off 时保持默认色」。
|
|
81
|
+
*
|
|
82
|
+
* ★ 同一份逻辑在 hooks/abs.pi.ts 里也有一份(`effortColorToken`)。不抽公共模块是故意的:
|
|
83
|
+
* 安装后 `abs.ts` 与 `abs-rounded-editor/` 是两个可独立删除的扩展,抽成共享文件会让
|
|
84
|
+
* 「删掉输入框样式目录」连带把 todo 面板搞崩。
|
|
85
|
+
*/
|
|
86
|
+
export function thinkingColorToken(level: string | null | undefined): string {
|
|
87
|
+
const l = String(level || '').trim()
|
|
88
|
+
if (!l || l === 'off') return ''
|
|
89
|
+
return 'thinking' + l[0].toUpperCase() + l.slice(1)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 把一行边框文本换成「左圆角 + 左标签 + 中段 + 右圆角」,**显示宽度**保持原样。
|
|
38
94
|
* 边框行内含 ANSI 时直接 slice 会切坏转义序列,所以先剥色、改字、再整行着色。
|
|
39
95
|
*
|
|
40
|
-
*
|
|
96
|
+
* ★ 宽度一律按 textWidth(CJK = 2 列)算,不按字符数 —— 标签里的中文昵称
|
|
97
|
+
* 按字符数算会少算一半,多出的宽度把右下角圆角挤到屏幕外(2026-11-07 实报)。
|
|
98
|
+
*
|
|
99
|
+
* leftLabel 贴在左圆角之后;label 贴最右端。哪个放不下就丢掉哪个
|
|
100
|
+
* (宁可没有,也不让边框溢出折行)。
|
|
41
101
|
*/
|
|
42
102
|
export function roundBorder(
|
|
43
103
|
line: string,
|
|
@@ -45,16 +105,32 @@ export function roundBorder(
|
|
|
45
105
|
left: string,
|
|
46
106
|
right: string,
|
|
47
107
|
label = '',
|
|
108
|
+
leftLabel = '',
|
|
48
109
|
): string {
|
|
49
|
-
// 宽度按字符数算(不是显示宽度)。成立的前提:line 只含 ASCII 的 `─` 与空格
|
|
50
|
-
// —— pi 的边框行正是如此。若将来边框里出现中日韩宽字符,这里要改用 visibleWidth。
|
|
51
110
|
const plain = stripAnsi(line)
|
|
52
111
|
if (plain.length === 0) return line
|
|
53
|
-
//
|
|
112
|
+
// mid 是两端圆角之间的那段;总宽 = 1(左圆角) + mid 宽 + 1(右圆角)。
|
|
54
113
|
let mid = plain
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
114
|
+
|
|
115
|
+
// 右 label 先做(从右端拿 `─` 换),再做左侧 —— 否则两侧都从右端切,位置会互相错。
|
|
116
|
+
// 注意:剥掉 **lw** 列后接 ` label `,两者宽度相等,故总宽不变。
|
|
117
|
+
if (label) {
|
|
118
|
+
const lw = textWidth(label) + 2
|
|
119
|
+
if (lw < textWidth(mid)) mid = dropCols(mid, lw) + ' ' + label + ' '
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 左 label 插到最左端(圆角之后,即 working 图标之前)。
|
|
123
|
+
// ★ 从**中间**剥等长的 `─`,不能从右端剥 —— 右端可能已经是右 label,
|
|
124
|
+
// 再从右端切就会把它末尾拼掉(实测:「8.0%/1.0M m」变成「8.0」)。
|
|
125
|
+
// 两侧各留 1 空格:否则 `╰fanchao` 贴死圆角,看着像缺了一角。
|
|
126
|
+
if (leftLabel) {
|
|
127
|
+
const lw = textWidth(leftLabel) + 2
|
|
128
|
+
// 右 label 已占的尾巴宽度:保留它,从它之前切。
|
|
129
|
+
const tailLen = label && textWidth(label) + 2 < textWidth(mid) ? textWidth(label) + 2 : 0
|
|
130
|
+
if (lw + tailLen < textWidth(mid)) {
|
|
131
|
+
const body = dropCols(mid.slice(0, mid.length - tailLen), lw)
|
|
132
|
+
mid = ' ' + leftLabel + ' ' + body + mid.slice(mid.length - tailLen)
|
|
133
|
+
}
|
|
58
134
|
}
|
|
59
135
|
return fg(left + mid + right)
|
|
60
136
|
}
|
|
@@ -18,13 +18,41 @@
|
|
|
18
18
|
* 注意:自动补全菜单行排在底边之下、且自带内容宽度,不套竖线(否则菜单被框住会错位)。
|
|
19
19
|
*/
|
|
20
20
|
import { CustomEditor } from '@earendil-works/pi-coding-agent'
|
|
21
|
-
import {
|
|
21
|
+
import { readFile } from 'node:fs/promises'
|
|
22
|
+
import { homedir } from 'node:os'
|
|
23
|
+
import { join } from 'node:path'
|
|
24
|
+
import { buildStatus, roundBorder, shortenModel, thinkingColorToken } from './format.ts'
|
|
25
|
+
// 昵称池跟 todo 面板共用一份 —— 2026-11-07 用户要求把「作者名 · 随机语」从面板
|
|
26
|
+
// 搬到输入框左下角,两边必须用同一个池才叫「搬」。
|
|
27
|
+
// 为何用 ../abs.ts:安装后两者分别在 ~/.pi/agent/extensions/abs.ts 与
|
|
28
|
+
// extensions/abs-rounded-editor/index.ts,相对路径稳定。
|
|
29
|
+
// 代价:删掉 abs.ts 会让本扩展导入失败 —— 可接受,因为 abs.ts 是主扩展,
|
|
30
|
+
// 输入框样式是它的附属;单独删主扩展就不是合法使用形态了。
|
|
31
|
+
import { pickNickname } from '../abs.ts'
|
|
22
32
|
|
|
23
33
|
const SIDE = '│'
|
|
24
34
|
|
|
25
35
|
/** 右上角状态文本,如 `8.0%/1.0M deepseek-v4.1-flash`。render() 拿不到 ctx,故由事件回调维护。 */
|
|
26
36
|
let statusLabel = ''
|
|
27
37
|
|
|
38
|
+
/** 左下角标签:`fanchao · 昵称`(2026-11-07 用户要求从面板搬到这里)。
|
|
39
|
+
* session_start 时算一次 —— 不能每帧抽,否则每帧重绘就是个疯狂滚动的老虎机。
|
|
40
|
+
* 空串 = 不显示(无用户名也无昵称时)。 */
|
|
41
|
+
let cornerLabel = ''
|
|
42
|
+
|
|
43
|
+
/** 当前推理强度(off/minimal/low/medium/high/xhigh/max)。render() 拿不到 ctx,同样这里存。
|
|
44
|
+
* 用途:输入文字随推理强度变色(2026-11-06 用户需求)。 */
|
|
45
|
+
let effortLevel = ''
|
|
46
|
+
|
|
47
|
+
/** 取当前主题实例的 getter。
|
|
48
|
+
*
|
|
49
|
+
* ★ 为何不能把主题从构造函数里拿:pi 传给编辑器了的是 `getEditorTheme()`,
|
|
50
|
+
* 那对象只有 `{ borderColor, selectList }`,**没有 `fg`** —— 直接用它上色会静默无效
|
|
51
|
+
* (不报错,只是没颜色)。真正带 fg 的是 `ctx.ui.theme`(interactive-mode 的 getter)。
|
|
52
|
+
* ★ 为何存 getter 而不是存实例:`/theme` 切主题后 ctx.ui.theme 返回**新实例**,
|
|
53
|
+
* 存旧实例会継续用旧主题的色板。 */
|
|
54
|
+
let themeGetter: (() => any) | null = null
|
|
55
|
+
|
|
28
56
|
export class RoundedEditor extends CustomEditor {
|
|
29
57
|
render(width: number): string[] {
|
|
30
58
|
if (width < 4) return super.render(width)
|
|
@@ -34,6 +62,11 @@ export class RoundedEditor extends CustomEditor {
|
|
|
34
62
|
|
|
35
63
|
const borderColor = (this as any).borderColor?.bind(this)
|
|
36
64
|
const wrap = (s: string) => (borderColor ? borderColor(s) : s)
|
|
65
|
+
// 文字色 = 推理强度色;off(或取不到)时为空 → 不着色,用终端默认前景色。
|
|
66
|
+
const token = thinkingColorToken(effortLevel)
|
|
67
|
+
const themeNow = themeGetter?.()
|
|
68
|
+
const effortFg =
|
|
69
|
+
token && typeof themeNow?.fg === 'function' ? (s: string) => themeNow.fg(token, s) : null
|
|
37
70
|
const out: string[] = []
|
|
38
71
|
|
|
39
72
|
// 上边框:原本是整行 `─`(含 working 图标与滚动提示),两端换圆角,右端贴状态。
|
|
@@ -43,15 +76,47 @@ export class RoundedEditor extends CustomEditor {
|
|
|
43
76
|
const bottomIdx = lines.length - 1 - autocompleteHeight
|
|
44
77
|
|
|
45
78
|
for (let i = 1; i < bottomIdx; i++) {
|
|
46
|
-
|
|
79
|
+
const body = lines[i]
|
|
80
|
+
// 推理强度色染**输入文字**(2026-11-06 用户)。边框不动 —— 边框本来就是 pi 按
|
|
81
|
+
// 同一档强度上的色(interactive-mode 的 updateEditorBorderColor),两者自然同色。
|
|
82
|
+
// 整行包裹而不逐字:光标由 \x1b[7m 反转标记,与前景色互不干扰;
|
|
83
|
+
// 输入框内容是用户手打的纯文本,行内不会有 reset 把后半行冲掉。
|
|
84
|
+
out.push(wrap(SIDE) + (effortFg ? effortFg(body) : body) + wrap(SIDE))
|
|
47
85
|
}
|
|
48
|
-
|
|
86
|
+
// 下边框:作者名 + 昵称贴左侧(2026-11-07 用户要求)。
|
|
87
|
+
out.push(roundBorder(lines[bottomIdx], wrap, '╰', '╯', '', cornerLabel))
|
|
49
88
|
// 自动补全行原样透传(不套竖线)。
|
|
50
89
|
for (let i = bottomIdx + 1; i < lines.length; i++) out.push(lines[i])
|
|
51
90
|
return out
|
|
52
91
|
}
|
|
53
92
|
}
|
|
54
93
|
|
|
94
|
+
/** 读出作者名:与 abs.pi.ts 的 currentUser 同规则(ABS_USER > ~/.abs/config.json)。
|
|
95
|
+
* 不在该文件导出 currentUser 是因为它依赖更多模块级状态,且本处只需这两行。 */
|
|
96
|
+
async function readUser(): Promise<string> {
|
|
97
|
+
const env = String(process.env.ABS_USER || '').trim()
|
|
98
|
+
if (env) return env
|
|
99
|
+
try {
|
|
100
|
+
const dir = process.env.ABS_CONFIG_DIR || join(homedir(), '.abs')
|
|
101
|
+
const raw = await readFile(join(dir, 'config.json'), 'utf8')
|
|
102
|
+
const u = JSON.parse(raw).user
|
|
103
|
+
return typeof u === 'string' ? u.trim() : ''
|
|
104
|
+
} catch {
|
|
105
|
+
return ''
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** 算出左下角标签:`fanchao · 昵称`。缺哪段就省哪段,都为空格返回空串(不画)。 */
|
|
110
|
+
async function refreshCornerLabel(): Promise<void> {
|
|
111
|
+
try {
|
|
112
|
+
const who = await readUser()
|
|
113
|
+
const nick = pickNickname()
|
|
114
|
+
cornerLabel = [who, nick].filter(Boolean).join(' · ')
|
|
115
|
+
} catch {
|
|
116
|
+
cornerLabel = ''
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
55
120
|
/** 刷新右上角状态文本(每次渲染前由事件与定时器调用)。 */
|
|
56
121
|
function refreshStatus(ctx: any): void {
|
|
57
122
|
try {
|
|
@@ -59,12 +124,29 @@ function refreshStatus(ctx: any): void {
|
|
|
59
124
|
} catch {
|
|
60
125
|
statusLabel = shortenModel(ctx?.model?.id ?? '')
|
|
61
126
|
}
|
|
127
|
+
refreshEffort(ctx)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** 读出当前推理强度。`ctx.thinkingLevel` 是 pi 官方给扩展的入口(effective level)。 */
|
|
131
|
+
function refreshEffort(ctx: any): void {
|
|
132
|
+
try {
|
|
133
|
+
effortLevel = String(ctx?.thinkingLevel || '')
|
|
134
|
+
} catch {
|
|
135
|
+
effortLevel = ''
|
|
136
|
+
}
|
|
62
137
|
}
|
|
63
138
|
|
|
64
139
|
export default function piRoundedEditor(pi: any): void {
|
|
65
140
|
pi.on('session_start', (_event: unknown, ctx: any) => {
|
|
66
141
|
if (!ctx?.ui?.setEditorComponent) return
|
|
142
|
+
// 主题 getter(带 fg 的那个)—— 见 themeGetter 注释。
|
|
143
|
+
themeGetter = () => ctx.ui?.theme
|
|
67
144
|
refreshStatus(ctx)
|
|
145
|
+
// 左下角标签:每次会话开一次(含 reload/new/resume),会话内固定不变。
|
|
146
|
+
// 与面板旧的昵称行为一致 —— 用户要的是「搬过去」,不是改规则。
|
|
147
|
+
refreshCornerLabel()
|
|
148
|
+
.then(() => ctx.ui?.requestRender?.())
|
|
149
|
+
.catch(() => {})
|
|
68
150
|
// 第四个参数 embedWorkingStatus: true —— working 状态进上边框,不再单独占一行。
|
|
69
151
|
ctx.ui.setEditorComponent(
|
|
70
152
|
(tui: any, theme: any, keybindings: any) =>
|
|
@@ -80,6 +162,14 @@ export default function piRoundedEditor(pi: any): void {
|
|
|
80
162
|
ctx?.ui?.requestRender?.()
|
|
81
163
|
})
|
|
82
164
|
|
|
165
|
+
// 切推理强度时重刷输入框文字色(2026-11-06 用户需求)。
|
|
166
|
+
// 为何不能只靠 session_start 那一次:/model 的强度档位、Alt+T 之类的切换不改会话,
|
|
167
|
+
// 不重建编辑器也就不会重新读 effortLevel —— 文字会停在旧色。
|
|
168
|
+
pi.on('thinking_level_select', (_event: any, ctx: any) => {
|
|
169
|
+
refreshEffort(ctx)
|
|
170
|
+
ctx?.ui?.requestRender?.()
|
|
171
|
+
})
|
|
172
|
+
|
|
83
173
|
// 上下文占用随每轮对话变化 —— 轮次结束与消息结束时刷新。
|
|
84
174
|
for (const ev of ['turn_end', 'message_end', 'tool_execution_end']) {
|
|
85
175
|
pi.on(ev, (_event: any, ctx: any) => {
|
package/hooks/abs.omp.ts
CHANGED
|
@@ -503,11 +503,15 @@ function clipToWidth(s: string, width: number): string {
|
|
|
503
503
|
|
|
504
504
|
// 「进行中」行的迷你动画:三个小符号依次由空心变实心。
|
|
505
505
|
// 帧宽固定 3 列 —— 宽度不变才不会让面板每帧抖动。
|
|
506
|
-
const TODO_ANIM_FRAMES = ['
|
|
507
|
-
const TODO_ANIM_MS =
|
|
506
|
+
const TODO_ANIM_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
|
507
|
+
const TODO_ANIM_MS = 100
|
|
508
508
|
|
|
509
509
|
/** 动画相位(每 tick +1)。模块级 —— 闭包 flag 在重复注册时不共享。 */
|
|
510
510
|
let animPhase = 0
|
|
511
|
+
|
|
512
|
+
/** 当前推理强度。模块级理由同上:widget factory 拿不到 ctx。
|
|
513
|
+
* omp 宿主有无这个字段未验证 —— 取不到就是空串,面板退回 accent,行为与以前一致。 */
|
|
514
|
+
let effortLevel = ''
|
|
511
515
|
let animTimer: ReturnType<typeof setInterval> | undefined
|
|
512
516
|
|
|
513
517
|
/** 启动/保持动画定时器。重复调用不会叠加(已存在则不动)。
|
|
@@ -532,6 +536,21 @@ export function todoAnimFrame(phase: number): string {
|
|
|
532
536
|
return TODO_ANIM_FRAMES[i]
|
|
533
537
|
}
|
|
534
538
|
|
|
539
|
+
/**
|
|
540
|
+
* 推理强度 → 主题色 token。空串表示「不上推理色,用兼底色」。
|
|
541
|
+
*
|
|
542
|
+
* 规则:`thinking` + 首字母大写(thinkingMinimal…thinkingMax)。
|
|
543
|
+
* off 返回空串而不是 `thinkingOff`:那个色接近背景,染文字会看不清(2026-11-06 用户定)。
|
|
544
|
+
*
|
|
545
|
+
* ★ 与 pi 版(hooks/abs.pi.ts 的 effortToken)逐字同义,但不共享:两个模板分别装到不同宿主,
|
|
546
|
+
* 独立成文件才能各自删除/升级。
|
|
547
|
+
*/
|
|
548
|
+
export function effortToken(level: string | null | undefined): string {
|
|
549
|
+
const l = String(level || '').trim()
|
|
550
|
+
if (!l || l === 'off') return ''
|
|
551
|
+
return 'thinking' + l[0].toUpperCase() + l.slice(1)
|
|
552
|
+
}
|
|
553
|
+
|
|
535
554
|
export function renderPanelLines(
|
|
536
555
|
data: { total: number; rows: { state: string; id: string; desc: string; kids?: { done: boolean; text: string }[] }[]; hidden: number },
|
|
537
556
|
who: string,
|
|
@@ -539,38 +558,48 @@ export function renderPanelLines(
|
|
|
539
558
|
fg: (color: string, s: string) => string,
|
|
540
559
|
animPhase = -1,
|
|
541
560
|
nickname = '',
|
|
561
|
+
effort = '',
|
|
542
562
|
): string[] {
|
|
543
563
|
// ★ 空看板不返回空数组:面板是「机制在不在」的常驻指示,进去就看不见 = 不知道机制活着。
|
|
544
|
-
|
|
564
|
+
// ★ 主色跟推理强度走(2026-11-06 与 pi 版同步):标题、[状态]、树线、复选框、
|
|
565
|
+
// 进行中动画同用一色,与输入框描边同色系。
|
|
566
|
+
// ★ off 时整块面板用灰阶:高亮色只在真的开着推理时给。
|
|
567
|
+
// 主色 muted(中灰),描述与子项降一阶到 dim(深灰),两阶保留层次。
|
|
568
|
+
// omp 宿主目前不传 effort → 走 off 分支,面板为灰阶。
|
|
569
|
+
const off = !effortToken(effort)
|
|
570
|
+
const main = effortToken(effort) || 'muted'
|
|
571
|
+
const flow = off ? 'dim' : 'muted'
|
|
545
572
|
const lines: string[] = []
|
|
546
573
|
// 上描边行已删除(2026-11-04 与 pi 版同步,用户要求「去掉上边框」)。首行现在是标题行。
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
574
|
+
// 标题只有计数(2026-11-07 与 pi 版同步):作者名与昵称已搬到输入框左下角,
|
|
575
|
+
// 面板不再重复占宽度。**不要再往标题后拼作者名/昵称**。
|
|
576
|
+
const title = `Todo (${data.total})`
|
|
577
|
+
// 标题顶最左(2026-11-04 用户)。旧代码空看板时多缩 1 格,2026-11-06 与 pi 版同步删去 ——
|
|
578
|
+
// 空看板已整块不显示(见 refreshTodoPanel)。
|
|
579
|
+
lines.push(clipToWidth(fg(main, title), width))
|
|
551
580
|
|
|
552
581
|
const lastIdx = data.rows.length - 1
|
|
553
|
-
// 空看板只显示标题行(2026-11-04 用户要求去掉「(无未完成任务)」那行)。
|
|
554
|
-
if (data.total === 0) return lines
|
|
555
582
|
data.rows.forEach((t, i) => {
|
|
556
583
|
// `└─` 只给最后一条 —— 不管它有没有子项(否则末条带子项时全篇没有收尾)。
|
|
557
584
|
const isLast = i === lastIdx
|
|
558
585
|
const branch = isLast ? '└─' : '├─'
|
|
559
|
-
|
|
586
|
+
// 单字符 spinner 紧跟 [进行中](2026-11-06 与 pi 版同步)。
|
|
587
|
+
const anim = t.state === '进行中' && animPhase >= 0 ? ' ' + fg(main, todoAnimFrame(animPhase)) : ''
|
|
560
588
|
// 复选框 `[ ]`:面板是 todo.md 的只读视图,就该长成 todo 原貌。
|
|
561
|
-
const box = fg(
|
|
562
|
-
|
|
563
|
-
const
|
|
589
|
+
const box = fg(main, '[ ]')
|
|
590
|
+
// 状态标签统一跟主色(2026-11-06 与 pi 版同步:靠符号和文字区分状态,不靠颜色)。
|
|
591
|
+
const head = `${fg(main, branch)} ${box} ${fg(main, `[${t.state}]`)}${anim} ${fg('text', t.id)}`
|
|
592
|
+
const desc = t.desc ? ` ${fg('dim', '—')} ${fg(flow, t.desc)}` : ''
|
|
564
593
|
lines.push(clipToWidth(head + desc, width))
|
|
565
594
|
if (t.kids && t.kids.length) {
|
|
566
595
|
// 子项:标准 md 嵌套列表 → 画成树
|
|
567
|
-
const stem = isLast ? ' ' : fg(
|
|
596
|
+
const stem = isLast ? ' ' : fg(main, '│ ')
|
|
568
597
|
const n = t.kids.length
|
|
569
598
|
t.kids.forEach((k, ki) => {
|
|
570
599
|
const kb = ki === n - 1 ? '└─' : '├─'
|
|
571
|
-
const kbox = fg(
|
|
572
|
-
const ktext = fg(k.done ? 'dim' :
|
|
573
|
-
lines.push(clipToWidth(`${stem} ${fg(
|
|
600
|
+
const kbox = fg(main, k.done ? '[x]' : '[ ]')
|
|
601
|
+
const ktext = fg(k.done ? 'dim' : flow, k.text)
|
|
602
|
+
lines.push(clipToWidth(`${stem} ${fg(main, kb)} ${kbox} ${ktext}`, width))
|
|
574
603
|
})
|
|
575
604
|
}
|
|
576
605
|
})
|
|
@@ -597,6 +626,9 @@ async function refreshTodoPanel(ui: ExtensionUIContext | undefined, cwd: string)
|
|
|
597
626
|
const md = await readFile(join(cwd, '.brain', 'todo.md'), 'utf8')
|
|
598
627
|
const who = await currentUser()
|
|
599
628
|
const data = parseOpenTasks(md, PANEL_MAX_ROWS, who)
|
|
629
|
+
// ★ 无任务就不显示面板(2026-11-06 与 pi 版同步,推翻了「空看板也画标题行」)。
|
|
630
|
+
// **不要再改回空看板也显示** —— 用户刚把那个行为提掉。
|
|
631
|
+
if (data.total === 0) { hidePanel(ui); return }
|
|
600
632
|
let tuiRef: RenderTui | null = null
|
|
601
633
|
ui.setWidget(
|
|
602
634
|
PANEL_KEY,
|
|
@@ -605,7 +637,7 @@ async function refreshTodoPanel(ui: ExtensionUIContext | undefined, cwd: string)
|
|
|
605
637
|
lastTui = tui // 供 hidePanel 在「无动画」时也能请求重绘
|
|
606
638
|
return {
|
|
607
639
|
render: (width: number) =>
|
|
608
|
-
renderPanelLines(data, who, width, (c: string, s: string) => theme.fg(c, s), animPhase, sessionNickname),
|
|
640
|
+
renderPanelLines(data, who, width, (c: string, s: string) => theme.fg(c, s), animPhase, sessionNickname, effortLevel),
|
|
609
641
|
invalidate: () => {},
|
|
610
642
|
}
|
|
611
643
|
},
|
|
@@ -736,7 +768,8 @@ export default function absOmpExtension(pi: ExtensionAPI): void {
|
|
|
736
768
|
resetThrottle()
|
|
737
769
|
// 本会话的随机昵称 —— 在这里抽一次(不是每帧抽,否则面板会疯狂闪)。
|
|
738
770
|
sessionNickname = pickNickname()
|
|
739
|
-
|
|
771
|
+
effortLevel = String((ctx && (ctx as any).thinkingLevel) || '')
|
|
772
|
+
logHook(`session_start nickname=${sessionNickname || '(off)'} effort=${effortLevel || '(off)'}`).catch(() => {})
|
|
740
773
|
// 启动时就画出面板。时序:omp 在 session_start 时已给 ctx.ui(无 UI 时是 noOp)。
|
|
741
774
|
// ⚠ omp 的 session_start 事件**没有 reason 字段**(pi 有)—— 别照抄 pi 版的 reason= 。
|
|
742
775
|
const cwd = (ctx && ctx.cwd) || process.cwd()
|
|
@@ -789,6 +822,14 @@ export default function absOmpExtension(pi: ExtensionAPI): void {
|
|
|
789
822
|
refreshTodoPanel(ctx.ui, ctx.cwd).catch(() => {})
|
|
790
823
|
})
|
|
791
824
|
|
|
825
|
+
// 切推理强度 → 面板主色跟着变(2026-11-06 与 pi 版同步)。
|
|
826
|
+
// omp 是否发这个事件尚未验证 —— 不发就只是一个空订阅,不影响任何现有行为。
|
|
827
|
+
pi.on("thinking_level_select", (event: any, ctx: any) => {
|
|
828
|
+
effortLevel = String(event?.level || (ctx && ctx.thinkingLevel) || "")
|
|
829
|
+
logHook(`thinking_level_select level=${effortLevel || "(off)"}`).catch(() => {})
|
|
830
|
+
refreshTodoPanel(ctx?.ui, (ctx && ctx.cwd) || process.cwd()).catch(() => {})
|
|
831
|
+
})
|
|
832
|
+
|
|
792
833
|
// 往 system prompt 注入静态指引 —— **omp 的通道与 pi 不同**:
|
|
793
834
|
// omp 返回 {systemPrompt: string[]} 是**整体替换**(不是 pi 的追加式 guidelines),
|
|
794
835
|
// 故必须「event.systemPrompt 基线 + 我们的段」拼接后整体返回。
|