openpi plan mode 待改动清单(最小实现版)
仓库:tt-a1i/openpi
原则:能不加的抽象不加。三项互相独立,可分开提交、分开验证。
背景与设计取舍见 01-plan-mode-设计调研.md。
改动 ①:footer 指示器改短 + 空间够时内联
现状
extensions/plan-mode/index.ts 里 ctx.ui.setStatus("plan-mode", "plan mode · read-only")(ready 态是 "plan mode · ready")。extensions/ui-customization/footer.ts 的 renderFooter() 对 statuses 无条件 lines.push(...),永远另起一行。- 效果:footer 第一行右边空着一大片,plan mode 却单独占了第二行。
改法(最小)
extensions/plan-mode/index.ts:文案缩短
- planning →
"plan mode" - ready →
"plan ready"
(去掉 · read-only / · ready 后缀;「只读」这个信息在被拦时的报错里已经说清楚了,不需要常驻占位。)
extensions/ui-customization/footer.ts 的 renderFooter():状态行能塞进主行就塞
- 规则尽量简单:当只有一条 status、且它能放进第一行剩余空隙(含分隔符)时,把它并到第一行右端;否则维持现在的独占行行为。
- 不新增配置项,不新增 footer item 枚举,不改
PlanModeState 结构——避免为一个标签引入一套新机制。
验收
- 宽终端:
~/gh/openpi main PR #26 plan mode · seal/glm-5.3 · 25% · $0.21 单行。 - 窄终端:自动退回两行,不截断主行信息。
测试(extensions/ui-customization/footer.test.ts)
- 新增:宽度充足时 status 内联进第一行。
- 新增:宽度不足时 status 仍为独立行。
- 注意现有测试
operational statuses always append after layout lines 锁定了旧行为,需要按新规则调整(多条 status 的场景保持不变)。
改动 ②:让 plan mode 一定能退出
现状(这就是「关不掉」的根因)
- 裸
/plan 在 planning 态不是 toggle,只 notify 一句「Plan mode is already active. /plan done requests completion; /plan off cancels.」 plan_ready 之后进 ready 态,选择菜单 PLAN_READY_ACTIONS 只有三项:Continue planning / Implement in this session / Start a fresh session,没有「关闭」。- ready 态下所有工具调用被拦,报错文案是「Wait for the user to choose the next action with
/plan」,没提 /plan off。 - 于是不知道
/plan off 这个确切写法的人,会觉得根本退不出去。
改法(最小)
extensions/plan-mode/index.ts
PLAN_READY_ACTIONS 增加一项 off: "Turn plan mode off",在 showReadyActions() 里对应调用 clearPlan(ctx)。- 裸
/plan 在 planning 态改为弹同一个 select(Continue planning / Finalize now / Turn plan mode off),而不是只 notify。这样「再敲一次 /plan」这个最自然的动作永远有出口。 - ready 态的 block 文案补上
/plan off 这个出口。
不做的事(避免过度设计)
- 不加键盘快捷键(pi 的
shift+tab 已被 app.thinking.cycle 占用,另起一个组合键收益不大)。 - 不改状态持久化结构。
已知的次要问题(本次可不修,记录备查)
- 状态是 branch-local 的:
/plan off 只往当前分支写 inactive,用 /tree 切到另一个最新状态是 planning 的分支会自动重新进入 plan mode。属于设计使然,但对用户是惊喜。 session_shutdown 只清内存、不落 inactive;同一分支重开会话会由持久化状态重新 arm。同上。
测试(extensions/plan-mode/index.test.ts)
- planning 态裸
/plan 选「off」后,planning === false 且后续写工具不再被拦。 - ready 态选「off」后同上。
改动 ③:bash 门禁从「语法黑名单」改成「效果白名单」
现状(extensions/plan-mode/bash-policy.ts)
- 先用
SHELL_METACHARACTERS = /[;&|<>$\!*?{}()[]\n\r#]/、QUOTES = /["']/、TILDE_EXPANSION` 三道正则一票否决。 - 程序白名单只有
git / gh,其余一律拒绝,理由是「plan mode 已经给了 read/ls/grep/fd/rg 工具」。 - 结果:
rg -l "foo" --glob '*.ts' 被拒;而宿主会话若没注册 rg/fd 工具,就完全没有搜索手段。
改法(最小,分两步,第一步就能解决 90% 的痛)
第一步:扩程序白名单 + 允许「安全的引号」
- 新增只读程序及各自的 flag 白名单(准入标准沿用现有那条:flag 只能塑形输出,不能命名要执行的程序、不能指定要写的文件):
rg:允许 -n -i -l -c -w -F -e -g/--glob -t --files --hidden --no-ignore -A -B -C -m/--max-count -o --sort;必须排除--pre、--pre-glob、--hostname-bin、-z/--search-zip(会执行外部程序)。fd:允许 -e -t -d/--max-depth -H -I -g -F -p -a -l;必须排除-x/--exec、-X/--exec-batch。ls:-l -a -h -t -r -R -d -1 -S(ls 没有写/执行能力的 flag)。wc:-l -w -c;head/tail:-n -c,排除 tail -f/--follow(会挂住)。
- 引号处理:不再一刀切拒绝。引号内的内容按字面量处理,只要引号是配对的、且内部不含
$、反引号、\,就放行;未配对引号仍拒绝。这样 rg -l "foo bar" --glob '*.ts' 能跑,而 "$(...)" 依然被拒。 - glob 处理:加了引号的 glob 是字面参数,放行(
--glob '*.ts' 交给 rg 自己解析,shell 不展开);未加引号的 glob 仍拒绝,报错里直接告诉怎么改:把 glob 加引号:--glob '*.ts'。 - 报错文案统一改成「可行动」的形式:说清楚是哪一部分被拦、怎么写才能过。例:
- 旧:
plan mode only runs unquoted commands while planning - 新:
plan mode 允许只读命令;此次被拦是因为未加引号的 glob 会被 shell 展开,请写成 --glob '*.ts'
第二步(可选,视需要再做)
- 支持管道与
&& / ;:按段拆开,每段都在白名单内才整体放行;重定向 > >> <、$()、反引号、后台 & 仍然拒绝。 - 理由:白名单里的程序都不写文件,所以「组合只读命令」不产生新能力;
rg -l foo | head 这类高频用法能省很多来回。 - 但这一步需要一个真正的分段器(tree-sitter-bash 或
shell-quote 的 parse),成本比第一步高。如果第一步之后不觉得别扭,就别做。
测试(extensions/plan-mode/bash-policy.test.ts)
- 放行:
rg -n "foo bar" src、rg -l --glob '*.ts' pattern、fd -e ts、ls -la、现有全部 git/gh 用例。 - 拒绝并检查报错文案包含改法提示:
rg --pre cat x、fd -x rm、tail -f log、rg foo > out、rg $(whoami)、未配对引号、裸 *.ts。
建议实施顺序
- ② 先做——最小、纯收益、解决「关不掉」这个真 bug。
- ① 再做——纯 UI,风险低。
- ③ 第一步 最后做——改动面最大,需要补一批测试。③ 第二步先不做。
三项都改完后跑:bun run check + bun run test(当前基线 713 + 29 全绿;bun run check 有 13 条 file-search 的既存 lint warning,不是本次引入的)。
openpi plan mode 待改动清单(最小实现版)
改动 ①:footer 指示器改短 + 空间够时内联
现状
extensions/plan-mode/index.ts里ctx.ui.setStatus("plan-mode", "plan mode · read-only")(ready 态是"plan mode · ready")。extensions/ui-customization/footer.ts的renderFooter()对statuses无条件lines.push(...),永远另起一行。改法(最小)
extensions/plan-mode/index.ts:文案缩短"plan mode""plan ready"(去掉
· read-only/· ready后缀;「只读」这个信息在被拦时的报错里已经说清楚了,不需要常驻占位。)extensions/ui-customization/footer.ts的renderFooter():状态行能塞进主行就塞PlanModeState结构——避免为一个标签引入一套新机制。验收
~/gh/openpi main PR #26 plan mode · seal/glm-5.3 · 25% · $0.21单行。测试(
extensions/ui-customization/footer.test.ts)operational statuses always append after layout lines锁定了旧行为,需要按新规则调整(多条 status 的场景保持不变)。改动 ②:让 plan mode 一定能退出
现状(这就是「关不掉」的根因)
/plan在 planning 态不是 toggle,只 notify 一句「Plan mode is already active./plan donerequests completion;/plan offcancels.」plan_ready之后进 ready 态,选择菜单PLAN_READY_ACTIONS只有三项:Continue planning/Implement in this session/Start a fresh session,没有「关闭」。/plan」,没提/plan off。/plan off这个确切写法的人,会觉得根本退不出去。改法(最小)
extensions/plan-mode/index.tsPLAN_READY_ACTIONS增加一项off: "Turn plan mode off",在showReadyActions()里对应调用clearPlan(ctx)。/plan在 planning 态改为弹同一个 select(Continue planning/Finalize now/Turn plan mode off),而不是只 notify。这样「再敲一次/plan」这个最自然的动作永远有出口。/plan off这个出口。不做的事(避免过度设计)
shift+tab已被app.thinking.cycle占用,另起一个组合键收益不大)。已知的次要问题(本次可不修,记录备查)
/plan off只往当前分支写inactive,用/tree切到另一个最新状态是planning的分支会自动重新进入 plan mode。属于设计使然,但对用户是惊喜。session_shutdown只清内存、不落inactive;同一分支重开会话会由持久化状态重新 arm。同上。测试(
extensions/plan-mode/index.test.ts)/plan选「off」后,planning === false且后续写工具不再被拦。改动 ③:bash 门禁从「语法黑名单」改成「效果白名单」
现状(
extensions/plan-mode/bash-policy.ts)SHELL_METACHARACTERS = /[;&|<>$\!*?{}()[]\n\r#]/、QUOTES = /["']/、TILDE_EXPANSION` 三道正则一票否决。git/gh,其余一律拒绝,理由是「plan mode 已经给了 read/ls/grep/fd/rg 工具」。rg -l "foo" --glob '*.ts'被拒;而宿主会话若没注册rg/fd工具,就完全没有搜索手段。改法(最小,分两步,第一步就能解决 90% 的痛)
第一步:扩程序白名单 + 允许「安全的引号」
rg:允许-n -i -l -c -w -F -e -g/--glob -t --files --hidden --no-ignore -A -B -C -m/--max-count -o --sort;必须排除--pre、--pre-glob、--hostname-bin、-z/--search-zip(会执行外部程序)。fd:允许-e -t -d/--max-depth -H -I -g -F -p -a -l;必须排除-x/--exec、-X/--exec-batch。ls:-l -a -h -t -r -R -d -1 -S(ls 没有写/执行能力的 flag)。wc:-l -w -c;head/tail:-n -c,排除tail -f/--follow(会挂住)。$、反引号、\,就放行;未配对引号仍拒绝。这样rg -l "foo bar" --glob '*.ts'能跑,而"$(...)"依然被拒。--glob '*.ts'交给 rg 自己解析,shell 不展开);未加引号的 glob 仍拒绝,报错里直接告诉怎么改:把 glob 加引号:--glob '*.ts'。plan mode only runs unquoted commands while planningplan mode 允许只读命令;此次被拦是因为未加引号的 glob 会被 shell 展开,请写成 --glob '*.ts'第二步(可选,视需要再做)
&&/;:按段拆开,每段都在白名单内才整体放行;重定向> >> <、$()、反引号、后台&仍然拒绝。rg -l foo | head这类高频用法能省很多来回。shell-quote的 parse),成本比第一步高。如果第一步之后不觉得别扭,就别做。测试(
extensions/plan-mode/bash-policy.test.ts)rg -n "foo bar" src、rg -l --glob '*.ts' pattern、fd -e ts、ls -la、现有全部 git/gh 用例。rg --pre cat x、fd -x rm、tail -f log、rg foo > out、rg $(whoami)、未配对引号、裸*.ts。建议实施顺序
三项都改完后跑:
bun run check+bun run test(当前基线 713 + 29 全绿;bun run check有 13 条 file-search 的既存 lint warning,不是本次引入的)。