fix: harden builtin TUI theme readability - #24
Open
xy200303 wants to merge 4 commits into
Open
Conversation
Collaborator
|
Thanks for the contribution! To get this ready to merge:
Full conventions: |
…bility # Conflicts: # docs/skills/pr-review-decision.md # package.json
Contributor
Author
|
Thanks! I've addressed the feedback on this PR:
This should be ready for review/merge when you have a chance. |
li-xiu-qi
added a commit
to li-xiu-qi/Step-Realtime-CLI
that referenced
this pull request
Aug 7, 2026
- 待办 stepfun-ai#17:delivered 事件此前在注入/入队时即时落盘,消息本体回合末 persist 才落盘,中间崩溃让对账误判已送达、通知丢失。改为 persist 时 扫 history 统一补写(pendingDeliveredEvents 纯函数,幂等键去重), 两者同生共死;Esc 丢弃队列的通知在丢弃处显式补写,保持「丢弃即送达」。 App.tsx 同文件还含 /think /provider 写回配置的调用点(上一笔的实现)。 - 待办 stepfun-ai#24 + #0 遗留:ModelPicker 块分页改居中锚定滑动窗口、截断/填充 改 displayWidth 口径(新增 padEndByWidth);ModelPicker/ThinkPicker 加 visibleRows 入参,App 按 resolveVisibleRows 同口径接线(多留 1 行 chrome 余量),小终端弹层不再越线。
li-xiu-qi
added a commit
to li-xiu-qi/Step-Realtime-CLI
that referenced
this pull request
Aug 7, 2026
- 待办 stepfun-ai#17:delivered 事件此前在注入/入队时即时落盘,消息本体回合末 persist 才落盘,中间崩溃让对账误判已送达、通知丢失。改为 persist 时 扫 history 统一补写(pendingDeliveredEvents 纯函数,幂等键去重), 两者同生共死;Esc 丢弃队列的通知在丢弃处显式补写,保持「丢弃即送达」。 App.tsx 同文件还含 /think /provider 写回配置的调用点(上一笔的实现)。 - 待办 stepfun-ai#24 + #0 遗留:ModelPicker 块分页改居中锚定滑动窗口、截断/填充 改 displayWidth 口径(新增 padEndByWidth);ModelPicker/ThinkPicker 加 visibleRows 入参,App 按 resolveVisibleRows 同口径接线(多留 1 行 chrome 余量),小终端弹层不再越线。
li-xiu-qi
added a commit
to li-xiu-qi/Step-Realtime-CLI
that referenced
this pull request
Aug 20, 2026
- 待办 stepfun-ai#17:delivered 事件此前在注入/入队时即时落盘,消息本体回合末 persist 才落盘,中间崩溃让对账误判已送达、通知丢失。改为 persist 时 扫 history 统一补写(pendingDeliveredEvents 纯函数,幂等键去重), 两者同生共死;Esc 丢弃队列的通知在丢弃处显式补写,保持「丢弃即送达」。 App.tsx 同文件还含 /think /provider 写回配置的调用点(上一笔的实现)。 - 待办 stepfun-ai#24 + #0 遗留:ModelPicker 块分页改居中锚定滑动窗口、截断/填充 改 displayWidth 口径(新增 padEndByWidth);ModelPicker/ThinkPicker 加 visibleRows 入参,App 按 resolveVisibleRows 同口径接线(多留 1 行 chrome 余量),小终端弹层不再越线。
li-xiu-qi
added a commit
to li-xiu-qi/Step-Realtime-CLI
that referenced
this pull request
Aug 21, 2026
- 待办 stepfun-ai#17:delivered 事件此前在注入/入队时即时落盘,消息本体回合末 persist 才落盘,中间崩溃让对账误判已送达、通知丢失。改为 persist 时 扫 history 统一补写(pendingDeliveredEvents 纯函数,幂等键去重), 两者同生共死;Esc 丢弃队列的通知在丢弃处显式补写,保持「丢弃即送达」。 App.tsx 同文件还含 /think /provider 写回配置的调用点(上一笔的实现)。 - 待办 stepfun-ai#24 + #0 遗留:ModelPicker 块分页改居中锚定滑动窗口、截断/填充 改 displayWidth 口径(新增 padEndByWidth);ModelPicker/ThinkPicker 加 visibleRows 入参,App 按 resolveVisibleRows 同口径接线(多留 1 行 chrome 余量),小终端弹层不再越线。
ZouR-Ma
added a commit
that referenced
this pull request
Aug 21, 2026
* feat: step doctor config 显示 media_keep_recent 当前生效值 **改动**:doctor 输出加一行 info 提示 media_keep_recent 的当前值: - 已配置:`info: media_keep_recent = 5(媒体降级时保留最近 5 张图,0 = 全部换占位)` - 未配置:`info: media_keep_recent 未配置,缺省 3(媒体降级时保留最近 3 张图)` **价值**:用户可能不知道这个键的存在,doctor 输出明示当前生效值,避免「配了但不知道有没有生效」。 **测试**:configDoctor.test.ts 9 例全绿,防漂移测试通过。 * fix: tool_result 裸对象 content 防御(cc-switch #6170 边缘 case) **问题**:tool_result 的 content 字段如果是裸对象(非 string、非数组), for...of 会抛 TypeError: c is not iterable,整个请求崩溃。 **真实场景**:MCP 工具可能输出裸 image/text 对象作为 tool_result content (多厂商代理层实测,cc-switch #6170)。严格网关(部分国产端点)会因此 400。 **修复**:toolResultContent 加裸对象分支,按 type 分发: - 裸 image 对象 → image_url(data URI) - 裸 text 对象 → string - 无法识别的裸对象 → 空字符串(丢弃,不崩溃) **测试**:+3 例(裸 image / 裸 text / 无法识别对象),9 例全绿。 全量 2348 测试通过。 * feat: 媒体降级推广到全通道 + 方言识别 + 默认保留 10 张 - degrader.isReprojectableError:400 不再一刀切,只在文案命中媒体方言 (stepfun/Anthropic/Gemini/vLLM/OpenAI 兼容,全部来自公开 issue 与实测) 且非上下文溢出时可重投影;413 语义唯一保持直通。裸 400 参数错误不再被 降级掩盖。 - 降级占位文本保留「原图因 API 限制被移除」语义(IMAGE_DEGRADED_TEXT), 与主动降级的 image omitted 区分——防模型失忆(公开 issue 实录教训)。 - 新增 withMediaDegradation wrapper:把 StepfunAdapter.send 的错误驱动 重投影抽成通道无关装饰器,包装任意 ChatProvider 的 stream.finalMessage, 每档每请求一次熔断(防 400 无限循环)。 - factory 全通道接入:openai/openai_responses/anthropic 均包 wrapper; stepfun 沿用 adapter(其 send 已有等价逻辑)。 - media_keep_recent 缺省 3→10(step-3.7 实测 60 张上限的 1/6 安全值); [models.*] 下可按别名覆盖(resolveModelEntry 合并)。 - 单测:degrader 方言识别 6 例(含裸 400 反向例)、mediaDegradation wrapper 7 例;adapter/degrader 旧断言同步新文案。全量测试绿。 - 端到端:step37-plan 与 k3 读图正常,wrapper 不破坏正常请求。 实测(2026-08-06 直连 API):step-3.7-flash 图片上限 60 张/请求、 单张最长边 8000px、60×1024px 组合载荷 27MB 通过。 * feat: read_media 加 probe 模式 + region 超限自动建议,消除分块猜测试错 读大图/长图时 agent 不知道图片实际尺寸,只能猜 region 参数,猜错就报 「region 超出图片范围」,再猜再错——30000px 长截图的真实痛点(一次 分块阅读要试 3 次才能算准末块边界)。 - probe:true:只回元数据(格式/尺寸/字节数)+ 建议分块方案,不交付图片。 建议 region 按交付长边上限(1568px)切块,长图按高度方向、宽图按宽度 方向,末块用 min(剩余, 块高) 自动收窄——调用方照抄即可不超界。 短边图直接说「无需分块」。 - region 超限错误:自动给出 clamp 后的可重试 region(起点收进图内、 跨度按剩余收窄),并提示可用 probe:true 拿完整方案。 - 工具 description 补 probe 用法提示。 单测 5 例(probe 小图/长图/宽图、region 建议两种超界形态),其中长图 用例逐个回读 20 个建议 region 验证照抄不超界。全量 2362 测试绿。 端到端:508×30173 真实长图 probe 返回 20 块方案,末块 381px 正确收窄。 * docs: 媒体降级全通道与 read_media probe 补进配置/工具文档(中英)+ CHANGELOG - configuration.md(zh/en):顶层键表与别名表补 media_keep_recent; 新增「媒体降级 / Media degradation」节(降级档位、方言触发识别、 默认值 10 的实测依据、全通道生效、prompt cache 边界)。 - tools.md(zh/en):read_media 参数表补 probe;行为边界补 「超限自动建议 clamp region」与「读大/长图先 probe」。 - CHANGELOG Unreleased:媒体降级全通道与 probe 两条进 Added。 * fix: streamBuffer delta 按到达顺序消费 + logo 左肩斜切 - streamBuffer flush 从固定 text→thinking 顺序改为按到达顺序队列消费, 修复 thinking 泄露到正文下方的问题 - WelcomeBox logo 左肩加斜切角(▐ 右半格),削出左肩斜面 * feat: 斜杠命令参数补全(/goal /permission /provider /lang /plugin) - 各命令注册 getArgumentCompletions,返回子命令候选 + 描述 - /provider 补全自定义渠道 id + 内置预设名 - /plugin 补全磁盘发现的插件 id - 补全上下文注入 providers/providerPresets/pluginIds * docs: CHANGELOG 0.1.1 版本挂牌(2026-08-06) - [Unreleased] 转 [0.1.1] - 2026-08-06 - 新增空 [Unreleased] 段 - 本次 PR 包含:命令参数补全、贴图双轨制、媒体降级全通道、read_media probe、模型切换写回、streamBuffer 修复等 * docs: 文档与 licenses 对齐更新 - README/README_CN:补媒体降级全通道与贴图路径回读的一句话描述 - docs/zh+en/interactive.md:补 Shift+Enter 仅本会话切换模型说明 - ModelPicker onSelect 回调加 sessionOnly 参数,Enter 传 key.shift 控制是否写回 config - modelPicker 测试断言同步双参数签名 - licenses/NOTICE.md:删除一段专有软件说明,补两条开源协议登记 - licenses/ 新增两个开源协议文件 * fix: CI 环境两处测试失败修复 - firstFrameSmoke:CI 无 API key 时进程直接退出,跳过而非失败(环境限制非代码 bug) - bash.outputcap:close 事件可能在 stderr data 之前触发,漏掉尾部错误标记; 改为等 stdout/stderr end 与 close 三者齐了再 snapshot * fix: bash.outputcap 测试改用 console.error 确保 stderr flush CI 管道环境下 process.stderr.write 可能不触发 data 事件, 导致 BOOM-MARKER-42 未到达收集器。console.error 会强制 flush。 * fix: bash.outputcap 测试用 process.exit(3) 替代 exitCode=3 process.exitCode=3 时进程自然退出,CI 管道环境下 stderr 的 data 事件 可能在 close 之后才触发或根本不触发。process.exit(3) 强制 flush stdio。 * fix: 模型别名展开后保留原始别名指针(modelAlias) 多别名指向同一真实 id 时(step37/step37-plan 同为 step-3.7-flash), App 侧用真实 id 反查别名会命中第一个(step37),导致: - 状态栏显示 Step 3.7 Flash 而非 Step 3.7 Flash(Plan) - currentModelAliasRef 为 null,/reload 时 provider 重建策略错误 - defaultModelPointerRef 为真实 id,/model 切换时误写真实 id 到 config 修复:StepCodeConfig 新增 modelAlias 字段,loadConfig 展开别名时保留 原始别名指针。App 初始化 currentModelAliasRef/modelLabel/ defaultModelPointerRef 时优先使用 config.modelAlias。 * docs: 修正 Release 状态说明 + 首次运行 API key 配置引导 文档修正(Release 尚未发布): - quickstart.md / installation.md / SKILL.md 标注单文件可执行与 Release tarball 为「尚未发布」,避免用户访问空 Releases 页面 - 推荐路径收敛为 npm 预构建分支 / npm 源码分支 / 源码安装三种 首次运行引导(对齐主流 CLI 的引导体验): - 交互模式启动时检测无 API key,不直接退出,渲染 FirstRunSetup 组件 - 提供「粘贴 key 自动写入配置」与「查看文档手动配置」两个选项 - 粘贴后自动写入 ~/.step-code/config.toml 顶层 api_key 并继续启动 - 非交互模式(-p / --reflect)保持原有报错退出行为 * chore: bump version to 0.1.1 * feat(first-run): 引导向导支持选择接入方式 + 可编辑输入框 - 选择 StepFun Plan / API 按量 / 自定义 base_url,写入对应渠道 section - 输入框复用 promptEdit 原语 + usePaste,支持光标移动/删除/bracketed paste - 新增 saveProviderKey 写 [providers.<name>] section 字段 - 修正 version.ts 与 package.json 同步(0.1.1),修两处版本测试 * fix(first-run): 粘贴 key/base_url 剥除换行符,防止写入非法 TOML 毁掉 config 终端粘贴的长 key 常被折行,换行符若进 TOML 字符串即成非法控制字符, 导致整个 config.toml 解析失败、无法启动。两层修复: - EditableInput 插入前 toSingleLine 剥掉所有 \r/\n(粘贴与逐字符两路径) - saveProviderKey 写入前再剥一次(兜底,防其他调用方传入脏值) * feat(first-run): 坏 TOML 配置也能进引导修复,不再直接退出 此前 config.toml 语法错误时 loadConfig 在启动早期直接 exit(1), 恰恰是 config 已坏、最需要修复的用户被挡在引导门外。 - 新增 TomlParseError 类型,cli 用 instanceof 区分坏 TOML 与其他配置错误 - 坏 TOML + 交互模式:先把坏文件改名备份(config.toml.broken-<ts>), 再进引导写入全新配置、重载生效;备份而非删除,供用户抢救其他配置 - 非交互模式(-p/--reflect)照旧报错退出,不阻塞脚本 - 改名失败(占用/权限)退守复制备份后继续 * feat(first-run): 引导新增选择默认模型步骤,绑定渠道与模型 修复真实设计缺陷:此前引导只写 [providers.X],但启动用顶层 model 别名 所属渠道,用户配了 stepfun-plan、顶层 model 却指向 stepfun 别名,启动仍缺 key。 改为配完渠道后让用户选一个模型,该模型显式绑定刚配的渠道并设为默认 (model -> models.X.provider -> providers.X 三层硬绑定,配完即可用)。 - PROVIDER_OPTIONS 每个渠道带预设模型列表(Plan: router/step37;API: step37/step35) - custom 渠道用 EditableInput 输入自定义模型 id - 新增 saveModelAlias 写 [models.<别名>] 段,复用 section 级写入风格 - 选定后 saveDefaultModel 把顶层 model 指向新别名,下次启动即用 - confirm 页显示默认模型;Esc 返回链路相应调整 * feat(logo): TUI banner 换用 space-invader 造型 + 头部左端斜切角 沿用经典 space-invader 三行造型为基底,头部最左端叠加 ▖→▗ 两级 对角块构成左低右高的斜向上角。logo 由 3 行变 4 行,右侧标题块 改 justifyContent=center 垂直居中对齐主体三行。 顺带修复:引导 API 按量渠道 baseUrl 补 /v1(原 https://api.stepfun.com 少了 /v1,会导致该渠道请求 404)。 最终稿与选型记录见产品设计文档「step-code-logo设计.md」。 * fix(cli): 引导 Esc 取消退出不再触发 unsettled top-level await 警告 main.ts 顶层 await import('./cli.js') 时,cli 模块内的 process.exit (如引导里按 Esc 取消)发生在模块执行中途,进程退出时 main 的顶层 await 仍未 settle,Node 24 打 unsettled 警告。 改为 import().catch() 形式:main 模块立即执行完、进程靠 cli 的事件 循环存活,警告消除,cli 内 process.exit 的退出码不受影响;加载失败 由 catch 打印并以码 1 退出。env.test.ts 的「先设 NODE_ENV 后加载」 断言相应放宽为匹配 import('./cli.js')(不绑定是否顶层 await)。 * fix(tui): thinking 落成瞬间不定稿,避免在途 assistant 双渲染 thinking 落成定稿条目且恰在 items 末尾时,countSettledItems 会把 settled 推到 items.length,仍在流式增长的 assistant 一并冻结进 <Static>(append-only), 下一帧 text 又新开条目进动态区——Static 旧副本 + 动态新副本 = 正文重复, 表现为 thinking 残句碎片混排在正文开头。 修复:countSettledItems 在有效末尾为 thinking 时把 settled 停在其下标, thinking 留动态区随其后正文一起定稿。间歇性来源是 StreamBuffer 50ms 合帧 时序(thinking_end 与 text 是否同窗)。 补两个针对测试(thinking 是末尾 / thinking 后仅跟透明 note)。 根因登记见产品设计文档「20260807-thinking泄漏到正文-Static定稿时序」。 * fix(tui): text 事件到来时 thinking 与正文原子落定,消除合帧时序竞争 继 4750afc(countSettledItems 止血)之后,从事件处理顺序上根治: thinking 落成与首个 text 追加改为在同一次 setItems 里原子完成, thinking 不再单独成为 items 末尾的瞬间,countSettledItems 看到的末尾 恒为 assistant。这一层提供「正文前先落定思考」的强顺序保证, 独立于 StreamBuffer 50ms 合帧时序。 4750afc 的 countSettledItems 守卫保留作防御纵深(两层冗余但安全)。 * fix(dynamic-workflow): 沙箱销毁后在途 spawn 回调短路,修复 UseAfterFree 进程崩溃 Esc 中断/超时触发 sandbox.dispose() 后,仍在运行的子 agent 完成时 其 .then 回调调 deferred.resolve() 指向已销毁的 QuickJS context, 导致 QuickJSUseAfterFree + JS_FreeRuntime gc_obj_list assert, 整个 Node 进程崩溃。 修复:sandbox 暴露 isDisposed 只读标志,__agent 的 resolve/reject 回调开头检查该标志,沙箱已销毁时直接返回不再触碰 ctx。 同 commit 修复关联问题:恢复空会话(崩溃留下的空壳)时无提示, 用户无法区分「恢复成功但历史为空」与「全新会话」。cli.tsx 新增 resumeHit 标志,空会话恢复时 stderr + TUI 双层提示。 * feat(agent): thinking 预算耗尽自动降档重试(thinking_downgrade) - runTurn 检测 stop_reason=max_tokens + 正文零输出 + 档位可降(非 low/off/未重试过)时, 自动降到 low 档重发请求 1 次;成功后恢复原档位,用户零操作。 - 降级过程透出 thinking_downgrade 事件(fromLevel/toLevel),TUI 落一条 boundary note 告知自动恢复发生;降级后仍耗尽退到原提示路径(兜底)。 - 修复 i18n 英文缺键(typecheck 阻断)。 - 补 4 个针对测试:high 降 low 成功 / low 降无可降 / off 不触发 / 重试仍耗尽只试 1 次。 全量 2368 全绿。 恒思考模型(不可降档)的 max_tokens 翻倍恢复路径见产品设计文档调研汇总, 本期不做(需模型能力注册表先行)。 * feat(agent): thinking 流死循环检测与诱导跳出(thinking_loop) step-3.7 恒思考(reasoning_effort 最低 low 档,不可关),thinking 死循环时 预算耗尽降档(thinking_downgrade)只在终点介入,且注入预填对恒思考模型无效。 本 commit 在流式中途检测循环并主动跳出: - 新增 agent/thinkingLoop.ts:纯函数检测器。短周期逐字复读(周期 1-12 字符 精确循环)+ 长重复段落级(末尾连续窗口在更早内容中重复 ≥3 次)双路径; MIN_CHARS=400 门槛防早期误判;fired 一次性防重复触发。只扫流末尾, 天然避开代码/列表在中部的合理重复(误报控制)。 - runTurn 流式循环内联检测:命中即中止当前流(已吐正文时不中止,守 「正文不进重试」铁律),构造「诱导跳出」提示追加为新 user 消息重试 1 次。 注入会污染上下文(reasoning leakage,arXiv:2510.11713),故用 「终止+新请求」而非同流续写。 - 透出 thinking_loop 事件(sample/repeats/retried),TUI 落 boundary note。 - 测试 9 例:检测器 7 例(复读/段落重复/正常文本/列表不误报/fired 一次性/ 门槛/reset)+ 端到端 2 例(注入重试成功 / 二次循环不重复注入)。 全量 2377 全绿。 设计文档与调研汇总见产品设计仓。 * feat: thinking 统一回灌 + /resume 懒加载 + AgentGroup 限高折叠 三个子 agent 并发执行的改动汇总: 1. thinking 统一回灌(openaiCommon/openaiChat/factory): Chat 通道不再无条件剥离 thinking,按 capability.reasoning 决定。 reasoning=true 时保留并序列化为 reasoning_content,false 时剥离。 修复 Step/Kimi/DeepSeek 经 Chat 通道的兼容性问题。 2. /resume 懒加载(session/store): 索引缓存方案(_index.json),list() 从 O(N IO) 降为 O(1 IO)。 save/delete/rename 时同步更新索引,崩溃/过期自动重建。 3. AgentGroup 限高折叠(tui/AgentGroup): MAX_VISIBLE_AGENTS=8,超出折叠提示「还有 N 个,/tasks 查看全部」。 排序:running 优先 + startedAt 降序。agentGroupRows 与渲染侧一致。 测试:2394/2394 全部通过(含新增 148 个测试)。 * feat(tui): busy 时 chrome 面板让位——流式正文优先 busy(流式输出)期间 AgentGroup/TodoPanel/QueuePreview 压缩: - AgentGroup 从列表面板压缩为单行摘要(⠶ N 个子 agent,/tasks 查看) - TodoPanel / QueuePreview 隐藏 - thinking preview 压缩到 ≤2 行 - idle 时恢复完整显示 实测效果:30 个子 agent busy 时 liveMaxRows 从 ~6 行提升到 ~termRows-10。 测试:2402/2402 全部通过(新增 11 条 busy 相关测试)。 * feat(agent): think-only 自动恢复——降档仍耗尽时落盘 thinking 并注入「直接回答」 降档重试(thinking_downgrade)后仍 max_tokens 零正文时,不再直接走提示路径: 把原始耗尽轮次的 thinking 落盘为独立 assistant 消息(注入提示「基于已有的 分析」所指的分析主体),追加「停止推理直接作答」user 消息后用同一份历史 发新请求,最多 1 次。 - 落盘语义:thinking 单独一条 assistant,恢复正文另起一条(保留「前轮只思考、 后轮直接作答」的轨迹分界,供 resume 与排查);注入的 user 消息同样落盘。 - 恢复轮次仍耗尽:thinking/注入已落盘,恢复轮的空 thinking 不落盘(与 「空响应不落盘」一致),走原提示路径,不二次注入。 - 中断:恢复流中途 abort 时,thinking 与注入消息已落定,正文未成不落盘。 - skipFinalPush 标记让恢复路径与循环外统一 push 互斥,避免重复落盘。 - 透出 thinking_recover 事件,TUI 落 boundary note 告知自动恢复发生。 - 7 个针对测试(成功/仍耗尽/abort/注入文案/落盘轨迹/off 不触发/不可恢复)。 - 顺带:openaiChat 测试注释去除竞品名(措辞合规)。 全量 2409 全绿。 * fix(tui): resume 恢复会话后状态栏/别名指针显示错误的模型名 根因:resume 会话的 session.model 存的是别名(如 step37-plan),cli.tsx 用它对 provider 重建是正确的; 但 App 启动时 modelLabel 与 currentModelAliasRef 都优先读了全局 config.modelAlias(如 router),导致 状态栏显示 step-router-v1,实际请求是 step37-plan,显示与实际分叉。 修法:新增纯函数 resolveStartupModelAlias(sessionModel, config),按 session.model 是否命中 config.models 返回命中的别名;modelLabel 与 currentModelAliasRef 初始化优先取 resume 会话别名, 未命中(新建会话 session.model 是展开后的真实 id / 旧格式存真实 id / 空串)回退到原 config.modelAlias / 真实 id 反查路径。defaultModelPointerRef 保持 config.modelAlias 不变 (默认模型指针语义,不应被 resume 污染)。 新增 4 个单元测试钉住别名命中 / 旧格式真实 id / 空串 / undefined 四种输入。 typecheck 0 错,全量 2413 全绿,措辞合规通过。 * feat(tui): banner logo 换为字母 S(Small 字体),红色改蓝色 - 沿用 space-invader 多年的像素小生物 logo 换为字母 S(Step 首字母意象), 用 FIGlet Small 字体(4 行),与右侧 2 行标题高度协调。 - 渲染颜色由红改蓝。 - 选型过程与备选(Cards / 渐变阴影 / Delta Corps / Z 系列)记录于产品设计 文档 step-code-logo设计.md;字符画生成器 TAAG 已收录知识库。 - 顺带清理两个 logo 探索临时脚本(未跟踪,直接删除)。 * docs: 收敛子 agent 统计字段的注释表述 tool_uses/duration_ms 相关注释与测试描述,改为只陈述字段含义与设计理由。 功能行为不变(纯措辞)。 * test(session): list 倒序用假时钟错开 updatedAt,消除同毫秒 flaky * docs: CHANGELOG 补 0.1.1 挂牌后合入的条目;英文安装页标注单文件与 tarball 尚未发布 * docs: CHANGELOG 补登 0.1.1 挂牌后合入的引导重做、思考恢复链、resume 懒加载等条目 * fix: 子会话锁加 stale 检测(pid 判活回收)+ 只读白名单补 5 个纯查询工具 - 子 agent 会话锁:进程被强杀后锁文件残留导致子会话永远无法 resume/delete。 锁文件写入 pid,acquireLock/delete 发现锁存在时判活,死锁回收后继续。 旧格式锁(无 pid)按 stale 处理,向后兼容。 - plan 模式只读白名单补 tool_search/task_list/task_output/get_goal/cron_list, 消除纯查询操作的确认弹窗。 * fix: /new 与 /fork 换绑 BackgroundManager 的 tasksDir 此前仅 /resume 换绑,切换会话后新会话的后台任务仍落盘到旧会话目录, 任务归属错乱。/new /fork 补上同样的换绑;旧管理器直接丢弃不做对账 (旧会话的任务由它自己 resume 时的 reconcile 处理),与 /resume 语义对齐。 * test(tui): SessionPicker 真实 ANSI 流捕获测试——无清屏、帧重写有界 ink-testing-library 剥离 ANSI 只存整帧,测不到「清屏/整页闪换」。 用真实 ink render 写 fake TTY stdout 捕获原始字节流: 连续滚动/搜索收缩/极小终端下断言无 \x1b[2J、\x1b[3J(清 scrollback 是滚动跳顶根因),帧重写行数稳定不整屏。覆盖待办 #26 人工清单中 可自动化的部分(清单 2/5/8 的客观断言)。 * feat: 斜杠命令参数补全落地——/goal /permission /provider /lang /mcp /plugin /history 此前 CHANGELOG 已登记该功能但代码未随提交落盘(commit message 与内容 脱节),本次真正实现:7 个命令注册 getArgumentCompletions,静态子命令 + 动态渠道/插件 id(CompletionContext 扩展 providers/pluginIds, App 经 completionCtx 注入,cli 传入已发现的 plugin id 列表)。 * fix: edit_file diff 截断的「Ctrl+O 展开」假承诺改为完整 diff 落盘 截断发生在工具侧,被隐藏的改动从未进 content,Ctrl+O 展开无从恢复。 现在超限时完整 diff 落盘 .step-code/tool-output/edit-diff-*.log(保留 20 个),提示行换成真实可用的路径;落盘失败退回原提示文案。 * fix: 子 agent 会话区改滑动窗口——第 6 条起不再无法选中 此前子区 slice(0,5) 硬截断,游标最多走到第 5 条子会话。现在游标走 全程,5 行显示窗口居中锚定跟随;区头附窗口区间指示(并入区头行, 不额外占行,帧预算口径不变)。 * docs: CHANGELOG 补登锁 stale 检测、tasksDir 换绑、白名单补全等五项修复 * fix: 锁判活拦截 pid<=0 非法数据 + stale 测试改用真实死进程 pid POSIX 下 kill(-1, 0) 是「发信号给全部进程」的特殊语义会误判存活, Linux/macOS CI 上 stale 回收测试假失败(Windows 本地绿)。实现侧把 pid<=0 视为非法锁按 stale 回收;测试改用真实启动后立即退出的子进程 pid,覆盖「pid 合法但进程已死」的真实产品场景。 * feat: /think /provider 切换写回配置 + [tools.web] 缓存三阈值接入 config.toml - /think 写回 [thinking].default_level(off 仅会话级不落盘),/provider 写回顶层 provider;幂等判断(值相同不写文件),失败只提示不阻断。 App.tsx 调用点在下一笔(与通知修复同文件一并提交)。 - web 缓存三阈值(max_size/max_bytes/max_entry_bytes)从硬编码默认值 改为可配置:resolveWebCacheConfig 解析 + WebResultCache.configure 运行时重配 + 启动与 /reload 时应用 + doctor config 显示生效值。 - docs 中英配置参考同步补 [tools.web] 段,updateConfig skill schema 同步。 * fix: 通知 delivered 事件与消息本体同刻落盘 + picker 视口感知 - 待办 #17:delivered 事件此前在注入/入队时即时落盘,消息本体回合末 persist 才落盘,中间崩溃让对账误判已送达、通知丢失。改为 persist 时 扫 history 统一补写(pendingDeliveredEvents 纯函数,幂等键去重), 两者同生共死;Esc 丢弃队列的通知在丢弃处显式补写,保持「丢弃即送达」。 App.tsx 同文件还含 /think /provider 写回配置的调用点(上一笔的实现)。 - 待办 #24 + #0 遗留:ModelPicker 块分页改居中锚定滑动窗口、截断/填充 改 displayWidth 口径(新增 padEndByWidth);ModelPicker/ThinkPicker 加 visibleRows 入参,App 按 resolveVisibleRows 同口径接线(多留 1 行 chrome 余量),小终端弹层不再越线。 * perf: 子 agent 会话列表加 _index.json 索引缓存 SubagentStore.list 此前对每个快照逐个读盘+全量 JSON 解析(快照含完整 messages,226 条实测环境下每次调用都全量扫)。照搬主会话 store 的索引 模式:list 优先读索引,缺失/过期(目录内任何文件 mtime 晚于 rebuiltAt) 自动重建,create/save/delete 同步更新;重建只收录 .json 快照,.lock 与 .jsonl 不计入。 * feat: 全局 /resume 只列主会话,新增 /agents 查看当前会话的子 agent 子 agent 会话混在全局切换列表里语义错位(只能只读下钻、不能切换过去 对话)、占行挤掉主会话、且其全量解析是 /resume 卡顿主因。现在: - /resume 只列主会话(走索引缓存,不再触碰子会话目录) - /agents 列出当前会话派生的子 agent(parentId 过滤),选中下钻只读 回看历史;选择器复用 SessionPicker(新模式下不提供删除/重命名, 标题文案区分) - SessionPicker 加 titleKey 可选入参 * docs: CHANGELOG 补登 /agents 命令、写回配置、picker 视口与通知时序修复 * docs: 0.1.2 版本挂牌——CHANGELOG 重组 + /agents 命令文档(中英)+ 版本号 CHANGELOG:0.1.1 段只保留 tag 内内容,tag 后修复与变更归入新 [0.1.2] 段 (参数补全条目代码实际落在本周期,一并归入)。docs:interactive 命令表 加 /agents(中英),sessions 的子 agent 会话节改为 /agents 入口, agents.md 工具说明里的下钻引用同步修正。 * docs: 致谢与许可证收录措辞中性化 + 代码注释自述化 - README 中英致谢段、licenses/NOTICE.md 开头段:去掉「研究/参考/受启发 于 X」类表述,只陈述「源码自行编写、无隶属」与许可证收录的合规事实。 - 5 处代码注释删除对第三方实现的对照性描述(含源码坐标引用),改为 只陈述我们自身的机制与设计理由。 - docs 中英 sessions.md 明确标注 /agents 下钻为只读、不能在子会话中 继续对话。 * test: 两处测试标题同步自述化(规则表扩词后检出) * test: 子会话索引测试用假时钟错开 updatedAt,消除同毫秒倒序 flaky(Linux CI 检出) * build(release): 安装链路收敛为 latest 永久链接,dist-npm 预构建分支退役 - Release 资产新增固定名 step-code.tgz 副本,latest/download 永久链接可装最新版 - 删除 CI dist-branch job 与 make-dist-branch.mjs(分支已在远程删除) - 安装文档/快速上手/README(中英)与 step-code-install skill 对齐为四种安装方式 - 单文件可执行与 tarball 段落启用真实下载链接,去除「未发布」标注 * feat(tui): 任务清单面板按状态优先级裁剪,折叠行带状态分布 - 新增 selectVisibleTodos:进行中全保留,最新一条已完成做进度上下文, 剩余名额按原顺序填待办,待办不足时从最近回补已完成 - 替换 slice(0,5) 硬切:已完成在清单前部堆积时不再把进行中/待办挤出可视区 - 折叠行从 +N more 改为带隐藏条目状态分布(如 +3(1 已完成 · 2 待办)) - i18n 新增 todo.status.* 三个 key(中英),todo.more 改含 detail 占位 - 测试补 8 个用例:裁剪优先级 6 个 + 渲染 2 个 * fix(goal): 工具描述写明预算默认不设上限,纠正模型顺手设预算的倾向 * feat(team): /team 多 agent 团队模式——worktree 物理隔离 + 任务范围互斥 + 依赖硬化门控 + 六道门审阅收编 - 模式与工具:team_init/plan/spawn/merge/teardown/send/inbox/status 八工具 + /team 命令(init/status/exit/teardown),session 级开关,快照随会话落盘恢复 - 隔离:每任务独立 git worktree(.teams/worktrees/,写 .git/info/exclude 不动 .gitignore);写文件 per-worker 硬拦(runner 层 wrapWriteGuard);协调者在模式活跃时不能写代码 - 门控:build 任务范围两两互斥;deps 未全 merged 系统拒绝启动(代码强制,不靠自觉);依赖满足自动解锁 - 收编六道门:门〇空 diff 拒绝(防 worker 直提主仓绕过流程)/ 已审阅 / tip 未移动 / 依赖全并 / 无范围外文件 / --no-ff;冲突自动 merge --abort 救回;成功后自动清理干净 worktree(dirty 保留并提示) - 生命周期:closedAt 落盘标记(resume 不复活,init 重进清标记保留状态);teardown quit_only 应急强退;lost 联动(BackgroundManager onLost → 仅 active 任务标 blocked,可 respawn 重派);completed 可 rework 返工 - 可指定基准:--repo 指挥其他仓、--base 指定基准分支(缺省当前分支,收编校验 checkout 防合错) - 引导:内置 team skill(参数组合/反面教材/工作期用法/收编后验证/dist 错位运维知识)+ drift 锚点测试强制同步;init 报错意图识别(dir 误指 git 仓时引导改用 repo) - TUI:状态栏 team 徽标、/team 忙时即时执行;subagent 请求支持 cwd/writeAllowRoot - 测试:team 系列 30+ 用例(真实临时 git 仓跑 worktree/merge/门控/信箱/teardown) * fix(cli): resume 恢复会话后 session.model 同步为真实模型 ID——恢复路径 provider 用别名解析重建成功但漏了同步,别名原样发给服务端导致 404 model_invalid;状态栏正常是因 displayName 走别名查表的另一路径 * feat(tui): 子 agent 可见性 + 队列自动发送保留输入框草稿——spawn 卡片显示角色与任务描述;/tasks 运行中优先、同级新触发在前;状态栏 bg 徽章带最近任务名;submit 加 fromQueue 守卫,队列 drain 与空闲通知注入不再误清用户正在编辑的草稿 * feat(agent): 跨回合零进展检测——回合指纹 + 两段式处置(连续 3 轮警告注入 / 第 4 轮硬停) 模型在 tool_use 分支陷入「看到相同结果 → 做出相同反应」的稳态时(每轮 assistant 消息与工具结果逐字节相同、零信息增量),此前只能烧到 maxIterations=500 大断路器才被拦下。本提交在 loop.ts 的 tool_use 分支 挂上确定性检测: - 新增 src/agent/roundLoop.ts(纯函数,无 IO):fingerprintRound 取 messages 尾部 assistant + tool_result 对拼稳定指纹(排除每轮必变的 调用 id 与消息 ts;工具结果参与指纹,合法轮询结果在变则不触发); createRoundLoopDetector 维护 streak,连续相同第 3 轮 warn、第 4 轮 stop - loop.ts 接线:warn 注入 user 消息 + yield notice(不打断,给一次机会); stop 走 notice + turn_done 收尾(模型行为问题不是系统故障,不走 error) - i18n 新增 loop.roundLoop.{warn,inject,stop} 中英双语 - tests/agent/roundLoop.test.ts 穷举:指纹各成分参与度、streak 清零、 合法轮询不误伤、is_error 参与、warn/stop 的 runAgent 级集成断言 - subagent.test.ts 压缩估算用例的工具参数逐轮区分:完全同构序列正是 本检测要拦的形态,会在压缩触发前被硬停,测不到压缩路径 - CHANGELOG 补条目 完整设计见内部产品设计文档(跨回合零进展检测设计)。 * fix(session): 索引过期判定改 >= 修同毫秒竞态——rebuiltAt 是毫秒精度墙钟,写索引与同毫秒内的快照直改 mtime 相等时 > 漏判索引过期(fork Windows CI 的 cleanup ttl_days 随机失败根因,upstream 同内容全绿佐证 flake);测试用 utimesSync 把直改 mtime 拨到未来消除时序依赖;主 SessionStore 同源同修 * feat(agent): 死循环防线加固——round-loop 改滑动窗口 + 单轮步数分级预警 真实事故驱动:一个会话在同一条 bash grep 上原地复读 497 轮,直到撞 500 轮硬上限 才被中止,期间用户侧没有任何提示。回放该会话(1070 条消息 / 518 个回合对)确认 指纹算法本身有效(最大连续相同 streak 达 497,远超阈值),但暴露两个缺口: 一、round-loop 只认「与上一轮相同」。单槽 lastFingerprint 一旦不等就清零 streak, 于是周期≥2 的交替循环(A→B→A→B)每步都与上一步不同,streak 永远停在 1,检测一次 都不会触发;而「读 A 发现要看 B,读 B 又回头看 A」这种两态摆动在工具链路里比原地 复读更常见。改为大小 8 的滑动窗口 + 窗口内出现次数计数:出现 4 次及以上判 stop, 恰好 3 次判 warn。周期 1 的行为与原实现逐条一致,原有用例全部保留守住回归。 二、500 轮硬上限是唯一真正生效过的防线,且中间完全静默、撞线才硬停。现在在 maxIterations 的 50% / 80% 两档各注入一次自查提醒(notice + injection,照 roundLoop 的现成范式),阈值按比例计算而非硬编码,每档一轮交互只触发一次。提醒挂在循环顶部 而非 tool_use 分支——挂分支内会被 roundLoop.stop 的提前 return 吞掉。 i18n 同步:roundLoop 文案措辞从「连续」改为窗口语义,新增 turnWarning.mid / late 双语。 * feat(tools): bash 写操作静态守卫——三档判定纯函数(尚未接线) bash 工具此前对写操作零检查:write_file / edit_file 受 runner.ts 的 wrapWriteGuard 限制在 allowRoot 内,而 bash 的重定向、mv、rm、sed -i 一律直通,安全边界完全寄托在 模型自觉上(AGENTS.md 与 team skill 都只是把这一点写成告知)。 新增 checkBashWrite(command, cwd, allowRoot) 纯函数,引号感知地拆段后三档判定: - A 档,目标可解析且越界 → 拒绝。相对与绝对路径统一 resolve 后再判定,路径遍历 由 resolve 自身处理,不用正则去理解 ..;边界判定避开兄弟目录前缀陷阱。 - B 档,有写入迹象但目标不可解析 → 拒绝,并要求改写成 allowRoot 内的显式路径。 覆盖变量、命令替换、eval、python -c / node -e / perl -e 内联写入、以及命令内 cd 越界后的相对路径。这里不做 fail-open:放行等于没拦,而模型收到明确反馈会改写。 - C 档,无写入迹象 → 放行。动态路径判定附加「该段确有写入迹象」门槛,否则 ls $HOME、grep $PAT f 这类只读命令会被误拦,误拦比漏拦更伤可用性。 生效边界:allowRoot 缺省或为空时一律放行。主 agent 没有 allowRoot,必须保持能往 仓库外写文件(既定行为);只有 team worker 场景才受约束。 本 commit 只落纯函数与单测(93 例,含误报防线;Windows 语义的路径断言按平台跳过),尚未接入 bash.ts 的 execute()。接线、ctx 传参与用户可见文案另做。 已知残留缺口:>> 接文件描述符编号、heredoc 的 <<- 变体、cmd.exe 原生语法不覆盖。 * fix(tui): busy 摘要行补 Ctrl+B 转后台提示——提示原先只在它没用的场景显示 AgentGroup 的 backgroundHint 只挂在非 busy 的完整渲染路径(AgentGroup.tsx:215), 而 agentGroupRows 在 busy 时 return 1、面板压缩成单行摘要。前台派生子 agent 时主 agent 必然 busy,于是这条提示在真正需要它的场景永远不显示;只有后台派生且主 agent 空闲时 才出现——那时任务早已在后台,提示没有意义。 不是回归:Ctrl+B 的按键处理(App.tsx:1150)与文案 key 一直都在,缺的是 busy 摘要行 上的入口提示。 修法是在 busy 摘要行按 running > 0 追加一条紧凑提示,风格与已有的「· /tasks 查看全部」 一致,因此新增 agentGroup.busyBackgroundHint 而不复用带括号的 backgroundHint。 摘要行本身是 wrap="truncate",追加文本不会增加行数,agentGroupRows 仍返回 1—— AgentGroup.tsx:101 的注释记着一次「加 backgroundHint 导致帧高超预算越过 rows-1 红线」 的事故,这次刻意避开同一个坑,并在测试里断言行数不变。 补两条回归守卫:有 running 时含提示且行数不变、全部 done 时不含提示。 * test(tui): tasksViewer 预览切换改用确定性 startedAt——真因是列表排序不确定 现象:「↓/j 选择下一条,tail 预览跟着切换」这条用例在 CI 上间歇失败,断言期望预览显示(ta),实际是(tb)。 先前把它归因为「等待不够」并把固定 delay 换成 vi.waitFor,归因错了——换完之后 windows CI 仍挂在同一条断言。真因是列表顺序本身不确定:TasksViewer 的排序是「运行中优先,同级按 startedAt 倒序」(TasksViewer.tsx:165-170),用例里两个任务同为 running,谁排在前完全由 startedAt 决定;而 makeTask 的默认 startedAt 是各自现取 new Date(),于是顺序取决于「两次调用是否落在同一毫秒」。落在同一毫秒时稳定排序保持声明顺序(ta 在前,凑巧满足断言),一旦跨过时钟 tick,tb 变新排到最前,初始选中项就成了 tb。 修法是显式给两个任务 startedAt 并让 ta 比 tb 新(偏移 50ms,两条仍算刚启动,时长显示不受影响)。诊断与修复都用注入法验证过:把 makeTask 的默认值改成「tb 晚 1 秒」以模拟 CI 条件,修复前这条用例失败、其余 19 条全过(与 CI 表现一致),修复后同一模拟下 20 条全过——说明对时钟的依赖被真正消除,不是碰巧通过。 vi.waitFor 保留:预览区确实要等一次 ink re-render 才出现,固定 20ms 在 CI 负载高时不够。它解决的是「等得够不够」,与排序不确定是两个独立问题,注释里已写清以免下次再误判。 * fix(first-run): 恢复被三重失效吞掉的 [4] 文档出口,并清理同源死文案 设计文档「首次运行引导设计」第 20-21 行要求 select 步骤有四个选项,第四项是「查看文档,稍后手动配置」,i18n 的 firstRun.optionDocs 中英文案齐备,但该出口在实现上被三处叠加吞掉,实际彻底不可达(用户想跳过配置只剩 Esc):渲染只 map PROVIDER_OPTIONS(3 项 provider),第四项从未画出;方向键取模用 PROVIDER_OPTIONS.length,光标最大到索引 2,画出来也选不中;数字键路由里 idx === 3 分支写在 const option = PROVIDER_OPTIONS[idx]! 与 if (!option) return 之后,PROVIDER_OPTIONS[3] 恒为 undefined,按 4 被守卫提前 return,分支是死代码。修法对应三处:追加文档出口渲染行、取模改用 SELECT_OPTION_COUNT(渠道数 + 1)、把出口判断提到取值守卫之前。 选项 label 原为硬编码中文字面量('StepFun Plan 订阅' 等),i18n 里配套的 firstRun.optionPlan/optionApi/optionCustom 三条从未被用——后果不是提示缺失,而是英文用户在向导里看到中文。改为在 ProviderOption 存 labelKey、渲染时 t():PROVIDER_OPTIONS 是模块级常量,若在其中直接调 t() 会在 import 时求值把语言固化,运行时 /lang 切换不生效。硬编码的配置文档链接提示一并接线为新增的 firstRun.docsNotice。 顺带清理普查出的 26 条零引用文案(zh/en 双表对称删除,tsc 保证键集一致):app.goal/permission/provider/lang/plugin.arg.* 与 app.skill.list/none 共 22 条系重构遗留,实际替代者是 commands.ts 的 cmd.<命令>.sub.<子命令>;agentGroup.status.running/queued 在状态改用彩点后失效(done/error 仍在用);firstRun.optionPaste 属旧流程遗留、optionManual 与 optionDocs 语义重复。app.image.unsupported 同为零引用但保留并加注释:全仓没有任何 supportsImage/vision/multimodal 能力判断,该文案是「贴图给非多模态模型静默无提示」这个缺口的唯一线索。 * feat(agent): 注入当前时间快照与跨天提醒——模型不再靠猜「今天几号」 此前 system prompt 里没有任何时间信息,模型只能拿训练截止日期当「现在」,后果是搜索时用错年份、写文档与 commit 时写错日期、判断「最新版本」基于过时认知,且它意识不到自己在猜。 为什么是「启动快照 + 诚实标注」而不是逐轮刷新真实时间:system 整块打 cache_control(provider/prepare.ts 的 buildSystemBlocks),逐轮改写会让 system 断点连同其后的 tools 与历史断点一起失效,等于每轮按全上下文重算 input token。所以这里放弃「保持准确」,改为明说它是启动快照、可能过时数小时、真要准确时间就去跑 date——换来 system 永久静态,精度反而可以给到分钟而不必压到天。 跨天是唯一不可容忍的过时(日期错一天会让写文档、判断时效系统性出错),用一条 injection 消息修正,复用 loop 既有注入通道(与 turnWarning、后台通知同形)。baseline 取「最后一条消息的本地日期」而非局部状态变量:runLoop 每个 prompt 回合重新调用,局部变量在回合边界就重置了,而跨天几乎总发生在回合之间——用局部变量等于永远检测不到。注入的提醒自身成为最后一条消息、ts 即今天,故天然只注入一次,无需 warned 标记;resume 后同样成立。 时间一律用本地时区并显式标注 UTC 偏移与 IANA 名。直接给 toISOString() 会错日期:实测 new Date(2026,7,9,7,30).toISOString() 在东八区得到 2026-08-08T23:30:00Z,每天 00:00 到 08:00 报给模型的日期都比本地早一天,而模型无法自行修正(它不知道用户在哪个时区)。crossedLocalMidnight 另加坏时间戳防御:Invalid Date 的日期键是 NaN-NaN-NaN,与今天必然不等,不拦掉会导致每轮都注入一次「日期已变更」。 三处注入点:主 agent(systemPrompt.ts,now 可注入以保证测试不随真实日期漂移)、子 agent(runner.ts,联网调查最依赖当前日期)、--print 纯净模式。25 个新用例:nowContext 20 个覆盖时区换算、半小时时区、跨午夜判定与坏 ts(时区靠运行时改 process.env.TZ 切换,其中 UTC 对照组兼作方法自验——本机是 UTC+8,若切换在 vitest 里不生效该组必然失败);loopDateChange 5 个覆盖注入接线本身,判定函数写对而接线接错的情形纯函数测不出来,并用「临时把注入条件短路」反向验证过——禁用后期望注入的 3 条失败、期望不注入的 2 条仍过。 * feat(team): bash 写守卫接线——worker 的越界写不再只靠合并时的事后 diff bashWriteGuard 的三档判定纯函数与 93 个单测早已落地,但没有任何调用方:wrapWriteGuard 只拦 write_file/edit_file,bash 直接透传给 base。后果是 worker 一句重定向或 cp 就能写到工作间外,范围互斥只剩 team_merge 合并时的 diff 检查兜着——已经发生过踩穿。 接线点选在 wrapWriteGuard 而非原计划的 bash.ts execute():前者本就是 per-worker 写隔离的位置、已单测、已在 runner 里接到子 agent 的 hooks 上,改动只是加一个分支;接在 execute() 则要把 allowRoot 一路穿进 ToolContext,而这对非 team 场景没有意义。hook 层拦截还有个好处是能把守卫给出的 reason 直接回给模型,比工具层抛错更容易让它自己改对。 拦 bash 曾被担心会拦死 git(worker 要在工作间里提交),实测不成立:守卫只看命令行里的显式写入语法(重定向、cp/mv/rm/tee/sed -i/dd/truncate),git 子命令与 npm/npx 一律判为无写入迹象放行。接线前用 21 条 worker 典型命令量过误报面,唯一被误拦的是 ls -la > /dev/null——/dev/null 会被解析成绝对路径、天然落在 allowRoot 外,而这是最常见的丢弃输出写法,拦下去会卡死正常命令。故本 commit 一并加丢弃型设备白名单(/dev/null、/dev/stdout、/dev/stderr、/dev/tty、NUL、/dev/fd/N、/proc/self/fd/N),并留一条用例锁住「白名单不是所有 /dev/*」——dd of=/dev/sda 仍按越界拦。 测试:守卫层加 6 例(99 例)覆盖特殊设备与块设备对照;接线层加 5 例覆盖两端——git 提交/跑测试/丢弃输出必须放行,越界重定向/拷贝/删除与动态路径必须拦。两侧都做了反向验证:把 bash 分支短路后,期望拦截的 2 例失败、期望放行的 3 例仍过,确认用例测的是接线本身而不是恒真。 已知边界(未变):tee FILE < input 形态下取错 token 而漏判(tee FILE 与 cmd | tee -a FILE 能正确拦);>> 接文件描述符编号、heredoc 的 <<- 变体、cmd.exe 原生语法仍不覆盖。这些形态的越界写仍由 team_merge 的事后 diff 兜住。 * test(agent): 补跨天提醒在 resume 路径的保护——ts 被持久化重写会让判定永久失效 缺口:loopDateChange.test.ts 覆盖的是 runAgent 层接线,但它直接构造内存里的 StoredMessage,绕过了持久化。而 resume 恰恰是跨天最典型的场景(昨天的会话今天接着聊),这条链路多了落盘与加载两个环节。只要其中任一步把 ts 重写成写入时刻或加载时刻,跨天判定就永久失效——baseline 恒为今天,接线测试与纯函数单测都发现不了。 三个用例:save → load 后 ts 保真;用加载回来的昨日历史跑 runAgent 仍注入提醒;同一天内保存又恢复不注入(避免把 resume 本身误判成跨天)。 测试有效性用注入法验证过,不靠「跑绿了」当证据:在 SessionStore.save 里临时植入「把所有消息 ts 重写为当前时刻」的 bug,前两个用例立即失败(报 expected 2026-08-10T03:11 to be 2026-08-09T01:11,正是被重写的证据),第三个仍通过——它测的是反向行为,本来就抓不到该 bug,符合预期。移除注入后三条全过,store.ts 无残留改动。 时间一律用「26 小时前」构造:往前推 26 小时必然落在不同的本地日期(一天只有 24 小时),因此无论几点跑都成立,不需要等到跨过真实午夜才能验证。 * test(tools): readMedia 长图 probe 用例放宽 per-test timeout 到 60s 该用例单跑基线 11–15s(编码 508×30173 PNG + 20 次 region 全解码裁剪),全量并发下放大到 27.8s,撞全局 20s 阈值。2026-08-10 三轮全量测试里两轮挂在它上面。 不动全局 testTimeout:20s 是明确设计过的(vitest.config.ts 注释写明是给 16 核并发下 3 倍放大预留、防假阳性训练人忽略红灯),全局放宽会把其它测试的死锁假阴性窗口一起放大。只给这一条用例单独放宽到 60s,约 2.2 倍并发余量。 per-test timeout 对象形态经实验验证生效:构造 2s 睡眠 + 3s per-test timeout 的临时用例,全局 20s 下通过,说明 vitest 4 支持 it(name, {timeout}, fn) 三参签名。 * docs(readme): 前置非官方社区探索声明 * feat(memory): 记忆观察池——agent 自主沉淀的观察层,默认关闭、回顾确认后才生效 定位是观察池而非直接生效的记忆层:agent 用既有文件工具把观察写进两级 markdown 目录(全局 ~/.step-code/memory/ 与项目 .step-code/memory/,同主题项目层优先),system 尾部注入目录说明与索引并显式标注「未经确认,与规范冲突时以规范为准」;定期回顾经用户确认后才晋升进用户自维护的规范层。确认权不旁落是这个设计的核心约束。 存储为 markdown 正文 + HTML 注释藏 JSON 字段(version 单调递增、occurrences 记「第 N 次出现」、updated_at、source),人可读可手改、机器可解析;解析失败进 /memory 待修复列表而不阻塞。写侧刻意不做专用工具,复用 write_file / edit_file。 开关 [memory] enabled 默认 false:关闭时不注入、不建目录、已有文件原样保留。/memory 无参列出两层条目(含索引字符用量、坏文件、超阈值回顾提示),on/off 写回 config 并即时生效。中途开启(off→on)时经 injection 通道注入一条回看引导,agent 补沉淀本次会话的遗留观察——与跨天提醒同一机制。子 agent 只读注入(有索引无写入引导),并行写冲突与临时上下文噪声由此规避。 实现顺手修了两处既有问题:saveSectionKey 不支持 boolean(字符串 'true' 会带引号落盘,布尔解析恒 false——本功能第一个使用者就会踩);drift 保险要求的 doctor 顶层键清单与 update-config skill 内嵌表同步加 [memory] 段。21 个新用例覆盖解析/两层合并/索引预算/坏文件容错/只读变体/boolean 裸值回写。 * feat(reflect): 产出同步注入会话流——「记住第 N 条」的两阶段搭配不再断链 此前 /reflect 的清单只 pushItem 到界面、不进会话消息流,用户看到清单后说「记住第 2 条」时 agent 上下文里并没有这份清单,反思到沉淀的衔接是断的。现在非占位产出会以 injection 消息同步进 history(与跨天提醒、memory 开启引导同一通道,落盘、resume 后可见),注入文案说明来源并约束 agent 不主动写、等用户挑选。reflect 与 memory 保持松耦合:两者互不知晓,搭配发生在会话流里。 两个占位文案(空历史/未提炼出经验)提为导出常量,App 用 === 判断跳过注入;新增用例锁引用相等——若有人把返回改成同内容的新字面量,占位判断会静默失效,占位文本就会被写进会话流。 * docs: CHANGELOG 补第四批与 memory/reflect 条目;README 功能列表加 memory;interactive 的 /reflect 行补会话流注入(中英) * feat(session): 第一轮回答后 AI 自动生成会话标题——语义化 /resume 列表 - title.ts:generateSessionTitle(复用主会话 provider,输入截断 300 字/条)+ cleanGeneratedTitle(去 think 块/引号/截断 50 字)+ canOverwriteTitle 判定 - 覆盖纪律:rename 过(name 非空)永不覆盖;title 非派生结果不动;每会话只尝试一次 - store.updateTitle:直写文件不刷新 updatedAt,不把会话顶到列表最前(同 rename 模式) - App:submit 收尾 persist 后 fire-and-forget 触发;写回前重新 load 再判一次防竞态 * fix(background): 后台任务防失控三件套——output.log 滚动截断 + stdin 立即 EOF + Windows 进程树终止 2026-08-10 事故:失控 python REPL 死循环 3 天写 287 GB output.log,C 盘归零 致 pagefile 无法扩展、V8 FatalOOM 闪退;任务标 killed 后孙进程逃逸又写 24 GB。 - output.log 磁盘滚动截断:超 maxOutputFileBytes(默认 32MB)保留尾部一半重写, 单 chunk 超保留量时自身先截尾;截断标记行进文件,outputTruncated/outputTotalBytes 落 meta - spawn stdio ['ignore','pipe','pipe']:stdin 立即 EOF。agent 本无 stdin 通道, pipe 永不关闭只会让读 stdin 的命令挂起或 REPL 死循环(前后台统一) - terminateProcTree:Windows taskkill /T /F 杀整树,POSIX detached 建组 + kill(-pid); stop/超时/前台中断/前台超时四处 kill 路径统一改走 * feat(team): worktree 挂载主仓 node_modules 目录联接——worker 可在工作间自验测试 worktree 只 checkout 版本控制文件,node_modules 被 gitignore,worker 一跑 vitest/tsc 就因缺依赖卡到超时被杀,只能提交未验证代码(实际发生过两次)。用目录联接共享 主仓依赖而非复制(数百 MB 且 pnpm 链接农场不宜复制)。失败静默跳过不阻塞启动; 依赖共享的副作用(worker 装依赖会写主仓)靠任务提示约束。 * fix(session): session.model 统一存别名,providerModel 单独承载真实 id 问题:新建会话 / resume 后 session.model 被写成真实模型 id, 当真实 id 恰好是某个别名的 key 时(如 qwen-for-coding), resolveStartupModelAlias 反查会命中 [models.qwen-for-coding] 而非用户实际选择的 [models.song],导致 currentModelAliasRef 指向错误渠道,状态栏丢失别名 displayName。 修复: - cli.tsx 新建会话 / resume 回退时 session.model 存别名 (config.modelAlias ?? config.model) - cli.tsx resume 命中别名时 session.model 保持别名, 新增 providerModel 变量承载真实 id 给 provider.stream - cli.tsx 非交互模式 runAgent 与 App 传参改用 providerModel - App.tsx /new 时 store.create 用 currentModelAliasRef.current ?? model (与 persist 口径一致) 更新 tests/cli/resumeModelAlias.test.ts 匹配新逻辑。 * feat(tui): 统一编辑器 TextEditField 替换 8 处输入框——一处修行为处处生效;ModelPicker 渠道 tab 视口跟随(选中项滚出可视区时自动跟随);补 unified-text-editor 设计文档(中英) * feat(config): 图片输入上限按别名声明——[models.*] image_max_edge_px 覆盖全局保守值,read_media 降采样阈值与轻量刷新按当前别名解析 * feat(tui): 子 agent 工具调用嵌套可见性——spawn_agent 条目下实时回传子工具事件,运行中滚动窗口保留最近 3 条、完成折叠计数,Ctrl+O 看全过程;runner onEvent 回传链 + tool_end 事件;连带:events billedDelta 注释口径修正、runner 图片上限透传(配置本体在上一笔) * fix(compaction): 压缩摘要压到最低思考档(按模型能力门控)+ billedTokens 计费口径修正——Anthropic input_tokens 本身已排除缓存命中,不再重复减 cache_read * feat(headless): stream-json 协议 v3——新增 session.not_found 与 result 事件、-p 支持 --output-format json;stdin 防护与别名解析修复;连带:cli 接线图片上限别名配置(配置本体见前笔) * revert(skill): 撤除 skill 激活次数上限——误抄嵌套深度语义成总次数,正常多 skill 协作被误拦;防循环由 maxIterations 与 token 预算兜底;保留「同一 skill 多次激活不受限」回归用例 * feat(tui): 聊天区排版第二轮——用户消息淡色背景块提升可扫描性;超宽 URL 独占行整体溢出不再字符级拆断;列表悬挂缩进修「续行顶格像突然换行」;连带第一批 OSC 8 超链接渲染 * fix(tui): 正文排版三修——折行超宽溢出字符顶格、列表项粗体星号裸露、段落软断行句中硬断 - 前缀行布局(● / › / 列表 marker)的内容盒 flexShrink 改显式定宽: Ink 在 flexShrink 下按父行整宽折行再套缩进,续行超宽被终端硬折行、 溢出字符顶到第 0 列(实录:2025/中AI/Pexo 顶格碎片) - renderInline/extractCellSegments 的 text 分支递归子 tokens: marked 给列表项的是块级 text 容器,直接输出原文导致 **粗体** 裸露 - 段落软断行软化:CJK 边界直连、拉丁边界补空格、域名不断 (thepaper.\ncn 类模型断碎可复原),测量路径同步 - 回归契约:渲染行永不超终端宽(四档宽度)+ 列表悬挂缩进 + 星号不裸露 * fix(image): Alt+V 贴图在 OpenAI 系通道被静默丢弃 + Windows 读图多路径与自诊断 - 协议翻译层:messagesToOpenAi / messagesToResponsesInput 处理 user 消息时 此前只拼接文本块(blocksToText),image 块被直接丢弃——走 openai/ openai_responses 通道的渠道(Kimi Coding 等)贴图界面显示已附加,实际请求 里没有图片,模型完全收不到,无任何报错。有图片时 content 升级为多模态 parts(image_url / input_image data URI),无图片保持原 string 路径 - Windows 剪贴板读取:单次 PowerShell 多路径尝试——WinForms GetImage → PNG 格式 → FileDrop 图片文件;全部失败输出剪贴板格式清单,UI 三级 诊断(缺工具 / 格式清单 / 通用无图),不再一句「没有图片」兜底 - 回归:user 图片 parts 形态、纯文本不回归、混合消息顺序、parseWinOutput 解析契约 * fix(compaction): 手动压缩假成功提示 + 复述闸门密度误判 - 复述判定从「出现即拒」改密度口径(≥1 处/千字符才拦):高保真交接笔记 零星引用 [image …] 等序列化标记被误判为照抄历史,导致压缩三连败后放弃 (真实会话回放:9.4 万字符合格摘要被拒,随后两次重试退化为百字符短摘要) - 复述被拒后追加反复述提示原样重试,不再 dropOldest(丢消息不治输出行为) - 重试循环全程记日志:API 失败 status/原因、闸门拦截候选长度/原因、放弃总结 - UI:fullCompact 同引用返回(未压缩)时明确提示「未能压缩」,不再打印 「已压缩:X → X」假成功 * fix(degrader): 识别「端点只收 text part」的 400 为媒体问题并自动降级 智谱 BigModel 实测(2026-08-13):含图历史切到只收纯文本 content 的端点 (messages.content.type 参数非法,取值范围 ['text'])每条请求都 400。 该报错等价于「不收图片」——我们发出的非 text part 只有图片。登记进媒体 方言表后沿降级链把图片换占位文本重试,会话可继续。 * docs: 标注 step-code-pi 为 pi-tui 实验仓(定位与边界声明) * feat(tui-pi): M1 主循环——pi-tui 前端跑通输入/流式/审批/持久化 平行目录接线(--pi 开关),Ink 版原样保留可对照: - Transcript/ItemBlock:DisplayItem → 行数组,块级渲染缓存 - StatusLine/ActivityLine:两行式状态栏 + spinner/思考预览 - ChatEditor:Esc/Ctrl+C 交回控制器,补全打开时让位编辑器 - PiChat:runAgent 消费、Esc 三态、Ctrl+C 三态、发送队列、persist 顺序不变量 - InlineApproval:y/a/n 最简审批(完整三桥在 M2) 实测推翻了设计文档的裁剪方案:任何裁剪都必然触发全屏重绘 + CSI 3J 清 scrollback, 而不裁剪的成本仅 3.47ms/帧(12000 行历史),故默认不裁剪,maxTurns 降级为安全阀。 测试:23 个渲染/交互用例 + 1 个进程级首帧冒烟;上游未导出 VirtualTerminal,改用自写 FakeTerminal * feat(tui-pi): M2 审批三桥——工具审批/计划确认/向用户提问 ChoiceBlock 抽出三桥共用的交互(↑↓ 选择 + Enter + 直选键 + Esc + 内联反馈输入): Ink 版三个组件各写一遍同构状态机,命令式重写下抽基类更自然。 - InlineApproval:四选项(y/a/n/f)+ bash 危险模式警告 + diff/写入预览 Ctrl+E 展开 - PlanApproval:exit_plan_mode 确认,计划正文走 markdown,拒绝可附修订意见 - QuestionPrompt:ask_user 多题逐题问,空格多选、←→ 切题、自由输入项、Esc 回空字典 - PiChat:plan 模式守卫 + exit_plan_mode 确认链路 + ctx.askUser 接线 + /plan 命令 - 弹层激活时暂停 spinner(用户在读弹层,动画只是噪声与无谓重绘) 弹层互斥由「overlayHost 同时只放一个块」保证,替掉 Ink 版 9 个 useInput 早退分支。 测试:18 个三桥交互用例,真实模型跑通 ask_user 全链路(提问弹出 → 选择 → 答案回灌) * M3:pi-tui 侧三个选择器(模型 / 会话 / 思考深度) `src/tui-pi/pickers.ts` - `PickerOverlay`:SelectList + 标题行(含过滤串)+ 提示行的覆盖层组件。 可打印字符进过滤串、退格删字、Enter 结算、Esc 取消,全部走 `handleInput`,不用全局钩子(理由同 M1 的 Esc:语义依赖当前状态)。 - `modelItems`:按渠道分组排序,label 用 displayName,description 给 「渠道 · 真实模型 id · 上下文窗口」,当前别名前面标 ●。 - `sessionItems`:展示口径 name ?? title,description 给相对时间 + 消息数 + 短 id。`relativeTime` 按秒/分/小时/天分档,超 30 天给日期。 - `thinkItems`:high/medium/low/off + 跟随配置默认五档。 `src/tui-pi/PiChat.ts` - `/model`、`/resume`、`/think` 路由到对应选择器;选中后分别调 `applyModel` / `applySession` / `applyThink`。 - `session` 字段从 readonly 改为可变(恢复会话要换实例)。 `tests/tui-pi/pickers.test.ts`:候选项构造 + 按键序列结算,9 个用例。 实测(`dist/main.js --pi`,见提交说明外的实测记录):27 个模型别名 分组显示正确,输入 step35 过滤到 2 项,Enter 切换后状态栏与提示条 同步更新;/resume 列出 3 个历史会话;/think 五档当前项标记正确。 全量 2913 个用例绿,tsc --noEmit 干净。 * M4:命令注册表接线 + 19 条命令 + wire 事件落盘 分发改成走注册表,不再手写 switch 匹配命令名: `parseSlash` / `busyRoute` / `helpText` 直接复用 `src/tui/commands.ts` (纯逻辑、不 import react),两版共用同一张 SLASH_COMMANDS 表,命令集与 别名不会漂移。回合进行中的分流也由 `busyRoute` 决定:只读命令即时执行, 改动 turn 前提的排队到回合边界;双态命令(/model /think /resume)无参 只读即时、带参变更排队。 新实现的命令:/permission /yolo /auto /lang /mcp /usage /tasks /memory /restore /compact /export-debug-zip /fork,以及重写的 /new(原来只清历史, 没换会话,也没换绑后台任务目录)。加上 M1-M3 的 /help /model /think /resume /plan /clear /exit,pi 版现有 19 条命令。 wire 事件落盘(这是 M4 的前提,不是附带项): `appendWire` + runAgent 的 `onWireEvent`,权限模式/plan/思考深度/压缩应用 四类状态变更各落一条。M4 之前 pi 版只写 appendFull 快照,事件日志是空的, `/usage` 与 `/export-debug-zip` 拿不到任何数据。 队列排空改走 `drainQueue`:队列里可能混着排队的斜杠命令,原来无条件 `runTurn(next)` 会把命令当普通消息发给模型。 `src/tui-pi/commandText.ts`:`formatTaskList` / `formatMemoryList` / `NOT_WIRED`。文本生成从控制器里拆出来是为了能直接断言,Ink 版这些输出与 setState 混在 case 分支里,只能起整棵 React 树才能验证。 未接线的 10 条(/goal /team /loop /history /reflect /agents /skill /provider /reload /plugin)显式列在 NOT_WIRED,提示「pi 版尚未接线」而不是 落到「未知命令」——命令是存在的,提示要能区分打错了和这版还没有。 顺带修了 Ink 版两处误导文案(pi 版已改,主仓待回写): 1. 历史短于 keepRecent 时 fullCompact 原样返回,与摘要失败同一出口, Ink 版打「多次尝试均未产出可用摘要」,而实际一次请求都没发。 2. 摘要可能比被替换的原文更长(实测 1346 → 1535),Ink 版无条件打 「已压缩:X → Y」,Y 比 X 大会被读成程序算错。 测试:`tests/tui-pi/commands.test.ts` 13 个用例(注册表分流语义 + 文本生成)。全量 2926 个用例绿,tsc --noEmit 干净。 实测(`dist/main.js --pi`,逐条发命令抓 stdout): 19 条命令逐条验证输出正确;一轮真实对话后 /usage 报出 18.7k input(证明 wire 事件真的落盘了);4 轮对话后 /compact 实跑 10s,摘要生成成功,历史 替换、context 用量从 19.2k 回落。 * M4b:输入补全接线(/命令 · 命令参数 · @文件) `src/tui-pi/completion.ts`:把既有补全引擎适配到 pi-tui Editor 的 `AutocompleteProvider` 接口,候选仍由 `computeCompletions` 算,菜单渲染与 键位交给 Editor。 没用 pi-tui 自带的 `CombinedAutocompleteProvider`,两个原因: 1. 它自己扫盘做文件补全,而本项目的 @ 补全用 `scanFileIndex` 的索引 (排除 node_modules/dist 与点目录、有条数上限)。换掉等于悄悄改了补全 范围,可能把 node_modules 的路径提上来。 2. 命令名匹配的既有语义是「前缀优先 + 两字符查询才启用子序列回退」, 有测试钉住;换成它的模糊匹配会改变命中集。 文件索引在 `start()` 里后台扫,不阻塞首帧;扫完前 @ 补全为空,扫描失败降级 为空索引,命令补全不受影响。 `tests/tui-pi/completion.test.ts` 9 个用例,重点在 applyCompletion 的文本 替换(写坏了表现为补全后输入框留…
li-xiu-qi
added a commit
to li-xiu-qi/Step-Realtime-CLI
that referenced
this pull request
Sep 4, 2026
- 待办 stepfun-ai#17:delivered 事件此前在注入/入队时即时落盘,消息本体回合末 persist 才落盘,中间崩溃让对账误判已送达、通知丢失。改为 persist 时 扫 history 统一补写(pendingDeliveredEvents 纯函数,幂等键去重), 两者同生共死;Esc 丢弃队列的通知在丢弃处显式补写,保持「丢弃即送达」。 App.tsx 同文件还含 /think /provider 写回配置的调用点(上一笔的实现)。 - 待办 stepfun-ai#24 + #0 遗留:ModelPicker 块分页改居中锚定滑动窗口、截断/填充 改 displayWidth 口径(新增 padEndByWidth);ModelPicker/ThinkPicker 加 visibleRows 入参,App 按 resolveVisibleRows 同口径接线(多留 1 行 chrome 余量),小终端弹层不再越线。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
/themeswitches on macOS terminals.mainand make the newly merged Windows-sensitive tests pass under normal Windows permissions.What Changed
mutedandlinecolors insrc/tui/theme.tsacrossdefault,sage,pop,helix, andsteel.SUMMARYlabel insrc/tui/app.tsxto usetheme.foregroundfor badge text while preserving existing badge background and rail colors./themestatus copy so users can recover quickly with/theme defaultwhen a custom or builtin theme is hard to read.src/tui/theme.test.tsto verify contrast constraints for foreground, muted text, selections, dividers/rails, and badge backgrounds.pnpm checkpasses under normal Windows permissions.Verification
pnpm exec vitest run src/tui/theme.test.tspnpm exec vitest run packages/utils/src/path.test.ts packages/utils/src/shell.test.tspnpm exec tsc --noEmit --pretty falsepnpm exec prettier --check src/tui/theme.ts src/tui/app.tsx src/tui/theme.test.ts packages/utils/src/path.test.ts packages/utils/src/shell.test.ts docs/skills/pr-review-decision.mdpnpm checkNotes