mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-22 00:05: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,257 +1,300 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import chalk from 'chalk';
|
||||
import type { UUID } from 'crypto';
|
||||
import figures from 'figures';
|
||||
import * as React from 'react';
|
||||
import { getOriginalCwd, getSessionId } from '../../bootstrap/state.js';
|
||||
import type { CommandResultDisplay, ResumeEntrypoint } from '../../commands.js';
|
||||
import { LogSelector } from '../../components/LogSelector.js';
|
||||
import { MessageResponse } from '../../components/MessageResponse.js';
|
||||
import { Spinner } from '../../components/Spinner.js';
|
||||
import { useIsInsideModal } from '../../context/modalContext.js';
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
|
||||
import { setClipboard } from '../../ink/termio/osc.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js';
|
||||
import type { LogOption } from '../../types/logs.js';
|
||||
import { agenticSessionSearch } from '../../utils/agenticSessionSearch.js';
|
||||
import { checkCrossProjectResume } from '../../utils/crossProjectResume.js';
|
||||
import { getWorktreePaths } from '../../utils/getWorktreePaths.js';
|
||||
import { logError } from '../../utils/log.js';
|
||||
import { getLastSessionLog, getSessionIdFromLog, isCustomTitleEnabled, isLiteLog, loadAllProjectsMessageLogs, loadFullLog, loadSameRepoMessageLogs, searchSessionsByCustomTitle } from '../../utils/sessionStorage.js';
|
||||
import { validateUuid } from '../../utils/uuid.js';
|
||||
type ResumeResult = {
|
||||
resultType: 'sessionNotFound';
|
||||
arg: string;
|
||||
} | {
|
||||
resultType: 'multipleMatches';
|
||||
arg: string;
|
||||
count: number;
|
||||
};
|
||||
import chalk from 'chalk'
|
||||
import type { UUID } from 'crypto'
|
||||
import figures from 'figures'
|
||||
import * as React from 'react'
|
||||
import { getOriginalCwd, getSessionId } from '../../bootstrap/state.js'
|
||||
import type { CommandResultDisplay, ResumeEntrypoint } from '../../commands.js'
|
||||
import { LogSelector } from '../../components/LogSelector.js'
|
||||
import { MessageResponse } from '../../components/MessageResponse.js'
|
||||
import { Spinner } from '../../components/Spinner.js'
|
||||
import { useIsInsideModal } from '../../context/modalContext.js'
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
|
||||
import { setClipboard } from '../../ink/termio/osc.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js'
|
||||
import type { LogOption } from '../../types/logs.js'
|
||||
import { agenticSessionSearch } from '../../utils/agenticSessionSearch.js'
|
||||
import { checkCrossProjectResume } from '../../utils/crossProjectResume.js'
|
||||
import { getWorktreePaths } from '../../utils/getWorktreePaths.js'
|
||||
import { logError } from '../../utils/log.js'
|
||||
import {
|
||||
getLastSessionLog,
|
||||
getSessionIdFromLog,
|
||||
isCustomTitleEnabled,
|
||||
isLiteLog,
|
||||
loadAllProjectsMessageLogs,
|
||||
loadFullLog,
|
||||
loadSameRepoMessageLogs,
|
||||
searchSessionsByCustomTitle,
|
||||
} from '../../utils/sessionStorage.js'
|
||||
import { validateUuid } from '../../utils/uuid.js'
|
||||
|
||||
type ResumeResult =
|
||||
| { resultType: 'sessionNotFound'; arg: string }
|
||||
| { resultType: 'multipleMatches'; arg: string; count: number }
|
||||
|
||||
function resumeHelpMessage(result: ResumeResult): string {
|
||||
switch (result.resultType) {
|
||||
case 'sessionNotFound':
|
||||
return `Session ${chalk.bold(result.arg)} was not found.`;
|
||||
return `Session ${chalk.bold(result.arg)} was not found.`
|
||||
case 'multipleMatches':
|
||||
return `Found ${result.count} sessions matching ${chalk.bold(result.arg)}. Please use /resume to pick a specific session.`;
|
||||
return `Found ${result.count} sessions matching ${chalk.bold(result.arg)}. Please use /resume to pick a specific session.`
|
||||
}
|
||||
}
|
||||
function ResumeError(t0) {
|
||||
const $ = _c(10);
|
||||
const {
|
||||
message,
|
||||
args,
|
||||
onDone
|
||||
} = t0;
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== onDone) {
|
||||
t1 = () => {
|
||||
const timer = setTimeout(onDone, 0);
|
||||
return () => clearTimeout(timer);
|
||||
};
|
||||
t2 = [onDone];
|
||||
$[0] = onDone;
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
t2 = $[2];
|
||||
}
|
||||
React.useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[3] !== args) {
|
||||
t3 = <Text dimColor={true}>{figures.pointer} /resume {args}</Text>;
|
||||
$[3] = args;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
let t4;
|
||||
if ($[5] !== message) {
|
||||
t4 = <MessageResponse><Text>{message}</Text></MessageResponse>;
|
||||
$[5] = message;
|
||||
$[6] = t4;
|
||||
} else {
|
||||
t4 = $[6];
|
||||
}
|
||||
let t5;
|
||||
if ($[7] !== t3 || $[8] !== t4) {
|
||||
t5 = <Box flexDirection="column">{t3}{t4}</Box>;
|
||||
$[7] = t3;
|
||||
$[8] = t4;
|
||||
$[9] = t5;
|
||||
} else {
|
||||
t5 = $[9];
|
||||
}
|
||||
return t5;
|
||||
|
||||
function ResumeError({
|
||||
message,
|
||||
args,
|
||||
onDone,
|
||||
}: {
|
||||
message: string
|
||||
args: string
|
||||
onDone: () => void
|
||||
}): React.ReactNode {
|
||||
React.useEffect(() => {
|
||||
const timer = setTimeout(onDone, 0)
|
||||
return () => clearTimeout(timer)
|
||||
}, [onDone])
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text dimColor>
|
||||
{figures.pointer} /resume {args}
|
||||
</Text>
|
||||
<MessageResponse>
|
||||
<Text>{message}</Text>
|
||||
</MessageResponse>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
function ResumeCommand({
|
||||
onDone,
|
||||
onResume
|
||||
onResume,
|
||||
}: {
|
||||
onDone: (result?: string, options?: {
|
||||
display?: CommandResultDisplay;
|
||||
}) => void;
|
||||
onResume: (sessionId: UUID, log: LogOption, entrypoint: ResumeEntrypoint) => Promise<void>;
|
||||
onDone: (
|
||||
result?: string,
|
||||
options?: { display?: CommandResultDisplay },
|
||||
) => void
|
||||
onResume: (
|
||||
sessionId: UUID,
|
||||
log: LogOption,
|
||||
entrypoint: ResumeEntrypoint,
|
||||
) => Promise<void>
|
||||
}): React.ReactNode {
|
||||
const [logs, setLogs] = React.useState<LogOption[]>([]);
|
||||
const [worktreePaths, setWorktreePaths] = React.useState<string[]>([]);
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [resuming, setResuming] = React.useState(false);
|
||||
const [showAllProjects, setShowAllProjects] = React.useState(false);
|
||||
const {
|
||||
rows
|
||||
} = useTerminalSize();
|
||||
const insideModal = useIsInsideModal();
|
||||
const loadLogs = React.useCallback(async (allProjects: boolean, paths: string[]) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const allLogs = allProjects ? await loadAllProjectsMessageLogs() : await loadSameRepoMessageLogs(paths);
|
||||
const resumable = filterResumableSessions(allLogs, getSessionId());
|
||||
if (resumable.length === 0) {
|
||||
onDone('No conversations found to resume');
|
||||
return;
|
||||
const [logs, setLogs] = React.useState<LogOption[]>([])
|
||||
const [worktreePaths, setWorktreePaths] = React.useState<string[]>([])
|
||||
const [loading, setLoading] = React.useState(true)
|
||||
const [resuming, setResuming] = React.useState(false)
|
||||
const [showAllProjects, setShowAllProjects] = React.useState(false)
|
||||
const { rows } = useTerminalSize()
|
||||
const insideModal = useIsInsideModal()
|
||||
|
||||
const loadLogs = React.useCallback(
|
||||
async (allProjects: boolean, paths: string[]) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const allLogs = allProjects
|
||||
? await loadAllProjectsMessageLogs()
|
||||
: await loadSameRepoMessageLogs(paths)
|
||||
const resumable = filterResumableSessions(allLogs, getSessionId())
|
||||
if (resumable.length === 0) {
|
||||
onDone('No conversations found to resume')
|
||||
return
|
||||
}
|
||||
setLogs(resumable)
|
||||
} catch (_err) {
|
||||
onDone('Failed to load conversations')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
setLogs(resumable);
|
||||
} catch (_err) {
|
||||
onDone('Failed to load conversations');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [onDone]);
|
||||
},
|
||||
[onDone],
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
async function init() {
|
||||
const paths_0 = await getWorktreePaths(getOriginalCwd());
|
||||
setWorktreePaths(paths_0);
|
||||
void loadLogs(false, paths_0);
|
||||
const paths = await getWorktreePaths(getOriginalCwd())
|
||||
setWorktreePaths(paths)
|
||||
void loadLogs(false, paths)
|
||||
}
|
||||
void init();
|
||||
}, [loadLogs]);
|
||||
void init()
|
||||
}, [loadLogs])
|
||||
|
||||
const handleToggleAllProjects = React.useCallback(() => {
|
||||
const newValue = !showAllProjects;
|
||||
setShowAllProjects(newValue);
|
||||
void loadLogs(newValue, worktreePaths);
|
||||
}, [showAllProjects, loadLogs, worktreePaths]);
|
||||
const newValue = !showAllProjects
|
||||
setShowAllProjects(newValue)
|
||||
void loadLogs(newValue, worktreePaths)
|
||||
}, [showAllProjects, loadLogs, worktreePaths])
|
||||
|
||||
async function handleSelect(log: LogOption) {
|
||||
const sessionId = validateUuid(getSessionIdFromLog(log));
|
||||
const sessionId = validateUuid(getSessionIdFromLog(log))
|
||||
if (!sessionId) {
|
||||
onDone('Failed to resume conversation');
|
||||
return;
|
||||
onDone('Failed to resume conversation')
|
||||
return
|
||||
}
|
||||
|
||||
// Load full messages for lite logs
|
||||
const fullLog = isLiteLog(log) ? await loadFullLog(log) : log;
|
||||
const fullLog = isLiteLog(log) ? await loadFullLog(log) : log
|
||||
|
||||
// Check if this conversation is from a different directory
|
||||
const crossProjectCheck = checkCrossProjectResume(fullLog, showAllProjects, worktreePaths);
|
||||
const crossProjectCheck = checkCrossProjectResume(
|
||||
fullLog,
|
||||
showAllProjects,
|
||||
worktreePaths,
|
||||
)
|
||||
if (crossProjectCheck.isCrossProject) {
|
||||
if (crossProjectCheck.isSameRepoWorktree) {
|
||||
// Same repo worktree - can resume directly
|
||||
setResuming(true);
|
||||
void onResume(sessionId, fullLog, 'slash_command_picker');
|
||||
return;
|
||||
setResuming(true)
|
||||
void onResume(sessionId, fullLog, 'slash_command_picker')
|
||||
return
|
||||
}
|
||||
|
||||
// Different project - show command instead of resuming
|
||||
const crossCmd = (crossProjectCheck as { isCrossProject: true; isSameRepoWorktree: false; command: string }).command;
|
||||
const raw = await setClipboard(crossCmd);
|
||||
if (raw) process.stdout.write(raw);
|
||||
const raw = await setClipboard(crossProjectCheck.command)
|
||||
if (raw) process.stdout.write(raw)
|
||||
|
||||
// Format the output message
|
||||
const message = ['', 'This conversation is from a different directory.', '', 'To resume, run:', ` ${crossCmd}`, '', '(Command copied to clipboard)', ''].join('\n');
|
||||
onDone(message, {
|
||||
display: 'user'
|
||||
});
|
||||
return;
|
||||
const message = [
|
||||
'',
|
||||
'This conversation is from a different directory.',
|
||||
'',
|
||||
'To resume, run:',
|
||||
` ${crossProjectCheck.command}`,
|
||||
'',
|
||||
'(Command copied to clipboard)',
|
||||
'',
|
||||
].join('\n')
|
||||
|
||||
onDone(message, { display: 'user' })
|
||||
return
|
||||
}
|
||||
|
||||
// Same directory - proceed with resume
|
||||
setResuming(true);
|
||||
void onResume(sessionId, fullLog, 'slash_command_picker');
|
||||
setResuming(true)
|
||||
void onResume(sessionId, fullLog, 'slash_command_picker')
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
onDone('Resume cancelled', {
|
||||
display: 'system'
|
||||
});
|
||||
onDone('Resume cancelled', { display: 'system' })
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <Box>
|
||||
return (
|
||||
<Box>
|
||||
<Spinner />
|
||||
<Text> Loading conversations…</Text>
|
||||
</Box>;
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
if (resuming) {
|
||||
return <Box>
|
||||
return (
|
||||
<Box>
|
||||
<Spinner />
|
||||
<Text> Resuming conversation…</Text>
|
||||
</Box>;
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
return <LogSelector logs={logs} maxHeight={insideModal ? Math.floor(rows / 2) : rows - 2} onCancel={handleCancel} onSelect={handleSelect} onLogsChanged={() => loadLogs(showAllProjects, worktreePaths)} showAllProjects={showAllProjects} onToggleAllProjects={handleToggleAllProjects} onAgenticSearch={agenticSessionSearch} />;
|
||||
|
||||
return (
|
||||
<LogSelector
|
||||
logs={logs}
|
||||
maxHeight={insideModal ? Math.floor(rows / 2) : rows - 2}
|
||||
onCancel={handleCancel}
|
||||
onSelect={handleSelect}
|
||||
onLogsChanged={() => loadLogs(showAllProjects, worktreePaths)}
|
||||
showAllProjects={showAllProjects}
|
||||
onToggleAllProjects={handleToggleAllProjects}
|
||||
onAgenticSearch={agenticSessionSearch}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export function filterResumableSessions(logs: LogOption[], currentSessionId: string): LogOption[] {
|
||||
return logs.filter(l => !l.isSidechain && getSessionIdFromLog(l) !== currentSessionId);
|
||||
|
||||
export function filterResumableSessions(
|
||||
logs: LogOption[],
|
||||
currentSessionId: string,
|
||||
): LogOption[] {
|
||||
return logs.filter(
|
||||
l => !l.isSidechain && getSessionIdFromLog(l) !== currentSessionId,
|
||||
)
|
||||
}
|
||||
|
||||
export const call: LocalJSXCommandCall = async (onDone, context, args) => {
|
||||
const onResume = async (sessionId: UUID, log: LogOption, entrypoint: ResumeEntrypoint) => {
|
||||
const onResume = async (
|
||||
sessionId: UUID,
|
||||
log: LogOption,
|
||||
entrypoint: ResumeEntrypoint,
|
||||
) => {
|
||||
try {
|
||||
await context.resume?.(sessionId, log, entrypoint);
|
||||
onDone(undefined, {
|
||||
display: 'skip'
|
||||
});
|
||||
await context.resume?.(sessionId, log, entrypoint)
|
||||
onDone(undefined, { display: 'skip' })
|
||||
} catch (error) {
|
||||
logError(error as Error);
|
||||
onDone(`Failed to resume: ${(error as Error).message}`);
|
||||
logError(error as Error)
|
||||
onDone(`Failed to resume: ${(error as Error).message}`)
|
||||
}
|
||||
};
|
||||
const arg = args?.trim();
|
||||
}
|
||||
|
||||
const arg = args?.trim()
|
||||
|
||||
// No argument provided - show picker
|
||||
if (!arg) {
|
||||
return <ResumeCommand key={Date.now()} onDone={onDone} onResume={onResume} />;
|
||||
return (
|
||||
<ResumeCommand key={Date.now()} onDone={onDone} onResume={onResume} />
|
||||
)
|
||||
}
|
||||
|
||||
// Load logs to search (includes same-repo worktrees)
|
||||
const worktreePaths = await getWorktreePaths(getOriginalCwd());
|
||||
const logs = await loadSameRepoMessageLogs(worktreePaths);
|
||||
const worktreePaths = await getWorktreePaths(getOriginalCwd())
|
||||
const logs = await loadSameRepoMessageLogs(worktreePaths)
|
||||
if (logs.length === 0) {
|
||||
const message = 'No conversations found to resume.';
|
||||
return <ResumeError message={message} args={arg} onDone={() => onDone(message)} />;
|
||||
const message = 'No conversations found to resume.'
|
||||
return (
|
||||
<ResumeError
|
||||
message={message}
|
||||
args={arg}
|
||||
onDone={() => onDone(message)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// First, check if arg is a valid UUID
|
||||
const maybeSessionId = validateUuid(arg);
|
||||
const maybeSessionId = validateUuid(arg)
|
||||
if (maybeSessionId) {
|
||||
const matchingLogs = logs.filter(l => getSessionIdFromLog(l) === maybeSessionId).sort((a, b) => b.modified.getTime() - a.modified.getTime());
|
||||
const matchingLogs = logs
|
||||
.filter(l => getSessionIdFromLog(l) === maybeSessionId)
|
||||
.sort((a, b) => b.modified.getTime() - a.modified.getTime())
|
||||
|
||||
if (matchingLogs.length > 0) {
|
||||
const log = matchingLogs[0]!;
|
||||
const fullLog = isLiteLog(log) ? await loadFullLog(log) : log;
|
||||
void onResume(maybeSessionId, fullLog, 'slash_command_session_id');
|
||||
return null;
|
||||
const log = matchingLogs[0]!
|
||||
const fullLog = isLiteLog(log) ? await loadFullLog(log) : log
|
||||
void onResume(maybeSessionId, fullLog, 'slash_command_session_id')
|
||||
return null
|
||||
}
|
||||
|
||||
// Enriched logs didn't find it — try direct file lookup. This handles
|
||||
// sessions filtered out by enrichLogs (e.g., first message >16KB makes
|
||||
// firstPrompt extraction fail, causing the session to be dropped).
|
||||
const directLog = await getLastSessionLog(maybeSessionId);
|
||||
const directLog = await getLastSessionLog(maybeSessionId)
|
||||
if (directLog) {
|
||||
void onResume(maybeSessionId, directLog, 'slash_command_session_id');
|
||||
return null;
|
||||
void onResume(maybeSessionId, directLog, 'slash_command_session_id')
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Next, try exact custom title match (only if feature is enabled)
|
||||
if (isCustomTitleEnabled()) {
|
||||
const titleMatches = await searchSessionsByCustomTitle(arg, {
|
||||
exact: true
|
||||
});
|
||||
exact: true,
|
||||
})
|
||||
if (titleMatches.length === 1) {
|
||||
const log = titleMatches[0]!;
|
||||
const sessionId = getSessionIdFromLog(log);
|
||||
const log = titleMatches[0]!
|
||||
const sessionId = getSessionIdFromLog(log)
|
||||
if (sessionId) {
|
||||
const fullLog = isLiteLog(log) ? await loadFullLog(log) : log;
|
||||
void onResume(sessionId, fullLog, 'slash_command_title');
|
||||
return null;
|
||||
const fullLog = isLiteLog(log) ? await loadFullLog(log) : log
|
||||
void onResume(sessionId, fullLog, 'slash_command_title')
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,16 +303,21 @@ export const call: LocalJSXCommandCall = async (onDone, context, args) => {
|
||||
const message = resumeHelpMessage({
|
||||
resultType: 'multipleMatches',
|
||||
arg,
|
||||
count: titleMatches.length
|
||||
});
|
||||
return <ResumeError message={message} args={arg} onDone={() => onDone(message)} />;
|
||||
count: titleMatches.length,
|
||||
})
|
||||
return (
|
||||
<ResumeError
|
||||
message={message}
|
||||
args={arg}
|
||||
onDone={() => onDone(message)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// No match found - show error
|
||||
const message = resumeHelpMessage({
|
||||
resultType: 'sessionNotFound',
|
||||
arg
|
||||
});
|
||||
return <ResumeError message={message} args={arg} onDone={() => onDone(message)} />;
|
||||
};
|
||||
const message = resumeHelpMessage({ resultType: 'sessionNotFound', arg })
|
||||
return (
|
||||
<ResumeError message={message} args={arg} onDone={() => onDone(message)} />
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user