@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 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 必跑。Rules + 图谱计数 + 任务板 + 最近流水
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 — 开机读状态(Rules 全量 + 图谱计数 + todo 看板 + 最近 log)',
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
- // 无参=查看 log.md;带参=追加一行 (用户/AI 主动记; hook 生命周期事件走技术日志 hooks.log, 不经这里)
653
- // ★ log 有双职责,不能并进 show:带参时它是**写**操作。
654
- // 所以只把「无参查看」这一半指向 show,写的那半保持不变、也不提示改名。
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 | search <词> | status | evolve | add [--dir <项目根>]');
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
- * 推理强度 → 面板与输入框的主色(2026-11-07 用户需求)。
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
- * 为何自己拼 ANSI 而不是走 theme.fg:fg() 只认主题里的 token 名,主题那七档色相跟这组不一样。
85
- * 终端支持 24 位色就给 38;2,否则退到 256 色板里的一档(见 ansi256)。
86
- * ★ 256 色板里没有这种灰蓝:只能退到灰阶档(250/249/247/244/243/241),保住“一档比一档暗”、
87
- * 丢掉蓝调 —— 24 位色终端才是标称效果。
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 里也有一份(`effortAnsi`)。不抽公共模块是故意的:
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: '172;184;205', ansi256: 250 },
95
- xhigh: { rgb: '156;171;199', ansi256: 249 },
96
- high: { rgb: '141;151;170', ansi256: 247 },
97
- medium: { rgb: '116;126;147', ansi256: 244 },
98
- low: { rgb: '101;111;131', ansi256: 243 },
99
- minimal: { rgb: '88;97;113', ansi256: 241 },
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 fg(left + mid + right)
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
- // 文字色 + 边框色 = 推理强度的主色;off(或取不到)时为空 → 文字不上色、边框交回 pi 自己的边框色。
50
- // 选 24 位色还是 256 色,问主题自己的 getColorMode(),与 pi 其他文字一致。
51
- const themeNow = themeGetter?.()
52
- let colorMode = 'truecolor'
53
- try { colorMode = String(themeNow?.getColorMode?.() || 'truecolor') } catch {}
54
- const ansi = effortAnsi(effortLevel, colorMode)
55
- const effortFg = ansi ? (s: string) => paintAnsi(ansi, s) : null
56
- // ★ 边框也跟着上主色(2026-11-07 用户补提:「输入框也是要改颜色」)。
57
- // 前一轮只染了文字,结果输入框空着时框里没字、屏幕上只剩 pi 那圈紫/橙色边框,
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
- const body = lines[i]
71
- // 推理强度主色染**输入文字**(2026-11-06 用户需求,2026-11-07 换成用户指定的六档色)。
72
- // 边框同色(见上面 wrap)。整行包裹而不逐字:光标由 \x1b[7m 反转标记,与前景色互不干扰;
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 版不再字面对应:pi 版 2026-11-07 已改成绿色阶(`effortAnsi`,用户要求),
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
- /** 推理强度 → 面板与输入框的主色(2026-11-07 用户需求)。
978
- * 色值起于用户 2026-11-07 给的六档灰蓝,之后用户要求「xhigh/high 再亮一点,往下都稍亮」,
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
- * 为何自己拼 ANSI 而不是 theme.fg:fg() 只认主题里的 token 名,主题那七档色相跟这组不一样。
987
- * 终端支持 24 位色就给 38;2,否则退到 256 色板最近的一档(见 ansi256)。
988
- * ★ 256 色板里没有这种灰蓝:只能退到灰阶档(250/249/247/244/243/241),保住“一档比一档暗”、
989
- * 丢掉蓝调 —— 24 位色终端才是标称效果。
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 里也有一份(`effortAnsi`)。
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: '172;184;205', ansi256: 250 },
997
- xhigh: { rgb: '156;171;199', ansi256: 249 },
998
- high: { rgb: '141;151;170', ansi256: 247 },
999
- medium: { rgb: '116;126;147', ansi256: 244 },
1000
- low: { rgb: '101;111;131', ansi256: 243 },
1001
- minimal: { rgb: '88;97;113', ansi256: 241 },
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
- // ★ 2026-11-07 用户把色子换成自己指定的六档主色:max 最亮 → minimal 最暗(见 effortAnsi)。
1032
- // ★ 2026-11-07 用户再改:面板里**所有文字都用这一色**(标题、状态、id、描述、子项、+N more),
1033
- // 不再给描述/子项降成灰 —— 拿“颜色深浅”区分主次的写法作废了,层次只靠符号与缩进。
1034
- // 别把描述改回 muted:那是用户主动去掉的(原话「所有文字都一致吧」)。
1035
- // ★ off 时整块面板用灰阶(2026-11-06 用户补提:「当推理关闭的时候 todo 面板文字应该全是灰色」)——
1020
+ // 进行中动画同用一色,与 pi 输入框描边/文字同色系。
1021
+ // ★ off 时整块面板用灰阶(同日用户补提:「当推理关闭的时候 todo 面板文字应该全是灰色」)——
1036
1022
  // 高亮色只在真的开着推理时给,否则「推理关了面板还在发光」让人误以为还在深度思考。
1037
- // 既然要求“一致”,off 也用同一个 muted,不再分两阶灰。
1038
- const mainAnsi = effortAnsi(effort, colorMode)
1039
- const main = mainAnsi ? (s: string) => paintAnsi(mainAnsi, s) : (s: string) => fg('muted', s)
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(main(title), width))
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 anim = t.state === '进行中' && animPhase >= 0 ? ' ' + main(todoAnimFrame(animPhase)) : ''
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 box = main('[ ]')
1067
- // 状态标签、id、描述全跟主色(2026-11-06 定:靠符号和文字区分状态,不靠颜色;
1068
- // 2026-11-07 再定:描述也不单独降灰)。
1069
- const head = `${main(branch)} ${box} ${main(`[${t.state}]`)}${anim} ${main(t.id)}`
1070
- const desc = t.desc ? ' ' + main(`— ${t.desc}`) : ''
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
- // 2026-11-07 用户要求全面板同色:勾过的子项也不再压暗,完成与否靠 [x] 区分。
1078
- const stem = isLast ? ' ' : main('│ ')
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 = main(k.done ? '[x]' : '[ ]')
1083
- const ktext = main(k.text)
1084
- lines.push(clipToWidth(`${stem} ${main(kb)} ${kbox} ${ktext}`, width))
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(main(` +${data.hidden} more`), width))
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fanchao8609/agent_brain_sync",
3
- "version": "1.28.6",
3
+ "version": "1.29.0",
4
4
  "description": "agent-brain-sync: 跨会话 AI 编码记忆 — hook 纯触发 + CLI/MCP 读写 .brain markdown 图谱, 防并发写保护。",
5
5
  "type": "module",
6
6
  "license": "MIT",