更新大量 tsx 原始文件; 已经迁移 login panel; 部分 (#121)

* style(B1-1): 格式化 ink/buddy/cli/context/screens/tasks/services/keybindings/state (43 files)

纯格式化:移除分号、React Compiler import、import 多行展开。
修复了 Box.tsx 和 ScrollBox.tsx 中无效的 global.d.ts import。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(B1-2): 格式化 commands (79 files)

纯格式化:移除分号、React Compiler import、import 多行展开。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(B1-3): 格式化 components/messages,permissions,mcp,sandbox,shell (104 files)

纯格式化:移除分号、React Compiler import、import 多行展开。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(B1-4): 格式化 components/PromptInput,FeedbackSurvey,tasks,agents,skills,design-system,wizard (73 files)

纯格式化:移除分号、React Compiler import、import 多行展开。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(B1-5): 格式化 components其余 + hooks + tools (232 files)

纯格式化:移除分号、React Compiler import、import 多行展开。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(B1-6): 格式化 main/entrypoints/utils/moreright (21 files)

纯格式化:移除分号、React Compiler import、import 多行展开。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: 更新 README,新增 Run.ps1/TODO.md,删除 V6.md

- README.md: 大幅重写,更详细版本历史和配置示例
- Run.ps1: 新增 Windows 启动脚本
- TODO.md: 新增包完成清单
- V6.md: 删除(架构重构规划已不适用)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: 修复以前的问题

* fix: 修复 login 面板的问题

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
claude-code-best
2026-04-04 23:24:27 +08:00
committed by GitHub
parent 02694918b5
commit 5b1a52b8e0
559 changed files with 103807 additions and 101817 deletions

View File

@@ -1,38 +1,51 @@
import figures from 'figures';
import React, { useCallback, useMemo, useRef, useState } from 'react';
import { useTerminalSize } from '../../../hooks/useTerminalSize.js';
import type { KeyboardEvent } from '../../../ink/events/keyboard-event.js';
import { Box, Text } from '../../../ink.js';
import { useKeybinding, useKeybindings } from '../../../keybindings/useKeybinding.js';
import { useAppState } from '../../../state/AppState.js';
import type { Question } from '../../../tools/AskUserQuestionTool/AskUserQuestionTool.js';
import { getExternalEditor } from '../../../utils/editor.js';
import { toIDEDisplayName } from '../../../utils/ide.js';
import { editPromptInEditor } from '../../../utils/promptEditor.js';
import { Divider } from '../../design-system/Divider.js';
import TextInput from '../../TextInput.js';
import { PermissionRequestTitle } from '../PermissionRequestTitle.js';
import { PreviewBox } from './PreviewBox.js';
import { QuestionNavigationBar } from './QuestionNavigationBar.js';
import type { QuestionState } from './use-multiple-choice-state.js';
import figures from 'figures'
import React, { useCallback, useMemo, useRef, useState } from 'react'
import { useTerminalSize } from '../../../hooks/useTerminalSize.js'
import type { KeyboardEvent } from '../../../ink/events/keyboard-event.js'
import { Box, Text } from '../../../ink.js'
import {
useKeybinding,
useKeybindings,
} from '../../../keybindings/useKeybinding.js'
import { useAppState } from '../../../state/AppState.js'
import type { Question } from '../../../tools/AskUserQuestionTool/AskUserQuestionTool.js'
import { getExternalEditor } from '../../../utils/editor.js'
import { toIDEDisplayName } from '../../../utils/ide.js'
import { editPromptInEditor } from '../../../utils/promptEditor.js'
import { Divider } from '../../design-system/Divider.js'
import TextInput from '../../TextInput.js'
import { PermissionRequestTitle } from '../PermissionRequestTitle.js'
import { PreviewBox } from './PreviewBox.js'
import { QuestionNavigationBar } from './QuestionNavigationBar.js'
import type { QuestionState } from './use-multiple-choice-state.js'
type Props = {
question: Question;
questions: Question[];
currentQuestionIndex: number;
answers: Record<string, string>;
questionStates: Record<string, QuestionState>;
hideSubmitTab?: boolean;
minContentHeight?: number;
minContentWidth?: number;
onUpdateQuestionState: (questionText: string, updates: Partial<QuestionState>, isMultiSelect: boolean) => void;
onAnswer: (questionText: string, label: string | string[], textInput?: string, shouldAdvance?: boolean) => void;
onTextInputFocus: (isInInput: boolean) => void;
onCancel: () => void;
onTabPrev?: () => void;
onTabNext?: () => void;
onRespondToClaude: () => void;
onFinishPlanInterview: () => void;
};
question: Question
questions: Question[]
currentQuestionIndex: number
answers: Record<string, string>
questionStates: Record<string, QuestionState>
hideSubmitTab?: boolean
minContentHeight?: number
minContentWidth?: number
onUpdateQuestionState: (
questionText: string,
updates: Partial<QuestionState>,
isMultiSelect: boolean,
) => void
onAnswer: (
questionText: string,
label: string | string[],
textInput?: string,
shouldAdvance?: boolean,
) => void
onTextInputFocus: (isInInput: boolean) => void
onCancel: () => void
onTabPrev?: () => void
onTabNext?: () => void
onRespondToClaude: () => void
onFinishPlanInterview: () => void
}
/**
* A side-by-side question view for questions with preview content.
@@ -54,188 +67,235 @@ export function PreviewQuestionView({
onTabPrev,
onTabNext,
onRespondToClaude,
onFinishPlanInterview
onFinishPlanInterview,
}: Props): React.ReactNode {
const isInPlanMode = useAppState(s => s.toolPermissionContext.mode) === 'plan';
const [isFooterFocused, setIsFooterFocused] = useState(false);
const [footerIndex, setFooterIndex] = useState(0);
const [isInNotesInput, setIsInNotesInput] = useState(false);
const [cursorOffset, setCursorOffset] = useState(0);
const editor = getExternalEditor();
const editorName = editor ? toIDEDisplayName(editor) : null;
const questionText = question.question;
const questionState = questionStates[questionText];
const isInPlanMode = useAppState(s => s.toolPermissionContext.mode) === 'plan'
const [isFooterFocused, setIsFooterFocused] = useState(false)
const [footerIndex, setFooterIndex] = useState(0)
const [isInNotesInput, setIsInNotesInput] = useState(false)
const [cursorOffset, setCursorOffset] = useState(0)
const editor = getExternalEditor()
const editorName = editor ? toIDEDisplayName(editor) : null
const questionText = question.question
const questionState = questionStates[questionText]
// Only real options — no "Other" for preview questions
const allOptions = question.options;
const allOptions = question.options
// Track which option is focused (for preview display)
const [focusedIndex, setFocusedIndex] = useState(0);
const [focusedIndex, setFocusedIndex] = useState(0)
// Reset focusedIndex when navigating to a different question
const prevQuestionText = useRef(questionText);
const prevQuestionText = useRef(questionText)
if (prevQuestionText.current !== questionText) {
prevQuestionText.current = questionText;
const selected = questionState?.selectedValue as string | undefined;
const idx = selected ? allOptions.findIndex(opt => opt.label === selected) : -1;
setFocusedIndex(idx >= 0 ? idx : 0);
prevQuestionText.current = questionText
const selected = questionState?.selectedValue as string | undefined
const idx = selected
? allOptions.findIndex(opt => opt.label === selected)
: -1
setFocusedIndex(idx >= 0 ? idx : 0)
}
const focusedOption = allOptions[focusedIndex];
const selectedValue = questionState?.selectedValue as string | undefined;
const notesValue = questionState?.textInputValue || '';
const handleSelectOption = useCallback((index: number) => {
const option = allOptions[index];
if (!option) return;
setFocusedIndex(index);
onUpdateQuestionState(questionText, {
selectedValue: option.label
}, false);
onAnswer(questionText, option.label);
}, [allOptions, questionText, onUpdateQuestionState, onAnswer]);
const handleNavigate = useCallback((direction: 'up' | 'down' | number) => {
if (isInNotesInput) return;
let newIndex: number;
if (typeof direction === 'number') {
newIndex = direction;
} else if (direction === 'up') {
newIndex = focusedIndex > 0 ? focusedIndex - 1 : focusedIndex;
} else {
newIndex = focusedIndex < allOptions.length - 1 ? focusedIndex + 1 : focusedIndex;
}
if (newIndex >= 0 && newIndex < allOptions.length) {
setFocusedIndex(newIndex);
}
}, [focusedIndex, allOptions.length, isInNotesInput]);
const focusedOption = allOptions[focusedIndex]
const selectedValue = questionState?.selectedValue as string | undefined
const notesValue = questionState?.textInputValue || ''
const handleSelectOption = useCallback(
(index: number) => {
const option = allOptions[index]
if (!option) return
setFocusedIndex(index)
onUpdateQuestionState(
questionText,
{ selectedValue: option.label },
false,
)
onAnswer(questionText, option.label)
},
[allOptions, questionText, onUpdateQuestionState, onAnswer],
)
const handleNavigate = useCallback(
(direction: 'up' | 'down' | number) => {
if (isInNotesInput) return
let newIndex: number
if (typeof direction === 'number') {
newIndex = direction
} else if (direction === 'up') {
newIndex = focusedIndex > 0 ? focusedIndex - 1 : focusedIndex
} else {
newIndex =
focusedIndex < allOptions.length - 1 ? focusedIndex + 1 : focusedIndex
}
if (newIndex >= 0 && newIndex < allOptions.length) {
setFocusedIndex(newIndex)
}
},
[focusedIndex, allOptions.length, isInNotesInput],
)
// Handle ctrl+g to open external editor for notes
useKeybinding('chat:externalEditor', async () => {
const currentValue = questionState?.textInputValue || '';
const result = await editPromptInEditor(currentValue);
if (result.content !== null && result.content !== currentValue) {
onUpdateQuestionState(questionText, {
textInputValue: result.content
}, false);
}
}, {
context: 'Chat',
isActive: isInNotesInput && !!editor
});
useKeybinding(
'chat:externalEditor',
async () => {
const currentValue = questionState?.textInputValue || ''
const result = await editPromptInEditor(currentValue)
if (result.content !== null && result.content !== currentValue) {
onUpdateQuestionState(
questionText,
{ textInputValue: result.content },
false,
)
}
},
{ context: 'Chat', isActive: isInNotesInput && !!editor },
)
// Handle left/right arrow and tab for question navigation.
// This must be in the child component (not just the parent) because child useInput
// handlers register first on the event emitter and fire before parent handlers.
// Without this, the parent's useKeybindings may not fire reliably depending on
// listener ordering in the event emitter.
useKeybindings({
'tabs:previous': () => onTabPrev?.(),
'tabs:next': () => onTabNext?.()
}, {
context: 'Tabs',
isActive: !isInNotesInput && !isFooterFocused
});
useKeybindings(
{
'tabs:previous': () => onTabPrev?.(),
'tabs:next': () => onTabNext?.(),
},
{ context: 'Tabs', isActive: !isInNotesInput && !isFooterFocused },
)
// Re-submit the answer (plain label) when exiting notes input.
// Notes are stored in questionStates and collected at submit time via annotations.
const handleNotesExit = useCallback(() => {
setIsInNotesInput(false);
onTextInputFocus(false);
setIsInNotesInput(false)
onTextInputFocus(false)
if (selectedValue) {
onAnswer(questionText, selectedValue);
onAnswer(questionText, selectedValue)
}
}, [selectedValue, questionText, onAnswer, onTextInputFocus]);
}, [selectedValue, questionText, onAnswer, onTextInputFocus])
const handleDownFromPreview = useCallback(() => {
setIsFooterFocused(true);
}, []);
setIsFooterFocused(true)
}, [])
const handleUpFromFooter = useCallback(() => {
setIsFooterFocused(false);
}, []);
setIsFooterFocused(false)
}, [])
// Handle keyboard input for option/footer/notes navigation.
// Always active — the handler routes internally based on isFooterFocused/isInNotesInput.
const handleKeyDown = useCallback((e: KeyboardEvent) => {
if (isFooterFocused) {
if (e.key === 'up' || e.ctrl && e.key === 'p') {
e.preventDefault();
if (footerIndex === 0) {
handleUpFromFooter();
} else {
setFooterIndex(0);
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
if (isFooterFocused) {
if (e.key === 'up' || (e.ctrl && e.key === 'p')) {
e.preventDefault()
if (footerIndex === 0) {
handleUpFromFooter()
} else {
setFooterIndex(0)
}
return
}
return;
}
if (e.key === 'down' || e.ctrl && e.key === 'n') {
e.preventDefault();
if (isInPlanMode && footerIndex === 0) {
setFooterIndex(1);
}
return;
}
if (e.key === 'return') {
e.preventDefault();
if (footerIndex === 0) {
onRespondToClaude();
} else {
onFinishPlanInterview();
}
return;
}
if (e.key === 'escape') {
e.preventDefault();
onCancel();
}
return;
}
if (isInNotesInput) {
// In notes input mode, handle escape to exit back to option navigation
if (e.key === 'escape') {
e.preventDefault();
handleNotesExit();
}
return;
}
// Handle option navigation (vertical)
if (e.key === 'up' || e.ctrl && e.key === 'p') {
e.preventDefault();
if (focusedIndex > 0) {
handleNavigate('up');
if (e.key === 'down' || (e.ctrl && e.key === 'n')) {
e.preventDefault()
if (isInPlanMode && footerIndex === 0) {
setFooterIndex(1)
}
return
}
if (e.key === 'return') {
e.preventDefault()
if (footerIndex === 0) {
onRespondToClaude()
} else {
onFinishPlanInterview()
}
return
}
if (e.key === 'escape') {
e.preventDefault()
onCancel()
}
return
}
} else if (e.key === 'down' || e.ctrl && e.key === 'n') {
e.preventDefault();
if (focusedIndex === allOptions.length - 1) {
// At bottom of options, go to footer
handleDownFromPreview();
} else {
handleNavigate('down');
if (isInNotesInput) {
// In notes input mode, handle escape to exit back to option navigation
if (e.key === 'escape') {
e.preventDefault()
handleNotesExit()
}
return
}
} else if (e.key === 'return') {
e.preventDefault();
handleSelectOption(focusedIndex);
} else if (e.key === 'n' && !e.ctrl && !e.meta) {
// Press 'n' to focus the notes input
e.preventDefault();
setIsInNotesInput(true);
onTextInputFocus(true);
} else if (e.key === 'escape') {
e.preventDefault();
onCancel();
} else if (e.key.length === 1 && e.key >= '1' && e.key <= '9') {
e.preventDefault();
const idx_0 = parseInt(e.key, 10) - 1;
if (idx_0 < allOptions.length) {
handleNavigate(idx_0);
// Handle option navigation (vertical)
if (e.key === 'up' || (e.ctrl && e.key === 'p')) {
e.preventDefault()
if (focusedIndex > 0) {
handleNavigate('up')
}
} else if (e.key === 'down' || (e.ctrl && e.key === 'n')) {
e.preventDefault()
if (focusedIndex === allOptions.length - 1) {
// At bottom of options, go to footer
handleDownFromPreview()
} else {
handleNavigate('down')
}
} else if (e.key === 'return') {
e.preventDefault()
handleSelectOption(focusedIndex)
} else if (e.key === 'n' && !e.ctrl && !e.meta) {
// Press 'n' to focus the notes input
e.preventDefault()
setIsInNotesInput(true)
onTextInputFocus(true)
} else if (e.key === 'escape') {
e.preventDefault()
onCancel()
} else if (e.key.length === 1 && e.key >= '1' && e.key <= '9') {
e.preventDefault()
const idx = parseInt(e.key, 10) - 1
if (idx < allOptions.length) {
handleNavigate(idx)
}
}
}
}, [isFooterFocused, footerIndex, isInPlanMode, isInNotesInput, focusedIndex, allOptions.length, handleUpFromFooter, handleDownFromPreview, handleNavigate, handleSelectOption, handleNotesExit, onRespondToClaude, onFinishPlanInterview, onCancel, onTextInputFocus]);
const previewContent = focusedOption?.preview || null;
},
[
isFooterFocused,
footerIndex,
isInPlanMode,
isInNotesInput,
focusedIndex,
allOptions.length,
handleUpFromFooter,
handleDownFromPreview,
handleNavigate,
handleSelectOption,
handleNotesExit,
onRespondToClaude,
onFinishPlanInterview,
onCancel,
onTextInputFocus,
],
)
const previewContent = focusedOption?.preview || null
// The right panel's available width is terminal minus the left panel and gap.
const LEFT_PANEL_WIDTH = 30;
const GAP = 4;
const {
columns
} = useTerminalSize();
const previewMaxWidth = columns - LEFT_PANEL_WIDTH - GAP;
const LEFT_PANEL_WIDTH = 30
const GAP = 4
const { columns } = useTerminalSize()
const previewMaxWidth = columns - LEFT_PANEL_WIDTH - GAP
// Lines used within the content area that aren't preview content:
// 1: marginTop on side-by-side box
@@ -245,19 +305,34 @@ export function PreviewQuestionView({
// 1: "Chat about this" line
// 1: plan mode line (may or may not show)
// 2: help text (marginTop=1 + text)
const PREVIEW_OVERHEAD = 11;
const PREVIEW_OVERHEAD = 11
// Compute the max lines available for preview content from the parent's
// height budget to prevent terminal overflow. We do NOT pad shorter options
// to match the tallest — the outer box's minHeight handles cross-question
// layout consistency, and within-question shifts are acceptable.
const previewMaxLines = useMemo(() => {
return minContentHeight ? Math.max(1, minContentHeight - PREVIEW_OVERHEAD) : undefined;
}, [minContentHeight]);
return <Box flexDirection="column" marginTop={1} tabIndex={0} autoFocus onKeyDown={handleKeyDown}>
return minContentHeight
? Math.max(1, minContentHeight - PREVIEW_OVERHEAD)
: undefined
}, [minContentHeight])
return (
<Box
flexDirection="column"
marginTop={1}
tabIndex={0}
autoFocus
onKeyDown={handleKeyDown}
>
<Divider color="inactive" />
<Box flexDirection="column" paddingTop={0}>
<QuestionNavigationBar questions={questions} currentQuestionIndex={currentQuestionIndex} answers={answers} hideSubmitTab={hideSubmitTab} />
<QuestionNavigationBar
questions={questions}
currentQuestionIndex={currentQuestionIndex}
answers={answers}
hideSubmitTab={hideSubmitTab}
/>
<PermissionRequestTitle title={question.question} color={'text'} />
<Box flexDirection="column" minHeight={minContentHeight}>
@@ -265,33 +340,71 @@ export function PreviewQuestionView({
<Box marginTop={1} flexDirection="row" gap={4}>
{/* Left panel: vertical option list */}
<Box flexDirection="column" width={30}>
{allOptions.map((option_0, index_0) => {
const isFocused = focusedIndex === index_0;
const isSelected = selectedValue === option_0.label;
return <Box key={option_0.label} flexDirection="row">
{isFocused ? <Text color="suggestion">{figures.pointer}</Text> : <Text> </Text>}
<Text dimColor> {index_0 + 1}.</Text>
<Text color={isSelected ? 'success' : isFocused ? 'suggestion' : undefined} bold={isFocused}>
{allOptions.map((option, index) => {
const isFocused = focusedIndex === index
const isSelected = selectedValue === option.label
return (
<Box key={option.label} flexDirection="row">
{isFocused ? (
<Text color="suggestion">{figures.pointer}</Text>
) : (
<Text> </Text>
)}
<Text dimColor> {index + 1}.</Text>
<Text
color={
isSelected
? 'success'
: isFocused
? 'suggestion'
: undefined
}
bold={isFocused}
>
{' '}
{option_0.label}
{option.label}
</Text>
{isSelected && <Text color="success"> {figures.tick}</Text>}
</Box>;
})}
</Box>
)
})}
</Box>
{/* Right panel: preview + notes */}
<Box flexDirection="column" flexGrow={1}>
<PreviewBox content={previewContent || 'No preview available'} maxLines={previewMaxLines} minWidth={minContentWidth} maxWidth={previewMaxWidth} />
<PreviewBox
content={previewContent || 'No preview available'}
maxLines={previewMaxLines}
minWidth={minContentWidth}
maxWidth={previewMaxWidth}
/>
<Box marginTop={1} flexDirection="row" gap={1}>
<Text color="suggestion">Notes:</Text>
{isInNotesInput ? <TextInput value={notesValue} placeholder="Add notes on this design…" onChange={value => {
onUpdateQuestionState(questionText, {
textInputValue: value
}, false);
}} onSubmit={handleNotesExit} onExit={handleNotesExit} focus={true} showCursor={true} columns={60} cursorOffset={cursorOffset} onChangeCursorOffset={setCursorOffset} /> : <Text dimColor italic>
{isInNotesInput ? (
<TextInput
value={notesValue}
placeholder="Add notes on this design…"
onChange={value => {
onUpdateQuestionState(
questionText,
{ textInputValue: value },
false,
)
}}
onSubmit={handleNotesExit}
onExit={handleNotesExit}
focus={true}
showCursor={true}
columns={60}
cursorOffset={cursorOffset}
onChangeCursorOffset={setCursorOffset}
/>
) : (
<Text dimColor italic>
{notesValue || 'press n to add notes'}
</Text>}
</Text>
)}
</Box>
</Box>
</Box>
@@ -300,28 +413,53 @@ export function PreviewQuestionView({
<Box flexDirection="column" marginTop={1}>
<Divider color="inactive" />
<Box flexDirection="row" gap={1}>
{isFooterFocused && footerIndex === 0 ? <Text color="suggestion">{figures.pointer}</Text> : <Text> </Text>}
<Text color={isFooterFocused && footerIndex === 0 ? 'suggestion' : undefined}>
{isFooterFocused && footerIndex === 0 ? (
<Text color="suggestion">{figures.pointer}</Text>
) : (
<Text> </Text>
)}
<Text
color={
isFooterFocused && footerIndex === 0
? 'suggestion'
: undefined
}
>
Chat about this
</Text>
</Box>
{isInPlanMode && <Box flexDirection="row" gap={1}>
{isFooterFocused && footerIndex === 1 ? <Text color="suggestion">{figures.pointer}</Text> : <Text> </Text>}
<Text color={isFooterFocused && footerIndex === 1 ? 'suggestion' : undefined}>
{isInPlanMode && (
<Box flexDirection="row" gap={1}>
{isFooterFocused && footerIndex === 1 ? (
<Text color="suggestion">{figures.pointer}</Text>
) : (
<Text> </Text>
)}
<Text
color={
isFooterFocused && footerIndex === 1
? 'suggestion'
: undefined
}
>
Skip interview and plan immediately
</Text>
</Box>}
</Box>
)}
</Box>
<Box marginTop={1}>
<Text color="inactive" dimColor>
Enter to select · {figures.arrowUp}/{figures.arrowDown} to
navigate · n to add notes
{questions.length > 1 && <> · Tab to switch questions</>}
{isInNotesInput && editorName && <> · ctrl+g to edit in {editorName}</>}{' '}
{isInNotesInput && editorName && (
<> · ctrl+g to edit in {editorName}</>
)}{' '}
· Esc to cancel
</Text>
</Box>
</Box>
</Box>
</Box>;
</Box>
)
}