@fanchao8609/agent_brain_sync 1.28.4 → 1.28.6

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.
@@ -72,21 +72,42 @@ function dropCols(s: string, n: number): string {
72
72
  }
73
73
 
74
74
  /**
75
- * 推理强度 → 主题色 token。空串表示「不上色,沿用默认色」。
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 定过:推理关着就保持原样。
76
83
  *
77
- * 规则:`thinking` + 首字母大写,正好对上主题里的 thinkingMinimal…thinkingMax 七档。
78
- * 为什么 off 返回空串而不是 `thinkingOff`:主题给 thinkingOff 的是接近背景的暗灰
79
- * (dark.json: `okhsl(229 8% 49%)`),拿它去染文字会看不清。用户 2026-11-06 定的是
80
- * 「off 时保持默认色」。
84
+ * 为何自己拼 ANSI 而不是走 theme.fg:fg() 只认主题里的 token 名,主题那七档色相跟这组不一样。
85
+ * 终端支持 24 位色就给 38;2,否则退到 256 色板里的一档(见 ansi256)。
86
+ * ★ 256 色板里没有这种灰蓝:只能退到灰阶档(250/249/247/244/243/241),保住“一档比一档暗”、
87
+ * 丢掉蓝调 —— 24 位色终端才是标称效果。
81
88
  *
82
- * ★ 同一份逻辑在 hooks/abs.pi.ts 里也有一份(`effortColorToken`)。不抽公共模块是故意的:
89
+ * ★ 同一份逻辑在 hooks/abs.pi.ts 里也有一份(`effortAnsi`)。不抽公共模块是故意的:
83
90
  * 安装后 `abs.ts` 与 `abs-rounded-editor/` 是两个可独立删除的扩展,抽成共享文件会让
84
91
  * 「删掉输入框样式目录」连带把 todo 面板搞崩。
85
92
  */
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)
93
+ 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 },
100
+ }
101
+
102
+ export function effortAnsi(level: string | null | undefined, colorMode = 'truecolor'): string {
103
+ const g = EFFORT_COLOR[String(level || '').trim().toLowerCase()]
104
+ if (!g) return ''
105
+ return colorMode === 'truecolor' ? `38;2;${g.rgb}` : `38;5;${g.ansi256}`
106
+ }
107
+
108
+ /** 用 effortAnsi 的结果包一段文字;空串原样返回(off 时不着色)。 */
109
+ export function paintAnsi(ansi: string, text: string): string {
110
+ return ansi ? `\x1b[${ansi}m${text}\x1b[39m` : text
90
111
  }
91
112
 
92
113
  /**
@@ -18,7 +18,7 @@
18
18
  * 注意:自动补全菜单行排在底边之下、且自带内容宽度,不套竖线(否则菜单被框住会错位)。
19
19
  */
20
20
  import { CustomEditor } from '@earendil-works/pi-coding-agent'
21
- import { buildStatus, roundBorder, shortenModel, thinkingColorToken } from './format.ts'
21
+ import { buildStatus, effortAnsi, paintAnsi, roundBorder, shortenModel } from './format.ts'
22
22
 
23
23
  const SIDE = '│'
24
24
 
@@ -46,12 +46,18 @@ 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
- const wrap = (s: string) => (borderColor ? borderColor(s) : s)
50
- // 文字色 = 推理强度色;off(或取不到)时为空 → 不着色,用终端默认前景色。
51
- const token = thinkingColorToken(effortLevel)
49
+ // 文字色 + 边框色 = 推理强度的主色;off(或取不到)时为空 → 文字不上色、边框交回 pi 自己的边框色。
50
+ // 选 24 位色还是 256 色,问主题自己的 getColorMode(),与 pi 其他文字一致。
52
51
  const themeNow = themeGetter?.()
53
- const effortFg =
54
- token && typeof themeNow?.fg === 'function' ? (s: string) => themeNow.fg(token, s) : null
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))
55
61
  const out: string[] = []
56
62
 
57
63
  // 上边框:原本是整行 `─`(含 working 图标与滚动提示),两端换圆角,右端贴状态。
@@ -62,9 +68,8 @@ export class RoundedEditor extends CustomEditor {
62
68
 
63
69
  for (let i = 1; i < bottomIdx; i++) {
64
70
  const body = lines[i]
65
- // 推理强度色染**输入文字**(2026-11-06 用户)。边框不动 —— 边框本来就是 pi 按
66
- // 同一档强度上的色(interactive-mode 的 updateEditorBorderColor),两者自然同色。
67
- // 整行包裹而不逐字:光标由 \x1b[7m 反转标记,与前景色互不干扰;
71
+ // 推理强度主色染**输入文字**(2026-11-06 用户需求,2026-11-07 换成用户指定的六档色)。
72
+ // 边框同色(见上面 wrap)。整行包裹而不逐字:光标由 \x1b[7m 反转标记,与前景色互不干扰;
68
73
  // 输入框内容是用户手打的纯文本,行内不会有 reset 把后半行冲掉。
69
74
  out.push(wrap(SIDE) + (effortFg ? effortFg(body) : body) + wrap(SIDE))
70
75
  }
package/hooks/abs.omp.ts CHANGED
@@ -542,7 +542,8 @@ export function todoAnimFrame(phase: number): string {
542
542
  * 规则:`thinking` + 首字母大写(thinkingMinimal…thinkingMax)。
543
543
  * off 返回空串而不是 `thinkingOff`:那个色接近背景,染文字会看不清(2026-11-06 用户定)。
544
544
  *
545
- * ★ 与 pi 版(hooks/abs.pi.ts 的 effortToken)逐字同义,但不共享:两个模板分别装到不同宿主,
545
+ * ★ 与 pi 版不再字面对应:pi 版 2026-11-07 已改成绿色阶(`effortAnsi`,用户要求),
546
+ * omp 这边仍跟主题的 thinkingXxx 七色 —— 用户只提了 pi 面板。两个模板分别装到不同宿主,
546
547
  * 独立成文件才能各自删除/升级。
547
548
  */
548
549
  export function effortToken(level: string | null | undefined): string {
package/hooks/abs.pi.ts CHANGED
@@ -936,8 +936,11 @@ function clipToWidth(s: string, width: number): string {
936
936
  // 为何用盲文点阵而不是 ⠋⠙⠹ 之外的 ASCII 旋转符:
937
937
  // ⠿ 系符号在等宽字体里都是 1 列宽,且视觉上是个转动的方框,比 | / - \ 均匀。
938
938
  // 为何帧宽必须恒定:面板每 250ms 重绘,宽度一变整行就会左右抖。盲文恰好全是 1 列。
939
- const TODO_ANIM_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
940
- const TODO_ANIM_MS = 100
939
+ /** 进行中那行的小动画(2026-11-07 用户换掉了原来的盲文十帧旋转 ⠋⠙⠹…,嫌难看,改要圆圈)。
940
+ * 4 帧一圈 × 200ms ≈ 0.8 秒转一圈 —— 与旧动画(10 帧 × 100ms = 1 秒)节奏相当,
941
+ * 帧数少了若不降速会转成一团花。 */
942
+ const TODO_ANIM_FRAMES = ['◐', '◓', '◑', '◒']
943
+ const TODO_ANIM_MS = 200
941
944
 
942
945
  /** 动画相位(每 tick +1)。模块级 —— 同 agentEndSeen,闭包 flag 在重复注册时不共享。 */
943
946
  let animPhase = 0
@@ -971,22 +974,42 @@ export function todoAnimFrame(phase: number): string {
971
974
  return TODO_ANIM_FRAMES[i]
972
975
  }
973
976
 
974
- /**
975
- * 推理强度 → 主题色 token。空串表示「不上推理色,用调用方给的兼底色」。
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 定过,推理关着就别发光)。
976
985
  *
977
- * 规则:`thinking` + 首字母大写,正好对上主题里的 thinkingMinimal…thinkingMax 七档。
978
- * 为什么 off 返回空串而不是 `thinkingOff`:主题给 thinkingOff 的是接近背景的暗灰
979
- * (dark.json: `okhsl(229 8% 49%)`),拿它去染文字会看不清。用户 2026-11-06 定的是
980
- * 「off 时保持默认色」。
986
+ * 为何自己拼 ANSI 而不是 theme.fg:fg() 只认主题里的 token 名,主题那七档色相跟这组不一样。
987
+ * 终端支持 24 位色就给 38;2,否则退到 256 色板最近的一档(见 ansi256)。
988
+ * ★ 256 色板里没有这种灰蓝:只能退到灰阶档(250/249/247/244/243/241),保住“一档比一档暗”、
989
+ * 丢掉蓝调 —— 24 位色终端才是标称效果。
981
990
  *
982
- * ★ 同一份逻辑在 hooks/abs-rounded-editor/format.ts 里也有一份(`thinkingColorToken`)。
991
+ * ★ 同一份逻辑在 hooks/abs-rounded-editor/format.ts 里也有一份(`effortAnsi`)。
983
992
  * 不抽公共模块是故意的:安装后 `abs.ts` 与 `abs-rounded-editor/` 是两个可独立删除的扩展,
984
993
  * 抽成共享文件会让「删掉输入框样式目录」连带把 todo 面板搞崩。
985
994
  */
986
- export function effortToken(level: string | null | undefined): string {
987
- const l = String(level || '').trim()
988
- if (!l || l === 'off') return ''
989
- return 'thinking' + l[0].toUpperCase() + l.slice(1)
995
+ 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 },
1002
+ }
1003
+
1004
+ export function effortAnsi(level: string | null | undefined, colorMode = 'truecolor'): string {
1005
+ const g = EFFORT_COLOR[String(level || '').trim().toLowerCase()]
1006
+ if (!g) return ''
1007
+ return colorMode === 'truecolor' ? `38;2;${g.rgb}` : `38;5;${g.ansi256}`
1008
+ }
1009
+
1010
+ /** 用 effortAnsi 的结果包一段文字;空串原样返回(off 时不着色)。 */
1011
+ export function paintAnsi(ansi: string, text: string): string {
1012
+ return ansi ? `\x1b[${ansi}m${text}\x1b[39m` : text
990
1013
  }
991
1014
 
992
1015
  export function renderPanelLines(
@@ -997,21 +1020,23 @@ export function renderPanelLines(
997
1020
  animPhase = -1,
998
1021
  nickname = '',
999
1022
  effort = '',
1023
+ colorMode = 'truecolor',
1000
1024
  ): string[] {
1001
1025
  // ★ 空看板不再直接返回空数组(2026-10-06 用户要求:「即便 todo 没有列表,也要在 pi 上显示」)。
1002
1026
  // 理由与看板段一致:面板是「机制在不在」的常驻指示,进去就看不见 = 不知道机制活着。
1003
1027
  // 下面仍要画框和标题,所以这里只是不再提前 return。
1004
1028
  //
1005
1029
  // ★ 主色跟推理强度走(2026-11-06 用户需求):标题、[状态]、树线 ├─└─、复选框 [ ]、
1006
- // 进行中动画同用一色,与 pi 输入框描边/文字同色系。
1007
- // ★ off 时整块面板用灰阶(同日用户补提:「当推理关闭的时候 todo 面板文字应该全是灰色」)——
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 面板文字应该全是灰色」)——
1008
1036
  // 高亮色只在真的开着推理时给,否则「推理关了面板还在发光」让人误以为还在深度思考。
1009
- // 主色用 muted(中灰),描述与子项降一阶到 dim(深灰),两阶灰保留层次,不一整片平。
1010
- const off = !effortToken(effort)
1011
- const main = effortToken(effort) || 'muted'
1012
- // 描述/未勾子项的颜色:开着推理时 muted(比主色暗、又不至于暗到看不清);
1013
- // off 时主色已经是 muted,描述得再降一阶用 dim,否则两者同色分不出主次。
1014
- const flow = off ? 'dim' : 'muted'
1037
+ // 既然要求“一致”,off 也用同一个 muted,不再分两阶灰。
1038
+ const mainAnsi = effortAnsi(effort, colorMode)
1039
+ const main = mainAnsi ? (s: string) => paintAnsi(mainAnsi, s) : (s: string) => fg('muted', s)
1015
1040
  const lines: string[] = []
1016
1041
  // 上描边行已删除(2026-11-04 用户要求「去掉上边框」)。
1017
1042
  // 历史:那行曾用 `thinkingOff` 对齐 pi 输入框描边色(排查三轮才定色)。
@@ -1023,7 +1048,7 @@ export function renderPanelLines(
1023
1048
  const title = whoPart ? `Todo (${data.total}) — ${whoPart}` : `Todo (${data.total})`
1024
1049
  // 标题也顶最左(2026-11-04 用户)。旧代码曾在空看板时多缩 1 格,2026-11-06 空看板
1025
1050
  // 已整块不显示(见 refreshTodoPanel),那个分支没意义了。
1026
- lines.push(clipToWidth(fg(main, title), width))
1051
+ lines.push(clipToWidth(main(title), width))
1027
1052
 
1028
1053
  const lastIdx = data.rows.length - 1
1029
1054
  data.rows.forEach((t, i) => {
@@ -1035,32 +1060,33 @@ export function renderPanelLines(
1035
1060
  // 进行中的行带一个小动画(其余行不加,避免满屏都在跳)
1036
1061
  // 单字符 spinner 紧跟 [进行中]:靠前放才能一眼看到(放行尾会被长描述挤到前面去看不见)。
1037
1062
  // 2026-11-06 用户要求:「放在 [进行中] 后面」。
1038
- const anim = t.state === '进行中' && animPhase >= 0 ? ' ' + fg(main, todoAnimFrame(animPhase)) : ''
1063
+ const anim = t.state === '进行中' && animPhase >= 0 ? ' ' + main(todoAnimFrame(animPhase)) : ''
1039
1064
  // 复选框 `[ ]`(2026-10-16 用户要求):面板是 todo.md 的只读视图,
1040
1065
  // 就该长成 todo 原貌 —— 摆成与文件里 `- [ ]` 同形,读到就认得出对应哪一行。
1041
- const box = fg(main, '[ ]')
1042
- // 状态标签统一跟主色(2026-11-06 用户定:靠符号和文字区分状态,不靠颜色)。
1043
- const head = `${fg(main, branch)} ${box} ${fg(main, `[${t.state}]`)}${anim} ${fg('text', t.id)}`
1044
- const desc = t.desc ? ` ${fg('dim', '—')} ${fg(flow, t.desc)}` : ''
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}`) : ''
1045
1071
  lines.push(clipToWidth(head + desc, width))
1046
1072
  if (t.kids && t.kids.length) {
1047
1073
  // 子项:标准 md 嵌套列表 → 画成树
1048
1074
  // ├─ [ ] 子项一
1049
1075
  // └─ [x] 子项二
1050
1076
  // 树线前缀 = 父任务的 │(非末条)或空格(末条),与父任务分支对齐。
1051
- // 勾过的用 dim 压暗(一眼看出哪些已完成),未勾用正常色。
1052
- const stem = isLast ? ' ' : fg(main, '│ ')
1077
+ // 2026-11-07 用户要求全面板同色:勾过的子项也不再压暗,完成与否靠 [x] 区分。
1078
+ const stem = isLast ? ' ' : main('│ ')
1053
1079
  const n = t.kids.length
1054
1080
  t.kids.forEach((k, ki) => {
1055
1081
  const kb = ki === n - 1 ? '└─' : '├─'
1056
- const kbox = fg(main, k.done ? '[x]' : '[ ]')
1057
- const ktext = fg(k.done ? 'dim' : flow, k.text)
1058
- lines.push(clipToWidth(`${stem} ${fg(main, kb)} ${kbox} ${ktext}`, width))
1082
+ const kbox = main(k.done ? '[x]' : '[ ]')
1083
+ const ktext = main(k.text)
1084
+ lines.push(clipToWidth(`${stem} ${main(kb)} ${kbox} ${ktext}`, width))
1059
1085
  })
1060
1086
  }
1061
1087
  })
1062
1088
 
1063
- if (data.hidden > 0) lines.push(clipToWidth(fg('dim', ` +${data.hidden} more`), width))
1089
+ if (data.hidden > 0) lines.push(clipToWidth(main(` +${data.hidden} more`), width))
1064
1090
  return lines
1065
1091
  }
1066
1092
  /** 隐藏面板。**必须主动 requestRender** —— 否则数据清了、屏幕还留着旧画面。
@@ -1099,6 +1125,11 @@ async function refreshTodoPanel(ui: any, cwd: string): Promise<void> {
1099
1125
  (tui: any, theme: any) => {
1100
1126
  tuiRef = tui
1101
1127
  lastTui = tui // 供 hidePanel 在"无动画"时也能请求重绘
1128
+ // 主色要按终端的色深选编码(24 位色 / 256 色)—— getColorMode() 是 pi 主题自己
1129
+ // 算好的那一个;取不到就当支持 24 位色(真拿不准时宁可亮一点也别一格都不出)。
1130
+ const colorMode = (() => {
1131
+ try { return String(theme?.getColorMode?.() || 'truecolor') } catch { return 'truecolor' }
1132
+ })()
1102
1133
  return {
1103
1134
  render: (width: number) =>
1104
1135
  renderPanelLines(
@@ -1109,6 +1140,7 @@ async function refreshTodoPanel(ui: any, cwd: string): Promise<void> {
1109
1140
  animPhase,
1110
1141
  sessionNickname,
1111
1142
  effortLevel,
1143
+ colorMode,
1112
1144
  ),
1113
1145
  invalidate: () => {},
1114
1146
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fanchao8609/agent_brain_sync",
3
- "version": "1.28.4",
3
+ "version": "1.28.6",
4
4
  "description": "agent-brain-sync: 跨会话 AI 编码记忆 — hook 纯触发 + CLI/MCP 读写 .brain markdown 图谱, 防并发写保护。",
5
5
  "type": "module",
6
6
  "license": "MIT",