Skip to content

Plan Mode 三项改进:指示器内联、必定可退出、bash 门禁改按副作用判定 #28

Description

@tt-a1i

openpi plan mode 待改动清单(最小实现版)

仓库:tt-a1i/openpi
原则:能不加的抽象不加。三项互相独立,可分开提交、分开验证。
背景与设计取舍见 01-plan-mode-设计调研.md


改动 ①:footer 指示器改短 + 空间够时内联

现状

  • extensions/plan-mode/index.tsctx.ui.setStatus("plan-mode", "plan mode · read-only")(ready 态是 "plan mode · ready")。
  • extensions/ui-customization/footer.tsrenderFooter()statuses 无条件 lines.push(...)永远另起一行
  • 效果:footer 第一行右边空着一大片,plan mode 却单独占了第二行。

改法(最小)

  1. extensions/plan-mode/index.ts:文案缩短

    • planning → "plan mode"
    • ready → "plan ready"
      (去掉 · read-only / · ready 后缀;「只读」这个信息在被拦时的报错里已经说清楚了,不需要常驻占位。)
  2. extensions/ui-customization/footer.tsrenderFooter():状态行能塞进主行就塞

    • 规则尽量简单:当只有一条 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 这个确切写法的人,会觉得根本退不出去。

改法(最小)

  1. extensions/plan-mode/index.ts

    • PLAN_READY_ACTIONS 增加一项 off: "Turn plan mode off",在 showReadyActions() 里对应调用 clearPlan(ctx)
    • /plan 在 planning 态改为弹同一个 selectContinue planning / Finalize now / Turn plan mode off),而不是只 notify。这样「再敲一次 /plan」这个最自然的动作永远有出口。
    • ready 态的 block 文案补上 /plan off 这个出口。
  2. 不做的事(避免过度设计)

    • 不加键盘快捷键(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 -chead/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" srcrg -l --glob '*.ts' patternfd -e tsls -la、现有全部 git/gh 用例。
  • 拒绝并检查报错文案包含改法提示:rg --pre cat xfd -x rmtail -f logrg foo > outrg $(whoami)、未配对引号、裸 *.ts

建议实施顺序

  1. 先做——最小、纯收益、解决「关不掉」这个真 bug。
  2. 再做——纯 UI,风险低。
  3. ③ 第一步 最后做——改动面最大,需要补一批测试。③ 第二步先不做。

三项都改完后跑:bun run check + bun run test(当前基线 713 + 29 全绿;bun run check 有 13 条 file-search 的既存 lint warning,不是本次引入的)。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
       blocks
      (function() {
      function addCopyButtons() {
      document.querySelectorAll('pre code').forEach(function(codeBlock) {
      if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
      codeBlock.parentElement.setAttribute('data-copy-added', 'true');
      var btn = document.createElement('button');
      btn.textContent = 'Copy';
      btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
      btn.onmouseover = function() { this.style.opacity = '1'; };
      btn.onmouseout = function() { this.style.opacity = '0.7'; };
      btn.onclick = function() {
      navigator.clipboard.writeText(codeBlock.textContent).then(function() {
      btn.textContent = 'Copied!';
      setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
      });
      };
      codeBlock.parentElement.style.position = 'relative';
      codeBlock.parentElement.appendChild(btn);
      });
      }
      addCopyButtons();
      // Re-run on dynamic content
      var observer = new MutationObserver(addCopyButtons);
      observer.observe(document.body, { childList: true, subtree: true });
      })();
      }
      } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
      })();
      (function(){
      try {
      var __m = "github.com";
      var __re = new RegExp('^' + "github\\.com" + '
      Plan Mode 三项改进:指示器内联、必定可退出、bash 门禁改按副作用判定 · Issue #28 · openpi-dev/openpi · GitHub
      Skip to content

      Plan Mode 三项改进:指示器内联、必定可退出、bash 门禁改按副作用判定 #28

      Description

      @tt-a1i

      openpi plan mode 待改动清单(最小实现版)

      仓库:tt-a1i/openpi
      原则:能不加的抽象不加。三项互相独立,可分开提交、分开验证。
      背景与设计取舍见 01-plan-mode-设计调研.md


      改动 ①:footer 指示器改短 + 空间够时内联

      现状

      • extensions/plan-mode/index.tsctx.ui.setStatus("plan-mode", "plan mode · read-only")(ready 态是 "plan mode · ready")。
      • extensions/ui-customization/footer.tsrenderFooter()statuses 无条件 lines.push(...)永远另起一行
      • 效果:footer 第一行右边空着一大片,plan mode 却单独占了第二行。

      改法(最小)

      1. extensions/plan-mode/index.ts:文案缩短

        • planning → "plan mode"
        • ready → "plan ready"
          (去掉 · read-only / · ready 后缀;「只读」这个信息在被拦时的报错里已经说清楚了,不需要常驻占位。)
      2. extensions/ui-customization/footer.tsrenderFooter():状态行能塞进主行就塞

        • 规则尽量简单:当只有一条 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 这个确切写法的人,会觉得根本退不出去。

      改法(最小)

      1. extensions/plan-mode/index.ts

        • PLAN_READY_ACTIONS 增加一项 off: "Turn plan mode off",在 showReadyActions() 里对应调用 clearPlan(ctx)
        • /plan 在 planning 态改为弹同一个 selectContinue planning / Finalize now / Turn plan mode off),而不是只 notify。这样「再敲一次 /plan」这个最自然的动作永远有出口。
        • ready 态的 block 文案补上 /plan off 这个出口。
      2. 不做的事(避免过度设计)

        • 不加键盘快捷键(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 -chead/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" srcrg -l --glob '*.ts' patternfd -e tsls -la、现有全部 git/gh 用例。
      • 拒绝并检查报错文案包含改法提示:rg --pre cat xfd -x rmtail -f logrg foo > outrg $(whoami)、未配对引号、裸 *.ts

      建议实施顺序

      1. 先做——最小、纯收益、解决「关不掉」这个真 bug。
      2. 再做——纯 UI,风险低。
      3. ③ 第一步 最后做——改动面最大,需要补一批测试。③ 第二步先不做。

      三项都改完后跑:bun run check + bun run test(当前基线 713 + 29 全绿;bun run check 有 13 条 file-search 的既存 lint warning,不是本次引入的)。

      Metadata

      Metadata

      Assignees

      No one assigned

        Labels

        No labels
        No labels

        Type

        No type

        Projects

        No projects

          Milestone

          No milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Plan Mode 三项改进:指示器内联、必定可退出、bash 门禁改按副作用判定 · Issue #28 · openpi-dev/openpi · GitHub
          Skip to content

          Plan Mode 三项改进:指示器内联、必定可退出、bash 门禁改按副作用判定 #28

          Description

          @tt-a1i

          openpi plan mode 待改动清单(最小实现版)

          仓库:tt-a1i/openpi
          原则:能不加的抽象不加。三项互相独立,可分开提交、分开验证。
          背景与设计取舍见 01-plan-mode-设计调研.md


          改动 ①:footer 指示器改短 + 空间够时内联

          现状

          • extensions/plan-mode/index.tsctx.ui.setStatus("plan-mode", "plan mode · read-only")(ready 态是 "plan mode · ready")。
          • extensions/ui-customization/footer.tsrenderFooter()statuses 无条件 lines.push(...)永远另起一行
          • 效果:footer 第一行右边空着一大片,plan mode 却单独占了第二行。

          改法(最小)

          1. extensions/plan-mode/index.ts:文案缩短

            • planning → "plan mode"
            • ready → "plan ready"
              (去掉 · read-only / · ready 后缀;「只读」这个信息在被拦时的报错里已经说清楚了,不需要常驻占位。)
          2. extensions/ui-customization/footer.tsrenderFooter():状态行能塞进主行就塞

            • 规则尽量简单:当只有一条 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 这个确切写法的人,会觉得根本退不出去。

          改法(最小)

          1. extensions/plan-mode/index.ts

            • PLAN_READY_ACTIONS 增加一项 off: "Turn plan mode off",在 showReadyActions() 里对应调用 clearPlan(ctx)
            • /plan 在 planning 态改为弹同一个 selectContinue planning / Finalize now / Turn plan mode off),而不是只 notify。这样「再敲一次 /plan」这个最自然的动作永远有出口。
            • ready 态的 block 文案补上 /plan off 这个出口。
          2. 不做的事(避免过度设计)

            • 不加键盘快捷键(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 -chead/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" srcrg -l --glob '*.ts' patternfd -e tsls -la、现有全部 git/gh 用例。
          • 拒绝并检查报错文案包含改法提示:rg --pre cat xfd -x rmtail -f logrg foo > outrg $(whoami)、未配对引号、裸 *.ts

          建议实施顺序

          1. 先做——最小、纯收益、解决「关不掉」这个真 bug。
          2. 再做——纯 UI,风险低。
          3. ③ 第一步 最后做——改动面最大,需要补一批测试。③ 第二步先不做。

          三项都改完后跑:bun run check + bun run test(当前基线 713 + 29 全绿;bun run check 有 13 条 file-search 的既存 lint warning,不是本次引入的)。

          Metadata

          Metadata

          Assignees

          No one assigned

            Labels

            No labels
            No labels

            Type

            No type

            Projects

            No projects

              Milestone

              No milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Plan Mode 三项改进:指示器内联、必定可退出、bash 门禁改按副作用判定 · Issue #28 · openpi-dev/openpi · GitHub
              Skip to content

              Plan Mode 三项改进:指示器内联、必定可退出、bash 门禁改按副作用判定 #28

              Description

              @tt-a1i

              openpi plan mode 待改动清单(最小实现版)

              仓库:tt-a1i/openpi
              原则:能不加的抽象不加。三项互相独立,可分开提交、分开验证。
              背景与设计取舍见 01-plan-mode-设计调研.md


              改动 ①:footer 指示器改短 + 空间够时内联

              现状

              • extensions/plan-mode/index.tsctx.ui.setStatus("plan-mode", "plan mode · read-only")(ready 态是 "plan mode · ready")。
              • extensions/ui-customization/footer.tsrenderFooter()statuses 无条件 lines.push(...)永远另起一行
              • 效果:footer 第一行右边空着一大片,plan mode 却单独占了第二行。

              改法(最小)

              1. extensions/plan-mode/index.ts:文案缩短

                • planning → "plan mode"
                • ready → "plan ready"
                  (去掉 · read-only / · ready 后缀;「只读」这个信息在被拦时的报错里已经说清楚了,不需要常驻占位。)
              2. extensions/ui-customization/footer.tsrenderFooter():状态行能塞进主行就塞

                • 规则尽量简单:当只有一条 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 这个确切写法的人,会觉得根本退不出去。

              改法(最小)

              1. extensions/plan-mode/index.ts

                • PLAN_READY_ACTIONS 增加一项 off: "Turn plan mode off",在 showReadyActions() 里对应调用 clearPlan(ctx)
                • /plan 在 planning 态改为弹同一个 selectContinue planning / Finalize now / Turn plan mode off),而不是只 notify。这样「再敲一次 /plan」这个最自然的动作永远有出口。
                • ready 态的 block 文案补上 /plan off 这个出口。
              2. 不做的事(避免过度设计)

                • 不加键盘快捷键(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 -chead/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" srcrg -l --glob '*.ts' patternfd -e tsls -la、现有全部 git/gh 用例。
              • 拒绝并检查报错文案包含改法提示:rg --pre cat xfd -x rmtail -f logrg foo > outrg $(whoami)、未配对引号、裸 *.ts

              建议实施顺序

              1. 先做——最小、纯收益、解决「关不掉」这个真 bug。
              2. 再做——纯 UI,风险低。
              3. ③ 第一步 最后做——改动面最大,需要补一批测试。③ 第二步先不做。

              三项都改完后跑:bun run check + bun run test(当前基线 713 + 29 全绿;bun run check 有 13 条 file-search 的既存 lint warning,不是本次引入的)。

              Metadata

              Metadata

              Assignees

              No one assigned

                Labels

                No labels
                No labels

                Type

                No type

                Projects

                No projects

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions

                  , 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Plan Mode 三项改进:指示器内联、必定可退出、bash 门禁改按副作用判定 · Issue #28 · openpi-dev/openpi · GitHub
                  Skip to content

                  Plan Mode 三项改进:指示器内联、必定可退出、bash 门禁改按副作用判定 #28

                  Description

                  @tt-a1i

                  openpi plan mode 待改动清单(最小实现版)

                  仓库:tt-a1i/openpi
                  原则:能不加的抽象不加。三项互相独立,可分开提交、分开验证。
                  背景与设计取舍见 01-plan-mode-设计调研.md


                  改动 ①:footer 指示器改短 + 空间够时内联

                  现状

                  • extensions/plan-mode/index.tsctx.ui.setStatus("plan-mode", "plan mode · read-only")(ready 态是 "plan mode · ready")。
                  • extensions/ui-customization/footer.tsrenderFooter()statuses 无条件 lines.push(...)永远另起一行
                  • 效果:footer 第一行右边空着一大片,plan mode 却单独占了第二行。

                  改法(最小)

                  1. extensions/plan-mode/index.ts:文案缩短

                    • planning → "plan mode"
                    • ready → "plan ready"
                      (去掉 · read-only / · ready 后缀;「只读」这个信息在被拦时的报错里已经说清楚了,不需要常驻占位。)
                  2. extensions/ui-customization/footer.tsrenderFooter():状态行能塞进主行就塞

                    • 规则尽量简单:当只有一条 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 这个确切写法的人,会觉得根本退不出去。

                  改法(最小)

                  1. extensions/plan-mode/index.ts

                    • PLAN_READY_ACTIONS 增加一项 off: "Turn plan mode off",在 showReadyActions() 里对应调用 clearPlan(ctx)
                    • /plan 在 planning 态改为弹同一个 selectContinue planning / Finalize now / Turn plan mode off),而不是只 notify。这样「再敲一次 /plan」这个最自然的动作永远有出口。
                    • ready 态的 block 文案补上 /plan off 这个出口。
                  2. 不做的事(避免过度设计)

                    • 不加键盘快捷键(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 -chead/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" srcrg -l --glob '*.ts' patternfd -e tsls -la、现有全部 git/gh 用例。
                  • 拒绝并检查报错文案包含改法提示:rg --pre cat xfd -x rmtail -f logrg foo > outrg $(whoami)、未配对引号、裸 *.ts

                  建议实施顺序

                  1. 先做——最小、纯收益、解决「关不掉」这个真 bug。
                  2. 再做——纯 UI,风险低。
                  3. ③ 第一步 最后做——改动面最大,需要补一批测试。③ 第二步先不做。

                  三项都改完后跑:bun run check + bun run test(当前基线 713 + 29 全绿;bun run check 有 13 条 file-search 的既存 lint warning,不是本次引入的)。

                  Metadata

                  Metadata

                  Assignees

                  No one assigned

                    Labels

                    No labels
                    No labels

                    Type

                    No type

                    Projects

                    No projects

                      Milestone

                      No milestone

                      Relationships

                      None yet

                      Development

                      No branches or pull requests

                      Issue actions

                      , 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Plan Mode 三项改进:指示器内联、必定可退出、bash 门禁改按副作用判定 · Issue #28 · openpi-dev/openpi · GitHub
                      Skip to content

                      Plan Mode 三项改进:指示器内联、必定可退出、bash 门禁改按副作用判定 #28

                      Description

                      @tt-a1i

                      openpi plan mode 待改动清单(最小实现版)

                      仓库:tt-a1i/openpi
                      原则:能不加的抽象不加。三项互相独立,可分开提交、分开验证。
                      背景与设计取舍见 01-plan-mode-设计调研.md


                      改动 ①:footer 指示器改短 + 空间够时内联

                      现状

                      • extensions/plan-mode/index.tsctx.ui.setStatus("plan-mode", "plan mode · read-only")(ready 态是 "plan mode · ready")。
                      • extensions/ui-customization/footer.tsrenderFooter()statuses 无条件 lines.push(...)永远另起一行
                      • 效果:footer 第一行右边空着一大片,plan mode 却单独占了第二行。

                      改法(最小)

                      1. extensions/plan-mode/index.ts:文案缩短

                        • planning → "plan mode"
                        • ready → "plan ready"
                          (去掉 · read-only / · ready 后缀;「只读」这个信息在被拦时的报错里已经说清楚了,不需要常驻占位。)
                      2. extensions/ui-customization/footer.tsrenderFooter():状态行能塞进主行就塞

                        • 规则尽量简单:当只有一条 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 这个确切写法的人,会觉得根本退不出去。

                      改法(最小)

                      1. extensions/plan-mode/index.ts

                        • PLAN_READY_ACTIONS 增加一项 off: "Turn plan mode off",在 showReadyActions() 里对应调用 clearPlan(ctx)
                        • /plan 在 planning 态改为弹同一个 selectContinue planning / Finalize now / Turn plan mode off),而不是只 notify。这样「再敲一次 /plan」这个最自然的动作永远有出口。
                        • ready 态的 block 文案补上 /plan off 这个出口。
                      2. 不做的事(避免过度设计)

                        • 不加键盘快捷键(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 -chead/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" srcrg -l --glob '*.ts' patternfd -e tsls -la、现有全部 git/gh 用例。
                      • 拒绝并检查报错文案包含改法提示:rg --pre cat xfd -x rmtail -f logrg foo > outrg $(whoami)、未配对引号、裸 *.ts

                      建议实施顺序

                      1. 先做——最小、纯收益、解决「关不掉」这个真 bug。
                      2. 再做——纯 UI,风险低。
                      3. ③ 第一步 最后做——改动面最大,需要补一批测试。③ 第二步先不做。

                      三项都改完后跑:bun run check + bun run test(当前基线 713 + 29 全绿;bun run check 有 13 条 file-search 的既存 lint warning,不是本次引入的)。

                      Metadata

                      Metadata

                      Assignees

                      No one assigned

                        Labels

                        No labels
                        No labels

                        Type

                        No type

                        Projects

                        No projects

                          Milestone

                          No milestone

                          Relationships

                          None yet

                          Development

                          No branches or pull requests

                          Issue actions

                          , 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Plan Mode 三项改进:指示器内联、必定可退出、bash 门禁改按副作用判定 · Issue #28 · openpi-dev/openpi · GitHub
                          Skip to content

                          Plan Mode 三项改进:指示器内联、必定可退出、bash 门禁改按副作用判定 #28

                          Description

                          @tt-a1i

                          openpi plan mode 待改动清单(最小实现版)

                          仓库:tt-a1i/openpi
                          原则:能不加的抽象不加。三项互相独立,可分开提交、分开验证。
                          背景与设计取舍见 01-plan-mode-设计调研.md


                          改动 ①:footer 指示器改短 + 空间够时内联

                          现状

                          • extensions/plan-mode/index.tsctx.ui.setStatus("plan-mode", "plan mode · read-only")(ready 态是 "plan mode · ready")。
                          • extensions/ui-customization/footer.tsrenderFooter()statuses 无条件 lines.push(...)永远另起一行
                          • 效果:footer 第一行右边空着一大片,plan mode 却单独占了第二行。

                          改法(最小)

                          1. extensions/plan-mode/index.ts:文案缩短

                            • planning → "plan mode"
                            • ready → "plan ready"
                              (去掉 · read-only / · ready 后缀;「只读」这个信息在被拦时的报错里已经说清楚了,不需要常驻占位。)
                          2. extensions/ui-customization/footer.tsrenderFooter():状态行能塞进主行就塞

                            • 规则尽量简单:当只有一条 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 这个确切写法的人,会觉得根本退不出去。

                          改法(最小)

                          1. extensions/plan-mode/index.ts

                            • PLAN_READY_ACTIONS 增加一项 off: "Turn plan mode off",在 showReadyActions() 里对应调用 clearPlan(ctx)
                            • /plan 在 planning 态改为弹同一个 selectContinue planning / Finalize now / Turn plan mode off),而不是只 notify。这样「再敲一次 /plan」这个最自然的动作永远有出口。
                            • ready 态的 block 文案补上 /plan off 这个出口。
                          2. 不做的事(避免过度设计)

                            • 不加键盘快捷键(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 -chead/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" srcrg -l --glob '*.ts' patternfd -e tsls -la、现有全部 git/gh 用例。
                          • 拒绝并检查报错文案包含改法提示:rg --pre cat xfd -x rmtail -f logrg foo > outrg $(whoami)、未配对引号、裸 *.ts

                          建议实施顺序

                          1. 先做——最小、纯收益、解决「关不掉」这个真 bug。
                          2. 再做——纯 UI,风险低。
                          3. ③ 第一步 最后做——改动面最大,需要补一批测试。③ 第二步先不做。

                          三项都改完后跑:bun run check + bun run test(当前基线 713 + 29 全绿;bun run check 有 13 条 file-search 的既存 lint warning,不是本次引入的)。

                          Metadata

                          Metadata

                          Assignees

                          No one assigned

                            Labels

                            No labels
                            No labels

                            Type

                            No type

                            Projects

                            No projects

                              Milestone

                              No milestone

                              Relationships

                              None yet

                              Development

                              No branches or pull requests

                              Issue actions

                              , 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Plan Mode 三项改进:指示器内联、必定可退出、bash 门禁改按副作用判定 · Issue #28 · openpi-dev/openpi · GitHub
                              Skip to content

                              Plan Mode 三项改进:指示器内联、必定可退出、bash 门禁改按副作用判定 #28

                              Description

                              @tt-a1i

                              openpi plan mode 待改动清单(最小实现版)

                              仓库:tt-a1i/openpi
                              原则:能不加的抽象不加。三项互相独立,可分开提交、分开验证。
                              背景与设计取舍见 01-plan-mode-设计调研.md


                              改动 ①:footer 指示器改短 + 空间够时内联

                              现状

                              • extensions/plan-mode/index.tsctx.ui.setStatus("plan-mode", "plan mode · read-only")(ready 态是 "plan mode · ready")。
                              • extensions/ui-customization/footer.tsrenderFooter()statuses 无条件 lines.push(...)永远另起一行
                              • 效果:footer 第一行右边空着一大片,plan mode 却单独占了第二行。

                              改法(最小)

                              1. extensions/plan-mode/index.ts:文案缩短

                                • planning → "plan mode"
                                • ready → "plan ready"
                                  (去掉 · read-only / · ready 后缀;「只读」这个信息在被拦时的报错里已经说清楚了,不需要常驻占位。)
                              2. extensions/ui-customization/footer.tsrenderFooter():状态行能塞进主行就塞

                                • 规则尽量简单:当只有一条 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 这个确切写法的人,会觉得根本退不出去。

                              改法(最小)

                              1. extensions/plan-mode/index.ts

                                • PLAN_READY_ACTIONS 增加一项 off: "Turn plan mode off",在 showReadyActions() 里对应调用 clearPlan(ctx)
                                • /plan 在 planning 态改为弹同一个 selectContinue planning / Finalize now / Turn plan mode off),而不是只 notify。这样「再敲一次 /plan」这个最自然的动作永远有出口。
                                • ready 态的 block 文案补上 /plan off 这个出口。
                              2. 不做的事(避免过度设计)

                                • 不加键盘快捷键(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 -chead/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" srcrg -l --glob '*.ts' patternfd -e tsls -la、现有全部 git/gh 用例。
                              • 拒绝并检查报错文案包含改法提示:rg --pre cat xfd -x rmtail -f logrg foo > outrg $(whoami)、未配对引号、裸 *.ts

                              建议实施顺序

                              1. 先做——最小、纯收益、解决「关不掉」这个真 bug。
                              2. 再做——纯 UI,风险低。
                              3. ③ 第一步 最后做——改动面最大,需要补一批测试。③ 第二步先不做。

                              三项都改完后跑:bun run check + bun run test(当前基线 713 + 29 全绿;bun run check 有 13 条 file-search 的既存 lint warning,不是本次引入的)。

                              Metadata

                              Metadata

                              Assignees

                              No one assigned

                                Labels

                                No labels
                                No labels

                                Type

                                No type

                                Projects

                                No projects

                                  Milestone

                                  No milestone

                                  Relationships

                                  None yet

                                  Development

                                  No branches or pull requests

                                  Issue actions