mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-18 14:25:51 +00:00
更新大量 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:
@@ -1,122 +1,139 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useTerminalSize } from 'src/hooks/useTerminalSize.js';
|
||||
import { type CodeSession, fetchCodeSessionsFromSessionsAPI } from 'src/utils/teleport/api.js';
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { useTerminalSize } from 'src/hooks/useTerminalSize.js'
|
||||
import {
|
||||
type CodeSession,
|
||||
fetchCodeSessionsFromSessionsAPI,
|
||||
} from 'src/utils/teleport/api.js'
|
||||
// eslint-disable-next-line custom-rules/prefer-use-keybindings -- raw j/k/arrow list navigation
|
||||
import { Box, Text, useInput } from '../ink.js';
|
||||
import { useKeybinding } from '../keybindings/useKeybinding.js';
|
||||
import { useShortcutDisplay } from '../keybindings/useShortcutDisplay.js';
|
||||
import { logForDebugging } from '../utils/debug.js';
|
||||
import { detectCurrentRepository } from '../utils/detectRepository.js';
|
||||
import { formatRelativeTime } from '../utils/format.js';
|
||||
import { ConfigurableShortcutHint } from './ConfigurableShortcutHint.js';
|
||||
import { Select } from './CustomSelect/index.js';
|
||||
import { Byline } from './design-system/Byline.js';
|
||||
import { KeyboardShortcutHint } from './design-system/KeyboardShortcutHint.js';
|
||||
import { Spinner } from './Spinner.js';
|
||||
import { TeleportError } from './TeleportError.js';
|
||||
import { Box, Text, useInput } from '../ink.js'
|
||||
import { useKeybinding } from '../keybindings/useKeybinding.js'
|
||||
import { useShortcutDisplay } from '../keybindings/useShortcutDisplay.js'
|
||||
import { logForDebugging } from '../utils/debug.js'
|
||||
import { detectCurrentRepository } from '../utils/detectRepository.js'
|
||||
import { formatRelativeTime } from '../utils/format.js'
|
||||
import { ConfigurableShortcutHint } from './ConfigurableShortcutHint.js'
|
||||
import { Select } from './CustomSelect/index.js'
|
||||
import { Byline } from './design-system/Byline.js'
|
||||
import { KeyboardShortcutHint } from './design-system/KeyboardShortcutHint.js'
|
||||
import { Spinner } from './Spinner.js'
|
||||
import { TeleportError } from './TeleportError.js'
|
||||
|
||||
type Props = {
|
||||
onSelect: (session: CodeSession) => void;
|
||||
onCancel: () => void;
|
||||
isEmbedded?: boolean;
|
||||
};
|
||||
type LoadErrorType = 'network' | 'auth' | 'api' | 'other';
|
||||
const UPDATED_STRING = 'Updated';
|
||||
const SPACE_BETWEEN_TABLE_COLUMNS = ' ';
|
||||
onSelect: (session: CodeSession) => void
|
||||
onCancel: () => void
|
||||
isEmbedded?: boolean
|
||||
}
|
||||
|
||||
type LoadErrorType = 'network' | 'auth' | 'api' | 'other'
|
||||
|
||||
const UPDATED_STRING = 'Updated'
|
||||
const SPACE_BETWEEN_TABLE_COLUMNS = ' '
|
||||
|
||||
export function ResumeTask({
|
||||
onSelect,
|
||||
onCancel,
|
||||
isEmbedded = false
|
||||
isEmbedded = false,
|
||||
}: Props): React.ReactNode {
|
||||
const {
|
||||
rows
|
||||
} = useTerminalSize();
|
||||
const [sessions, setSessions] = useState<CodeSession[]>([]);
|
||||
const [currentRepo, setCurrentRepo] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [loadErrorType, setLoadErrorType] = useState<LoadErrorType | null>(null);
|
||||
const [retrying, setRetrying] = useState(false);
|
||||
const [hasCompletedTeleportErrorFlow, setHasCompletedTeleportErrorFlow] = useState(false);
|
||||
const { rows } = useTerminalSize()
|
||||
const [sessions, setSessions] = useState<CodeSession[]>([])
|
||||
const [currentRepo, setCurrentRepo] = useState<string | null>(null)
|
||||
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [loadErrorType, setLoadErrorType] = useState<LoadErrorType | null>(null)
|
||||
const [retrying, setRetrying] = useState(false)
|
||||
|
||||
const [hasCompletedTeleportErrorFlow, setHasCompletedTeleportErrorFlow] =
|
||||
useState(false)
|
||||
|
||||
// Track focused index for scroll position display in title
|
||||
const [focusedIndex, setFocusedIndex] = useState(1);
|
||||
const escKey = useShortcutDisplay('confirm:no', 'Confirmation', 'Esc');
|
||||
const [focusedIndex, setFocusedIndex] = useState(1)
|
||||
|
||||
const escKey = useShortcutDisplay('confirm:no', 'Confirmation', 'Esc')
|
||||
|
||||
const loadSessions = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setLoadErrorType(null);
|
||||
setLoading(true)
|
||||
setLoadErrorType(null)
|
||||
|
||||
// Detect current repository
|
||||
const detectedRepo = await detectCurrentRepository();
|
||||
setCurrentRepo(detectedRepo);
|
||||
logForDebugging(`Current repository: ${detectedRepo || 'not detected'}`);
|
||||
const codeSessions = await fetchCodeSessionsFromSessionsAPI();
|
||||
const detectedRepo = await detectCurrentRepository()
|
||||
setCurrentRepo(detectedRepo)
|
||||
logForDebugging(`Current repository: ${detectedRepo || 'not detected'}`)
|
||||
|
||||
const codeSessions = await fetchCodeSessionsFromSessionsAPI()
|
||||
|
||||
// Filter sessions by current repository if detected
|
||||
let filteredSessions = codeSessions;
|
||||
let filteredSessions = codeSessions
|
||||
if (detectedRepo) {
|
||||
filteredSessions = codeSessions.filter(session => {
|
||||
if (!session.repo) return false;
|
||||
const sessionRepo = `${session.repo.owner.login}/${session.repo.name}`;
|
||||
return sessionRepo === detectedRepo;
|
||||
});
|
||||
logForDebugging(`Filtered ${filteredSessions.length} sessions for repo ${detectedRepo} from ${codeSessions.length} total`);
|
||||
if (!session.repo) return false
|
||||
const sessionRepo = `${session.repo.owner.login}/${session.repo.name}`
|
||||
return sessionRepo === detectedRepo
|
||||
})
|
||||
logForDebugging(
|
||||
`Filtered ${filteredSessions.length} sessions for repo ${detectedRepo} from ${codeSessions.length} total`,
|
||||
)
|
||||
}
|
||||
|
||||
// Sort by updated_at (newest first)
|
||||
const sortedSessions = [...filteredSessions].sort((a, b) => {
|
||||
const dateA = new Date(a.updated_at);
|
||||
const dateB = new Date(b.updated_at);
|
||||
return dateB.getTime() - dateA.getTime();
|
||||
});
|
||||
setSessions(sortedSessions);
|
||||
const dateA = new Date(a.updated_at)
|
||||
const dateB = new Date(b.updated_at)
|
||||
return dateB.getTime() - dateA.getTime()
|
||||
})
|
||||
|
||||
setSessions(sortedSessions)
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||
logForDebugging(`Error loading code sessions: ${errorMessage}`);
|
||||
setLoadErrorType(determineErrorType(errorMessage));
|
||||
const errorMessage = err instanceof Error ? err.message : String(err)
|
||||
logForDebugging(`Error loading code sessions: ${errorMessage}`)
|
||||
setLoadErrorType(determineErrorType(errorMessage))
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRetrying(false);
|
||||
setLoading(false)
|
||||
setRetrying(false)
|
||||
}
|
||||
}, []);
|
||||
}, [])
|
||||
|
||||
const handleRetry = () => {
|
||||
setRetrying(true);
|
||||
void loadSessions();
|
||||
};
|
||||
setRetrying(true)
|
||||
void loadSessions()
|
||||
}
|
||||
|
||||
// Handle escape via keybinding
|
||||
useKeybinding('confirm:no', onCancel, {
|
||||
context: 'Confirmation'
|
||||
});
|
||||
useKeybinding('confirm:no', onCancel, { context: 'Confirmation' })
|
||||
|
||||
useInput((input, key) => {
|
||||
// We need to handle ctrl+c in case we don't render a <Select>
|
||||
if (key.ctrl && input === 'c') {
|
||||
onCancel();
|
||||
return;
|
||||
onCancel()
|
||||
return
|
||||
}
|
||||
|
||||
// Handle retry in error state with 'ctrl+r'
|
||||
if (key.ctrl && input === 'r' && loadErrorType) {
|
||||
handleRetry();
|
||||
return;
|
||||
handleRetry()
|
||||
return
|
||||
}
|
||||
|
||||
// Handle enter key for error states to allow continuation with regular teleport
|
||||
if (loadErrorType !== null && key.return) {
|
||||
onCancel(); // This will continue with regular teleport flow
|
||||
return;
|
||||
onCancel() // This will continue with regular teleport flow
|
||||
return
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
const handleErrorComplete = useCallback(() => {
|
||||
setHasCompletedTeleportErrorFlow(true);
|
||||
void loadSessions();
|
||||
}, [setHasCompletedTeleportErrorFlow, loadSessions]);
|
||||
setHasCompletedTeleportErrorFlow(true)
|
||||
void loadSessions()
|
||||
}, [setHasCompletedTeleportErrorFlow, loadSessions])
|
||||
|
||||
// Show error dialog if needed
|
||||
if (!hasCompletedTeleportErrorFlow) {
|
||||
return <TeleportError onComplete={handleErrorComplete} />;
|
||||
return <TeleportError onComplete={handleErrorComplete} />
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <Box flexDirection="column" padding={1}>
|
||||
return (
|
||||
<Box flexDirection="column" padding={1}>
|
||||
<Box flexDirection="row">
|
||||
<Spinner />
|
||||
<Text bold>Loading Claude Code sessions…</Text>
|
||||
@@ -124,10 +141,13 @@ export function ResumeTask({
|
||||
<Text dimColor>
|
||||
{retrying ? 'Retrying…' : 'Fetching your Claude Code sessions…'}
|
||||
</Text>
|
||||
</Box>;
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
if (loadErrorType) {
|
||||
return <Box flexDirection="column" padding={1}>
|
||||
return (
|
||||
<Box flexDirection="column" padding={1}>
|
||||
<Text bold color="error">
|
||||
Error loading Claude Code sessions
|
||||
</Text>
|
||||
@@ -138,10 +158,13 @@ export function ResumeTask({
|
||||
Press <Text bold>Ctrl+R</Text> to retry · Press{' '}
|
||||
<Text bold>{escKey}</Text> to cancel
|
||||
</Text>
|
||||
</Box>;
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
if (sessions.length === 0) {
|
||||
return <Box flexDirection="column" padding={1}>
|
||||
return (
|
||||
<Box flexDirection="column" padding={1}>
|
||||
<Text bold>
|
||||
No Claude Code sessions found
|
||||
{currentRepo && <Text> for {currentRepo}</Text>}
|
||||
@@ -151,42 +174,53 @@ export function ResumeTask({
|
||||
Press <Text bold>{escKey}</Text> to cancel
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>;
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
const sessionMetadata = sessions.map(session_0 => ({
|
||||
...session_0,
|
||||
timeString: formatRelativeTime(new Date(session_0.updated_at))
|
||||
}));
|
||||
const maxTimeStringLength = Math.max(UPDATED_STRING.length, ...sessionMetadata.map(meta => meta.timeString.length));
|
||||
const options = sessionMetadata.map(({
|
||||
timeString,
|
||||
title,
|
||||
id
|
||||
}) => {
|
||||
const paddedTime = timeString.padEnd(maxTimeStringLength, ' ');
|
||||
|
||||
const sessionMetadata = sessions.map(session => ({
|
||||
...session,
|
||||
timeString: formatRelativeTime(new Date(session.updated_at)),
|
||||
}))
|
||||
const maxTimeStringLength = Math.max(
|
||||
UPDATED_STRING.length,
|
||||
...sessionMetadata.map(meta => meta.timeString.length),
|
||||
)
|
||||
|
||||
const options = sessionMetadata.map(({ timeString, title, id }) => {
|
||||
const paddedTime = timeString.padEnd(maxTimeStringLength, ' ')
|
||||
|
||||
// TODO: include branch name when API returns it
|
||||
return {
|
||||
label: `${paddedTime} ${title}`,
|
||||
value: id
|
||||
};
|
||||
});
|
||||
value: id,
|
||||
}
|
||||
})
|
||||
|
||||
// Adjust layout for embedded vs full-screen rendering
|
||||
// Overhead: padding (2) + title (1) + marginY (2) + header (1) + footer (1) = 7
|
||||
const layoutOverhead = 7;
|
||||
const maxVisibleOptions = Math.max(1, isEmbedded ? Math.min(sessions.length, 5, rows - 6 - layoutOverhead) : Math.min(sessions.length, rows - 1 - layoutOverhead));
|
||||
const maxHeight = maxVisibleOptions + layoutOverhead;
|
||||
const layoutOverhead = 7
|
||||
const maxVisibleOptions = Math.max(
|
||||
1,
|
||||
isEmbedded
|
||||
? Math.min(sessions.length, 5, rows - 6 - layoutOverhead)
|
||||
: Math.min(sessions.length, rows - 1 - layoutOverhead),
|
||||
)
|
||||
const maxHeight = maxVisibleOptions + layoutOverhead
|
||||
|
||||
// Show scroll position in title when list needs scrolling
|
||||
const showScrollPosition = sessions.length > maxVisibleOptions;
|
||||
return <Box flexDirection="column" padding={1} height={maxHeight}>
|
||||
const showScrollPosition = sessions.length > maxVisibleOptions
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" padding={1} height={maxHeight}>
|
||||
<Text bold>
|
||||
Select a session to resume
|
||||
{showScrollPosition && <Text dimColor>
|
||||
{showScrollPosition && (
|
||||
<Text dimColor>
|
||||
{' '}
|
||||
({focusedIndex} of {sessions.length})
|
||||
</Text>}
|
||||
</Text>
|
||||
)}
|
||||
{currentRepo && <Text dimColor> ({currentRepo})</Text>}:
|
||||
</Text>
|
||||
<Box flexDirection="column" marginTop={1} flexGrow={1}>
|
||||
@@ -197,71 +231,117 @@ export function ResumeTask({
|
||||
{'Session Title'}
|
||||
</Text>
|
||||
</Box>
|
||||
<Select visibleOptionCount={maxVisibleOptions} options={options} onChange={value => {
|
||||
const session_1 = sessions.find(s => s.id === value);
|
||||
if (session_1) {
|
||||
onSelect(session_1);
|
||||
}
|
||||
}} onFocus={value_0 => {
|
||||
const index = options.findIndex(o => o.value === value_0);
|
||||
if (index >= 0) {
|
||||
setFocusedIndex(index + 1);
|
||||
}
|
||||
}} />
|
||||
<Select
|
||||
visibleOptionCount={maxVisibleOptions}
|
||||
options={options}
|
||||
onChange={value => {
|
||||
const session = sessions.find(s => s.id === value)
|
||||
if (session) {
|
||||
onSelect(session)
|
||||
}
|
||||
}}
|
||||
onFocus={value => {
|
||||
const index = options.findIndex(o => o.value === value)
|
||||
if (index >= 0) {
|
||||
setFocusedIndex(index + 1)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box flexDirection="row">
|
||||
<Text dimColor>
|
||||
<Byline>
|
||||
<KeyboardShortcutHint shortcut="↑/↓" action="select" />
|
||||
<KeyboardShortcutHint shortcut="Enter" action="confirm" />
|
||||
<ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="cancel" />
|
||||
<ConfigurableShortcutHint
|
||||
action="confirm:no"
|
||||
context="Confirmation"
|
||||
fallback="Esc"
|
||||
description="cancel"
|
||||
/>
|
||||
</Byline>
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>;
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the type of error based on the error message
|
||||
*/
|
||||
function determineErrorType(errorMessage: string): LoadErrorType {
|
||||
const message = errorMessage.toLowerCase();
|
||||
if (message.includes('fetch') || message.includes('network') || message.includes('timeout')) {
|
||||
return 'network';
|
||||
const message = errorMessage.toLowerCase()
|
||||
|
||||
if (
|
||||
message.includes('fetch') ||
|
||||
message.includes('network') ||
|
||||
message.includes('timeout')
|
||||
) {
|
||||
return 'network'
|
||||
}
|
||||
if (message.includes('auth') || message.includes('token') || message.includes('permission') || message.includes('oauth') || message.includes('not authenticated') || message.includes('/login') || message.includes('console account') || message.includes('403')) {
|
||||
return 'auth';
|
||||
|
||||
if (
|
||||
message.includes('auth') ||
|
||||
message.includes('token') ||
|
||||
message.includes('permission') ||
|
||||
message.includes('oauth') ||
|
||||
message.includes('not authenticated') ||
|
||||
message.includes('/login') ||
|
||||
message.includes('console account') ||
|
||||
message.includes('403')
|
||||
) {
|
||||
return 'auth'
|
||||
}
|
||||
if (message.includes('api') || message.includes('rate limit') || message.includes('500') || message.includes('529')) {
|
||||
return 'api';
|
||||
|
||||
if (
|
||||
message.includes('api') ||
|
||||
message.includes('rate limit') ||
|
||||
message.includes('500') ||
|
||||
message.includes('529')
|
||||
) {
|
||||
return 'api'
|
||||
}
|
||||
return 'other';
|
||||
|
||||
return 'other'
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders error-specific troubleshooting guidance
|
||||
*/
|
||||
function renderErrorSpecificGuidance(errorType: LoadErrorType): React.ReactNode {
|
||||
function renderErrorSpecificGuidance(
|
||||
errorType: LoadErrorType,
|
||||
): React.ReactNode {
|
||||
switch (errorType) {
|
||||
case 'network':
|
||||
return <Box marginY={1} flexDirection="column">
|
||||
return (
|
||||
<Box marginY={1} flexDirection="column">
|
||||
<Text dimColor>Check your internet connection</Text>
|
||||
</Box>;
|
||||
</Box>
|
||||
)
|
||||
|
||||
case 'auth':
|
||||
return <Box marginY={1} flexDirection="column">
|
||||
return (
|
||||
<Box marginY={1} flexDirection="column">
|
||||
<Text dimColor>Teleport requires a Claude account</Text>
|
||||
<Text dimColor>
|
||||
Run <Text bold>/login</Text> and select "Claude account with
|
||||
subscription"
|
||||
</Text>
|
||||
</Box>;
|
||||
</Box>
|
||||
)
|
||||
|
||||
case 'api':
|
||||
return <Box marginY={1} flexDirection="column">
|
||||
return (
|
||||
<Box marginY={1} flexDirection="column">
|
||||
<Text dimColor>Sorry, Claude encountered an error</Text>
|
||||
</Box>;
|
||||
</Box>
|
||||
)
|
||||
|
||||
case 'other':
|
||||
return <Box marginY={1} flexDirection="row">
|
||||
return (
|
||||
<Box marginY={1} flexDirection="row">
|
||||
<Text dimColor>Sorry, Claude Code encountered an error</Text>
|
||||
</Box>;
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user