设计要点: - /effort 无参 → 横向 slider 面板(low/medium/high/xhigh/max/ultracode) - ←/→ 移动光标,Enter 确认,Esc 取消 - ultracode 仅视觉占位,确认后提示走 /ultracode <context> - env override 时双标记 + 顶部警告 - 模型不支持时面板禁用 - 两阶段交付:先基础面板 commit,再做 ultracode 波纹动画 Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win>
16 KiB
Effort 交互面板(EffortPanel)设计
日期: 2026-06-14
作者: brainstorming session 产物
状态: 待实施
关联: src/commands/effort/、src/utils/effort.ts、src/components/EffortPanel/(新增)
1. 概述
把当前的 /effort slash 命令从纯文本式交互升级为终端内的可视化选择面板。
- 触发:
/effort(无参)打开面板;/effort <level>直跳路径保留 - 视觉:横向 slider,两端标
Faster/Smarter,刻度为low / medium / high / xhigh / max / ultracode - 交互:
←/→移动光标,Enter确认,Esc取消 - ultracode 仅作视觉占位,确认后提示用户走
/ultracode <context>启动 - 第二阶段加波纹动画(详见 §6)
2. 用户故事
- 作为开发者,我希望按
/effort就能可视化地选择努力等级,而不用记 5 个枚举值 - 作为高频用户,我希望
/effort high这种直跳仍可用,避免脚本/习惯被打断 - 作为设置了
CLAUDE_CODE_EFFORT_LEVEL的用户,我希望面板提示我"env 优先级更高",而不是默默忽略我的选择 - 作为想试 ultracode 的用户,我希望面板让我知道这个"档位"存在,但落地要走它自己的命令
3. 不在本期范围
- 不修改
EffortValue/EffortLevel类型 - 不修改
src/utils/effort.ts的任何纯函数 - 不新增专用全局热键(仅通过
/effort触发) - 不在面板里包含
auto选项(仍走/effort auto) - 不真正"启用 ultracode"——面板对 ultracode 仅作视觉提示与文案引导
4. 架构与文件结构
src/
├── commands/effort/
│ ├── effort.tsx ← 改造:call() 在 args 为空时返回 <EffortPanel>,
│ │ 有参时维持原 executeEffort() 路径
│ └── index.ts ← 不变
├── components/EffortPanel/
│ ├── EffortPanel.tsx ← 新增:面板主体(渲染 + 键盘交互 + onDone 通道)
│ ├── effortPanelState.ts ← 新增:纯函数 reducer(移动光标、确定选项),
│ │ 抽离便于单测
│ └── __tests__/
│ ├── EffortPanel.test.tsx ← 渲染 / 键盘交互 / env 警告 / ultracode 提示
│ └── effortPanelState.test.ts ← reducer 纯函数测试
复用清单(不重写)
executeEffort()/setEffortValue()/unsetEffortLevel():留在effort.tsx,面板确认时调用EFFORT_LEVELS/getDisplayedEffortLevel()/getEffortEnvOverride()/getEffortValueDescription()/modelSupportsEffort():从src/utils/effort.ts直接 importuseInput或useKeyboard:从@anthropic/ink取<ApplyEffortAndClose>组件:作为面板 Enter 后的"写入并退出"流程组件复用(或迁入 EffortPanel 内部)
类型层面
不动 EffortValue / EffortLevel。面板内部用一个新类型 PanelPosition 表示光标位置:
type PanelPosition = 'low' | 'medium' | 'high' | 'xhigh' | 'max' | 'ultracode';
它仅在面板内部使用,不进入 AppState、不进入 settings.json、不参与 API 调用。
5. 交互流程
触发与初始光标
/effort<回车>(无参)
→ call() 检测 args === ''
→ 渲染 <EffortPanel onDone={onDone} appStateEffort={effortValue} model={model} />
→ 光标初始位置:
env override 存在时 → env 设定的档位(让用户立刻看到生效值)
否则 → getDisplayedEffortLevel(model, appStateEffort)
状态机
状态:{ cursor: PanelPosition }
事件:
← (ArrowLeft) → cursor 左移一位(low 处不左移,保持 low)
→ (ArrowRight) → cursor 右移一位(ultracode 处不右移,保持 ultracode)
Home / h → cursor = low
End / l → cursor = ultracode
Enter → 确认分支(见下)
Esc / Ctrl+C / q → 取消,onDone("Effort unchanged.")
确认后的两条分支
分支 A:cursor ∈ {low, medium, high, xhigh, max}
调 executeEffort(cursor)
→ setEffortValue 写 settings + AppState
→ 拿到 result.message
onDone(result.message)
(与现有 /effort high 完全一致的消息体例,含 env override 警告)
分支 B:cursor === 'ultracode'
不调 executeEffort
onDone("ultracode 不是 effort 档位。请使用 /ultracode <context> 启动多 agent workflow。")
取消路径
不调 executeEffort、不写 AppState、不写 settings。onDone("Effort unchanged.")。
不变路径(仍走原 effort.tsx 逻辑)
/effort low|medium|high|xhigh|max:直跳/effort auto|unset:unsetEffortLevel/effort help|-h|--help:help 文本/effort current|status:ShowCurrentEffort
焦点与键盘独占
面板挂载时通过 Ink useInput 抢占键盘;卸载时自动释放(与 AskUserQuestionPermissionRequest 一致)。
6. 视觉布局
基本形态(无 env override)
Effort
Faster Smarter
─────────────────────────▲──────────────────────────────────────────────
low medium high xhigh max ultracode
xhigh + workflows
←/→ adjust · Enter confirm · Esc cancel
视觉规则
| 元素 | 规则 |
|---|---|
▲ 光标 |
跟随 cursor 状态移动,永远指向当前 cursor 位置 |
| 当前生效档位(active) | 当 cursor ≠ active 时,active 档渲染为加粗 + 旁标 (active);当 cursor === active 时只显示 ▲,避免双标记 |
| ultracode 副标签 | 固定字符串 xhigh + workflows,dim 色 |
两极文字 Faster / Smarter |
与面板等宽左右对齐;中间用一行 ─ 填充 |
| 底栏提示 | ←/→ adjust · Enter confirm · Esc cancel,dim 色 |
标题 Effort |
加粗,居中或左对齐 |
双标记渲染(cursor ≠ active)
env override 时会出现,例如:
Effort
⚠ CLAUDE_CODE_EFFORT_LEVEL=high overrides this session
Faster Smarter
────────────────────────▲────────────────────────▲──────────────────────
low medium (high) active xhigh max ultracode
xhigh + workflows
←/→ adjust · Enter confirm · Esc cancel
▲上方:cursor 位置(xhigh)(high) active:env 锁定的真实生效档位
两个标记视觉上必须区分:cursor 用三角符号,active 用括号文字 + 颜色。
模型不支持 effort 时(modelSupportsEffort(model) === false)
Effort
当前模型 <model> 不支持 effort 参数。面板已禁用。
Faster Smarter
────────────────────────────────────────────────────────────────────────
low medium high xhigh max ultracode
Esc to close
光标不显示,左右键无效,Enter 无效,只能 Esc 退出。
终端窄屏(< 60 cols)适配
简化策略:宽度 < 60 时退化为垂直列表,每档一行;否则保持横向 slider。这一项不阻塞首版,先按横向渲染,必要时溢出,后续看实际效果再调。
7. 背景波纹动画(第二阶段,单独 commit)
触发条件
仅在 cursor 停在 ultracode 时启动波纹;移开时立即停止(不淡出,干脆)。常态零干扰。
视觉概念
ultracode 是面板的"能量溢出口"。波纹从 ultracode 字符位置(右下区域)为震源,向左/向上辐射同心圆波,铺满整个面板的留白区域(文字字符之间的空隙、─ 分隔线的空白段)。文字层永远清晰可读。
字符集(强度 → 字符)
| 强度 | 字符 |
|---|---|
| 0.0 | (空格) |
| 0.1 | · |
| 0.3 | ∙ |
| 0.5 | ░ |
| 0.7 | ▒ |
| 0.9 | ▓ |
| 波峰 | ~ → ◌ → ○ → ◑ → ● 循环 |
波纹数学
对每个字符格:
dx = x - sourceX
dy = (y - sourceY) * 1.5
dist = sqrt(dx*dx + dy*dy)
phase = dist * 0.4 - time * 0.012
wave = sin(phase)
falloff = max(0, 1 - dist / 40)
intensity = max(0, wave) * falloff
if (dist < 6): // 震源附近高频涟漪
intensity = max(intensity, 0.5 + 0.5 * sin(time * 0.02 - dist * 1.2))
char = pick(intensity)
参数上线后调。
渲染策略(双层不冲突)
Ink 不支持真正的 z-index 层叠,用字符替换模拟:
- 每帧生成
height × width字符矩阵(背景层) - 渲染每个面板行时,先取该行对应的波纹字符序列,然后在文字字符应该出现的位置覆盖背景字符
- 文字字符永远胜出,波纹只占空隙
实现位置
新增(第二阶段):
src/components/EffortPanel/rippleAnimation.ts—pickChar/computeRippleLine/mergeLayers纯函数src/components/EffortPanel/useRippleFrame.ts— hook,内部调useAnimationFrame(60)返回当前帧矩阵- 在
EffortPanel.tsx的 render 中叠加(仅 cursor === 'ultracode' 时启用)
性能预算
- 面板 80×10 = 800 格,每帧 800 次 sin/sqrt ≈ 0.05ms
- Ink 重绘 10 行
<Text>节点,与现有 Spinner 同量级 - 帧率 16fps,
useAnimationFrame自带 viewport 不可见暂停 + 失焦减速
风险与对策
| 风险 | 对策 |
|---|---|
| 波纹干扰文字可读性 | 文字字符覆盖背景字符,永远胜出;波纹颜色用 theme.textDisabled |
| 终端窄屏 < 60 cols | sourceX 跟随 ultracode 实际位置;窄屏时降级为单行波纹 |
| 性能(旧机器) | useAnimationFrame 已自带暂停/减速 |
| 测试稳定性 | 字符选择是纯函数,可固定 time 注入做帧快照测试 |
8. 数据流
状态来源
┌─────────────────────────────────────────────────┐
│ src/state/AppState.tsx │
│ effortValue: EffortValue | undefined │
└─────────────────────────────────────────────────┘
▲
│ useAppState(s => s.effortValue)
│
┌─────────────────────────────────────────────────┐
│ EffortPanel.tsx │
│ props: appStateEffort, model, onDone │
│ local: cursor: PanelPosition │
└─────────────────────────────────────────────────┘
│
│ Enter 确认
▼
┌─────────────────────────────────────────────────┐
│ executeEffort(cursor) │
│ → updateSettingsForSource('userSettings', …) │
│ → logEvent('tengu_effort_command', …) │
│ → 返回 { message, effortUpdate? } │
└─────────────────────────────────────────────────┘
│
│ <ApplyEffortAndClose> setAppState(...)
▼
┌─────────────────────────────────────────────────┐
│ onDone(result.message) │
│ → REPL 渲染 assistant 消息 │
└─────────────────────────────────────────────────┘
优先级链(不修改)
env CLAUDE_CODE_EFFORT_LEVEL > AppState.effortValue > model default
面板只写 AppState + settings.json,不直接操作 env。env 存在时,面板可操作但顶部警告(详见 §6 双标记)。
9. 边界与错误处理
| 场景 | 行为 |
|---|---|
| 模型不支持 effort | 面板挂载但禁用,文字提示 + 仅允许 Esc(详见 §6) |
| env override 设定 | 顶部加黄色警告行 ⚠ CLAUDE_CODE_EFFORT_LEVEL=<value> overrides this session;光标可移动;Enter 仍写 settings 但顶部警告解释生效值不变 |
| cursor === 'ultracode' 时 Enter | 走分支 B,输出引导文案,不调 executeEffort |
| settings 写入失败(磁盘满/权限) | executeEffort 现有错误路径会返回 result.error,面板沿用,onDone 输出错误消息 |
| 终端窄屏 < 60 cols | 退化为垂直列表,不阻塞首版 |
| 用户按 Ctrl+C 之外的中断信号 | 视同 Esc,onDone("Effort unchanged.") |
面板挂载后 AppState 被外部改变(如 /model 切换) |
cursor 不订阅 active 变化,挂载时计算一次初始值后只跟随用户操作。若用户切了 model 想看新档位,关掉面板重开即可。简化实现,行为可预测 |
10. 测试计划
纯函数(effortPanelState.test.ts)
moveLeft(cursor)在 low 处保持 lowmoveRight(cursor)在 ultracode 处保持 ultracodehome(cursor)/end(cursor)边界getInitialCursor(appStateEffort, envOverride, model)优先级isUltracode(cursor)守卫
组件(EffortPanel.test.tsx)
渲染:
- 无 env 时显示基本形态
- env override 时顶部警告 + 双标记
- 模型不支持时禁用面板
- ultracode 副标签
xhigh + workflows出现
键盘:
←移动光标、→移动光标、Home/End跳转- Enter 在普通档位 → 调用 executeEffort、onDone 收到正确 message
- Enter 在 ultracode → 不调 executeEffort、onDone 收到引导文案
- Esc → 不调 executeEffort、onDone 收到
"Effort unchanged."
集成(effort.tsx 的 call 函数):
- 无参 → 返回
<EffortPanel>JSX - 有参 → 不渲染面板,走 executeEffort
波纹相关(第二阶段)
pickChar(intensity)各强度边界computeRippleLine固定 time 快照mergeLayers文字覆盖背景、文字字符永远胜出useRippleFrame仅在 cursor === 'ultracode' 时订阅时钟
11. 实现阶段划分(两个 commit)
Commit 1:基础面板(先做)
- 新增
src/components/EffortPanel/EffortPanel.tsx - 新增
src/components/EffortPanel/effortPanelState.ts - 新增
src/components/EffortPanel/__tests__/EffortPanel.test.tsx - 新增
src/components/EffortPanel/__tests__/effortPanelState.test.ts - 改造
src/commands/effort/effort.tsx:无参时返回<EffortPanel>,有参维持原状 - 运行
bun run precheck,必须零错误通过 - commit message:
feat(effort): /effort 无参时打开横向 slider 选择面板
Commit 2:波纹动画(基础稳定后再做)
- 新增
src/components/EffortPanel/rippleAnimation.ts - 新增
src/components/EffortPanel/useRippleFrame.ts - 新增对应测试
- 在
EffortPanel.tsx中叠加渲染(仅 cursor === 'ultracode' 时) - 运行
bun run precheck - commit message:
feat(effort): ultracode 档位铺满波纹背景动画
两阶段切开的好处:动画是创意工作,可能在调参上反复;基础功能稳定后即使动画翻车也能直接 revert 第二个 commit,不影响主功能。
12. 验收清单
/effort无参打开面板,光标停在当前生效档←/→移动光标,到边界不再继续- Enter 在 5 档之一时写 settings + AppState + 输出与
/effort X同款消息 - Enter 在 ultracode 时输出引导文案,不写任何状态
- Esc 时不写任何状态,输出
"Effort unchanged." - env override 时顶部警告 + 双标记
- 模型不支持时面板禁用,仅 Esc 可退出
/effort low|auto|help|current等原有路径行为不变bun run precheck零错误