mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-18 06:15: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,230 +1,300 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import type { CommandResultDisplay } from 'src/commands.js';
|
||||
import { logEvent } from 'src/services/analytics/index.js';
|
||||
import { logForDebugging } from 'src/utils/debug.js';
|
||||
import { Box, Text } from '../ink.js';
|
||||
import { execFileNoThrow } from '../utils/execFileNoThrow.js';
|
||||
import { getPlansDirectory } from '../utils/plans.js';
|
||||
import { setCwd } from '../utils/Shell.js';
|
||||
import { cleanupWorktree, getCurrentWorktreeSession, keepWorktree, killTmuxSession } from '../utils/worktree.js';
|
||||
import { Select } from './CustomSelect/select.js';
|
||||
import { Dialog } from './design-system/Dialog.js';
|
||||
import { Spinner } from './Spinner.js';
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import type { CommandResultDisplay } from 'src/commands.js'
|
||||
import { logEvent } from 'src/services/analytics/index.js'
|
||||
import { logForDebugging } from 'src/utils/debug.js'
|
||||
import { Box, Text } from '../ink.js'
|
||||
import { execFileNoThrow } from '../utils/execFileNoThrow.js'
|
||||
import { getPlansDirectory } from '../utils/plans.js'
|
||||
import { setCwd } from '../utils/Shell.js'
|
||||
import {
|
||||
cleanupWorktree,
|
||||
getCurrentWorktreeSession,
|
||||
keepWorktree,
|
||||
killTmuxSession,
|
||||
} from '../utils/worktree.js'
|
||||
import { Select } from './CustomSelect/select.js'
|
||||
import { Dialog } from './design-system/Dialog.js'
|
||||
import { Spinner } from './Spinner.js'
|
||||
|
||||
// Inline require breaks the cycle this file would otherwise close:
|
||||
// sessionStorage → commands → exit → ExitFlow → here. All call sites
|
||||
// are inside callbacks, so the lazy require never sees an undefined import.
|
||||
function recordWorktreeExit(): void {
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
;
|
||||
(require('../utils/sessionStorage.js') as typeof import('../utils/sessionStorage.js')).saveWorktreeState(null);
|
||||
;(
|
||||
require('../utils/sessionStorage.js') as typeof import('../utils/sessionStorage.js')
|
||||
).saveWorktreeState(null)
|
||||
/* eslint-enable @typescript-eslint/no-require-imports */
|
||||
}
|
||||
|
||||
type Props = {
|
||||
onDone: (result?: string, options?: {
|
||||
display?: CommandResultDisplay;
|
||||
}) => void;
|
||||
onCancel?: () => void;
|
||||
};
|
||||
onDone: (
|
||||
result?: string,
|
||||
options?: { display?: CommandResultDisplay },
|
||||
) => void
|
||||
onCancel?: () => void
|
||||
}
|
||||
|
||||
export function WorktreeExitDialog({
|
||||
onDone,
|
||||
onCancel
|
||||
onCancel,
|
||||
}: Props): React.ReactNode {
|
||||
const [status, setStatus] = useState<'loading' | 'asking' | 'keeping' | 'removing' | 'done'>('loading');
|
||||
const [changes, setChanges] = useState<string[]>([]);
|
||||
const [commitCount, setCommitCount] = useState<number>(0);
|
||||
const [resultMessage, setResultMessage] = useState<string | undefined>();
|
||||
const worktreeSession = getCurrentWorktreeSession();
|
||||
const [status, setStatus] = useState<
|
||||
'loading' | 'asking' | 'keeping' | 'removing' | 'done'
|
||||
>('loading')
|
||||
const [changes, setChanges] = useState<string[]>([])
|
||||
const [commitCount, setCommitCount] = useState<number>(0)
|
||||
const [resultMessage, setResultMessage] = useState<string | undefined>()
|
||||
const worktreeSession = getCurrentWorktreeSession()
|
||||
|
||||
useEffect(() => {
|
||||
async function loadChanges() {
|
||||
let changeLines: string[] = [];
|
||||
const gitStatus = await execFileNoThrow('git', ['status', '--porcelain']);
|
||||
let changeLines: string[] = []
|
||||
const gitStatus = await execFileNoThrow('git', ['status', '--porcelain'])
|
||||
if (gitStatus.stdout) {
|
||||
changeLines = gitStatus.stdout.split('\n').filter(_ => _.trim() !== '');
|
||||
setChanges(changeLines);
|
||||
changeLines = gitStatus.stdout.split('\n').filter(_ => _.trim() !== '')
|
||||
setChanges(changeLines)
|
||||
}
|
||||
|
||||
// Check for commits to eject
|
||||
if (worktreeSession) {
|
||||
// Get commits in worktree that are not in original branch
|
||||
const {
|
||||
stdout: commitsStr
|
||||
} = await execFileNoThrow('git', ['rev-list', '--count', `${worktreeSession.originalHeadCommit}..HEAD`]);
|
||||
const count = parseInt(commitsStr.trim()) || 0;
|
||||
setCommitCount(count);
|
||||
const { stdout: commitsStr } = await execFileNoThrow('git', [
|
||||
'rev-list',
|
||||
'--count',
|
||||
`${worktreeSession.originalHeadCommit}..HEAD`,
|
||||
])
|
||||
const count = parseInt(commitsStr.trim()) || 0
|
||||
setCommitCount(count)
|
||||
|
||||
// If no changes and no commits, clean up silently
|
||||
if (changeLines.length === 0 && count === 0) {
|
||||
setStatus('removing');
|
||||
void cleanupWorktree().then(() => {
|
||||
process.chdir(worktreeSession.originalCwd);
|
||||
setCwd(worktreeSession.originalCwd);
|
||||
recordWorktreeExit();
|
||||
getPlansDirectory.cache.clear?.();
|
||||
setResultMessage('Worktree removed (no changes)');
|
||||
}).catch(error => {
|
||||
logForDebugging(`Failed to clean up worktree: ${error}`, {
|
||||
level: 'error'
|
||||
});
|
||||
setResultMessage('Worktree cleanup failed, exiting anyway');
|
||||
}).then(() => {
|
||||
setStatus('done');
|
||||
});
|
||||
return;
|
||||
setStatus('removing')
|
||||
void cleanupWorktree()
|
||||
.then(() => {
|
||||
process.chdir(worktreeSession.originalCwd)
|
||||
setCwd(worktreeSession.originalCwd)
|
||||
recordWorktreeExit()
|
||||
getPlansDirectory.cache.clear?.()
|
||||
setResultMessage('Worktree removed (no changes)')
|
||||
})
|
||||
.catch(error => {
|
||||
logForDebugging(`Failed to clean up worktree: ${error}`, {
|
||||
level: 'error',
|
||||
})
|
||||
setResultMessage('Worktree cleanup failed, exiting anyway')
|
||||
})
|
||||
.then(() => {
|
||||
setStatus('done')
|
||||
})
|
||||
return
|
||||
} else {
|
||||
setStatus('asking');
|
||||
setStatus('asking')
|
||||
}
|
||||
}
|
||||
}
|
||||
void loadChanges();
|
||||
void loadChanges()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: intentional
|
||||
}, [worktreeSession]);
|
||||
}, [worktreeSession])
|
||||
|
||||
useEffect(() => {
|
||||
if (status === 'done') {
|
||||
onDone(resultMessage);
|
||||
onDone(resultMessage)
|
||||
}
|
||||
}, [status, onDone, resultMessage]);
|
||||
}, [status, onDone, resultMessage])
|
||||
|
||||
if (!worktreeSession) {
|
||||
onDone('No active worktree session found', {
|
||||
display: 'system'
|
||||
});
|
||||
return null;
|
||||
onDone('No active worktree session found', { display: 'system' })
|
||||
return null
|
||||
}
|
||||
|
||||
if (status === 'loading' || status === 'done') {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
async function handleSelect(value: string) {
|
||||
if (!worktreeSession) return;
|
||||
const hasTmux = Boolean(worktreeSession.tmuxSessionName);
|
||||
if (!worktreeSession) return
|
||||
|
||||
const hasTmux = Boolean(worktreeSession.tmuxSessionName)
|
||||
|
||||
if (value === 'keep' || value === 'keep-with-tmux') {
|
||||
setStatus('keeping');
|
||||
setStatus('keeping')
|
||||
logEvent('tengu_worktree_kept', {
|
||||
commits: commitCount,
|
||||
changed_files: changes.length
|
||||
});
|
||||
await keepWorktree();
|
||||
process.chdir(worktreeSession.originalCwd);
|
||||
setCwd(worktreeSession.originalCwd);
|
||||
recordWorktreeExit();
|
||||
getPlansDirectory.cache.clear?.();
|
||||
changed_files: changes.length,
|
||||
})
|
||||
await keepWorktree()
|
||||
process.chdir(worktreeSession.originalCwd)
|
||||
setCwd(worktreeSession.originalCwd)
|
||||
recordWorktreeExit()
|
||||
getPlansDirectory.cache.clear?.()
|
||||
if (hasTmux) {
|
||||
setResultMessage(`Worktree kept. Your work is saved at ${worktreeSession.worktreePath} on branch ${worktreeSession.worktreeBranch}. Reattach to tmux session with: tmux attach -t ${worktreeSession.tmuxSessionName}`);
|
||||
setResultMessage(
|
||||
`Worktree kept. Your work is saved at ${worktreeSession.worktreePath} on branch ${worktreeSession.worktreeBranch}. Reattach to tmux session with: tmux attach -t ${worktreeSession.tmuxSessionName}`,
|
||||
)
|
||||
} else {
|
||||
setResultMessage(`Worktree kept. Your work is saved at ${worktreeSession.worktreePath} on branch ${worktreeSession.worktreeBranch}`);
|
||||
setResultMessage(
|
||||
`Worktree kept. Your work is saved at ${worktreeSession.worktreePath} on branch ${worktreeSession.worktreeBranch}`,
|
||||
)
|
||||
}
|
||||
setStatus('done');
|
||||
setStatus('done')
|
||||
} else if (value === 'keep-kill-tmux') {
|
||||
setStatus('keeping');
|
||||
setStatus('keeping')
|
||||
logEvent('tengu_worktree_kept', {
|
||||
commits: commitCount,
|
||||
changed_files: changes.length
|
||||
});
|
||||
changed_files: changes.length,
|
||||
})
|
||||
if (worktreeSession.tmuxSessionName) {
|
||||
await killTmuxSession(worktreeSession.tmuxSessionName);
|
||||
await killTmuxSession(worktreeSession.tmuxSessionName)
|
||||
}
|
||||
await keepWorktree();
|
||||
process.chdir(worktreeSession.originalCwd);
|
||||
setCwd(worktreeSession.originalCwd);
|
||||
recordWorktreeExit();
|
||||
getPlansDirectory.cache.clear?.();
|
||||
setResultMessage(`Worktree kept at ${worktreeSession.worktreePath} on branch ${worktreeSession.worktreeBranch}. Tmux session terminated.`);
|
||||
setStatus('done');
|
||||
await keepWorktree()
|
||||
process.chdir(worktreeSession.originalCwd)
|
||||
setCwd(worktreeSession.originalCwd)
|
||||
recordWorktreeExit()
|
||||
getPlansDirectory.cache.clear?.()
|
||||
setResultMessage(
|
||||
`Worktree kept at ${worktreeSession.worktreePath} on branch ${worktreeSession.worktreeBranch}. Tmux session terminated.`,
|
||||
)
|
||||
setStatus('done')
|
||||
} else if (value === 'remove' || value === 'remove-with-tmux') {
|
||||
setStatus('removing');
|
||||
setStatus('removing')
|
||||
logEvent('tengu_worktree_removed', {
|
||||
commits: commitCount,
|
||||
changed_files: changes.length
|
||||
});
|
||||
changed_files: changes.length,
|
||||
})
|
||||
if (worktreeSession.tmuxSessionName) {
|
||||
await killTmuxSession(worktreeSession.tmuxSessionName);
|
||||
await killTmuxSession(worktreeSession.tmuxSessionName)
|
||||
}
|
||||
try {
|
||||
await cleanupWorktree();
|
||||
process.chdir(worktreeSession.originalCwd);
|
||||
setCwd(worktreeSession.originalCwd);
|
||||
recordWorktreeExit();
|
||||
getPlansDirectory.cache.clear?.();
|
||||
await cleanupWorktree()
|
||||
process.chdir(worktreeSession.originalCwd)
|
||||
setCwd(worktreeSession.originalCwd)
|
||||
recordWorktreeExit()
|
||||
getPlansDirectory.cache.clear?.()
|
||||
} catch (error) {
|
||||
logForDebugging(`Failed to clean up worktree: ${error}`, {
|
||||
level: 'error'
|
||||
});
|
||||
setResultMessage('Worktree cleanup failed, exiting anyway');
|
||||
setStatus('done');
|
||||
return;
|
||||
level: 'error',
|
||||
})
|
||||
setResultMessage('Worktree cleanup failed, exiting anyway')
|
||||
setStatus('done')
|
||||
return
|
||||
}
|
||||
const tmuxNote = hasTmux ? ' Tmux session terminated.' : '';
|
||||
const tmuxNote = hasTmux ? ' Tmux session terminated.' : ''
|
||||
if (commitCount > 0 && changes.length > 0) {
|
||||
setResultMessage(`Worktree removed. ${commitCount} ${commitCount === 1 ? 'commit' : 'commits'} and uncommitted changes were discarded.${tmuxNote}`);
|
||||
setResultMessage(
|
||||
`Worktree removed. ${commitCount} ${commitCount === 1 ? 'commit' : 'commits'} and uncommitted changes were discarded.${tmuxNote}`,
|
||||
)
|
||||
} else if (commitCount > 0) {
|
||||
setResultMessage(`Worktree removed. ${commitCount} ${commitCount === 1 ? 'commit' : 'commits'} on ${worktreeSession.worktreeBranch} ${commitCount === 1 ? 'was' : 'were'} discarded.${tmuxNote}`);
|
||||
setResultMessage(
|
||||
`Worktree removed. ${commitCount} ${commitCount === 1 ? 'commit' : 'commits'} on ${worktreeSession.worktreeBranch} ${commitCount === 1 ? 'was' : 'were'} discarded.${tmuxNote}`,
|
||||
)
|
||||
} else if (changes.length > 0) {
|
||||
setResultMessage(`Worktree removed. Uncommitted changes were discarded.${tmuxNote}`);
|
||||
setResultMessage(
|
||||
`Worktree removed. Uncommitted changes were discarded.${tmuxNote}`,
|
||||
)
|
||||
} else {
|
||||
setResultMessage(`Worktree removed.${tmuxNote}`);
|
||||
setResultMessage(`Worktree removed.${tmuxNote}`)
|
||||
}
|
||||
setStatus('done');
|
||||
setStatus('done')
|
||||
}
|
||||
}
|
||||
|
||||
if (status === 'keeping') {
|
||||
return <Box flexDirection="row" marginY={1}>
|
||||
return (
|
||||
<Box flexDirection="row" marginY={1}>
|
||||
<Spinner />
|
||||
<Text>Keeping worktree…</Text>
|
||||
</Box>;
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
if (status === 'removing') {
|
||||
return <Box flexDirection="row" marginY={1}>
|
||||
return (
|
||||
<Box flexDirection="row" marginY={1}>
|
||||
<Spinner />
|
||||
<Text>Removing worktree…</Text>
|
||||
</Box>;
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
const branchName = worktreeSession.worktreeBranch;
|
||||
const hasUncommitted = changes.length > 0;
|
||||
const hasCommits = commitCount > 0;
|
||||
let subtitle = '';
|
||||
|
||||
const branchName = worktreeSession.worktreeBranch
|
||||
const hasUncommitted = changes.length > 0
|
||||
const hasCommits = commitCount > 0
|
||||
|
||||
let subtitle = ''
|
||||
if (hasUncommitted && hasCommits) {
|
||||
subtitle = `You have ${changes.length} uncommitted ${changes.length === 1 ? 'file' : 'files'} and ${commitCount} ${commitCount === 1 ? 'commit' : 'commits'} on ${branchName}. All will be lost if you remove.`;
|
||||
subtitle = `You have ${changes.length} uncommitted ${changes.length === 1 ? 'file' : 'files'} and ${commitCount} ${commitCount === 1 ? 'commit' : 'commits'} on ${branchName}. All will be lost if you remove.`
|
||||
} else if (hasUncommitted) {
|
||||
subtitle = `You have ${changes.length} uncommitted ${changes.length === 1 ? 'file' : 'files'}. These will be lost if you remove the worktree.`;
|
||||
subtitle = `You have ${changes.length} uncommitted ${changes.length === 1 ? 'file' : 'files'}. These will be lost if you remove the worktree.`
|
||||
} else if (hasCommits) {
|
||||
subtitle = `You have ${commitCount} ${commitCount === 1 ? 'commit' : 'commits'} on ${branchName}. The branch will be deleted if you remove the worktree.`;
|
||||
subtitle = `You have ${commitCount} ${commitCount === 1 ? 'commit' : 'commits'} on ${branchName}. The branch will be deleted if you remove the worktree.`
|
||||
} else {
|
||||
subtitle = 'You are working in a worktree. Keep it to continue working there, or remove it to clean up.';
|
||||
subtitle =
|
||||
'You are working in a worktree. Keep it to continue working there, or remove it to clean up.'
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
if (onCancel) {
|
||||
// Abort exit and return to the session
|
||||
onCancel();
|
||||
return;
|
||||
onCancel()
|
||||
return
|
||||
}
|
||||
// Fallback: treat Escape as "keep" if no onCancel provided
|
||||
void handleSelect('keep');
|
||||
void handleSelect('keep')
|
||||
}
|
||||
const removeDescription = hasUncommitted || hasCommits ? 'All changes and commits will be lost.' : 'Clean up the worktree directory.';
|
||||
const hasTmuxSession = Boolean(worktreeSession.tmuxSessionName);
|
||||
const options = hasTmuxSession ? [{
|
||||
label: 'Keep worktree and tmux session',
|
||||
value: 'keep-with-tmux',
|
||||
description: `Stays at ${worktreeSession.worktreePath}. Reattach with: tmux attach -t ${worktreeSession.tmuxSessionName}`
|
||||
}, {
|
||||
label: 'Keep worktree, kill tmux session',
|
||||
value: 'keep-kill-tmux',
|
||||
description: `Keeps worktree at ${worktreeSession.worktreePath}, terminates tmux session.`
|
||||
}, {
|
||||
label: 'Remove worktree and tmux session',
|
||||
value: 'remove-with-tmux',
|
||||
description: removeDescription
|
||||
}] : [{
|
||||
label: 'Keep worktree',
|
||||
value: 'keep',
|
||||
description: `Stays at ${worktreeSession.worktreePath}`
|
||||
}, {
|
||||
label: 'Remove worktree',
|
||||
value: 'remove',
|
||||
description: removeDescription
|
||||
}];
|
||||
const defaultValue = hasTmuxSession ? 'keep-with-tmux' : 'keep';
|
||||
return <Dialog title="Exiting worktree session" subtitle={subtitle} onCancel={handleCancel}>
|
||||
<Select defaultFocusValue={defaultValue} options={options} onChange={handleSelect} />
|
||||
</Dialog>;
|
||||
|
||||
const removeDescription =
|
||||
hasUncommitted || hasCommits
|
||||
? 'All changes and commits will be lost.'
|
||||
: 'Clean up the worktree directory.'
|
||||
|
||||
const hasTmuxSession = Boolean(worktreeSession.tmuxSessionName)
|
||||
|
||||
const options = hasTmuxSession
|
||||
? [
|
||||
{
|
||||
label: 'Keep worktree and tmux session',
|
||||
value: 'keep-with-tmux',
|
||||
description: `Stays at ${worktreeSession.worktreePath}. Reattach with: tmux attach -t ${worktreeSession.tmuxSessionName}`,
|
||||
},
|
||||
{
|
||||
label: 'Keep worktree, kill tmux session',
|
||||
value: 'keep-kill-tmux',
|
||||
description: `Keeps worktree at ${worktreeSession.worktreePath}, terminates tmux session.`,
|
||||
},
|
||||
{
|
||||
label: 'Remove worktree and tmux session',
|
||||
value: 'remove-with-tmux',
|
||||
description: removeDescription,
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
label: 'Keep worktree',
|
||||
value: 'keep',
|
||||
description: `Stays at ${worktreeSession.worktreePath}`,
|
||||
},
|
||||
{
|
||||
label: 'Remove worktree',
|
||||
value: 'remove',
|
||||
description: removeDescription,
|
||||
},
|
||||
]
|
||||
|
||||
const defaultValue = hasTmuxSession ? 'keep-with-tmux' : 'keep'
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title="Exiting worktree session"
|
||||
subtitle={subtitle}
|
||||
onCancel={handleCancel}
|
||||
>
|
||||
<Select
|
||||
defaultFocusValue={defaultValue}
|
||||
options={options}
|
||||
onChange={handleSelect}
|
||||
/>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user