docs(effort): plan 修订执行前 review 发现的 5 处 gap

- Task 3.3 EffortPanel.tsx 草稿:Faster/Smarter padEnd 语法错乱重写;
  useKeybindings import 路径从 @anthropic/ink 修正为 ../../keybindings/useKeybinding.js;
  移除冗余 renderSeparatorLine;保留 renderPaddedLine
- Task 5.2 computeConfirmOutcome 改为注入 ApplyFn 模式:
  避免 effortPanelState → effort.tsx → EffortPanel 循环依赖;
  测试可注入 mockApply,无需 mock settings
- Step 5.3 测试代码对齐注入版签名

Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win>
This commit is contained in:
claude-code-best
2026-06-14 14:23:59 +08:00
parent 206bc80e4e
commit 14f3a1eadb

View File

@@ -406,42 +406,44 @@ Create `src/components/EffortPanel/EffortPanel.tsx`:
```tsx
import * as React from 'react'
import { Box, Text, useKeybindings } from '@anthropic/ink'
import { type EffortValue, getDisplayedEffortLevel, getEffortEnvOverride } from '../../utils/effort.js'
import { type PanelPosition, getInitialCursor, isUltracode, moveLeft, moveRight } from './effortPanelState.js'
import { executeEffort, type EffortCommandResult } from '../../commands/effort/effort.js'
import { Box, Text } from '@anthropic/ink'
import { useKeybindings } from '../../keybindings/useKeybinding.js'
import {
type EffortValue,
getDisplayedEffortLevel,
getEffortEnvOverride,
} from '../../utils/effort.js'
import {
type PanelPosition,
getInitialCursor,
isUltracode,
moveLeft,
moveRight,
PANEL_POSITIONS,
} from './effortPanelState.js'
import { executeEffort } from '../../commands/effort/effort.js'
import { useMainLoopModel } from '../../hooks/useMainLoopModel.js'
import { useAppState, useSetAppState } from '../../state/AppState.js'
import { useSetAppState } from '../../state/AppState.js'
const PANEL_WIDTH = 76 // 终端 ≥ 80 cols 时使用;窄屏适配第二阶段处理
// 终端 ≥ 80 cols 时使用;窄屏适配第二阶段处理
const PANEL_WIDTH = 76
type Props = {
appStateEffort: EffortValue | undefined
onDone: (message: string) => void
}
const POSITION_LABELS: ReadonlyArray<{ pos: PanelPosition; label: string }> = [
{ pos: 'low', label: 'low' },
{ pos: 'medium', label: 'medium' },
{ pos: 'high', label: 'high' },
{ pos: 'xhigh', label: 'xhigh' },
{ pos: 'max', label: 'max' },
{ pos: 'ultracode', label: 'ultracode' },
]
const SUBLABEL_ULTRACODE = 'xhigh + workflows'
function renderCursorLine(cursor: PanelPosition, width: number): string {
// 在 cursor 对应位置生成 ▲,对齐到下方档位文字
// 简化实现:均匀分布 6 档,▲ 落在 cursor 档的中心列
const segment = Math.floor(width / POSITION_LABELS.length)
const idx = POSITION_LABELS.findIndex(p => p.pos === cursor)
const col = segment * idx + Math.floor(segment / 2)
return ' '.repeat(col) + '▲'
// ▲ 落在每档中心列:均匀分布
function cursorColumn(cursor: PanelPosition): number {
const segment = Math.floor(PANEL_WIDTH / PANEL_POSITIONS.length)
const idx = PANEL_POSITIONS.indexOf(cursor)
return segment * idx + Math.floor(segment / 2)
}
function renderSeparatorLine(width: number, cursorCol: number): string {
return '─'.repeat(cursorCol) + '▲' + '─'.repeat(Math.max(0, width - cursorCol - 1))
function renderPaddedLine(cursor: PanelPosition): string {
const col = cursorColumn(cursor)
// ▲ 上方的"分隔线 + 光标"行:左侧 ─,到列处 ▲,右侧继续 ─
return `${'─'.repeat(col)}${'─'.repeat(Math.max(0, PANEL_WIDTH - col - 1))}`
}
export function EffortPanel({ appStateEffort, onDone }: Props): React.ReactNode {
@@ -460,13 +462,18 @@ export function EffortPanel({ appStateEffort, onDone }: Props): React.ReactNode
setDone(true)
if (isUltracode(cursor)) {
onDone('ultracode 不是 effort 档位。请使用 /ultracode <context> 启动多 agent workflow。')
onDone(
'ultracode 不是 effort 档位。请使用 /ultracode <context> 启动多 agent workflow。',
)
return
}
const result: EffortCommandResult = executeEffort(cursor)
const result = executeEffort(cursor)
if (result.effortUpdate) {
setAppState(prev => ({ ...prev, effortValue: result.effortUpdate!.value }))
setAppState(prev => ({
...prev,
effortValue: result.effortUpdate!.value,
}))
}
onDone(result.message)
}, [cursor, done, onDone, setAppState])
@@ -492,21 +499,31 @@ export function EffortPanel({ appStateEffort, onDone }: Props): React.ReactNode
const envActive = envOverride !== null && envOverride !== undefined
const envRaw = process.env.CLAUDE_CODE_EFFORT_LEVEL
// 两极文字行:左 Faster + 中间空格 + 右 Smarter
const fasterLen = 'Faster'.length
const smarterLen = 'Smarter'.length
const gap = Math.max(0, PANEL_WIDTH - fasterLen - smarterLen)
const poleLine = `Faster${' '.repeat(gap)}Smarter`
return (
<Box flexDirection="column" paddingX={1}>
<Text bold>Effort</Text>
{envActive && (
<Text color="yellow"> CLAUDE_CODE_EFFORT_LEVEL={envRaw} overrides this session</Text>
<Text color="yellow">
CLAUDE_CODE_EFFORT_LEVEL={envRaw} overrides this session
</Text>
)}
<Box marginTop={1}>
<Text>
{'Faster'.padEnd(PANEL_WIDTH - 'Smarter'.length)}Smarter'.replace(/Smarter'.*/, 'Smarter')}
</Text>
<Text>{poleLine}</Text>
</Box>
<Text>{'─'.repeat(PANEL_WIDTH)}</Text>
<Text>{renderCursorLine(cursor, PANEL_WIDTH)}</Text>
<Text>{POSITION_LABELS.map(p => p.label.padEnd(11)).join('').trimEnd()}</Text>
<Text dimColor>{' '.repeat(PANEL_WIDTH - SUBLABEL_ULTRACODE.length)}{SUBLABEL_ULTRACODE}</Text>
<Text>{renderPaddedLine(cursor)}</Text>
<Text>
{PANEL_POSITIONS.map(p => (p as string).padEnd(11)).join('').trimEnd()}
</Text>
<Text dimColor>
{' '.repeat(Math.max(0, PANEL_WIDTH - 'xhigh + workflows'.length))}
xhigh + workflows
</Text>
<Box marginTop={1}>
<Text dimColor>/ adjust · Enter confirm · Esc cancel</Text>
</Box>
@@ -515,13 +532,9 @@ export function EffortPanel({ appStateEffort, onDone }: Props): React.ReactNode
}
```
> ⚠️ 上面的字符串 padEnd/对齐是占位实现,第一版允许略微错位视觉精度在第二阶段调优。重点是标题、6 档名、底栏提示、▲ 标记必须出现。
> ⚠️ 对齐是粗糙实现padEnd 11 假设每档名宽度 ≤ 11实际 'ultracode' = 9 字符OK'xhigh' = 5第一版允许略微错位视觉精度在第二阶段调优。重点是标题、6 档名、底栏提示、▲ 标记必须出现。
> **Step 3.3 备注(如无 ink render helper** Step 3.1 的渲染断言测试替换为:
> - 用 `@anthropic/ink` 的 `render` 直接 render并通过键盘事件触发 callback
> - 或抽出 `handleConfirm`/`handleCancel` 为可单测的纯函数(接收 cursor 与 deps返回 onDone 参数)
>
> 优先采用抽出纯函数的方案,避免依赖 Ink 测试基础设施。
> **Step 3.3 备注(如无 ink render helper** Step 5 走纯函数抽取方案测分支;渲染层只做"包含字符串"断言。
- [ ] **Step 3.4: 运行测试,确认通过**
@@ -675,67 +688,131 @@ EOF
Ink 组件键盘测试在项目里没有现成 helper已通过 Task 3.2 探查确认)。直接走 **Step 5.2 的纯函数抽取方案**——把确认/取消决策逻辑抽到 `effortPanelState.ts`,用纯函数测试覆盖分支。键盘 → handler 的连接由 `useKeybindings` 注册保证,**不**单独测(与 `ModelPicker` 测试策略一致)。
- [ ] **Step 5.2: 抽取确认/取消为可测纯函数(可选重构**
- [ ] **Step 5.2: 抽取确认/取消为可测纯函数(注入 applyFn 避免循环依赖**
如 Step 5.1 测试无法直接做,`handleConfirm`/`handleCancel``effortPanelState.ts`
`handleConfirm`/`handleCancel` 的决策逻辑抽到 `effortPanelState.ts`**接受 `applyFn` 作为参数注入**,避免 `effortPanelState.ts``effort.tsx``EffortPanel.tsx``effortPanelState.ts` 的循环依赖,也避免测试触碰真实 settings。
`effortPanelState.ts` 末尾追加:
```ts
// 在 effortPanelState.ts 末尾追加
import type { EffortCommandResult } from '../../commands/effort/effort.js'
export type ConfirmOutcome =
| { kind: 'apply'; result: EffortCommandResult }
| {
kind: 'apply'
message: string
effortUpdate?: { value: EffortValue | undefined }
}
| { kind: 'ultracode-hint'; message: string }
export function computeConfirmOutcome(cursor: PanelPosition): ConfirmOutcome {
if (isUltracode(cursor)) {
return {
kind: 'ultracode-hint',
message: 'ultracode 不是 effort 档位。请使用 /ultracode <context> 启动多 agent workflow。',
}
}
return { kind: 'apply', result: executeEffort(cursor) }
}
export type ApplyFn = (
cursor: PanelPosition,
) => { message: string; effortUpdate?: { value: EffortValue | undefined } }
export const ULTRACODE_HINT =
'ultracode 不是 effort 档位。请使用 /ultracode <context> 启动多 agent workflow。'
export const CANCEL_MESSAGE = 'Effort unchanged.'
export function computeConfirmOutcome(cursor: PanelPosition, applyFn: ApplyFn): ConfirmOutcome {
if (isUltracode(cursor)) {
return { kind: 'ultracode-hint', message: ULTRACODE_HINT }
}
const result = applyFn(cursor)
return {
kind: 'apply',
message: result.message,
effortUpdate: result.effortUpdate,
}
}
```
然后在 `EffortPanel.tsx` 里改用 `computeConfirmOutcome(cursor)`
然后在 `EffortPanel.tsx` 里改用
> 注意:`executeEffort` 在 `effort.tsx` 里,从 effortPanelState import 会形成循环依赖effort.tsx 已 import EffortPanel。解决把 `executeEffort` 抽到独立文件 `src/commands/effort/executeEffort.ts`,或者把 `computeConfirmOutcome` 放在 `EffortPanel.tsx` 内部并接受 `executeEffort` 作为参数注入。**优先选第二种**(不增加文件、不引入循环)。
```tsx
// 顶部 import 新增
import {
type PanelPosition,
computeConfirmOutcome,
getInitialCursor,
isUltracode, // 不再需要computeConfirmOutcome 内部已用
moveLeft,
moveRight,
PANEL_POSITIONS,
} from './effortPanelState.js'
import { executeEffort } from '../../commands/effort/effort.js'
- [ ] **Step 5.3: 写分支测试(用纯函数版本)**
// handleConfirm 改为
const handleConfirm = React.useCallback(() => {
if (done) return
setDone(true)
const outcome = computeConfirmOutcome(cursor, executeEffort)
if (outcome.kind === 'apply' && outcome.effortUpdate) {
setAppState(prev => ({
...prev,
effortValue: outcome.effortUpdate!.value,
}))
}
onDone(outcome.message)
}, [cursor, done, onDone, setAppState])
`effortPanelState.test.ts` 或新建 `EffortPanel.test.tsx` 中:
// handleCancel 改为
const handleCancel = React.useCallback(() => {
if (done) return
setDone(true)
onDone(CANCEL_MESSAGE)
}, [done, onDone])
```
注意 import 里也加 `CANCEL_MESSAGE`
- [ ] **Step 5.3: 写分支测试(用注入版纯函数)**
`effortPanelState.test.ts` 末尾追加:
```ts
import { computeConfirmOutcome, CANCEL_MESSAGE } from '../effortPanelState.js'
import {
CANCEL_MESSAGE,
computeConfirmOutcome,
ULTRACODE_HINT,
type ApplyFn,
} from '../effortPanelState.js'
describe('computeConfirmOutcome', () => {
test('ultracode → kind=ultracode-hint含引导文案', () => {
const out = computeConfirmOutcome('ultracode')
expect(out.kind).toBe('ultracode-hint')
expect(out.kind === 'ultracode-hint' && out.message).toContain('/ultracode')
const mockApply: ApplyFn = cursor => ({
message: `applied:${cursor}`,
effortUpdate: { value: cursor as any },
})
test('low → kind=applyresult.message 非空', () => {
const out = computeConfirmOutcome('low')
expect(out.kind).toBe('apply')
// executeEffort 在测试环境可能因为 settings 不可写而失败,但仍要返回 message
if (out.kind === 'apply') {
expect(typeof out.result.message).toBe('string')
expect(out.result.message.length).toBeGreaterThan(0)
test('ultracode → kind=ultracode-hint含 /ultracode 引导', () => {
const out = computeConfirmOutcome('ultracode', mockApply)
expect(out.kind).toBe('ultracode-hint')
if (out.kind === 'ultracode-hint') {
expect(out.message).toBe(ULTRACODE_HINT)
expect(out.message).toContain('/ultracode')
}
})
test('low → kind=applymessage 来自 applyFneffortUpdate 透传', () => {
const out = computeConfirmOutcome('low', mockApply)
expect(out.kind).toBe('apply')
if (out.kind === 'apply') {
expect(out.message).toBe('applied:low')
expect(out.effortUpdate?.value).toBe('low')
}
})
test('high → apply 路径不调 ultracode 分支', () => {
const out = computeConfirmOutcome('high', mockApply)
expect(out.kind).toBe('apply')
})
})
test('CANCEL_MESSAGE', () => {
test('常量字符串', () => {
expect(CANCEL_MESSAGE).toBe('Effort unchanged.')
expect(ULTRACODE_HINT).toContain('/ultracode <context>')
})
```
> 注意:`computeConfirmOutcome` 调 `executeEffort`,后者会触发 `updateSettingsForSource` 写盘。测试需 mock `settings/settings.js`(参考 `tests/mocks/`
注意:因注入 mockApply**完全不需要 mock settings**——这是注入方案的最大红利
- [ ] **Step 5.4: 跑测试**