@fanchao8609/agent_brain_sync 1.29.0 → 1.31.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 +4 -9
- package/bin/abs.js +9 -64
- package/bin/mcp.js +2 -34
- package/hooks/abs-rounded-editor/format.ts +26 -37
- package/hooks/abs-rounded-editor/index.ts +34 -55
- package/hooks/abs.omp.ts +8 -50
- package/hooks/abs.pi.ts +67 -106
- package/package.json +1 -1
- package/skill/abs-agent-brain-sync/SKILL.md +11 -9
- package/src/brain/capture.js +0 -6
- package/src/brain/load.js +11 -244
- package/src/note.js +0 -14
- package/src/store.js +1 -1
- package/src/todo/common.js +19 -4
- package/src/todo/spec.js +3 -1
- package/src/todo/task-cmd.js +11 -5
- package/src/todo/tasks.js +32 -2
- package/src/todo.js +1 -1
- package/src/brain/db-cmd.js +0 -387
- package/src/brain/db.js +0 -667
- package/src/brain/evolve.js +0 -135
package/README.md
CHANGED
|
@@ -114,21 +114,16 @@ abs todo note TASK-1 --note "改到 X 文件 L40" # 追加一条子项(
|
|
|
114
114
|
abs todo child-done TASK-1 [--n 2] # 勾子项(n=第几条,缺省最后一条)
|
|
115
115
|
abs todo child-undone TASK-1 [--n 2] # 取消勾(勾错了退回)
|
|
116
116
|
abs todo memo TASK-1 "随手记一段(可多句,不上看板)" # 任务笔记 → sessions/;无参=查看
|
|
117
|
-
abs todo state TASK-1 --note "
|
|
117
|
+
abs todo state TASK-1 --note "进行中|讨论中|滞留中|搁置" # 改行首状态标记
|
|
118
|
+
abs todo state TASK-1 --note "滞留中" --n 2 # 改第 2 条子项的状态(子项也能标四态)
|
|
118
119
|
abs todo done TASK-1
|
|
119
120
|
|
|
120
121
|
abs note "一句话经验" --tags 坑,docker # 经验暂存 → sources/
|
|
121
122
|
abs log "完成 X" # 记一行流水;abs log 无参 = 查看
|
|
122
|
-
abs query <词> # 检索图谱(多词 OR
|
|
123
|
+
abs query <词> # 检索图谱(多词 OR 扫全 .brain/);已被推翻的经验默认隐藏(--all 可看)
|
|
123
124
|
abs capture [--layer 1-6] [--kind …] "一句话" # 按七层判层后落到位(L7 用 abs note;--kind: gotcha|decision|tradeoff|howto)
|
|
124
|
-
abs db rebuild # 扫所有登记项目 → 建图数据库(~/.abs/graph.db)
|
|
125
|
-
abs search <词> [--full] [--rules] # 检索:亮度排序+跨项目;--full 附正文,--rules 带铁律
|
|
126
|
-
abs db status # 库体检:七层分布 / 悬挂链接 / 已入库项目
|
|
127
|
-
abs db add # 把当前项目登记进 ~/.abs/projects.json
|
|
128
|
-
abs db evolve # 知识进化体检:被推翻/断链/主题堆积/僵化页
|
|
129
125
|
abs ingest --list # 列出还没落页的会话(只读,不写盘)
|
|
130
|
-
abs ingest <会话文件或 id> # 把一场 pi
|
|
131
|
-
# 已填 → 亮度×0.5;未填 → 整页不进检索
|
|
126
|
+
abs ingest <会话文件或 id> # 把一场 pi 会话摘要并进当天的 log-<日期>.md(## 会话 <id8> 段)
|
|
132
127
|
abs resolve <页名或id> # 反查页面路径(页改名后 id 不变)
|
|
133
128
|
abs supersede <页名> [--by <新页>] # 标记经验已失效(不删文件,保留历史;query 默认不再返回)
|
|
134
129
|
abs status # 当前项目 + 图谱概要
|
package/bin/abs.js
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
// bin/abs.js — abs CLI 入口。
|
|
3
3
|
// abs <cmd> [args]
|
|
4
4
|
// 命令: init / board / status / load / task / install / uninstall / help
|
|
5
|
-
import { cmdInit, cmdStatus, cmdLoad,
|
|
6
|
-
import { cmdDbRebuild, cmdDbSearch, cmdDbStatus, cmdDbEvolve, addProject } from '../src/brain/db-cmd.js';
|
|
5
|
+
import { cmdInit, cmdStatus, cmdLoad, cmdTask, cmdLog, cmdQuery, cmdLint, cmdNote, cmdConcept, cmdShow, cmdRepair, cmdWrapup, cmdRule, cmdTeardownCheck, cmdTodoArchive, cmdResolve, cmdSupersede, cmdReview } from '../src/store.js';
|
|
7
6
|
import { cmdCapture } from '../src/brain/capture.js';
|
|
8
7
|
import { cmdSummon } from '../src/brain/summon-cmd.js';
|
|
9
8
|
import { cmdIngest } from '../src/brain/ingest.js';
|
|
@@ -340,15 +339,13 @@ function parseArgv(args) {
|
|
|
340
339
|
}
|
|
341
340
|
const usage = `abs — 跨会话记忆(.brain/ 图谱)
|
|
342
341
|
|
|
343
|
-
怎么用:按你要做的事找。相近命令已折叠,旧别名已删(index/status/summon
|
|
342
|
+
怎么用:按你要做的事找。相近命令已折叠,旧别名已删(index/status/summon → show/ingest)。
|
|
344
343
|
|
|
345
344
|
【开工】读状态,接上次的活
|
|
346
345
|
abs load 必跑。未完成 todo + 用法 + 最近日志
|
|
347
|
-
abs recall [词…] 卡住时用:按当前处境推相关页(不给词也行)
|
|
348
346
|
|
|
349
347
|
【找东西】
|
|
350
|
-
abs
|
|
351
|
-
abs query <词…> [--all] 要字面精确时用:多词 OR,输出更短
|
|
348
|
+
abs query <词…> [--all] 多词 OR 搜全图,给页名+摘要(最省 token)
|
|
352
349
|
abs resolve <页名…> 有了页名,反查文件路径
|
|
353
350
|
|
|
354
351
|
【写知识】(三者落点不同,别混)
|
|
@@ -372,7 +369,6 @@ const usage = `abs — 跨会话记忆(.brain/ 图谱)
|
|
|
372
369
|
abs review 待确认的 draft 页(升 active 或否决)
|
|
373
370
|
abs supersede <页> --by <新页> 标经验已失效(不删文件)
|
|
374
371
|
abs show index|log|status 看完整索引 / 流水 / 概要
|
|
375
|
-
abs db rebuild|status|evolve 重建库 / 库体检 / 进化体检
|
|
376
372
|
abs init [--repair] 建图谱
|
|
377
373
|
|
|
378
374
|
【其它】
|
|
@@ -454,7 +450,7 @@ const subUsage = {
|
|
|
454
450
|
' abs todo child-done <id> [--n N] 勾子项(缺省最后一条)',
|
|
455
451
|
' abs todo child-undone <id> [--n N] 取消勾',
|
|
456
452
|
' abs todo memo <id> "随手记(可多句)" 任务笔记 → sessions/,不上看板;无文字=查看',
|
|
457
|
-
' abs todo state <id> --note 进行中|讨论中|滞留中|搁置',
|
|
453
|
+
' abs todo state <id> --note 进行中|讨论中|滞留中|搁置 [--n N] --n = 改第 N 条子项(缺省改任务本身)',
|
|
458
454
|
' abs todo remove <id> 删除任务(连子项,不可恢复;只是先不做请用 state 搁置)',
|
|
459
455
|
' abs todo done <id> [--as 落地|否决|仅方案] [结语文字]',
|
|
460
456
|
' abs todo archive [--keep-days N] [--dry-run]',
|
|
@@ -485,18 +481,6 @@ const subUsage = {
|
|
|
485
481
|
'参数:',
|
|
486
482
|
' --all 连 superseded(已失效)页一起返回',
|
|
487
483
|
].join('\n'),
|
|
488
|
-
recall: [
|
|
489
|
-
'abs recall — 卡住时主动取相关页(不需先想到检索词)',
|
|
490
|
-
'',
|
|
491
|
-
'用法:',
|
|
492
|
-
' abs recall [词1 词2 …] [--limit N]',
|
|
493
|
-
'',
|
|
494
|
-
'不给词时,从当前处境推:活跃 todo 的关键词 + 最近改过的文件名。',
|
|
495
|
-
'给了词就只用你给的词,不混处境词。',
|
|
496
|
-
'',
|
|
497
|
-
'与 load 的区别: load 只在开工跑一次;recall 随时可跑(卡住/换方向时)。',
|
|
498
|
-
'与 query 的区别: query 枚全图命中;recall 只给按亮度亮的几页。',
|
|
499
|
-
].join('\n'),
|
|
500
484
|
config: [
|
|
501
485
|
'abs config — 使用者姓名(写操作的作者标记)',
|
|
502
486
|
'',
|
|
@@ -597,35 +581,6 @@ async function main() {
|
|
|
597
581
|
rejectExtra(opts._, 'abs load');
|
|
598
582
|
console.log(await cmdLoad({ dir: opts.dir }));
|
|
599
583
|
break;
|
|
600
|
-
// 顶层 search(2026-11-06):MCP 侧一直叫 abs_search,CLI 却要敲 `db search` —— 名实不符。
|
|
601
|
-
// 现在 CLI 与 MCP 同名,`db search` 保留为别名(转指向这里并提示一次)。
|
|
602
|
-
case 'search': {
|
|
603
|
-
if (opts.help) {
|
|
604
|
-
console.log('abs search — 按亮度检索知识图谱\n\n'
|
|
605
|
-
+ '用法: abs search <词> [--limit N] [--full] [--no-rules] [--rules]\n\n'
|
|
606
|
-
+ ' 亮度排序 + 邻居扩散 + 跨项目(标 [跨项目])。\n'
|
|
607
|
-
+ ' 默认省略 L1 铁律(开工时 abs load 会给);要看加 --rules。\n'
|
|
608
|
-
+ ' --full 附正文;--limit 最多几页(默认 8)。\n\n'
|
|
609
|
-
+ '与 abs query 的区别:这是详略两档不是重复 ——\n'
|
|
610
|
-
+ ' query 给字面 OR 命中的页名+摘要,最省 token;\n'
|
|
611
|
-
+ ' search 给亮度与扩散,找「我上次怎么解决 X」更准。');
|
|
612
|
-
break;
|
|
613
|
-
}
|
|
614
|
-
if (!opts._.length) throw new Error('✗ 缺检索词\n 用法: abs search <词> [--limit N] [--full] [--no-rules]');
|
|
615
|
-
// 顶层 search 默认省略铁律(2026-11-06):它是「查一个主题」的工具,
|
|
616
|
-
// 18 条铁律占输出近一半、每次都是噪音。要看铁律显式 --rules,或跑 abs load。
|
|
617
|
-
// db search 那个别名仍保持旧默认(带铁律),不改变老脚本的行为。
|
|
618
|
-
console.log(await cmdDbSearch({
|
|
619
|
-
query: opts._.join(' '), limit: Number(opts.limit) || 8,
|
|
620
|
-
full: !!opts.full, noRules: !opts.rules, cmdName: 'search',
|
|
621
|
-
}));
|
|
622
|
-
break;
|
|
623
|
-
}
|
|
624
|
-
case 'recall': {
|
|
625
|
-
const limit = Number(opts.limit) > 0 ? Number(opts.limit) : 5;
|
|
626
|
-
console.log(await cmdRecall({ dir: opts.dir, terms: opts._, limit }));
|
|
627
|
-
break;
|
|
628
|
-
}
|
|
629
584
|
// ── 只读视图:统一到 abs show <视图>,旧名保留为别名并提示 ──
|
|
630
585
|
// (2026-11-06 命令面整理:index/log/status 都是「看某个文件的现状」,
|
|
631
586
|
// 三个顶层命令占三个名额却只有一个心智动作 —— 看。合成 show 一个入口。)
|
|
@@ -677,7 +632,7 @@ async function main() {
|
|
|
677
632
|
// memo 同:随手记常是多句带空格,位置参数收全段比 --note 顺手。
|
|
678
633
|
const isDone = action === 'done';
|
|
679
634
|
const isMemo = action === 'memo';
|
|
680
|
-
const usage = `abs todo ${sub} <id>${isDone ? ' [--as 落地|否决|仅方案] [--note "结语"]' : ' --note "…"'}`;
|
|
635
|
+
const usage = `abs todo ${sub} <id>${isDone ? ' [--as 落地|否决|仅方案] [--note "结语"]' : action === 'state' ? ' --note "…" [--n N]' : ' --note "…"'}`;
|
|
681
636
|
if (isDone) {
|
|
682
637
|
const note = opts.note || (rest2.length ? rest2.join(' ') : undefined);
|
|
683
638
|
console.log(await cmdTask({ dir: opts.dir, action, id, section: opts.section, note, as: opts.as, session: opts.session }));
|
|
@@ -700,7 +655,10 @@ async function main() {
|
|
|
700
655
|
}
|
|
701
656
|
rejectExtra(rest2, usage);
|
|
702
657
|
if (!id) throw new Error(`✗ 缺 <id>\n 用法: ${usage}`);
|
|
703
|
-
|
|
658
|
+
// --n:子项位置(state 用它改第 n 条子项,与 child-done 同一套坐标)
|
|
659
|
+
const nKid = opts.n ? Number(opts.n) : undefined;
|
|
660
|
+
if (opts.n && (!Number.isInteger(nKid) || nKid < 1)) throw new Error(`✗ --n 要是正整数(收到 "${opts.n}")`);
|
|
661
|
+
console.log(await cmdTask({ dir: opts.dir, action, id, section: opts.section, note: opts.note, as: opts.as, session: opts.session, n: nKid }));
|
|
704
662
|
break;
|
|
705
663
|
}
|
|
706
664
|
case 'ingest': {
|
|
@@ -757,19 +715,6 @@ async function main() {
|
|
|
757
715
|
console.log(await cmdRule({ dir: opts.dir, action: isAdd ? 'add' : 'list', text: rest3.join(' ') }));
|
|
758
716
|
break;
|
|
759
717
|
}
|
|
760
|
-
case 'db': {
|
|
761
|
-
const sub = opts._[0];
|
|
762
|
-
if (sub === 'rebuild') { console.log(await cmdDbRebuild({ dir: opts.dir })); break; }
|
|
763
|
-
if (sub === 'status') { console.log(await cmdDbStatus()); break; }
|
|
764
|
-
if (sub === 'evolve') { console.log(await cmdDbEvolve({ staleDays: Number(opts.staleDays) || 90 })); break; }
|
|
765
|
-
if (sub === 'add') {
|
|
766
|
-
const r = await addProject(opts.dir || process.cwd());
|
|
767
|
-
console.log(`✓ 已登记项目: ${r}\n 建库: abs db rebuild`);
|
|
768
|
-
break;
|
|
769
|
-
}
|
|
770
|
-
console.log('用法: abs db rebuild | status | evolve | add [--dir <项目根>]');
|
|
771
|
-
break;
|
|
772
|
-
}
|
|
773
718
|
case 'query': {
|
|
774
719
|
console.log(await cmdQuery({ dir: opts.dir, terms: opts._, includeSuperseded: opts.all }));
|
|
775
720
|
break;
|
package/bin/mcp.js
CHANGED
|
@@ -13,8 +13,6 @@ import { join, dirname } from 'node:path';
|
|
|
13
13
|
import { fileURLToPath } from 'node:url';
|
|
14
14
|
import { findBrainRoot, absLogDir, AbsError } from '../src/index.js';
|
|
15
15
|
import { cmdLoad, cmdStatus, cmdTask, cmdQuery, cmdLint, cmdNote, cmdConcept, cmdRule, cmdResolve, cmdSupersede } from '../src/store.js';
|
|
16
|
-
// abs_search 用:db search 的实现(它比 cmdQuery 强 —— 带亮度排序、能扩散、能跨项目)
|
|
17
|
-
import { cmdDbSearch } from '../src/brain/db-cmd.js';
|
|
18
16
|
|
|
19
17
|
// ---------- 技术日志: MCP 请求跟踪(调试用, 与图谱 log.md 完全分开) ----------
|
|
20
18
|
// 落 ~/.abs/log/mcp.log: 每次工具调用一行 [时间] tool cwd 参数摘要 → 耗时/结果摘要。
|
|
@@ -115,11 +113,11 @@ tool(
|
|
|
115
113
|
|
|
116
114
|
tool(
|
|
117
115
|
'abs_task',
|
|
118
|
-
'任务实时落盘(幂等键 = id)。start 登记进 Todo / note 补子项(**追加**,标准 md 嵌套列表,`abs todo note`)/ child_done、child_undone 勾/取消勾子项(n=第几条,缺省最后一条)/
|
|
116
|
+
'任务实时落盘(幂等键 = id)。start 登记进 Todo / note 补子项(**追加**,标准 md 嵌套列表,`abs todo note`)/ child_done、child_undone 勾/取消勾子项(n=第几条,缺省最后一条)/ state 改状态(带 n = 改该任务第 n 条子项,子项也能标四态)/ memo 任务随手记(多句/追加/不上看板)/ done 完成归位 Done(done 时 note=结语文字,as=结语类型:落地|否决|仅方案)。必填: cwd, action, id。多会话并行时,start/add 带上 session(值见 system prompt 的 [会话] 段)以标记「这条任务谁在做」。',
|
|
119
117
|
{
|
|
120
118
|
action: z.enum(['add', 'start', 'done', 'note', 'child_done', 'child_undone', 'memo', 'state', 'remove']),
|
|
121
119
|
id: z.string().describe('任务幂等键,如 TASK-xxx 或子任务名'),
|
|
122
|
-
n: z.number().int().positive().optional().describe('child_done/child_undone 用:第几条子项(1-based
|
|
120
|
+
'n': z.number().int().positive().optional().describe('child_done/child_undone/state 用:第几条子项(1-based,缺省=最后一条;state 不传 n = 改任务本身)'),
|
|
123
121
|
cwd: z.string().describe('项目根目录(.brain/ 所在处)'), note: z.string().optional().describe('add/start=做什么; note=追加一条子项(改到哪/下一步,一句话); memo=随手记(可多句、不定长,追加式;不传=查看已有笔记); state=进行中|讨论中|滞留中|搁置(搁置=用户改方向/不做了;滞留中=还要做只是卡住); done=结语文字(remove 不用此字段)'),
|
|
124
122
|
as: z.enum(['落地', '否决', '仅方案']).optional().describe('仅 done:结语类型。默认 落地。做了又撤/评估后不做用 否决,只设计过用 仅方案 —— 别让假【落地】污染看板'),
|
|
125
123
|
// 会话 id:多会话并行时标记「这条任务是谁在做」。
|
|
@@ -209,36 +207,6 @@ tool(
|
|
|
209
207
|
}
|
|
210
208
|
);
|
|
211
209
|
|
|
212
|
-
tool(
|
|
213
|
-
'abs_search',
|
|
214
|
-
'按亮度检索知识图谱(跨项目):比 abs_query 更强 —— 带亮度排序、能从邻居页扩散、能查到别的项目的经验(标 [跨项目])。'
|
|
215
|
-
+ '适合「我上次怎么解决 X」这类回忆。铁律默认省略(开工时 abs_load 会给),需要时传 include_rules=true。',
|
|
216
|
-
{
|
|
217
|
-
cwd: z.string().describe('项目根目录(.brain/ 所在处)'),
|
|
218
|
-
query: z.string().describe('检索词(一句话或几个词都行,中文两字以上即可)'),
|
|
219
|
-
limit: z.number().int().min(1).max(20).optional().describe('最多几页,默认 8'),
|
|
220
|
-
include_rules: z.boolean().optional().describe('true=连 L1 铁律一起返回(默认省略,省 token)'),
|
|
221
|
-
},
|
|
222
|
-
async ({ cwd, query, limit, include_rules }) => {
|
|
223
|
-
// 库是全局的(~/.abs/graph.db),不需要 findBrainRoot —— 但先确认项目本身有图谱,
|
|
224
|
-
// 否则用户在一个陌生目录调用会得到「别的项目的结果」而不自知。
|
|
225
|
-
const root = await findBrainRoot(cwd || process.cwd());
|
|
226
|
-
if (!root) return errNoBrain(cwd || process.cwd());
|
|
227
|
-
const text = await cmdDbSearch({
|
|
228
|
-
query: String(query || ''),
|
|
229
|
-
limit: Number(limit) || 8,
|
|
230
|
-
noRules: !include_rules,
|
|
231
|
-
});
|
|
232
|
-
if (/库还没建|无命中/.test(text)) {
|
|
233
|
-
return err(text, {
|
|
234
|
-
code: 'NO_MATCH',
|
|
235
|
-
fallback: '库可能是旧的:跑 abs db rebuild 重扫;或换更具体的词',
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
return { content: [{ type: 'text', text }] };
|
|
239
|
-
}
|
|
240
|
-
);
|
|
241
|
-
|
|
242
210
|
tool(
|
|
243
211
|
'abs_rule',
|
|
244
212
|
'读/写 index.md 的 ## Rules 硬规则区。action=list 列出;action=add 追加一条(只放一句话,≤42 字符、不带链接;展开写概念页)。',
|
|
@@ -72,24 +72,28 @@ function dropCols(s: string, n: number): string {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
/**
|
|
75
|
-
* 推理强度 →
|
|
75
|
+
* 推理强度 → 主色。六档灰蓝(同一色相族、由亮到暗),查不到或 off 时返回空串 = 不上色,用默认色。
|
|
76
76
|
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* 「off 时保持默认色」。
|
|
77
|
+
* ⚠ 当前**输入框不再用它**(2026-11-09 用户要求「完全对齐 pi 原版」:边框改用 pi 给的
|
|
78
|
+
* editor.borderColor,即主题那七档 thinking 色)。保留是因为两条:用户在这上面来回改过
|
|
79
|
+
* 两轮(改回去只改 render() 一行),且 todo 面板(abs.pi.ts)另有一份同值副本。
|
|
81
80
|
*
|
|
82
|
-
*
|
|
81
|
+
* 为什么 off 不上色:主题给 thinkingOff 的是接近背景的暗灰(dark.json: `okhsl(229 8% 49%)`),
|
|
82
|
+
* 拿它去染文字会看不清。用户 2026-11-06 定的是「off 时保持默认色」。
|
|
83
|
+
* 色值定于 2026-11-09:先「暗档提亮」把 medium/low/minimal 抬到看得清,试过一版中性灰又还原回蓝调。
|
|
84
|
+
* 对深色背景(#0D1117)对比度 11.84/9.22/7.72/6.53/5.42/4.51。
|
|
85
|
+
*
|
|
86
|
+
* ★ 同一份逻辑在 hooks/abs.pi.ts 里也有一份(`effortAnsi`)。不抽公共模块是故意的:
|
|
83
87
|
* 安装后 `abs.ts` 与 `abs-rounded-editor/` 是两个可独立删除的扩展,抽成共享文件会让
|
|
84
|
-
* 「删掉输入框样式目录」连带把 todo
|
|
88
|
+
* 「删掉输入框样式目录」连带把 todo 面板搞崩。改色值时要两边一起改。
|
|
85
89
|
*/
|
|
86
90
|
export const EFFORT_COLOR: Record<string, { rgb: string; ansi256: number }> = {
|
|
87
91
|
max: { rgb: '197;205;220', ansi256: 189 },
|
|
88
92
|
xhigh: { rgb: '168;182;204', ansi256: 152 },
|
|
89
93
|
high: { rgb: '150;166;198', ansi256: 110 },
|
|
90
|
-
medium: { rgb: '
|
|
91
|
-
low: { rgb: '
|
|
92
|
-
minimal: { rgb: '
|
|
94
|
+
medium: { rgb: '139;153;172', ansi256: 110 },
|
|
95
|
+
low: { rgb: '124;138;162', ansi256: 103 },
|
|
96
|
+
minimal: { rgb: '108;124;157', ansi256: 67 },
|
|
93
97
|
}
|
|
94
98
|
|
|
95
99
|
export function effortAnsi(level: string | null | undefined, colorMode = 'truecolor'): string {
|
|
@@ -98,31 +102,15 @@ export function effortAnsi(level: string | null | undefined, colorMode = 'trueco
|
|
|
98
102
|
return colorMode === 'truecolor' ? `38;2;${g.rgb}` : `38;5;${g.ansi256}`
|
|
99
103
|
}
|
|
100
104
|
|
|
101
|
-
/**
|
|
102
|
-
* 用途(2026-11-
|
|
103
|
-
*
|
|
105
|
+
/** 「擱置」橙 #F59E0B —— 与 todo 面板的 `[擱置]`、面板大标题同色(abs.pi.ts 的 `C.hold`/`C.title`)。
|
|
106
|
+
* 用途(2026-11-09 用户要求):输入框**右下角的状态文字**(如 `30.0%/1.0M DeepSeek-V4.1-Flash`)。
|
|
107
|
+
* ★ 色值在 abs.pi.ts 里也有一份,不抽公共模块是故意的(见 EFFORT_COLOR 的注释)。 */
|
|
104
108
|
export const HOLD_COLOR = { rgb: '245;158;11', ansi256: 214 }
|
|
105
109
|
|
|
106
110
|
export function holdAnsi(colorMode = 'truecolor'): string {
|
|
107
111
|
return colorMode === 'truecolor' ? `38;2;${HOLD_COLOR.rgb}` : `38;5;${HOLD_COLOR.ansi256}`
|
|
108
112
|
}
|
|
109
113
|
|
|
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
|
-
|
|
126
114
|
/** 用 effortAnsi 的结果包一段文字;空串原样返回(off 时不着色)。 */
|
|
127
115
|
export function paintAnsi(ansi: string, text: string): string {
|
|
128
116
|
return ansi ? `\x1b[${ansi}m${text}\x1b[39m` : text
|
|
@@ -137,9 +125,12 @@ export function paintAnsi(ansi: string, text: string): string {
|
|
|
137
125
|
* leftLabel 贴在左圆角之后;label 贴最右端。哪个放不下就丢掉哪个
|
|
138
126
|
* (宁可没有,也不让边框溢出折行)。
|
|
139
127
|
*
|
|
140
|
-
* opts.
|
|
141
|
-
*
|
|
142
|
-
*
|
|
128
|
+
* opts.labelFg:只给右边那个标签(右下角的状态文字)换个色,其余(圆角、虚线)仍用 fg。
|
|
129
|
+
* 2026-11-09 用户要求该状态文字用擱置橙,调用处传 holdAnsi;不传 opts 时整行一个色。
|
|
130
|
+
*
|
|
131
|
+
* 历史(别再重走):2026-11-08~09 曾有个 leadFg 开关,用来把 pi 嵌进上边框的
|
|
132
|
+
* `⠼ Working` 那段单独染色(橙)。现在 working 已还原成 pi 原版(不嵌边框),
|
|
133
|
+
* leadSpanOf/leadFg 一并删了 —— 要再嵌回去就照那时候的写法加。
|
|
143
134
|
*/
|
|
144
135
|
export function roundBorder(
|
|
145
136
|
line: string,
|
|
@@ -148,7 +139,7 @@ export function roundBorder(
|
|
|
148
139
|
right: string,
|
|
149
140
|
label = '',
|
|
150
141
|
leftLabel = '',
|
|
151
|
-
opts: {
|
|
142
|
+
opts: { labelFg?: (s: string) => string } = {},
|
|
152
143
|
): string {
|
|
153
144
|
const plain = stripAnsi(line)
|
|
154
145
|
if (plain.length === 0) return line
|
|
@@ -176,14 +167,12 @@ export function roundBorder(
|
|
|
176
167
|
}
|
|
177
168
|
}
|
|
178
169
|
return paintSpans(left + mid + right, fg, [
|
|
179
|
-
//
|
|
180
|
-
// (万一同一串又出现在左边,不能把左边那个也染成标签色)。
|
|
181
|
-
[leadSpanOf(plain), opts.leadFg, 'first'],
|
|
170
|
+
// 标签从**末尾**找:万一同一串又出现在左边,不能把左边那个也染成标签色。
|
|
182
171
|
[label, opts.labelFg, 'last'],
|
|
183
172
|
])
|
|
184
173
|
}
|
|
185
174
|
|
|
186
|
-
/** 把 out
|
|
175
|
+
/** 把 out 里的区间换成各自指定的色,其余整段用 fg。区间按位置排序、重叠则跳过。
|
|
187
176
|
* 为何不用「拼段」实现:标签与 working 段的字符是从 mid 里苹/插进去的,
|
|
188
177
|
* 只有拿到最终串才能准确定位(直接拼段会在 dropCols 后错位)。 */
|
|
189
178
|
function paintSpans(
|
|
@@ -3,9 +3,13 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 需求(2026-11-04 用户):
|
|
5
5
|
* ① 输入框左右都有竖线、四角圆角
|
|
6
|
-
* ②
|
|
7
|
-
* ③
|
|
8
|
-
*
|
|
6
|
+
* ② 右下角显示「上下文百分比/上限 模型名」,如 `8.0%/1.0M deepseek-v4.1-flash`
|
|
7
|
+
* ③ 输入框下方原有 footer 两行全部隐藏
|
|
8
|
+
*
|
|
9
|
+
* 2026-11-09 两项定案(用户来回试过):
|
|
10
|
+
* ① working 状态**嵌在上边框里**(`embedWorkingStatus: true`),不单独占一行;
|
|
11
|
+
* 颜色跟整行一致 = 框线色(roundBorder 会把整行重新上色,所以那一段不会是另一个色)。
|
|
12
|
+
* ② 边框颜色用 pi 给的 `editor.borderColor`(即原版那七档 thinking 色),框内文字不上色。
|
|
9
13
|
*
|
|
10
14
|
* 为什么要覆写 render():
|
|
11
15
|
* pi 的 Editor.render() 只画上下两条 `─`,每行是「左 padding + 文本 + 右 padding」,
|
|
@@ -18,25 +22,15 @@
|
|
|
18
22
|
* 注意:自动补全菜单行排在底边之下、且自带内容宽度,不套竖线(否则菜单被框住会错位)。
|
|
19
23
|
*/
|
|
20
24
|
import { CustomEditor } from '@earendil-works/pi-coding-agent'
|
|
21
|
-
import { buildStatus,
|
|
25
|
+
import { buildStatus, holdAnsi, paintAnsi, roundBorder, shortenModel } from './format.ts'
|
|
22
26
|
|
|
23
27
|
const SIDE = '│'
|
|
24
28
|
|
|
25
|
-
/**
|
|
29
|
+
/** 右下角状态文本,如 `8.0%/1.0M deepseek-v4.1-flash`。render() 拿不到 ctx,故由事件回调维护。 */
|
|
26
30
|
let statusLabel = ''
|
|
27
31
|
|
|
28
|
-
/**
|
|
29
|
-
|
|
30
|
-
let effortLevel = ''
|
|
31
|
-
|
|
32
|
-
/** 取当前主题实例的 getter。
|
|
33
|
-
*
|
|
34
|
-
* ★ 为何不能把主题从构造函数里拿:pi 传给编辑器了的是 `getEditorTheme()`,
|
|
35
|
-
* 那对象只有 `{ borderColor, selectList }`,**没有 `fg`** —— 直接用它上色会静默无效
|
|
36
|
-
* (不报错,只是没颜色)。真正带 fg 的是 `ctx.ui.theme`(interactive-mode 的 getter)。
|
|
37
|
-
* ★ 为何存 getter 而不是存实例:`/theme` 切主题后 ctx.ui.theme 返回**新实例**,
|
|
38
|
-
* 存旧实例会継续用旧主题的色板。 */
|
|
39
|
-
let themeGetter: (() => any) | null = null
|
|
32
|
+
/** 终端色模式(truecolor / 256color)。只在 session_start 取一次:它是终端能力,不随切主题改。 */
|
|
33
|
+
let colorMode = 'truecolor'
|
|
40
34
|
|
|
41
35
|
export class RoundedEditor extends CustomEditor {
|
|
42
36
|
render(width: number): string[] {
|
|
@@ -45,69 +39,60 @@ export class RoundedEditor extends CustomEditor {
|
|
|
45
39
|
const lines = super.render(width - 2)
|
|
46
40
|
if (lines.length < 2) return lines
|
|
47
41
|
|
|
48
|
-
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
const wrap =
|
|
55
|
-
// working 状态与右上角状态文字用「搁置」橙 #F59E0B(2026-11-08 用户要求):
|
|
56
|
-
// 上边框的圆角与虚线仍走正文色,只有这两段橙 —— 一眼区分「还在跑」与上下文占用。
|
|
57
|
-
const holdFg = (s: string) => paintAnsi(holdAnsi(colorMode), s)
|
|
42
|
+
// 边框颜色直接用 pi 给编辑器设好的那个(2026-11-09 用户要求「完全对齐原版」):
|
|
43
|
+
// pi 的 interactive-mode 在 updateEditorBorderColor() 里把 editor.borderColor 设成
|
|
44
|
+
// theme.getThinkingBorderColor(档位),bash 模式另给一套 —— 直接复用就等于原版那套
|
|
45
|
+
// (含 off 的暗灰),而且 /theme、切档、切 bash 模式时 pi 自己会换并请求重绘。
|
|
46
|
+
// 框内文字不上色(原版也不上)。光标色 / 输入框背景 / 聚焦外发光是 pi 内部画的,接管不了。
|
|
47
|
+
const border = (this as any).borderColor
|
|
48
|
+
const wrap = typeof border === 'function' ? border.bind(this) : (s: string) => s
|
|
58
49
|
const out: string[] = []
|
|
59
50
|
|
|
60
|
-
// 上边框:原本是整行
|
|
61
|
-
out.push(roundBorder(lines[0], wrap, '╭', '╮'
|
|
51
|
+
// 上边框:原本是整行 `─`(含滚动提示),两端换圆角。
|
|
52
|
+
out.push(roundBorder(lines[0], wrap, '╭', '╮'))
|
|
62
53
|
// 下边框在原结果的倒数第 (1 + 自动补全行数) 位 —— 补全菜单排在它后面。
|
|
63
54
|
const autocompleteHeight = (this as any).renderedAutocompleteHeight ?? 0
|
|
64
55
|
const bottomIdx = lines.length - 1 - autocompleteHeight
|
|
65
56
|
|
|
66
57
|
for (let i = 1; i < bottomIdx; i++) {
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
out.push(wrap(SIDE) + textFg(body) + wrap(SIDE))
|
|
58
|
+
// 内容行原样透传(不上文字色),只在两端各包一根竖线。
|
|
59
|
+
out.push(wrap(SIDE) + lines[i] + wrap(SIDE))
|
|
70
60
|
}
|
|
71
61
|
// 下边框(2026-11-07 还原:左下角的 fanchao·昵称已去掉 —— 用户嫌难看,
|
|
72
62
|
// 回 todo 面板标题;roundBorder 的 leftLabel 参数保留备用)。
|
|
73
|
-
|
|
63
|
+
// 状态文字(`30.0%/1.0M 模型名`)2026-11-09 从右上角搬到右下角,且用户要求上「擱置」橙
|
|
64
|
+
// (只染那一段,圆角与虚线仍是框线色)。
|
|
65
|
+
out.push(roundBorder(lines[bottomIdx], wrap, '╰', '╯', statusLabel, '', {
|
|
66
|
+
labelFg: (s: string) => paintAnsi(holdAnsi(colorMode), s),
|
|
67
|
+
}))
|
|
74
68
|
// 自动补全行原样透传(不套竖线)。
|
|
75
69
|
for (let i = bottomIdx + 1; i < lines.length; i++) out.push(lines[i])
|
|
76
70
|
return out
|
|
77
71
|
}
|
|
78
72
|
}
|
|
79
73
|
|
|
80
|
-
/**
|
|
74
|
+
/** 刷新右下角状态文本(每次渲染前由事件与定时器调用)。 */
|
|
81
75
|
function refreshStatus(ctx: any): void {
|
|
82
76
|
try {
|
|
83
77
|
statusLabel = buildStatus(ctx?.getContextUsage?.(), ctx?.model?.id ?? '')
|
|
84
78
|
} catch {
|
|
85
79
|
statusLabel = shortenModel(ctx?.model?.id ?? '')
|
|
86
80
|
}
|
|
87
|
-
refreshEffort(ctx)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** 读出当前推理强度。`ctx.thinkingLevel` 是 pi 官方给扩展的入口(effective level)。 */
|
|
91
|
-
function refreshEffort(ctx: any): void {
|
|
92
|
-
try {
|
|
93
|
-
effortLevel = String(ctx?.thinkingLevel || '')
|
|
94
|
-
} catch {
|
|
95
|
-
effortLevel = ''
|
|
96
|
-
}
|
|
97
81
|
}
|
|
98
82
|
|
|
99
83
|
export default function piRoundedEditor(pi: any): void {
|
|
100
84
|
pi.on('session_start', (_event: unknown, ctx: any) => {
|
|
101
85
|
if (!ctx?.ui?.setEditorComponent) return
|
|
102
|
-
//
|
|
103
|
-
|
|
86
|
+
// 终端色模式(256 色档位用);取不到就当真彩。
|
|
87
|
+
try { colorMode = String(ctx.ui?.theme?.getColorMode?.() || 'truecolor') } catch { colorMode = 'truecolor' }
|
|
104
88
|
refreshStatus(ctx)
|
|
105
|
-
// 第四个参数 embedWorkingStatus: true —— working
|
|
89
|
+
// 第四个参数 embedWorkingStatus: true —— working 状态进上边框,不单独占一行(2026-11-09 用户定案)。
|
|
90
|
+
// 颜色不用管:pi 嵌入时用 editor.borderColor,本扩展又把整行重上一次框线色,两边同为档位色。
|
|
106
91
|
ctx.ui.setEditorComponent(
|
|
107
92
|
(tui: any, theme: any, keybindings: any) =>
|
|
108
93
|
new RoundedEditor(tui, theme, keybindings, { embedWorkingStatus: true }),
|
|
109
94
|
)
|
|
110
|
-
//
|
|
95
|
+
// ③ 隐藏输入框下方的 footer 两行(路径/git/token、MCP/扩展状态),信息并入右下角。
|
|
111
96
|
ctx.ui.setFooter?.(() => ({ render: () => [], invalidate() {} }))
|
|
112
97
|
})
|
|
113
98
|
|
|
@@ -119,13 +104,7 @@ export default function piRoundedEditor(pi: any): void {
|
|
|
119
104
|
ctx?.ui?.requestRender?.()
|
|
120
105
|
})
|
|
121
106
|
|
|
122
|
-
//
|
|
123
|
-
// 为何不能只靠 session_start 那一次:/model 的强度档位、Alt+T 之类的切换不改会话,
|
|
124
|
-
// 不重建编辑器也就不会重新读 effortLevel —— 文字会停在旧色。
|
|
125
|
-
pi.on('thinking_level_select', (_event: any, ctx: any) => {
|
|
126
|
-
refreshEffort(ctx)
|
|
127
|
-
ctx?.ui?.requestRender?.()
|
|
128
|
-
})
|
|
107
|
+
// 切推理强度:本扩展不再需要处理(边框色是 pi 自己在 updateEditorBorderColor() 里换并请求重绘)。
|
|
129
108
|
|
|
130
109
|
// 上下文占用随每轮对话变化 —— 轮次结束与消息结束时刷新。
|
|
131
110
|
for (const ev of ['turn_end', 'message_end', 'tool_execution_end']) {
|
package/hooks/abs.omp.ts
CHANGED
|
@@ -502,40 +502,6 @@ function clipToWidth(s: string, width: number): string {
|
|
|
502
502
|
}
|
|
503
503
|
|
|
504
504
|
// 「进行中」行的迷你动画:三个小符号依次由空心变实心。
|
|
505
|
-
// 帧宽固定 3 列 —— 宽度不变才不会让面板每帧抖动。
|
|
506
|
-
const TODO_ANIM_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
|
507
|
-
const TODO_ANIM_MS = 100
|
|
508
|
-
|
|
509
|
-
/** 动画相位(每 tick +1)。模块级 —— 闭包 flag 在重复注册时不共享。 */
|
|
510
|
-
let animPhase = 0
|
|
511
|
-
|
|
512
|
-
/** 当前推理强度。模块级理由同上:widget factory 拿不到 ctx。
|
|
513
|
-
* omp 宿主有无这个字段未验证 —— 取不到就是空串,面板退回 accent,行为与以前一致。 */
|
|
514
|
-
let effortLevel = ''
|
|
515
|
-
let animTimer: ReturnType<typeof setInterval> | undefined
|
|
516
|
-
|
|
517
|
-
/** 启动/保持动画定时器。重复调用不会叠加(已存在则不动)。
|
|
518
|
-
* getTui 是为了每 tick 拿**当前**的 tui 引用 —— widget 可能因刷新被重建。 */
|
|
519
|
-
function startAnimTimer(getTui: () => RenderTui | null): void {
|
|
520
|
-
if (animTimer) return
|
|
521
|
-
animTimer = setInterval(() => {
|
|
522
|
-
animPhase++
|
|
523
|
-
try { getTui()?.requestRender?.() } catch {}
|
|
524
|
-
}, TODO_ANIM_MS)
|
|
525
|
-
animTimer.unref?.()
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
function stopAnimTimer(): void {
|
|
529
|
-
if (animTimer) { clearInterval(animTimer); animTimer = undefined }
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
/** 取当前动画帧。非进行中的行不传 phase(不显示)。 */
|
|
533
|
-
export function todoAnimFrame(phase: number): string {
|
|
534
|
-
const n = TODO_ANIM_FRAMES.length
|
|
535
|
-
const i = ((Math.floor(phase) % n) + n) % n
|
|
536
|
-
return TODO_ANIM_FRAMES[i]
|
|
537
|
-
}
|
|
538
|
-
|
|
539
505
|
/**
|
|
540
506
|
* 推理强度 → 主题色 token。空串表示「不上推理色,用兼底色」。
|
|
541
507
|
*
|
|
@@ -556,7 +522,6 @@ export function renderPanelLines(
|
|
|
556
522
|
who: string,
|
|
557
523
|
width: number,
|
|
558
524
|
fg: (color: string, s: string) => string,
|
|
559
|
-
animPhase = -1,
|
|
560
525
|
nickname = '',
|
|
561
526
|
effort = '',
|
|
562
527
|
): string[] {
|
|
@@ -584,12 +549,11 @@ export function renderPanelLines(
|
|
|
584
549
|
// `└─` 只给最后一条 —— 不管它有没有子项(否则末条带子项时全篇没有收尾)。
|
|
585
550
|
const isLast = i === lastIdx
|
|
586
551
|
const branch = isLast ? '└─' : '├─'
|
|
587
|
-
//
|
|
588
|
-
const anim = t.state === '进行中' && animPhase >= 0 ? ' ' + fg(main, todoAnimFrame(animPhase)) : ''
|
|
552
|
+
// 进行中曾带 spinner(2026-11-06 与 pi 版同步;2026-11-08 用户定「没啥用」与 pi 版一起去掉)。
|
|
589
553
|
// 复选框 `[ ]`:面板是 todo.md 的只读视图,就该长成 todo 原貌。
|
|
590
554
|
const box = fg(main, '[ ]')
|
|
591
555
|
// 状态标签统一跟主色(2026-11-06 与 pi 版同步:靠符号和文字区分状态,不靠颜色)。
|
|
592
|
-
const head = `${fg(main, branch)} ${box} ${fg(main, `[${t.state}]`)}
|
|
556
|
+
const head = `${fg(main, branch)} ${box} ${fg(main, `[${t.state}]`)} ${fg('text', t.id)}`
|
|
593
557
|
const desc = t.desc ? ` ${fg('dim', '—')} ${fg(flow, t.desc)}` : ''
|
|
594
558
|
lines.push(clipToWidth(head + desc, width))
|
|
595
559
|
if (t.kids && t.kids.length) {
|
|
@@ -610,12 +574,11 @@ export function renderPanelLines(
|
|
|
610
574
|
}
|
|
611
575
|
|
|
612
576
|
/** 隐藏面板。**必须主动 requestRender** —— 否则数据清了、屏幕还留着旧画面。
|
|
613
|
-
* 坑(pi 版 2026-10-05
|
|
614
|
-
*
|
|
615
|
-
*
|
|
577
|
+
* 坑(pi 版 2026-10-05 实报):当时唯一的重绘入口是动画定时器,而「任务全完成」恰好
|
|
578
|
+
* 让它停掉 → 从此没人请求重绘 → 屏幕冻结。动画已拆(2026-11-08),
|
|
579
|
+
* 但重绘只能靠事件主动请求这个事实没变,少一处就静默冻结。 */
|
|
616
580
|
function hidePanel(ui: ExtensionUIContext | undefined): void {
|
|
617
581
|
try { ui?.setWidget(PANEL_KEY, undefined) } catch {}
|
|
618
|
-
stopAnimTimer()
|
|
619
582
|
try { lastTui?.requestRender?.() } catch {}
|
|
620
583
|
}
|
|
621
584
|
|
|
@@ -638,18 +601,14 @@ async function refreshTodoPanel(ui: ExtensionUIContext | undefined, cwd: string)
|
|
|
638
601
|
lastTui = tui // 供 hidePanel 在「无动画」时也能请求重绘
|
|
639
602
|
return {
|
|
640
603
|
render: (width: number) =>
|
|
641
|
-
renderPanelLines(data, who, width, (c: string, s: string) => theme.fg(c, s),
|
|
604
|
+
renderPanelLines(data, who, width, (c: string, s: string) => theme.fg(c, s), sessionNickname, effortLevel),
|
|
642
605
|
invalidate: () => {},
|
|
643
606
|
}
|
|
644
607
|
},
|
|
645
608
|
{ placement: 'aboveEditor' },
|
|
646
609
|
)
|
|
647
|
-
//
|
|
648
|
-
|
|
649
|
-
if (needAnim) startAnimTimer(() => tuiRef)
|
|
650
|
-
else stopAnimTimer()
|
|
651
|
-
// 无动画时得自己刷一帧,否则从「有任务」变成「无任务」时屏幕冻结在旧帧。
|
|
652
|
-
if (!needAnim) try { lastTui?.requestRender?.() } catch {}
|
|
610
|
+
// 动画已拆(2026-11-08):没有定时器兜底了,每次刷新都得自己请求重绘。
|
|
611
|
+
try { lastTui?.requestRender?.() } catch {}
|
|
653
612
|
} catch {
|
|
654
613
|
hidePanel(ui)
|
|
655
614
|
}
|
|
@@ -906,7 +865,6 @@ export default function absOmpExtension(pi: ExtensionAPI): void {
|
|
|
906
865
|
})
|
|
907
866
|
|
|
908
867
|
pi.on("session_shutdown", (_e: SessionShutdownEvent, ctx: ExtensionContext) => {
|
|
909
|
-
stopAnimTimer() // 面板没了,动画定时器必须一起停(否则残留定时器白刷已丢弃的 tui)
|
|
910
868
|
snapshotWrapup(ctx.cwd)
|
|
911
869
|
snapshotSessionPage(ctx.cwd, sessionIdFor(ctx))
|
|
912
870
|
logHook("session_shutdown").catch(() => {})
|