@fanchao8609/agent_brain_sync 1.28.6 → 1.29.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/bin/abs.js +8 -41
- package/hooks/abs-rounded-editor/format.ts +77 -22
- package/hooks/abs-rounded-editor/index.ts +14 -19
- package/hooks/abs.omp.ts +1 -2
- package/hooks/abs.pi.ts +50 -51
- package/package.json +1 -1
- package/skill/abs-agent-brain-sync/SKILL.md +68 -68
- package/src/brain/ingest.js +132 -54
- package/src/brain/layers.js +11 -5
- package/src/brain/load.js +50 -65
- package/src/brain/maintain.js +15 -2
- package/src/brain/summon.js +25 -16
- package/src/lint.js +82 -14
- package/src/note.js +7 -0
- package/src/store.js +1 -1
- package/src/text.js +42 -11
- package/src/todo/archive.js +22 -1
- package/src/todo/spec.js +6 -0
- package/src/todo/task-cmd.js +18 -0
- package/src/todo.js +1 -1
package/bin/abs.js
CHANGED
|
@@ -340,10 +340,10 @@ function parseArgv(args) {
|
|
|
340
340
|
}
|
|
341
341
|
const usage = `abs — 跨会话记忆(.brain/ 图谱)
|
|
342
342
|
|
|
343
|
-
|
|
343
|
+
怎么用:按你要做的事找。相近命令已折叠,旧别名已删(index/status/summon/db search → show/search/ingest)。
|
|
344
344
|
|
|
345
345
|
【开工】读状态,接上次的活
|
|
346
|
-
abs load
|
|
346
|
+
abs load 必跑。未完成 todo + 用法 + 最近日志
|
|
347
347
|
abs recall [词…] 卡住时用:按当前处境推相关页(不给词也行)
|
|
348
348
|
|
|
349
349
|
【找东西】
|
|
@@ -380,9 +380,7 @@ const usage = `abs — 跨会话记忆(.brain/ 图谱)
|
|
|
380
380
|
abs update 升级并刷新 hook/skill
|
|
381
381
|
abs config set user <名字> 使用者姓名(写操作需先设置)
|
|
382
382
|
|
|
383
|
-
|
|
384
|
-
index/log/status → show | capture → concept | db search → search
|
|
385
|
-
summon → ingest --list | db add → init
|
|
383
|
+
视图折叠: show index|log|status 写知识: note / concept / rule
|
|
386
384
|
|
|
387
385
|
注: wrapup / teardown-check 是 hook 内部命令。
|
|
388
386
|
`;
|
|
@@ -390,7 +388,7 @@ const usage = `abs — 跨会话记忆(.brain/ 图谱)
|
|
|
390
388
|
/** 子命令级用法(abs <cmd> --help 时打印)。 */
|
|
391
389
|
const subUsage = {
|
|
392
390
|
load: [
|
|
393
|
-
'abs load —
|
|
391
|
+
'abs load — 开机读状态(未完成 todo + 用法 + 最近 log)',
|
|
394
392
|
'',
|
|
395
393
|
'用法:',
|
|
396
394
|
' abs load [--dir <项目根>]',
|
|
@@ -643,28 +641,12 @@ async function main() {
|
|
|
643
641
|
else console.log(`✗ 未知视图 "${v}"。可用: index | log | status`);
|
|
644
642
|
break;
|
|
645
643
|
}
|
|
646
|
-
case 'index':
|
|
647
|
-
rejectExtra(opts._, 'abs index');
|
|
648
|
-
console.log('(abs index 已改名 → abs show index)');
|
|
649
|
-
console.log(await cmdShow({ dir: opts.dir, view: 'index' }));
|
|
650
|
-
break;
|
|
651
644
|
case 'log': {
|
|
652
|
-
//
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
if (opts._.length) {
|
|
656
|
-
console.log(await cmdLog({ dir: opts.dir, title: opts._.join(' '), kind: opts.kind }));
|
|
657
|
-
} else {
|
|
658
|
-
console.log('(查看流水已改名 → abs show log;abs log "完成 X" 仍是记成果)');
|
|
659
|
-
console.log(await cmdShow({ dir: opts.dir, view: 'log' }));
|
|
660
|
-
}
|
|
645
|
+
// 只剩写:abs log "结论"。查看流水 = abs show log(2026-11-08 去掉无参查看的别名)。
|
|
646
|
+
if (!opts._.length) throw new Error('✗ 用法: abs log "一句话结论" [--kind dev|note|concept|ingest]\n 查看流水: abs show log');
|
|
647
|
+
console.log(await cmdLog({ dir: opts.dir, title: opts._.join(' '), kind: opts.kind }));
|
|
661
648
|
break;
|
|
662
649
|
}
|
|
663
|
-
case 'status':
|
|
664
|
-
rejectExtra(opts._, 'abs status');
|
|
665
|
-
console.log('(abs status 已改名 → abs show status)');
|
|
666
|
-
console.log(await cmdStatus({ dir: opts.dir }));
|
|
667
|
-
break;
|
|
668
650
|
// abs todo —— 无子命令=看板;带子命令=任务写操作
|
|
669
651
|
case 'todo': {
|
|
670
652
|
const [sub, id, ...rest2] = opts._;
|
|
@@ -721,13 +703,6 @@ async function main() {
|
|
|
721
703
|
console.log(await cmdTask({ dir: opts.dir, action, id, section: opts.section, note: opts.note, as: opts.as, session: opts.session }));
|
|
722
704
|
break;
|
|
723
705
|
}
|
|
724
|
-
case 'summon': {
|
|
725
|
-
// 别名(2026-11-06):并进 ingest --list —— 两者是同一件事的两半
|
|
726
|
-
// (summon 说「哪些没落页」,ingest 负责落)。分两个顶层命令只是多记一个词。
|
|
727
|
-
console.log('(abs summon 已改名 → abs ingest --list)');
|
|
728
|
-
console.log(await cmdSummon({ dir: opts.dir, mark: !!opts.mark, full: !!opts.full, status: !!opts.status, limit: Number(opts.limit) || 15 }));
|
|
729
|
-
break;
|
|
730
|
-
}
|
|
731
706
|
case 'ingest': {
|
|
732
707
|
// --list:只看「还有哪些会话没落页」,不写入(原来的 abs summon)
|
|
733
708
|
if (opts.list) {
|
|
@@ -784,15 +759,7 @@ async function main() {
|
|
|
784
759
|
}
|
|
785
760
|
case 'db': {
|
|
786
761
|
const sub = opts._[0];
|
|
787
|
-
const rest = opts._.slice(1);
|
|
788
762
|
if (sub === 'rebuild') { console.log(await cmdDbRebuild({ dir: opts.dir })); break; }
|
|
789
|
-
if (sub === 'search') {
|
|
790
|
-
// 别名(2026-11-06):顶层已改叫 abs search。留着是为了不打断现有习惯与脚本,
|
|
791
|
-
// 但每次提示一次 —— 不提示的话旧名会永远活着,命令面又变宽。
|
|
792
|
-
console.log('(abs db search 已改名 → abs search)');
|
|
793
|
-
console.log(await cmdDbSearch({ query: rest.join(' '), limit: Number(opts.limit) || 8, full: !!opts.full, noRules: !!opts['no-rules'] }));
|
|
794
|
-
break;
|
|
795
|
-
}
|
|
796
763
|
if (sub === 'status') { console.log(await cmdDbStatus()); break; }
|
|
797
764
|
if (sub === 'evolve') { console.log(await cmdDbEvolve({ staleDays: Number(opts.staleDays) || 90 })); break; }
|
|
798
765
|
if (sub === 'add') {
|
|
@@ -800,7 +767,7 @@ async function main() {
|
|
|
800
767
|
console.log(`✓ 已登记项目: ${r}\n 建库: abs db rebuild`);
|
|
801
768
|
break;
|
|
802
769
|
}
|
|
803
|
-
console.log('用法: abs db rebuild |
|
|
770
|
+
console.log('用法: abs db rebuild | status | evolve | add [--dir <项目根>]');
|
|
804
771
|
break;
|
|
805
772
|
}
|
|
806
773
|
case 'query': {
|
|
@@ -72,31 +72,24 @@ function dropCols(s: string, n: number): string {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
/**
|
|
75
|
-
* 推理强度 →
|
|
76
|
-
* 色值起于用户 2026-11-07 给的六档灰蓝,之后用户要求「xhigh/high 再亮一点,往下都稍亮」,
|
|
77
|
-
* 按**同色相**(H≈219°)提亮度得到现在这组:
|
|
78
|
-
* max #ACB8CD(不动) · xhigh #9CABC7 · high #8D97AA · medium #747E93
|
|
79
|
-
* low #656F83 · minimal #586171
|
|
80
|
-
* 提亮前用户给的原值:xhigh #95A5C3 · high #768298 · medium #697387 · low #5C6577 · minimal #505867
|
|
81
|
-
* 色相是用户挑的灰蓝(不是绿),换色只许按用户给的值改,别拿主题色或自己的审美去替换。
|
|
82
|
-
* off / 未知 → 空串,表示「不上色,沿用默认色」—— 用户 2026-11-06 定过:推理关着就保持原样。
|
|
75
|
+
* 推理强度 → 主题色 token。空串表示「不上色,沿用默认色」。
|
|
83
76
|
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
77
|
+
* 规则:`thinking` + 首字母大写,正好对上主题里的 thinkingMinimal…thinkingMax 七档。
|
|
78
|
+
* 为什么 off 返回空串而不是 `thinkingOff`:主题给 thinkingOff 的是接近背景的暗灰
|
|
79
|
+
* (dark.json: `okhsl(229 8% 49%)`),拿它去染文字会看不清。用户 2026-11-06 定的是
|
|
80
|
+
* 「off 时保持默认色」。
|
|
88
81
|
*
|
|
89
|
-
* ★ 同一份逻辑在 hooks/abs.pi.ts 里也有一份(`
|
|
82
|
+
* ★ 同一份逻辑在 hooks/abs.pi.ts 里也有一份(`effortColorToken`)。不抽公共模块是故意的:
|
|
90
83
|
* 安装后 `abs.ts` 与 `abs-rounded-editor/` 是两个可独立删除的扩展,抽成共享文件会让
|
|
91
84
|
* 「删掉输入框样式目录」连带把 todo 面板搞崩。
|
|
92
85
|
*/
|
|
93
86
|
export const EFFORT_COLOR: Record<string, { rgb: string; ansi256: number }> = {
|
|
94
|
-
max: { rgb: '
|
|
95
|
-
xhigh: { rgb: '
|
|
96
|
-
high: { rgb: '
|
|
97
|
-
medium: { rgb: '
|
|
98
|
-
low: { rgb: '
|
|
99
|
-
minimal: { rgb: '
|
|
87
|
+
max: { rgb: '197;205;220', ansi256: 189 },
|
|
88
|
+
xhigh: { rgb: '168;182;204', ansi256: 152 },
|
|
89
|
+
high: { rgb: '150;166;198', ansi256: 110 },
|
|
90
|
+
medium: { rgb: '122;138;160', ansi256: 110 },
|
|
91
|
+
low: { rgb: '94;108;132', ansi256: 103 },
|
|
92
|
+
minimal: { rgb: '70;82;106', ansi256: 240 },
|
|
100
93
|
}
|
|
101
94
|
|
|
102
95
|
export function effortAnsi(level: string | null | undefined, colorMode = 'truecolor'): string {
|
|
@@ -105,13 +98,37 @@ export function effortAnsi(level: string | null | undefined, colorMode = 'trueco
|
|
|
105
98
|
return colorMode === 'truecolor' ? `38;2;${g.rgb}` : `38;5;${g.ansi256}`
|
|
106
99
|
}
|
|
107
100
|
|
|
101
|
+
/** 「搁置」橙 #F59E0B —— 与 todo 面板的 `搁置` 标签同色(abs.pi.ts 的 `C.hold`)。
|
|
102
|
+
* 用途(2026-11-08 用户要求):输入框上边框的 working 状态与右上角状态文字。
|
|
103
|
+
* ★ 色值在 abs.pi.ts 里也有一份,不抽公共模块是故意的(见 EFFORT_COLOR 的注释)。 */
|
|
104
|
+
export const HOLD_COLOR = { rgb: '245;158;11', ansi256: 214 }
|
|
105
|
+
|
|
106
|
+
export function holdAnsi(colorMode = 'truecolor'): string {
|
|
107
|
+
return colorMode === 'truecolor' ? `38;2;${HOLD_COLOR.rgb}` : `38;5;${HOLD_COLOR.ansi256}`
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** 上边框最左那段「working 状态」(如 `⠼ Working`)。
|
|
111
|
+
*
|
|
112
|
+
* 为何要单独把它抠出来(2026-11-08 用户要求它和状态文字同色):那段由 pi 的
|
|
113
|
+
* WorkingStatusIndicator 画、夹在 `── ` 与后续虚线之间(见 pi 的 custom-editor
|
|
114
|
+
* `renderTopBorder`),而 roundBorder 会先剥掉整行的色再重新上色,
|
|
115
|
+
* 不能靠「它原本的颜色」认它。判据取形状:开头 `─+ ` 之后、下一个 `─` 之前的那段。
|
|
116
|
+
*
|
|
117
|
+
* 排除 `↑ N more` / `↓ N more`(pi 自己的滚动提示):那不是 working 状态,
|
|
118
|
+
* 染成橙色就是错色。 */
|
|
119
|
+
export function leadSpanOf(plain: string): string {
|
|
120
|
+
const m = String(plain).match(/^─+ ([^─]+?) ?─/)
|
|
121
|
+
if (!m) return ''
|
|
122
|
+
const text = m[1]
|
|
123
|
+
return /[↑↓]/.test(text) ? '' : text
|
|
124
|
+
}
|
|
125
|
+
|
|
108
126
|
/** 用 effortAnsi 的结果包一段文字;空串原样返回(off 时不着色)。 */
|
|
109
127
|
export function paintAnsi(ansi: string, text: string): string {
|
|
110
128
|
return ansi ? `\x1b[${ansi}m${text}\x1b[39m` : text
|
|
111
129
|
}
|
|
112
130
|
|
|
113
|
-
/**
|
|
114
|
-
* 把一行边框文本换成「左圆角 + 左标签 + 中段 + 右圆角」,**显示宽度**保持原样。
|
|
131
|
+
/** 把一行边框文本换成「左圆角 + 左标签 + 中段 + 右圆角」,**显示宽度**保持原样。
|
|
115
132
|
* 边框行内含 ANSI 时直接 slice 会切坏转义序列,所以先剥色、改字、再整行着色。
|
|
116
133
|
*
|
|
117
134
|
* ★ 宽度一律按 textWidth(CJK = 2 列)算,不按字符数 —— 标签里的中文昵称
|
|
@@ -119,6 +136,10 @@ export function paintAnsi(ansi: string, text: string): string {
|
|
|
119
136
|
*
|
|
120
137
|
* leftLabel 贴在左圆角之后;label 贴最右端。哪个放不下就丢掉哪个
|
|
121
138
|
* (宁可没有,也不让边框溢出折行)。
|
|
139
|
+
*
|
|
140
|
+
* opts.leadFg / opts.labelFg(2026-11-08 用户要求):只给「working 状态那段」
|
|
141
|
+
* 和「右标签」换个色,其余(圆角、虚线)仍用 fg。不传 opts 时整行一个色,
|
|
142
|
+
* 与旧行为逐字相同。
|
|
122
143
|
*/
|
|
123
144
|
export function roundBorder(
|
|
124
145
|
line: string,
|
|
@@ -127,6 +148,7 @@ export function roundBorder(
|
|
|
127
148
|
right: string,
|
|
128
149
|
label = '',
|
|
129
150
|
leftLabel = '',
|
|
151
|
+
opts: { leadFg?: (s: string) => string; labelFg?: (s: string) => string } = {},
|
|
130
152
|
): string {
|
|
131
153
|
const plain = stripAnsi(line)
|
|
132
154
|
if (plain.length === 0) return line
|
|
@@ -153,5 +175,38 @@ export function roundBorder(
|
|
|
153
175
|
mid = ' ' + leftLabel + ' ' + body + mid.slice(mid.length - tailLen)
|
|
154
176
|
}
|
|
155
177
|
}
|
|
156
|
-
return
|
|
178
|
+
return paintSpans(left + mid + right, fg, [
|
|
179
|
+
// working 段从**开头**找(它就在最左);右标签从**末尾**找
|
|
180
|
+
// (万一同一串又出现在左边,不能把左边那个也染成标签色)。
|
|
181
|
+
[leadSpanOf(plain), opts.leadFg, 'first'],
|
|
182
|
+
[label, opts.labelFg, 'last'],
|
|
183
|
+
])
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** 把 out 里的几个区间换成各自的色,其余整段用 fg。区间按位置排序、重叠则跳过。
|
|
187
|
+
* 为何不用「拼段」实现:标签与 working 段的字符是从 mid 里苹/插进去的,
|
|
188
|
+
* 只有拿到最终串才能准确定位(直接拼段会在 dropCols 后错位)。 */
|
|
189
|
+
function paintSpans(
|
|
190
|
+
out: string,
|
|
191
|
+
fg: (s: string) => string,
|
|
192
|
+
spans: [string, ((s: string) => string) | undefined, 'first' | 'last'][],
|
|
193
|
+
): string {
|
|
194
|
+
const found: [number, number, (s: string) => string][] = []
|
|
195
|
+
for (const [text, paint, side] of spans) {
|
|
196
|
+
if (!text || !paint) continue
|
|
197
|
+
const i = side === 'last' ? out.lastIndexOf(text) : out.indexOf(text)
|
|
198
|
+
if (i === -1) continue
|
|
199
|
+
found.push([i, i + text.length, paint])
|
|
200
|
+
}
|
|
201
|
+
found.sort((a, b) => a[0] - b[0])
|
|
202
|
+
let res = ''
|
|
203
|
+
let pos = 0
|
|
204
|
+
for (const [s, e, paint] of found) {
|
|
205
|
+
if (s < pos) continue
|
|
206
|
+
if (s > pos) res += fg(out.slice(pos, s)) // 空片段不上色,免得留下空色码对
|
|
207
|
+
res += paint(out.slice(s, e))
|
|
208
|
+
pos = e
|
|
209
|
+
}
|
|
210
|
+
if (pos < out.length) res += fg(out.slice(pos))
|
|
211
|
+
return res
|
|
157
212
|
}
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* 注意:自动补全菜单行排在底边之下、且自带内容宽度,不套竖线(否则菜单被框住会错位)。
|
|
19
19
|
*/
|
|
20
20
|
import { CustomEditor } from '@earendil-works/pi-coding-agent'
|
|
21
|
-
import { buildStatus, effortAnsi, paintAnsi, roundBorder, shortenModel } from './format.ts'
|
|
21
|
+
import { buildStatus, effortAnsi, holdAnsi, paintAnsi, roundBorder, shortenModel } from './format.ts'
|
|
22
22
|
|
|
23
23
|
const SIDE = '│'
|
|
24
24
|
|
|
@@ -46,32 +46,27 @@ export class RoundedEditor extends CustomEditor {
|
|
|
46
46
|
if (lines.length < 2) return lines
|
|
47
47
|
|
|
48
48
|
const borderColor = (this as any).borderColor?.bind(this)
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
// 看起来就像完全没改 —— 边框不跟着变,这个功能肉眼就看不见。
|
|
59
|
-
// roundBorder 本来就是「剥掉原色 → 整行重新上色」,故把上色函数传进 wrap 即可。
|
|
60
|
-
const wrap = effortFg ?? ((s: string) => (borderColor ? borderColor(s) : s))
|
|
49
|
+
// 现代暗色方案(2026-11-08):文字 #F8FAFC,边框 #444C56,不再跟推理强度变色。
|
|
50
|
+
// 光标色 / 输入框背景 / 聚焦外发光是 pi 编辑器内部画的,扩展接管不了,先不动。
|
|
51
|
+
const colorMode = (() => { try { return String(themeGetter?.().getColorMode()) } catch { return 'truecolor' } })()
|
|
52
|
+
const textFg = (s: string) => paintAnsi(effortAnsi(effortLevel, colorMode), s)
|
|
53
|
+
const lineFg = textFg
|
|
54
|
+
const wrap = lineFg
|
|
55
|
+
// working 状态与右上角状态文字用「搁置」橙 #F59E0B(2026-11-08 用户要求):
|
|
56
|
+
// 上边框的圆角与虚线仍走正文色,只有这两段橙 —— 一眼区分「还在跑」与上下文占用。
|
|
57
|
+
const holdFg = (s: string) => paintAnsi(holdAnsi(colorMode), s)
|
|
61
58
|
const out: string[] = []
|
|
62
59
|
|
|
63
60
|
// 上边框:原本是整行 `─`(含 working 图标与滚动提示),两端换圆角,右端贴状态。
|
|
64
|
-
out.push(roundBorder(lines[0], wrap, '╭', '╮', statusLabel))
|
|
61
|
+
out.push(roundBorder(lines[0], wrap, '╭', '╮', statusLabel, '', { leadFg: holdFg, labelFg: holdFg }))
|
|
65
62
|
// 下边框在原结果的倒数第 (1 + 自动补全行数) 位 —— 补全菜单排在它后面。
|
|
66
63
|
const autocompleteHeight = (this as any).renderedAutocompleteHeight ?? 0
|
|
67
64
|
const bottomIdx = lines.length - 1 - autocompleteHeight
|
|
68
65
|
|
|
69
66
|
for (let i = 1; i < bottomIdx; i++) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
// 输入框内容是用户手打的纯文本,行内不会有 reset 把后半行冲掉。
|
|
74
|
-
out.push(wrap(SIDE) + (effortFg ? effortFg(body) : body) + wrap(SIDE))
|
|
67
|
+
// 剥掉 pi 原生文字色,再包 #F8FAFC —— 否则内层色码赢。
|
|
68
|
+
const body = lines[i].replace(/\x1b\[(?!7m)[0-9;]*m/g, '')
|
|
69
|
+
out.push(wrap(SIDE) + textFg(body) + wrap(SIDE))
|
|
75
70
|
}
|
|
76
71
|
// 下边框(2026-11-07 还原:左下角的 fanchao·昵称已去掉 —— 用户嫌难看,
|
|
77
72
|
// 回 todo 面板标题;roundBorder 的 leftLabel 参数保留备用)。
|
package/hooks/abs.omp.ts
CHANGED
|
@@ -542,8 +542,7 @@ export function todoAnimFrame(phase: number): string {
|
|
|
542
542
|
* 规则:`thinking` + 首字母大写(thinkingMinimal…thinkingMax)。
|
|
543
543
|
* off 返回空串而不是 `thinkingOff`:那个色接近背景,染文字会看不清(2026-11-06 用户定)。
|
|
544
544
|
*
|
|
545
|
-
* ★ 与 pi
|
|
546
|
-
* omp 这边仍跟主题的 thinkingXxx 七色 —— 用户只提了 pi 面板。两个模板分别装到不同宿主,
|
|
545
|
+
* ★ 与 pi 版(hooks/abs.pi.ts 的 effortToken)逐字同义,但不共享:两个模板分别装到不同宿主,
|
|
547
546
|
* 独立成文件才能各自删除/升级。
|
|
548
547
|
*/
|
|
549
548
|
export function effortToken(level: string | null | undefined): string {
|
package/hooks/abs.pi.ts
CHANGED
|
@@ -936,9 +936,6 @@ function clipToWidth(s: string, width: number): string {
|
|
|
936
936
|
// 为何用盲文点阵而不是 ⠋⠙⠹ 之外的 ASCII 旋转符:
|
|
937
937
|
// ⠿ 系符号在等宽字体里都是 1 列宽,且视觉上是个转动的方框,比 | / - \ 均匀。
|
|
938
938
|
// 为何帧宽必须恒定:面板每 250ms 重绘,宽度一变整行就会左右抖。盲文恰好全是 1 列。
|
|
939
|
-
/** 进行中那行的小动画(2026-11-07 用户换掉了原来的盲文十帧旋转 ⠋⠙⠹…,嫌难看,改要圆圈)。
|
|
940
|
-
* 4 帧一圈 × 200ms ≈ 0.8 秒转一圈 —— 与旧动画(10 帧 × 100ms = 1 秒)节奏相当,
|
|
941
|
-
* 帧数少了若不降速会转成一团花。 */
|
|
942
939
|
const TODO_ANIM_FRAMES = ['◐', '◓', '◑', '◒']
|
|
943
940
|
const TODO_ANIM_MS = 200
|
|
944
941
|
|
|
@@ -974,31 +971,25 @@ export function todoAnimFrame(phase: number): string {
|
|
|
974
971
|
return TODO_ANIM_FRAMES[i]
|
|
975
972
|
}
|
|
976
973
|
|
|
977
|
-
/**
|
|
978
|
-
*
|
|
979
|
-
* 按**同色相**(H≈219°)提亮度得到现在这组:
|
|
980
|
-
* max #ACB8CD(不动) · xhigh #9CABC7 · high #8D97AA · medium #747E93
|
|
981
|
-
* low #656F83 · minimal #586171
|
|
982
|
-
* 提亮前用户给的原值:xhigh #95A5C3 · high #768298 · medium #697387 · low #5C6577 · minimal #505867
|
|
983
|
-
* 色相是用户挑的灰蓝(不是绿),换色只许按用户给的值改,别拿主题色或自己的审美去替换。
|
|
984
|
-
* off / 未知 → 空串,表示「不上色」:调用方用灰阶(用户 2026-11-06 定过,推理关着就别发光)。
|
|
974
|
+
/**
|
|
975
|
+
* 推理强度 → 主题色 token。空串表示「不上推理色,用调用方给的兼底色」。
|
|
985
976
|
*
|
|
986
|
-
*
|
|
987
|
-
*
|
|
988
|
-
*
|
|
989
|
-
*
|
|
977
|
+
* 规则:`thinking` + 首字母大写,正好对上主题里的 thinkingMinimal…thinkingMax 七档。
|
|
978
|
+
* 为什么 off 返回空串而不是 `thinkingOff`:主题给 thinkingOff 的是接近背景的暗灰
|
|
979
|
+
* (dark.json: `okhsl(229 8% 49%)`),拿它去染文字会看不清。用户 2026-11-06 定的是
|
|
980
|
+
* 「off 时保持默认色」。
|
|
990
981
|
*
|
|
991
|
-
* ★ 同一份逻辑在 hooks/abs-rounded-editor/format.ts 里也有一份(`
|
|
982
|
+
* ★ 同一份逻辑在 hooks/abs-rounded-editor/format.ts 里也有一份(`thinkingColorToken`)。
|
|
992
983
|
* 不抽公共模块是故意的:安装后 `abs.ts` 与 `abs-rounded-editor/` 是两个可独立删除的扩展,
|
|
993
984
|
* 抽成共享文件会让「删掉输入框样式目录」连带把 todo 面板搞崩。
|
|
994
985
|
*/
|
|
995
986
|
export const EFFORT_COLOR: Record<string, { rgb: string; ansi256: number }> = {
|
|
996
|
-
max: { rgb: '
|
|
997
|
-
xhigh: { rgb: '
|
|
998
|
-
high: { rgb: '
|
|
999
|
-
medium: { rgb: '
|
|
1000
|
-
low: { rgb: '
|
|
1001
|
-
minimal: { rgb: '
|
|
987
|
+
max: { rgb: '197;205;220', ansi256: 189 },
|
|
988
|
+
xhigh: { rgb: '168;182;204', ansi256: 152 },
|
|
989
|
+
high: { rgb: '150;166;198', ansi256: 110 },
|
|
990
|
+
medium: { rgb: '122;138;160', ansi256: 110 },
|
|
991
|
+
low: { rgb: '94;108;132', ansi256: 103 },
|
|
992
|
+
minimal: { rgb: '70;82;106', ansi256: 240 },
|
|
1002
993
|
}
|
|
1003
994
|
|
|
1004
995
|
export function effortAnsi(level: string | null | undefined, colorMode = 'truecolor'): string {
|
|
@@ -1020,23 +1011,34 @@ export function renderPanelLines(
|
|
|
1020
1011
|
animPhase = -1,
|
|
1021
1012
|
nickname = '',
|
|
1022
1013
|
effort = '',
|
|
1023
|
-
colorMode = 'truecolor',
|
|
1024
1014
|
): string[] {
|
|
1025
1015
|
// ★ 空看板不再直接返回空数组(2026-10-06 用户要求:「即便 todo 没有列表,也要在 pi 上显示」)。
|
|
1026
1016
|
// 理由与看板段一致:面板是「机制在不在」的常驻指示,进去就看不见 = 不知道机制活着。
|
|
1027
1017
|
// 下面仍要画框和标题,所以这里只是不再提前 return。
|
|
1028
1018
|
//
|
|
1029
1019
|
// ★ 主色跟推理强度走(2026-11-06 用户需求):标题、[状态]、树线 ├─└─、复选框 [ ]、
|
|
1030
|
-
// 进行中动画同用一色,与 pi
|
|
1031
|
-
// ★
|
|
1032
|
-
// ★ 2026-11-07 用户再改:面板里**所有文字都用这一色**(标题、状态、id、描述、子项、+N more),
|
|
1033
|
-
// 不再给描述/子项降成灰 —— 拿“颜色深浅”区分主次的写法作废了,层次只靠符号与缩进。
|
|
1034
|
-
// 别把描述改回 muted:那是用户主动去掉的(原话「所有文字都一致吧」)。
|
|
1035
|
-
// ★ off 时整块面板用灰阶(2026-11-06 用户补提:「当推理关闭的时候 todo 面板文字应该全是灰色」)——
|
|
1020
|
+
// 进行中动画同用一色,与 pi 输入框描边/文字同色系。
|
|
1021
|
+
// ★ off 时整块面板用灰阶(同日用户补提:「当推理关闭的时候 todo 面板文字应该全是灰色」)——
|
|
1036
1022
|
// 高亮色只在真的开着推理时给,否则「推理关了面板还在发光」让人误以为还在深度思考。
|
|
1037
|
-
//
|
|
1038
|
-
|
|
1039
|
-
|
|
1023
|
+
// 主色用 muted(中灰),描述与子项降一阶到 dim(深灰),两阶灰保留层次,不一整片平。
|
|
1024
|
+
// 现代暗色方案(2026-11-08 用户给定),不再跟推理强度走。
|
|
1025
|
+
// 标题色(2026-11-08 用户实测「太亮了」):从纯金 #FFD700 换成与「搁置」同一个橙 #F59E0B
|
|
1026
|
+
// —— 纯金在暗底上抢眼,标题只是分区标签,不该比当天要做的事更跳。
|
|
1027
|
+
// 代价:标题与 `[搁置]` 标签同色;两者一个在最左(标题)一个在行中(状态),位置分得开。
|
|
1028
|
+
const C = {
|
|
1029
|
+
title: '38;2;245;158;11',
|
|
1030
|
+
task: '38;2;226;232;240',
|
|
1031
|
+
desc: '38;2;148;163;184',
|
|
1032
|
+
tree: '38;2;150;170;198',
|
|
1033
|
+
doing: '38;2;16;185;129',
|
|
1034
|
+
talk: '38;2;59;130;246',
|
|
1035
|
+
hold: '38;2;245;158;11',
|
|
1036
|
+
stuck: '38;2;139;92;246',
|
|
1037
|
+
done: '38;2;100;116;139',
|
|
1038
|
+
}
|
|
1039
|
+
const stateAnsi = (st: string): string =>
|
|
1040
|
+
st === '进行中' ? C.doing : st === '讨论中' ? C.talk : st === '搁置' ? C.hold : st === '滞留中' ? C.stuck : C.task
|
|
1041
|
+
const paint = (ansi: string, t: string) => paintAnsi(ansi, t)
|
|
1040
1042
|
const lines: string[] = []
|
|
1041
1043
|
// 上描边行已删除(2026-11-04 用户要求「去掉上边框」)。
|
|
1042
1044
|
// 历史:那行曾用 `thinkingOff` 对齐 pi 输入框描边色(排查三轮才定色)。
|
|
@@ -1048,7 +1050,7 @@ export function renderPanelLines(
|
|
|
1048
1050
|
const title = whoPart ? `Todo (${data.total}) — ${whoPart}` : `Todo (${data.total})`
|
|
1049
1051
|
// 标题也顶最左(2026-11-04 用户)。旧代码曾在空看板时多缩 1 格,2026-11-06 空看板
|
|
1050
1052
|
// 已整块不显示(见 refreshTodoPanel),那个分支没意义了。
|
|
1051
|
-
lines.push(clipToWidth(
|
|
1053
|
+
lines.push(clipToWidth(paint(C.title, title), width))
|
|
1052
1054
|
|
|
1053
1055
|
const lastIdx = data.rows.length - 1
|
|
1054
1056
|
data.rows.forEach((t, i) => {
|
|
@@ -1060,33 +1062,36 @@ export function renderPanelLines(
|
|
|
1060
1062
|
// 进行中的行带一个小动画(其余行不加,避免满屏都在跳)
|
|
1061
1063
|
// 单字符 spinner 紧跟 [进行中]:靠前放才能一眼看到(放行尾会被长描述挤到前面去看不见)。
|
|
1062
1064
|
// 2026-11-06 用户要求:「放在 [进行中] 后面」。
|
|
1063
|
-
const
|
|
1065
|
+
const hasState = ['进行中', '讨论中', '搁置', '滞留中'].includes(t.state)
|
|
1066
|
+
const anim = t.state === '进行中' && animPhase >= 0 ? ' ' + paint(C.doing, todoAnimFrame(animPhase)) : ''
|
|
1064
1067
|
// 复选框 `[ ]`(2026-10-16 用户要求):面板是 todo.md 的只读视图,
|
|
1065
1068
|
// 就该长成 todo 原貌 —— 摆成与文件里 `- [ ]` 同形,读到就认得出对应哪一行。
|
|
1066
|
-
const
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1069
|
+
const eff = effortAnsi(effort, 'truecolor') || C.desc
|
|
1070
|
+
const box = hasState ? '' : paint(eff, '[ ] ')
|
|
1071
|
+
const stPart = hasState
|
|
1072
|
+
? paint(stateAnsi(t.state), `[${t.state}]`)
|
|
1073
|
+
: paint(eff, '[待办]')
|
|
1074
|
+
const head = `${paint(C.tree, branch)} ${box}${stPart}${anim} ${paint(eff, t.id)}`
|
|
1075
|
+
const desc = t.desc ? ' ' + paint(C.desc, `— ${t.desc}`) : ''
|
|
1071
1076
|
lines.push(clipToWidth(head + desc, width))
|
|
1072
1077
|
if (t.kids && t.kids.length) {
|
|
1073
1078
|
// 子项:标准 md 嵌套列表 → 画成树
|
|
1074
1079
|
// ├─ [ ] 子项一
|
|
1075
1080
|
// └─ [x] 子项二
|
|
1076
1081
|
// 树线前缀 = 父任务的 │(非末条)或空格(末条),与父任务分支对齐。
|
|
1077
|
-
//
|
|
1078
|
-
const stem = isLast ? ' ' :
|
|
1082
|
+
// 勾过的用 dim 压暗(一眼看出哪些已完成),未勾用正常色。
|
|
1083
|
+
const stem = isLast ? ' ' : paint(C.tree, '│ ')
|
|
1079
1084
|
const n = t.kids.length
|
|
1080
1085
|
t.kids.forEach((k, ki) => {
|
|
1081
1086
|
const kb = ki === n - 1 ? '└─' : '├─'
|
|
1082
|
-
const kbox =
|
|
1083
|
-
const ktext =
|
|
1084
|
-
lines.push(clipToWidth(`${stem} ${
|
|
1087
|
+
const kbox = paint(eff, k.done ? '[x]' : '[ ]')
|
|
1088
|
+
const ktext = k.done ? `\x1b[9m${paint(C.done, k.text)}\x1b[29m` : paint(C.desc, k.text)
|
|
1089
|
+
lines.push(clipToWidth(`${stem} ${paint(C.tree, kb)} ${kbox} ${ktext}`, width))
|
|
1085
1090
|
})
|
|
1086
1091
|
}
|
|
1087
1092
|
})
|
|
1088
1093
|
|
|
1089
|
-
if (data.hidden > 0) lines.push(clipToWidth(
|
|
1094
|
+
if (data.hidden > 0) lines.push(clipToWidth(paint(C.title, ` +${data.hidden} more`), width))
|
|
1090
1095
|
return lines
|
|
1091
1096
|
}
|
|
1092
1097
|
/** 隐藏面板。**必须主动 requestRender** —— 否则数据清了、屏幕还留着旧画面。
|
|
@@ -1125,11 +1130,6 @@ async function refreshTodoPanel(ui: any, cwd: string): Promise<void> {
|
|
|
1125
1130
|
(tui: any, theme: any) => {
|
|
1126
1131
|
tuiRef = tui
|
|
1127
1132
|
lastTui = tui // 供 hidePanel 在"无动画"时也能请求重绘
|
|
1128
|
-
// 主色要按终端的色深选编码(24 位色 / 256 色)—— getColorMode() 是 pi 主题自己
|
|
1129
|
-
// 算好的那一个;取不到就当支持 24 位色(真拿不准时宁可亮一点也别一格都不出)。
|
|
1130
|
-
const colorMode = (() => {
|
|
1131
|
-
try { return String(theme?.getColorMode?.() || 'truecolor') } catch { return 'truecolor' }
|
|
1132
|
-
})()
|
|
1133
1133
|
return {
|
|
1134
1134
|
render: (width: number) =>
|
|
1135
1135
|
renderPanelLines(
|
|
@@ -1140,7 +1140,6 @@ async function refreshTodoPanel(ui: any, cwd: string): Promise<void> {
|
|
|
1140
1140
|
animPhase,
|
|
1141
1141
|
sessionNickname,
|
|
1142
1142
|
effortLevel,
|
|
1143
|
-
colorMode,
|
|
1144
1143
|
),
|
|
1145
1144
|
invalidate: () => {},
|
|
1146
1145
|
}
|