From 96cc805ae574920b99c2530790188385edf8f324 Mon Sep 17 00:00:00 2001 From: claude-code-best Date: Sun, 14 Jun 2026 14:11:21 +0800 Subject: [PATCH] =?UTF-8?q?docs(effort):=20=E6=96=B0=E5=A2=9E=20/effort=20?= =?UTF-8?q?=E4=BA=A4=E4=BA=92=E9=9D=A2=E6=9D=BF=E8=AE=BE=E8=AE=A1=20spec?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 设计要点: - /effort 无参 → 横向 slider 面板(low/medium/high/xhigh/max/ultracode) - ←/→ 移动光标,Enter 确认,Esc 取消 - ultracode 仅视觉占位,确认后提示走 /ultracode - env override 时双标记 + 顶部警告 - 模型不支持时面板禁用 - 两阶段交付:先基础面板 commit,再做 ultracode 波纹动画 Co-Authored-By: glm-5.2 --- .../specs/2026-06-14-effort-panel-design.md | 394 ++++++++++++++++++ 1 file changed, 394 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-14-effort-panel-design.md diff --git a/docs/superpowers/specs/2026-06-14-effort-panel-design.md b/docs/superpowers/specs/2026-06-14-effort-panel-design.md new file mode 100644 index 000000000..3c4cbd66a --- /dev/null +++ b/docs/superpowers/specs/2026-06-14-effort-panel-design.md @@ -0,0 +1,394 @@ +# Effort 交互面板(EffortPanel)设计 + +**日期**: 2026-06-14 +**作者**: brainstorming session 产物 +**状态**: 待实施 +**关联**: `src/commands/effort/`、`src/utils/effort.ts`、`src/components/EffortPanel/`(新增) + +--- + +## 1. 概述 + +把当前的 `/effort` slash 命令从纯文本式交互升级为终端内的可视化选择面板。 + +- 触发:`/effort`(无参)打开面板;`/effort ` 直跳路径保留 +- 视觉:横向 slider,两端标 `Faster` / `Smarter`,刻度为 `low / medium / high / xhigh / max / ultracode` +- 交互:`←/→` 移动光标,`Enter` 确认,`Esc` 取消 +- ultracode 仅作视觉占位,确认后提示用户走 `/ultracode ` 启动 +- 第二阶段加波纹动画(详见 §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 为空时返回 , +│ │ 有参时维持原 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` 直接 import +- `useInput` 或 `useKeyboard`:从 `@anthropic/ink` 取 +- `` 组件:作为面板 Enter 后的"写入并退出"流程组件复用(或迁入 EffortPanel 内部) + +### 类型层面 + +不动 `EffortValue` / `EffortLevel`。面板内部用一个新类型 `PanelPosition` 表示光标位置: + +```ts +type PanelPosition = 'low' | 'medium' | 'high' | 'xhigh' | 'max' | 'ultracode'; +``` + +它仅在面板内部使用,不进入 AppState、不进入 settings.json、不参与 API 调用。 + +## 5. 交互流程 + +### 触发与初始光标 + +``` +/effort<回车>(无参) + → call() 检测 args === '' + → 渲染 + → 光标初始位置: + 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 启动多 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 + + 当前模型 不支持 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 层叠,用**字符替换**模拟: + +1. 每帧生成 `height × width` 字符矩阵(背景层) +2. 渲染每个面板行时,先取该行对应的波纹字符序列,然后在文字字符应该出现的位置**覆盖**背景字符 +3. 文字字符永远胜出,波纹只占空隙 + +### 实现位置 + +新增(第二阶段): +- `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 行 `` 节点,与现有 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? } │ +└─────────────────────────────────────────────────┘ + │ + │ 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= 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 处保持 low +- `moveRight(cursor)` 在 ultracode 处保持 ultracode +- `home(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 函数): +- 无参 → 返回 `` 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`:无参时返回 ``,有参维持原状 +- 运行 `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` 零错误