@fanchao8609/agent_brain_sync 1.30.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 +2 -1
- package/bin/abs.js +6 -3
- package/bin/mcp.js +2 -2
- package/hooks/abs-rounded-editor/format.ts +24 -23
- package/hooks/abs-rounded-editor/index.ts +34 -56
- package/hooks/abs.pi.ts +57 -53
- package/package.json +1 -1
- package/skill/abs-agent-brain-sync/SKILL.md +2 -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/README.md
CHANGED
|
@@ -114,7 +114,8 @@ 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/
|
package/bin/abs.js
CHANGED
|
@@ -450,7 +450,7 @@ const subUsage = {
|
|
|
450
450
|
' abs todo child-done <id> [--n N] 勾子项(缺省最后一条)',
|
|
451
451
|
' abs todo child-undone <id> [--n N] 取消勾',
|
|
452
452
|
' abs todo memo <id> "随手记(可多句)" 任务笔记 → sessions/,不上看板;无文字=查看',
|
|
453
|
-
' abs todo state <id> --note 进行中|讨论中|滞留中|搁置',
|
|
453
|
+
' abs todo state <id> --note 进行中|讨论中|滞留中|搁置 [--n N] --n = 改第 N 条子项(缺省改任务本身)',
|
|
454
454
|
' abs todo remove <id> 删除任务(连子项,不可恢复;只是先不做请用 state 搁置)',
|
|
455
455
|
' abs todo done <id> [--as 落地|否决|仅方案] [结语文字]',
|
|
456
456
|
' abs todo archive [--keep-days N] [--dry-run]',
|
|
@@ -632,7 +632,7 @@ async function main() {
|
|
|
632
632
|
// memo 同:随手记常是多句带空格,位置参数收全段比 --note 顺手。
|
|
633
633
|
const isDone = action === 'done';
|
|
634
634
|
const isMemo = action === 'memo';
|
|
635
|
-
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 "…"'}`;
|
|
636
636
|
if (isDone) {
|
|
637
637
|
const note = opts.note || (rest2.length ? rest2.join(' ') : undefined);
|
|
638
638
|
console.log(await cmdTask({ dir: opts.dir, action, id, section: opts.section, note, as: opts.as, session: opts.session }));
|
|
@@ -655,7 +655,10 @@ async function main() {
|
|
|
655
655
|
}
|
|
656
656
|
rejectExtra(rest2, usage);
|
|
657
657
|
if (!id) throw new Error(`✗ 缺 <id>\n 用法: ${usage}`);
|
|
658
|
-
|
|
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 }));
|
|
659
662
|
break;
|
|
660
663
|
}
|
|
661
664
|
case 'ingest': {
|
package/bin/mcp.js
CHANGED
|
@@ -113,11 +113,11 @@ tool(
|
|
|
113
113
|
|
|
114
114
|
tool(
|
|
115
115
|
'abs_task',
|
|
116
|
-
'任务实时落盘(幂等键 = 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 的 [会话] 段)以标记「这条任务谁在做」。',
|
|
117
117
|
{
|
|
118
118
|
action: z.enum(['add', 'start', 'done', 'note', 'child_done', 'child_undone', 'memo', 'state', 'remove']),
|
|
119
119
|
id: z.string().describe('任务幂等键,如 TASK-xxx 或子任务名'),
|
|
120
|
-
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 = 改任务本身)'),
|
|
121
121
|
cwd: z.string().describe('项目根目录(.brain/ 所在处)'), note: z.string().optional().describe('add/start=做什么; note=追加一条子项(改到哪/下一步,一句话); memo=随手记(可多句、不定长,追加式;不传=查看已有笔记); state=进行中|讨论中|滞留中|搁置(搁置=用户改方向/不做了;滞留中=还要做只是卡住); done=结语文字(remove 不用此字段)'),
|
|
122
122
|
as: z.enum(['落地', '否决', '仅方案']).optional().describe('仅 done:结语类型。默认 落地。做了又撤/评估后不做用 否决,只设计过用 仅方案 —— 别让假【落地】污染看板'),
|
|
123
123
|
// 会话 id:多会话并行时标记「这条任务是谁在做」。
|
|
@@ -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,10 +102,8 @@ 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
|
-
* ★ 左边的 working 状态(`⠼ Working`)**不用**这个色(用户实测要求):
|
|
104
|
-
* 它跟框线同色,橙色只标记「上下文占用」。
|
|
105
|
+
/** 「擱置」橙 #F59E0B —— 与 todo 面板的 `[擱置]`、面板大标题同色(abs.pi.ts 的 `C.hold`/`C.title`)。
|
|
106
|
+
* 用途(2026-11-09 用户要求):输入框**右下角的状态文字**(如 `30.0%/1.0M DeepSeek-V4.1-Flash`)。
|
|
105
107
|
* ★ 色值在 abs.pi.ts 里也有一份,不抽公共模块是故意的(见 EFFORT_COLOR 的注释)。 */
|
|
106
108
|
export const HOLD_COLOR = { rgb: '245;158;11', ansi256: 214 }
|
|
107
109
|
|
|
@@ -109,10 +111,6 @@ export function holdAnsi(colorMode = 'truecolor'): string {
|
|
|
109
111
|
return colorMode === 'truecolor' ? `38;2;${HOLD_COLOR.rgb}` : `38;5;${HOLD_COLOR.ansi256}`
|
|
110
112
|
}
|
|
111
113
|
|
|
112
|
-
/** 上边框最左那段(如 `⠼ Working`)由 pi 画、roundBorder 不单独给它上色:
|
|
113
|
-
* 它随整行用框线色。试过给它上橙,用户实测要求“跟框线同色”,已改回 ——
|
|
114
|
-
* 别再拆一段出来单独染色。 */
|
|
115
|
-
|
|
116
114
|
/** 用 effortAnsi 的结果包一段文字;空串原样返回(off 时不着色)。 */
|
|
117
115
|
export function paintAnsi(ansi: string, text: string): string {
|
|
118
116
|
return ansi ? `\x1b[${ansi}m${text}\x1b[39m` : text
|
|
@@ -127,9 +125,12 @@ export function paintAnsi(ansi: string, text: string): string {
|
|
|
127
125
|
* leftLabel 贴在左圆角之后;label 贴最右端。哪个放不下就丢掉哪个
|
|
128
126
|
* (宁可没有,也不让边框溢出折行)。
|
|
129
127
|
*
|
|
130
|
-
* opts.labelFg
|
|
131
|
-
*
|
|
132
|
-
*
|
|
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 一并删了 —— 要再嵌回去就照那时候的写法加。
|
|
133
134
|
*/
|
|
134
135
|
export function roundBorder(
|
|
135
136
|
line: string,
|
|
@@ -166,12 +167,12 @@ export function roundBorder(
|
|
|
166
167
|
}
|
|
167
168
|
}
|
|
168
169
|
return paintSpans(left + mid + right, fg, [
|
|
169
|
-
//
|
|
170
|
+
// 标签从**末尾**找:万一同一串又出现在左边,不能把左边那个也染成标签色。
|
|
170
171
|
[label, opts.labelFg, 'last'],
|
|
171
172
|
])
|
|
172
173
|
}
|
|
173
174
|
|
|
174
|
-
/** 把 out
|
|
175
|
+
/** 把 out 里的区间换成各自指定的色,其余整段用 fg。区间按位置排序、重叠则跳过。
|
|
175
176
|
* 为何不用「拼段」实现:标签与 working 段的字符是从 mid 里苹/插进去的,
|
|
176
177
|
* 只有拿到最终串才能准确定位(直接拼段会在 dropCols 后错位)。 */
|
|
177
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,70 +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
|
-
// 右上角状态文字用「搁置」橙 #F59E0B(2026-11-08 用户要求):
|
|
56
|
-
// 只染那一段 —— 圆角、虚线、以及 pi 画的 working 状态都跟框线同色
|
|
57
|
-
// (working 曾也上橙,用户实测「要跟输入框线条颜色一致」,已改回)。
|
|
58
|
-
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
|
|
59
49
|
const out: string[] = []
|
|
60
50
|
|
|
61
|
-
// 上边框:原本是整行
|
|
62
|
-
out.push(roundBorder(lines[0], wrap, '╭', '╮'
|
|
51
|
+
// 上边框:原本是整行 `─`(含滚动提示),两端换圆角。
|
|
52
|
+
out.push(roundBorder(lines[0], wrap, '╭', '╮'))
|
|
63
53
|
// 下边框在原结果的倒数第 (1 + 自动补全行数) 位 —— 补全菜单排在它后面。
|
|
64
54
|
const autocompleteHeight = (this as any).renderedAutocompleteHeight ?? 0
|
|
65
55
|
const bottomIdx = lines.length - 1 - autocompleteHeight
|
|
66
56
|
|
|
67
57
|
for (let i = 1; i < bottomIdx; i++) {
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
out.push(wrap(SIDE) + textFg(body) + wrap(SIDE))
|
|
58
|
+
// 内容行原样透传(不上文字色),只在两端各包一根竖线。
|
|
59
|
+
out.push(wrap(SIDE) + lines[i] + wrap(SIDE))
|
|
71
60
|
}
|
|
72
61
|
// 下边框(2026-11-07 还原:左下角的 fanchao·昵称已去掉 —— 用户嫌难看,
|
|
73
62
|
// 回 todo 面板标题;roundBorder 的 leftLabel 参数保留备用)。
|
|
74
|
-
|
|
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
|
+
}))
|
|
75
68
|
// 自动补全行原样透传(不套竖线)。
|
|
76
69
|
for (let i = bottomIdx + 1; i < lines.length; i++) out.push(lines[i])
|
|
77
70
|
return out
|
|
78
71
|
}
|
|
79
72
|
}
|
|
80
73
|
|
|
81
|
-
/**
|
|
74
|
+
/** 刷新右下角状态文本(每次渲染前由事件与定时器调用)。 */
|
|
82
75
|
function refreshStatus(ctx: any): void {
|
|
83
76
|
try {
|
|
84
77
|
statusLabel = buildStatus(ctx?.getContextUsage?.(), ctx?.model?.id ?? '')
|
|
85
78
|
} catch {
|
|
86
79
|
statusLabel = shortenModel(ctx?.model?.id ?? '')
|
|
87
80
|
}
|
|
88
|
-
refreshEffort(ctx)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** 读出当前推理强度。`ctx.thinkingLevel` 是 pi 官方给扩展的入口(effective level)。 */
|
|
92
|
-
function refreshEffort(ctx: any): void {
|
|
93
|
-
try {
|
|
94
|
-
effortLevel = String(ctx?.thinkingLevel || '')
|
|
95
|
-
} catch {
|
|
96
|
-
effortLevel = ''
|
|
97
|
-
}
|
|
98
81
|
}
|
|
99
82
|
|
|
100
83
|
export default function piRoundedEditor(pi: any): void {
|
|
101
84
|
pi.on('session_start', (_event: unknown, ctx: any) => {
|
|
102
85
|
if (!ctx?.ui?.setEditorComponent) return
|
|
103
|
-
//
|
|
104
|
-
|
|
86
|
+
// 终端色模式(256 色档位用);取不到就当真彩。
|
|
87
|
+
try { colorMode = String(ctx.ui?.theme?.getColorMode?.() || 'truecolor') } catch { colorMode = 'truecolor' }
|
|
105
88
|
refreshStatus(ctx)
|
|
106
|
-
// 第四个参数 embedWorkingStatus: true —— working
|
|
89
|
+
// 第四个参数 embedWorkingStatus: true —— working 状态进上边框,不单独占一行(2026-11-09 用户定案)。
|
|
90
|
+
// 颜色不用管:pi 嵌入时用 editor.borderColor,本扩展又把整行重上一次框线色,两边同为档位色。
|
|
107
91
|
ctx.ui.setEditorComponent(
|
|
108
92
|
(tui: any, theme: any, keybindings: any) =>
|
|
109
93
|
new RoundedEditor(tui, theme, keybindings, { embedWorkingStatus: true }),
|
|
110
94
|
)
|
|
111
|
-
//
|
|
95
|
+
// ③ 隐藏输入框下方的 footer 两行(路径/git/token、MCP/扩展状态),信息并入右下角。
|
|
112
96
|
ctx.ui.setFooter?.(() => ({ render: () => [], invalidate() {} }))
|
|
113
97
|
})
|
|
114
98
|
|
|
@@ -120,13 +104,7 @@ export default function piRoundedEditor(pi: any): void {
|
|
|
120
104
|
ctx?.ui?.requestRender?.()
|
|
121
105
|
})
|
|
122
106
|
|
|
123
|
-
//
|
|
124
|
-
// 为何不能只靠 session_start 那一次:/model 的强度档位、Alt+T 之类的切换不改会话,
|
|
125
|
-
// 不重建编辑器也就不会重新读 effortLevel —— 文字会停在旧色。
|
|
126
|
-
pi.on('thinking_level_select', (_event: any, ctx: any) => {
|
|
127
|
-
refreshEffort(ctx)
|
|
128
|
-
ctx?.ui?.requestRender?.()
|
|
129
|
-
})
|
|
107
|
+
// 切推理强度:本扩展不再需要处理(边框色是 pi 自己在 updateEditorBorderColor() 里换并请求重绘)。
|
|
130
108
|
|
|
131
109
|
// 上下文占用随每轮对话变化 —— 轮次结束与消息结束时刷新。
|
|
132
110
|
for (const ev of ['turn_end', 'message_end', 'tool_execution_end']) {
|
package/hooks/abs.pi.ts
CHANGED
|
@@ -780,13 +780,13 @@ export function pickNickname(rnd: () => number = Math.random): string {
|
|
|
780
780
|
* 子项行(缩进的 `- [ ]`)归属到它上一条任务,不单独成条。 */
|
|
781
781
|
export function parseOpenTasks(md: string, max = PANEL_MAX_ROWS, who = ''): {
|
|
782
782
|
total: number
|
|
783
|
-
rows: { state: string; id: string; desc: string; kids: { done: boolean; text: string }[]; sid: string }[]
|
|
783
|
+
rows: { state: string; id: string; desc: string; kids: { done: boolean; text: string; state: string | null }[]; sid: string }[]
|
|
784
784
|
hidden: number
|
|
785
785
|
} {
|
|
786
786
|
// 两阶段:先无过滤地按序解析(子项归属需要“上一条”是原文里真正的前一条),
|
|
787
787
|
// 再按作者过滤。若边解析边过滤,被滤掉的条目下方的子项会挂到它上面那一条
|
|
788
788
|
// —— 实测:别人任务的子项会显示在我的任务下面(2026-10-03 审查发现)。
|
|
789
|
-
type Row = { state: string; id: string; desc: string; kids: { done: boolean; text: string }[]; authors: string[]; sid: string }
|
|
789
|
+
type Row = { state: string; id: string; desc: string; kids: { done: boolean; text: string; state: string | null }[]; authors: string[]; sid: string }
|
|
790
790
|
const parsed: Row[] = []
|
|
791
791
|
let inTodo = false
|
|
792
792
|
for (const raw of String(md || '').split('\n')) {
|
|
@@ -861,12 +861,20 @@ async function currentUser(): Promise<string> {
|
|
|
861
861
|
* 为何不 import src:面板是宿主扩展,单独装到 ~/.pi/extensions/,与包内 src/ 不同目录
|
|
862
862
|
* (装完就断了)。故两端各一份 —— 规则简单(两步正则),且两边测试都盖。
|
|
863
863
|
* 标准 md:缩进的 `- [ ] 内容`。 */
|
|
864
|
-
|
|
864
|
+
/** 行首状态标记的四个词(任务行与子项共用;与 CLI 侧 common.js 的 TASK_STATES 同值)。
|
|
865
|
+
* 两个扩展各自独立安装,所以这里再拄一份,不跨包 import。 */
|
|
866
|
+
const STATE_WORDS = ['进行中', '讨论中', '滞留中', '搁置']
|
|
867
|
+
|
|
868
|
+
function parseChildLine(l: string): { done: boolean; text: string; indent: number; state: string | null } | null {
|
|
865
869
|
// 只认标准 md 缩进子项(2026-10-20 旧箭头写法已废弃)。
|
|
866
870
|
// 旧行由 CLI 侧 migrateLegacyChild 在读写时迁移掉,面板拿到的已是新格式。
|
|
867
871
|
const m = String(l || '').match(/^(\s+)-\s*\[([ x])\]\s*(.*)$/)
|
|
868
872
|
if (!m) return null
|
|
869
|
-
|
|
873
|
+
// 子项也能带状态标记(2026-11-09 用户要求,与任务行同形):只认四个词,
|
|
874
|
+
// 别的方括号(如 `[WIP]`)是正文的一部分,原样留在 text 里(同 CLI 侧 parseChild)。
|
|
875
|
+
const s = m[3].match(/^\[([^\]]+)\]\s*(.*)$/)
|
|
876
|
+
const state = s && STATE_WORDS.includes(s[1]) ? s[1] : null
|
|
877
|
+
return { done: m[2] === 'x', text: (state ? s![2] : m[3]).trim(), indent: m[1].length, state }
|
|
870
878
|
}
|
|
871
879
|
|
|
872
880
|
function dispWidth(s: string): number {
|
|
@@ -939,73 +947,65 @@ function clipToWidth(s: string, width: number): string {
|
|
|
939
947
|
* 拿不到 ctx,只能由事件回调先存好。2026-11-06 用户需求:面板主色跟推理强度走。 */
|
|
940
948
|
let effortLevel = ''
|
|
941
949
|
|
|
942
|
-
/**
|
|
943
|
-
* 推理强度 → 主题色 token。空串表示「不上推理色,用调用方给的兼底色」。
|
|
944
|
-
*
|
|
945
|
-
* 规则:`thinking` + 首字母大写,正好对上主题里的 thinkingMinimal…thinkingMax 七档。
|
|
946
|
-
* 为什么 off 返回空串而不是 `thinkingOff`:主题给 thinkingOff 的是接近背景的暗灰
|
|
947
|
-
* (dark.json: `okhsl(229 8% 49%)`),拿它去染文字会看不清。用户 2026-11-06 定的是
|
|
948
|
-
* 「off 时保持默认色」。
|
|
950
|
+
/** 档位 → pi 主题里那档 thinking 色(与输入框边框同一个来源)。
|
|
949
951
|
*
|
|
950
|
-
*
|
|
951
|
-
*
|
|
952
|
-
*
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
if (!g) return ''
|
|
966
|
-
return colorMode === 'truecolor' ? `38;2;${g.rgb}` : `38;5;${g.ansi256}`
|
|
952
|
+
* 为何要向主题要(2026-11-09 用户要求「todo 上的颜色也跟着 pi 输入框的颜色」):
|
|
953
|
+
* 那七档色写在主题里(dark.json 的 okhsl),自己拄一份就会跟着主题改而跑偏。
|
|
954
|
+
* 为何要抠一层正则:getFgAnsi() 返回的是整段 `\x1b[38;2;…m`,而面板内部用的是
|
|
955
|
+
* 「色码串 + paintAnsi 自己包转义」那套,只能取里面的 `38;2;…`。
|
|
956
|
+
* 空档位或主题不认这个 token → 返回空串,调用处回落到 C.desc。 */
|
|
957
|
+
export function thinkingAnsiOf(theme: any, level: string | null | undefined): string {
|
|
958
|
+
const lvl = String(level || '').trim().toLowerCase()
|
|
959
|
+
if (!lvl) return ''
|
|
960
|
+
const token = `thinking${lvl[0].toUpperCase()}${lvl.slice(1)}`
|
|
961
|
+
try {
|
|
962
|
+
const m = String(theme?.getFgAnsi?.(token) || '').match(/\x1b\[([0-9;]+)m/)
|
|
963
|
+
return m ? m[1] : ''
|
|
964
|
+
} catch {
|
|
965
|
+
return ''
|
|
966
|
+
}
|
|
967
967
|
}
|
|
968
968
|
|
|
969
|
-
/**
|
|
969
|
+
/** 用色码串包一段文字(如 `paint('38;2;16;185;129', 'x')`);空串原样返回。 */
|
|
970
970
|
export function paintAnsi(ansi: string, text: string): string {
|
|
971
971
|
return ansi ? `\x1b[${ansi}m${text}\x1b[39m` : text
|
|
972
972
|
}
|
|
973
973
|
|
|
974
974
|
export function renderPanelLines(
|
|
975
|
-
data: { total: number; rows: { state: string; id: string; desc: string; kids?: { done: boolean; text: string }[] }[]; hidden: number },
|
|
975
|
+
data: { total: number; rows: { state: string; id: string; desc: string; kids?: { done: boolean; text: string; state?: string | null }[] }[]; hidden: number },
|
|
976
976
|
who: string,
|
|
977
977
|
width: number,
|
|
978
978
|
fg: (color: string, s: string) => string,
|
|
979
979
|
nickname = '',
|
|
980
|
-
|
|
980
|
+
effortColor = '',
|
|
981
981
|
): string[] {
|
|
982
982
|
// ★ 空看板不再直接返回空数组(2026-10-06 用户要求:「即便 todo 没有列表,也要在 pi 上显示」)。
|
|
983
983
|
// 理由与看板段一致:面板是「机制在不在」的常驻指示,进去就看不见 = 不知道机制活着。
|
|
984
984
|
// 下面仍要画框和标题,所以这里只是不再提前 return。
|
|
985
985
|
//
|
|
986
|
-
// ★
|
|
987
|
-
//
|
|
988
|
-
//
|
|
989
|
-
//
|
|
990
|
-
//
|
|
991
|
-
//
|
|
992
|
-
// 标题色(2026-11-08 用户实测「太亮了」):从纯金 #FFD700 换成与「搁置」同一个橙 #F59E0B
|
|
993
|
-
// —— 纯金在暗底上抢眼,标题只是分区标签,不该比当天要做的事更跳。
|
|
994
|
-
// 代价:标题与 `[搁置]` 标签同色;两者一个在最左(标题)一个在行中(状态),位置分得开。
|
|
986
|
+
// ★ 文字色跟推理强度走(2026-11-09 用户要求「面板上的文字颜色要与输入框推理强度一致」):
|
|
987
|
+
// 标题、树线、任务 id、描述、子项、`+N more` 全用档位色(effortColor,由调用处从主题取)。
|
|
988
|
+
// ★ 四个状态标签(进行中/讨论中/滞留中/搁置)**保留各自语义色**,不跟档位走 ——
|
|
989
|
+
// 用户选的方案:否则四态只能靠文字看,一眼分不出。别再顺手把它们也统一了。
|
|
990
|
+
// 描述色 C.desc 降为「取不到档位色时的兼底」(主题没这个 token / 模型不支持推理)。
|
|
991
|
+
// 历史(勿重走):2026-11-08 曾定过「现代暗色方案:标题橙、树线蓝灰、描述次灰」,已作废。
|
|
995
992
|
const C = {
|
|
993
|
+
// 面板大标题回到擱置橙 #F59E0B(2026-11-09 用户试过灰白,看完要回橙;沿用 2026-11-08 的定)。
|
|
994
|
+
// 任务行的 id/描述**不用**这个色 —— 它们跟档位色跑。
|
|
996
995
|
title: '38;2;245;158;11',
|
|
997
996
|
task: '38;2;226;232;240',
|
|
998
997
|
desc: '38;2;148;163;184',
|
|
999
|
-
tree: '38;2;150;170;198',
|
|
1000
998
|
doing: '38;2;16;185;129',
|
|
1001
999
|
talk: '38;2;59;130;246',
|
|
1002
1000
|
hold: '38;2;245;158;11',
|
|
1003
1001
|
stuck: '38;2;139;92;246',
|
|
1004
|
-
done: '38;2;100;116;139',
|
|
1005
1002
|
}
|
|
1006
1003
|
const stateAnsi = (st: string): string =>
|
|
1007
1004
|
st === '进行中' ? C.doing : st === '讨论中' ? C.talk : st === '搁置' ? C.hold : st === '滞留中' ? C.stuck : C.task
|
|
1008
1005
|
const paint = (ansi: string, t: string) => paintAnsi(ansi, t)
|
|
1006
|
+
// 面板文字色 = 调用处从主题取的档位色;取不到(主题没这个 token / 模型不支持推理)回落描述灰。
|
|
1007
|
+
// 提到循环外:标题与 `+N more` 也在用它。
|
|
1008
|
+
const eff = effortColor || C.desc
|
|
1009
1009
|
const lines: string[] = []
|
|
1010
1010
|
// 上描边行已删除(2026-11-04 用户要求「去掉上边框」)。
|
|
1011
1011
|
// 历史:那行曾用 `thinkingOff` 对齐 pi 输入框描边色(排查三轮才定色)。
|
|
@@ -1028,35 +1028,38 @@ export function renderPanelLines(
|
|
|
1028
1028
|
const branch = isLast ? '└─' : '├─'
|
|
1029
1029
|
// 进行中的行曾带圆圈动画(2026-11-08 用户定:「这个动画没啥用」,整个去掉)。
|
|
1030
1030
|
// 去掉后 [进行中] 靠状态色区分,不再额外消耗一次 200ms 重绘。
|
|
1031
|
-
const hasState =
|
|
1031
|
+
const hasState = STATE_WORDS.includes(t.state)
|
|
1032
1032
|
// 复选框 `[ ]`(2026-10-16 用户要求):面板是 todo.md 的只读视图,
|
|
1033
1033
|
// 就该长成 todo 原貌 —— 摆成与文件里 `- [ ]` 同形,读到就认得出对应哪一行。
|
|
1034
|
-
const eff = effortAnsi(effort, 'truecolor') || C.desc
|
|
1035
1034
|
const box = hasState ? '' : paint(eff, '[ ] ')
|
|
1036
1035
|
const stPart = hasState
|
|
1037
1036
|
? paint(stateAnsi(t.state), `[${t.state}]`)
|
|
1038
1037
|
: paint(eff, '[待办]')
|
|
1039
|
-
const head = `${paint(
|
|
1040
|
-
const desc = t.desc ? ' ' + paint(
|
|
1038
|
+
const head = `${paint(eff, branch)} ${box}${stPart} ${paint(eff, t.id)}`
|
|
1039
|
+
const desc = t.desc ? ' ' + paint(eff, `— ${t.desc}`) : ''
|
|
1041
1040
|
lines.push(clipToWidth(head + desc, width))
|
|
1042
1041
|
if (t.kids && t.kids.length) {
|
|
1043
1042
|
// 子项:标准 md 嵌套列表 → 画成树
|
|
1044
1043
|
// ├─ [ ] 子项一
|
|
1045
1044
|
// └─ [x] 子项二
|
|
1046
1045
|
// 树线前缀 = 父任务的 │(非末条)或空格(末条),与父任务分支对齐。
|
|
1047
|
-
//
|
|
1048
|
-
const stem = isLast ? ' ' : paint(
|
|
1046
|
+
// 勾过的用删除线区分(颜色已统一为档位色),未勾不着修饰。
|
|
1047
|
+
const stem = isLast ? ' ' : paint(eff, '│ ')
|
|
1049
1048
|
const n = t.kids.length
|
|
1050
1049
|
t.kids.forEach((k, ki) => {
|
|
1051
1050
|
const kb = ki === n - 1 ? '└─' : '├─'
|
|
1052
|
-
|
|
1053
|
-
const
|
|
1054
|
-
|
|
1051
|
+
// 有状态的子项跟任务同形:不画 `[ ]`,改画状态标签(2026-11-09 用户要求子项也能标四态)。
|
|
1052
|
+
const kbox = k.state
|
|
1053
|
+
? paint(stateAnsi(k.state), `[${k.state}]`)
|
|
1054
|
+
: paint(eff, k.done ? '[x]' : '[ ]')
|
|
1055
|
+
// 勾过的子项靠删除线区分(颜色已统一为档位色,不再降灰)
|
|
1056
|
+
const ktext = k.done ? `\x1b[9m${paint(eff, k.text)}\x1b[29m` : paint(eff, k.text)
|
|
1057
|
+
lines.push(clipToWidth(`${stem} ${paint(eff, kb)} ${kbox} ${ktext}`, width))
|
|
1055
1058
|
})
|
|
1056
1059
|
}
|
|
1057
1060
|
})
|
|
1058
1061
|
|
|
1059
|
-
if (data.hidden > 0) lines.push(clipToWidth(paint(
|
|
1062
|
+
if (data.hidden > 0) lines.push(clipToWidth(paint(eff, ` +${data.hidden} more`), width))
|
|
1060
1063
|
return lines
|
|
1061
1064
|
}
|
|
1062
1065
|
/** 隐藏面板。**必须主动 requestRender** —— 否则数据清了、屏幕还留着旧画面。
|
|
@@ -1103,7 +1106,8 @@ async function refreshTodoPanel(ui: any, cwd: string): Promise<void> {
|
|
|
1103
1106
|
width,
|
|
1104
1107
|
(c: string, s: string) => theme.fg(c, s),
|
|
1105
1108
|
sessionNickname,
|
|
1106
|
-
|
|
1109
|
+
// 面板主色跟 pi 输入框一致:从主题现取当前档位的 thinking 色(2026-11-09 用户要求)。
|
|
1110
|
+
thinkingAnsiOf(theme, effortLevel),
|
|
1107
1111
|
),
|
|
1108
1112
|
invalidate: () => {},
|
|
1109
1113
|
}
|
package/package.json
CHANGED
|
@@ -61,7 +61,8 @@ Then run `abs lint` (dead links, over-size, NO-TAIL).
|
|
|
61
61
|
abs todo start <id> --note "做什么" # register (note required)
|
|
62
62
|
abs todo note <id> --note "到哪一步" # append child (one sentence)
|
|
63
63
|
abs todo done <id> --as 落地|否决|仅方案 "结语"
|
|
64
|
-
abs todo state <id> --note 进行中|讨论中|滞留中|搁置
|
|
64
|
+
abs todo state <id> --note 进行中|讨论中|滞留中|搁置 # 任务本身
|
|
65
|
+
abs todo state <id> --note 滞留中 --n 2 # 第 2 条子项(子项也带四态)
|
|
65
66
|
abs todo remove <id> # delete incl. children, no undo
|
|
66
67
|
```
|
|
67
68
|
|
package/src/todo/common.js
CHANGED
|
@@ -127,6 +127,19 @@ export function isChildLine(l) {
|
|
|
127
127
|
* 存量靠 migrateLegacyChild 一次性迁移(那是全仓唯一还认得 `↳` 的地方)。 */
|
|
128
128
|
export const CHILD_INDENT = ' ';
|
|
129
129
|
|
|
130
|
+
/** 行首状态标记的四个词(任务行与子项共用;2026-11-09 起子项也能标)。
|
|
131
|
+
* 放在本文件而不是 spec.js:parseChild / renderChild 要用它判「行首 [xx] 是不是状态」,
|
|
132
|
+
* 而 common.js 是不 import 任何模块的叶子(spec.js 反向 re-export),放 spec 会成环。 */
|
|
133
|
+
export const TASK_STATES = ['进行中', '讨论中', '滞留中', '搁置'];
|
|
134
|
+
|
|
135
|
+
/** 把 `[xx] 正文` 拆成 { state, text }:只认 TASK_STATES 里的词,
|
|
136
|
+
* 别的方括号(如 `[WIP]`)是正文的一部分,原样留在 text 里。 */
|
|
137
|
+
function splitStateMark(rest) {
|
|
138
|
+
const raw = String(rest ?? '');
|
|
139
|
+
const m = raw.match(/^\[([^\]]+)\]\s*(.*)$/);
|
|
140
|
+
return m && TASK_STATES.includes(m[1]) ? { state: m[1], text: m[2] } : { state: null, text: raw };
|
|
141
|
+
}
|
|
142
|
+
|
|
130
143
|
/** 唯一还认得旧 `↳` 写法的地方 —— 存量迁移。
|
|
131
144
|
*
|
|
132
145
|
* 为何保留这一个函数(2026-10-20 用户定:彻底去掉断点写法,但存量要自动迁移):
|
|
@@ -155,13 +168,15 @@ export function migrateLegacyChild(l) {
|
|
|
155
168
|
export function parseChild(l) {
|
|
156
169
|
const m = String(l || '').match(/^(\s+)-\s*\[([ x])\]\s*(.*)$/);
|
|
157
170
|
if (!m) return null;
|
|
158
|
-
|
|
171
|
+
const { state, text } = splitStateMark(m[3]);
|
|
172
|
+
return { done: m[2] === 'x', text: text.trim(), indent: m[1].length, state };
|
|
159
173
|
}
|
|
160
174
|
|
|
161
175
|
/** 生成子项行(标准 md,统一入口 —— 格式只有这一个真源)。
|
|
162
|
-
* indent 默认 2(挂在顶层任务下);多层嵌套时传更深的值。
|
|
163
|
-
|
|
164
|
-
|
|
176
|
+
* indent 默认 2(挂在顶层任务下);多层嵌套时传更深的值。
|
|
177
|
+
* state 传四态之一则写成 ` - [ ] [滞留中] 正文`(与任务行同形)。 */
|
|
178
|
+
export function renderChild(text, { done = false, indent = CHILD_INDENT, state = null } = {}) {
|
|
179
|
+
return `${indent}- ${done ? '[x]' : '[ ]'} ${state ? `[${state}] ` : ''}${text}`;
|
|
165
180
|
}
|
|
166
181
|
|
|
167
182
|
/** Todo 分区里「合法行」以外的东西 = 裸文本(总结/报告/清单)。
|
package/src/todo/spec.js
CHANGED
|
@@ -199,7 +199,9 @@ export function indexLinkSectionNeeds(section) {
|
|
|
199
199
|
* 搁置 = 不做了 / 用户改方向了 → 下会话不该捡,除非用户又提
|
|
200
200
|
* 混在一起会让下会话分不清「该不该接着干」。 */
|
|
201
201
|
|
|
202
|
-
|
|
202
|
+
// 四个状态词已挪到 common.js(2026-11-09:子项也能标状态,parseChild/renderChild 要用它)——
|
|
203
|
+
// 这里只做转发,别处的 import 路径不变。
|
|
204
|
+
export { TASK_STATES } from './common.js';
|
|
203
205
|
|
|
204
206
|
/** 结构重建(B 档):以标准分区表为准重排整个文件。
|
|
205
207
|
*
|
package/src/todo/task-cmd.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { brainPath, requireBrain } from '../index.js';
|
|
5
5
|
import { editFile, SKIP } from '../lock.js';
|
|
6
6
|
import { requireUser, atTag } from '../userconfig.js';
|
|
7
|
-
import { stripStateMark, checkTaskBox, stateOfTaskLine, upsertTask, today, setBreakpoint, setBreakpointDone, setMemo, readMemo, setStateMark, TASK_STATES, insertDoneGrouped, idOfTaskLine, DONE_KINDS, withDoneKind, sessionOf, parseChild, migrateLegacyChild, SEC } from '../todo.js';
|
|
7
|
+
import { stripStateMark, checkTaskBox, stateOfTaskLine, upsertTask, today, setBreakpoint, setBreakpointDone, setMemo, readMemo, setStateMark, setChildStateMark, TASK_STATES, insertDoneGrouped, idOfTaskLine, DONE_KINDS, withDoneKind, sessionOf, parseChild, migrateLegacyChild, SEC } from '../todo.js';
|
|
8
8
|
import { ensurePersonPage } from '../note.js';
|
|
9
9
|
|
|
10
10
|
// 纪律: task 过程动作(start/done/note/blocked)只改 todo.md, 不写 log.md。
|
|
@@ -131,8 +131,9 @@ export async function cmdTask({ dir, action, id, section, note, as, session, n }
|
|
|
131
131
|
return done ? `✓ 任务改名 ${id} → ${newId}` : `[NO_MATCH] 未找到含 "${id}" 的未完成任务行`;
|
|
132
132
|
}
|
|
133
133
|
if (action === 'state') {
|
|
134
|
-
// 改状态标记(原地,不搬区):进行中 / 讨论中 / 滞留中
|
|
135
|
-
|
|
134
|
+
// 改状态标记(原地,不搬区):进行中 / 讨论中 / 滞留中 / 搁置。
|
|
135
|
+
// 带 n = 改某任务的第 n 条子项(2026-11-09 用户要求给子项加状态);不带给 n = 改任务本身。
|
|
136
|
+
if (!note) throw new Error(`✗ 用法: abs todo state <id> --note "${TASK_STATES.join('|')}" [--n N]`);
|
|
136
137
|
if (!TASK_STATES.includes(note)) {
|
|
137
138
|
throw new Error(`✗ 状态只接受: ${TASK_STATES.join(' | ')}(收到 "${note}")`);
|
|
138
139
|
}
|
|
@@ -140,11 +141,16 @@ export async function cmdTask({ dir, action, id, section, note, as, session, n }
|
|
|
140
141
|
let cc = [];
|
|
141
142
|
await editFile(f, (cur) => {
|
|
142
143
|
if (cur === null) return SKIP;
|
|
143
|
-
const r = setStateMark(cur, id, note);
|
|
144
|
+
const r = n ? setChildStateMark(cur, id, n, note) : setStateMark(cur, id, note);
|
|
144
145
|
cc = r.changed;
|
|
145
146
|
return r.changed.length ? { text: r.text } : SKIP;
|
|
146
147
|
});
|
|
147
|
-
|
|
148
|
+
if (!cc.length) {
|
|
149
|
+
return n
|
|
150
|
+
? `[NO_MATCH] "${id}" 下没有第 ${n} 条子项(或任务不存在),或状态未变`
|
|
151
|
+
: `[NO_MATCH] 未找到含 "${id}" 的未完成任务行,或状态未变`;
|
|
152
|
+
}
|
|
153
|
+
return `✓ ${id} ${n ? `第 ${n} 条子项` : ''}状态 → [${note}]`;
|
|
148
154
|
}
|
|
149
155
|
// 删除任务(2026-11-05 用户要求「每次删除任务可烦人了」)。
|
|
150
156
|
// 与 state=搁置 的区别(别混):
|
package/src/todo/tasks.js
CHANGED
|
@@ -198,13 +198,43 @@ export async function setBreakpointDone(brainRoot, { id, n, done = true }) {
|
|
|
198
198
|
if (target === undefined) return SKIP;
|
|
199
199
|
const child = parseChild(lines[target]);
|
|
200
200
|
picked = child;
|
|
201
|
-
|
|
201
|
+
// 保留印过的状态标记(2026-11-09):只翻复选框,不能把 [滞留中] 这类标记抹掉。
|
|
202
|
+
lines[target] = renderChild(child.text, { done, indent: ' '.repeat(child.indent), state: child.state });
|
|
202
203
|
return { text: lines.join('\n'), ok: true };
|
|
203
204
|
});
|
|
204
205
|
if (res === SKIP || !picked) {
|
|
205
206
|
return { ok: false, msg: `[NO_MATCH] "${id}" 下没有第 ${n || '(最后)'} 条子项(或任务不存在)` };
|
|
206
207
|
}
|
|
207
|
-
return { ok: true, msg: `✓ 子项 ${done ? '已勾' : '已取消勾'} → ${id}\n ${renderChild(picked.text, { done })}` };
|
|
208
|
+
return { ok: true, msg: `✓ 子项 ${done ? '已勾' : '已取消勾'} → ${id}\n ${renderChild(picked.text, { done, state: picked.state })}` };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** 改**子项**的状态标记(2026-11-09 用户要求:子项也要能标四态)。
|
|
212
|
+
*
|
|
213
|
+
* 为何不复用 setStateMark:子项没有自己的 id,findTaskLine 只能定位任务行
|
|
214
|
+
* (idOfTaskLine 取的是行首第一个 token,子项那行取到的是正文首字),
|
|
215
|
+
* 所以子项只能按「某任务的第 n 条」定位(与 child-done 同一套坐标)。
|
|
216
|
+
* 任务行:`- [ ] [进行中] TASK-1 …`
|
|
217
|
+
* 子项行:` - [ ] [滞留中] 改到哪一步`
|
|
218
|
+
* n 必填(1-based):不带 n 的「改状态」= 改任务本身(setStateMark),
|
|
219
|
+
* 子项路径只由带 n 的调用进来。非正整数或越界 → 当没匹配,不静默改到别的行。 */
|
|
220
|
+
export function setChildStateMark(text, id, n, state) {
|
|
221
|
+
if (!TASK_STATES.includes(state)) return { text, changed: [] };
|
|
222
|
+
if (!Number.isInteger(n) || n < 1) return { text, changed: [] };
|
|
223
|
+
const lines = String(text ?? '').split('\n');
|
|
224
|
+
const idx = findTaskLine(lines, id);
|
|
225
|
+
if (idx === -1) return { text, changed: [] };
|
|
226
|
+
const kids = childRange(lines, idx);
|
|
227
|
+
const target = kids[n - 1];
|
|
228
|
+
if (target === undefined) return { text, changed: [] };
|
|
229
|
+
const child = parseChild(lines[target]);
|
|
230
|
+
if (!child || child.state === state) return { text, changed: [] };
|
|
231
|
+
const ord = kids.indexOf(target) + 1;
|
|
232
|
+
lines[target] = renderChild(child.text, {
|
|
233
|
+
done: child.done,
|
|
234
|
+
indent: ' '.repeat(child.indent),
|
|
235
|
+
state,
|
|
236
|
+
});
|
|
237
|
+
return { text: lines.join('\n'), changed: [`${id} 第 ${ord} 条子项状态 → ${state}`] };
|
|
208
238
|
}
|
|
209
239
|
|
|
210
240
|
// ---------- 任务随手记(memo)----------
|
package/src/todo.js
CHANGED
|
@@ -7,5 +7,5 @@ export { LEGACY_SECTION_RENAMES, TODO_SECTIONS, TODO_SUBSECTIONS, isAllowedTodoS
|
|
|
7
7
|
export { todoTemplate, rebuildStructure, dropRetiredSections, enforceBrainFormat, normalizeTodo, ensureStateMark, stateOfTaskLine, stripStateMark, checkTaskBox } from './todo/structure.js';
|
|
8
8
|
export { checkFileShape, violationSignatures, assertNoStrayBlank, assertTodoContent } from './todo/validate.js';
|
|
9
9
|
export { doneDateOf, doneKindOf, withDoneKind, collapseDone, archiveDoneInText, renderArchiveBody, upsertArchiveSection, groupDoneSection, insertDoneGrouped, archiveBrief } from './todo/archive.js';
|
|
10
|
-
export { readTodo, ensureTodo, setStateMark, addTask, upsertTask, idOfTaskLine, findTaskLine, setBreakpoint, setBreakpointDone, setMemo, readMemo, boardText } from './todo/tasks.js';
|
|
10
|
+
export { readTodo, ensureTodo, setStateMark, setChildStateMark, addTask, upsertTask, idOfTaskLine, findTaskLine, setBreakpoint, setBreakpointDone, setMemo, readMemo, boardText } from './todo/tasks.js';
|
|
11
11
|
export { migrateLegacyChild, isTaskLine, isChildLine, CHILD_INDENT } from './todo/common.js';
|