feat(effort): EffortPanel 集成波纹背景——cursor 停在 ultracode 时切换波纹模式

仅在 cursor === 'ultracode' 时启用 useRippleFrame,渲染 5 行波纹背景
+ overlay 文字(Faster/Smarter、分隔线、▲、档位名、副标签)。
其余档位保持原 PlainContent 渲染路径不动。

Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win>
This commit is contained in:
claude-code-best
2026-06-14 14:54:19 +08:00
parent 67ef7aea73
commit b9d0a4654a

View File

@@ -14,12 +14,28 @@ import {
import { executeEffort } from '../../commands/effort/effort.js';
import { useMainLoopModel } from '../../hooks/useMainLoopModel.js';
import { useSetAppState } from '../../state/AppState.js';
import { useRippleFrame } from './useRippleFrame.js';
import { type Overlay, computeRippleLine, mergeLayers } from './rippleAnimation.js';
// 每档固定宽度Ink Box 自动对齐。PANEL_WIDTH = SEGMENT * 6。
const SEGMENT = 12;
const PANEL_WIDTH = SEGMENT * PANEL_POSITIONS.length;
const SUBLABEL_ULTRACODE = 'xhigh + workflows';
// 波纹震源坐标相对波纹区域坐标系y=0 是档位名行)。
// ultracode 字符在 SEGMENT*5=60 起始段内居中9 字符 in 12 列 → 偏移 1.5 → 1
// 中心列 ≈ 60 + 1 + 4 = 65。
const RIPPLE_SOURCE_X = SEGMENT * 5 + 5;
const RIPPLE_SOURCE_Y = 0;
/**
* 计算某段 idx 内居中文字的起始列。
* 'ultracode' = 9 字符 in SEGMENT=12 → offset = floor((12-9)/2) = 1。
*/
function segmentTextStartX(idx: number, textLen: number): number {
return SEGMENT * idx + Math.max(0, Math.floor((SEGMENT - textLen) / 2));
}
type Props = {
appStateEffort: EffortValue | undefined;
onDone: (message: string) => void;
@@ -36,6 +52,9 @@ export function EffortPanel({ appStateEffort, onDone }: Props): React.ReactNode
const [cursor, setCursor] = React.useState<PanelPosition>(initialCursor);
const [done, setDone] = React.useState(false);
const rippleActive = cursor === 'ultracode';
const [rippleRef, time] = useRippleFrame(rippleActive);
const handleConfirm = React.useCallback(() => {
if (done) return;
setDone(true);
@@ -70,19 +89,49 @@ export function EffortPanel({ appStateEffort, onDone }: Props): React.ReactNode
const envActive = envOverride !== null && envOverride !== undefined;
const envRaw = process.env.CLAUDE_CODE_EFFORT_LEVEL;
// 波纹行渲染:返回 merge 后的单字符串。
const renderRippleLine = React.useCallback(
(relY: number, overlays: Overlay[]): string => {
const ripple = computeRippleLine({
y: relY + RIPPLE_SOURCE_Y,
width: PANEL_WIDTH,
time,
sourceX: RIPPLE_SOURCE_X,
sourceY: RIPPLE_SOURCE_Y,
});
return mergeLayers(ripple, overlays);
},
[time],
);
return (
<Box flexDirection="column" paddingX={1} width={PANEL_WIDTH + 2}>
<Box ref={rippleRef} flexDirection="column" paddingX={1} width={PANEL_WIDTH + 2}>
<Text bold color="suggestion">
Effort
</Text>
{envActive && <Text color="warning">{`⚠ CLAUDE_CODE_EFFORT_LEVEL=${envRaw} overrides this session`}</Text>}
{rippleActive ? (
<RippleContent time={time} renderLine={renderRippleLine} cursor={cursor} />
) : (
<PlainContent cursor={cursor} />
)}
<Box marginTop={1}>
<Text color="subtle">/ adjust · Enter confirm · Esc cancel</Text>
</Box>
</Box>
);
}
// ---- 普通模式(无波纹)----
function PlainContent({ cursor }: { cursor: PanelPosition }): React.ReactNode {
return (
<>
<Box marginTop={1} flexDirection="row" justifyContent="space-between">
<Text color="suggestion">Faster</Text>
<Text color="suggestion">Smarter</Text>
</Box>
{/* 分隔线 */}
<Text color="subtle">{'─'.repeat(PANEL_WIDTH)}</Text>
{/* ▲ 行:每段独立居中,与下方档位文字严格对齐 */}
<Box flexDirection="row">
{PANEL_POSITIONS.map(p => (
<Box key={`cursor-${p}`} width={SEGMENT} justifyContent="center">
@@ -92,7 +141,6 @@ export function EffortPanel({ appStateEffort, onDone }: Props): React.ReactNode
</Box>
))}
</Box>
{/* 档位名:选中 suggestion + bold未选中 subtle */}
<Box flexDirection="row">
{PANEL_POSITIONS.map(p => (
<Box key={`label-${p}`} width={SEGMENT} justifyContent="center">
@@ -102,16 +150,78 @@ export function EffortPanel({ appStateEffort, onDone }: Props): React.ReactNode
</Box>
))}
</Box>
{/* ultracode 副标签:右对齐到最末段 */}
<Box flexDirection="row">
<Box width={SEGMENT * (PANEL_POSITIONS.length - 1)} />
<Box width={SEGMENT} justifyContent="center">
<Text color="subtle">{SUBLABEL_ULTRACODE}</Text>
</Box>
</Box>
<Box marginTop={1}>
<Text color="subtle">/ adjust · Enter confirm · Esc cancel</Text>
</Box>
</Box>
</>
);
}
// ---- 波纹模式cursor === 'ultracode'----
type RippleContentProps = {
time: number;
renderLine: (relY: number, overlays: Overlay[]) => string;
cursor: PanelPosition;
};
function RippleContent({ renderLine }: RippleContentProps): React.ReactNode {
// 各档位名 overlay基于段中心对齐
const labelOverlays: Overlay[] = PANEL_POSITIONS.map((p, idx) => ({
text: p,
x: segmentTextStartX(idx, p.length),
}));
// ▲ overlay放在 ultracode 段中心
const cursorIdx = PANEL_POSITIONS.indexOf('ultracode');
const cursorOverlay: Overlay = {
text: '▲',
x: segmentTextStartX(cursorIdx, 1),
};
// 副标签 overlay放在 ultracode 段中心
const sublabelOverlay: Overlay = {
text: SUBLABEL_ULTRACODE,
x: segmentTextStartX(cursorIdx, SUBLABEL_ULTRACODE.length),
};
// Faster / Smarter overlay
const fasterOverlay: Overlay = { text: 'Faster', x: 0 };
const smarterOverlay: Overlay = {
text: 'Smarter',
x: PANEL_WIDTH - 'Smarter'.length,
};
// 分隔线 overlay
const separatorOverlay: Overlay = {
text: '─'.repeat(PANEL_WIDTH),
x: 0,
};
// 各行 y 坐标(相对震源 RIPPLE_SOURCE_Y = 档位名行)
// y=-3: Faster/Smarter
// y=-2: 分隔线
// y=-1: ▲
// y=0: 档位名(震源)
// y=1: 副标签
const fasterLine = renderLine(-3, [fasterOverlay, smarterOverlay]);
const separatorLine = renderLine(-2, [separatorOverlay]);
const cursorLine = renderLine(-1, [cursorOverlay]);
const labelLine = renderLine(0, labelOverlays);
const sublabelLine = renderLine(1, [sublabelOverlay]);
return (
<>
<Text color="subtle">{fasterLine}</Text>
<Text color="subtle">{separatorLine}</Text>
<Text color="subtle">{cursorLine}</Text>
<Text color="suggestion" bold>
{labelLine}
</Text>
<Text color="subtle">{sublabelLine}</Text>
</>
);
}