mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 12:55:51 +00:00
* 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>
522 lines
16 KiB
TypeScript
522 lines
16 KiB
TypeScript
import { execa } from 'execa'
|
|
import { readFile } from 'fs/promises'
|
|
import { join } from 'path'
|
|
import * as React from 'react'
|
|
import { useCallback, useEffect, useState } from 'react'
|
|
import type { CommandResultDisplay } from '../../commands.js'
|
|
import { Select } from '../../components/CustomSelect/select.js'
|
|
import { Dialog } from '../../components/design-system/Dialog.js'
|
|
import { Spinner } from '../../components/Spinner.js'
|
|
import instances from '../../ink/instances.js'
|
|
import { Box, Text } from '../../ink.js'
|
|
import { enablePluginOp } from '../../services/plugins/pluginOperations.js'
|
|
import { logForDebugging } from '../../utils/debug.js'
|
|
import { isENOENT, toError } from '../../utils/errors.js'
|
|
import { execFileNoThrow } from '../../utils/execFileNoThrow.js'
|
|
import { pathExists } from '../../utils/file.js'
|
|
import { logError } from '../../utils/log.js'
|
|
import { getPlatform } from '../../utils/platform.js'
|
|
import { clearAllCaches } from '../../utils/plugins/cacheUtils.js'
|
|
import { isPluginInstalled } from '../../utils/plugins/installedPluginsManager.js'
|
|
import {
|
|
addMarketplaceSource,
|
|
clearMarketplacesCache,
|
|
loadKnownMarketplacesConfig,
|
|
refreshMarketplace,
|
|
} from '../../utils/plugins/marketplaceManager.js'
|
|
import { OFFICIAL_MARKETPLACE_NAME } from '../../utils/plugins/officialMarketplace.js'
|
|
import { loadAllPlugins } from '../../utils/plugins/pluginLoader.js'
|
|
import { installSelectedPlugins } from '../../utils/plugins/pluginStartupCheck.js'
|
|
|
|
// Marketplace and plugin identifiers - varies by user type
|
|
const INTERNAL_MARKETPLACE_NAME = 'claude-code-marketplace'
|
|
const INTERNAL_MARKETPLACE_REPO = 'anthropics/claude-code-marketplace'
|
|
const OFFICIAL_MARKETPLACE_REPO = 'anthropics/claude-plugins-official'
|
|
|
|
function getMarketplaceName(): string {
|
|
return process.env.USER_TYPE === 'ant'
|
|
? INTERNAL_MARKETPLACE_NAME
|
|
: OFFICIAL_MARKETPLACE_NAME
|
|
}
|
|
|
|
function getMarketplaceRepo(): string {
|
|
return process.env.USER_TYPE === 'ant'
|
|
? INTERNAL_MARKETPLACE_REPO
|
|
: OFFICIAL_MARKETPLACE_REPO
|
|
}
|
|
|
|
function getPluginId(): string {
|
|
return `thinkback@${getMarketplaceName()}`
|
|
}
|
|
|
|
const SKILL_NAME = 'thinkback'
|
|
|
|
/**
|
|
* Get the thinkback skill directory from the installed plugin's cache path
|
|
*/
|
|
async function getThinkbackSkillDir(): Promise<string | null> {
|
|
const { enabled } = await loadAllPlugins()
|
|
const thinkbackPlugin = enabled.find(
|
|
p =>
|
|
p.name === 'thinkback' || (p.source && p.source.includes(getPluginId())),
|
|
)
|
|
|
|
if (!thinkbackPlugin) {
|
|
return null
|
|
}
|
|
|
|
const skillDir = join(thinkbackPlugin.path, 'skills', SKILL_NAME)
|
|
if (await pathExists(skillDir)) {
|
|
return skillDir
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
export async function playAnimation(skillDir: string): Promise<{
|
|
success: boolean
|
|
message: string
|
|
}> {
|
|
const dataPath = join(skillDir, 'year_in_review.js')
|
|
const playerPath = join(skillDir, 'player.js')
|
|
|
|
// Both files are prerequisites for the node subprocess. Read them here
|
|
// (not at call sites) so all callers get consistent error messaging. The
|
|
// subprocess runs with reject: false, so a missing file would otherwise
|
|
// silently return success. Using readFile (not access) per CLAUDE.md.
|
|
//
|
|
// Non-ENOENT errors (EACCES etc) are logged and returned as failures rather
|
|
// than thrown — the old pathExists-based code never threw, and one caller
|
|
// (handleSelect) uses `void playAnimation().then(...)` without a .catch().
|
|
try {
|
|
await readFile(dataPath)
|
|
} catch (e: unknown) {
|
|
if (isENOENT(e)) {
|
|
return {
|
|
success: false,
|
|
message: 'No animation found. Run /think-back first to generate one.',
|
|
}
|
|
}
|
|
logError(e)
|
|
return {
|
|
success: false,
|
|
message: `Could not access animation data: ${toError(e).message}`,
|
|
}
|
|
}
|
|
|
|
try {
|
|
await readFile(playerPath)
|
|
} catch (e: unknown) {
|
|
if (isENOENT(e)) {
|
|
return {
|
|
success: false,
|
|
message:
|
|
'Player script not found. The player.js file is missing from the thinkback skill.',
|
|
}
|
|
}
|
|
logError(e)
|
|
return {
|
|
success: false,
|
|
message: `Could not access player script: ${toError(e).message}`,
|
|
}
|
|
}
|
|
|
|
// Get ink instance for terminal takeover
|
|
const inkInstance = instances.get(process.stdout)
|
|
if (!inkInstance) {
|
|
return { success: false, message: 'Failed to access terminal instance' }
|
|
}
|
|
|
|
inkInstance.enterAlternateScreen()
|
|
try {
|
|
await execa('node', [playerPath], {
|
|
stdio: 'inherit',
|
|
cwd: skillDir,
|
|
reject: false,
|
|
})
|
|
} catch {
|
|
// Animation may have been interrupted (e.g., Ctrl+C)
|
|
} finally {
|
|
inkInstance.exitAlternateScreen()
|
|
}
|
|
|
|
// Open the HTML file in browser for video download
|
|
const htmlPath = join(skillDir, 'year_in_review.html')
|
|
if (await pathExists(htmlPath)) {
|
|
const platform = getPlatform()
|
|
const openCmd =
|
|
platform === 'macos'
|
|
? 'open'
|
|
: platform === 'windows'
|
|
? 'start'
|
|
: 'xdg-open'
|
|
void execFileNoThrow(openCmd, [htmlPath])
|
|
}
|
|
|
|
return { success: true, message: 'Year in review animation complete!' }
|
|
}
|
|
|
|
type InstallState =
|
|
| { phase: 'checking' }
|
|
| { phase: 'installing-marketplace' }
|
|
| { phase: 'installing-plugin' }
|
|
| { phase: 'enabling-plugin' }
|
|
| { phase: 'ready' }
|
|
| { phase: 'error'; message: string }
|
|
|
|
function ThinkbackInstaller({
|
|
onReady,
|
|
onError,
|
|
}: {
|
|
onReady: () => void
|
|
onError: (message: string) => void
|
|
}): React.ReactNode {
|
|
const [state, setState] = useState<InstallState>({ phase: 'checking' })
|
|
const [progressMessage, setProgressMessage] = useState('')
|
|
|
|
useEffect(() => {
|
|
async function checkAndInstall(): Promise<void> {
|
|
try {
|
|
// Check if marketplace is installed
|
|
const knownMarketplaces = await loadKnownMarketplacesConfig()
|
|
const marketplaceName = getMarketplaceName()
|
|
const marketplaceRepo = getMarketplaceRepo()
|
|
const pluginId = getPluginId()
|
|
const marketplaceInstalled = marketplaceName in knownMarketplaces
|
|
|
|
// Check if plugin is already installed first
|
|
const pluginAlreadyInstalled = isPluginInstalled(pluginId)
|
|
|
|
if (!marketplaceInstalled) {
|
|
// Install the marketplace
|
|
setState({ phase: 'installing-marketplace' })
|
|
logForDebugging(`Installing marketplace ${marketplaceRepo}`)
|
|
|
|
await addMarketplaceSource(
|
|
{ source: 'github', repo: marketplaceRepo },
|
|
message => {
|
|
setProgressMessage(message)
|
|
},
|
|
)
|
|
clearAllCaches()
|
|
logForDebugging(`Marketplace ${marketplaceName} installed`)
|
|
} else if (!pluginAlreadyInstalled) {
|
|
// Marketplace installed but plugin not installed - refresh to get latest plugins
|
|
// Only refresh when needed to avoid potentially destructive git operations
|
|
setState({ phase: 'installing-marketplace' })
|
|
setProgressMessage('Updating marketplace…')
|
|
logForDebugging(`Refreshing marketplace ${marketplaceName}`)
|
|
|
|
await refreshMarketplace(marketplaceName, message => {
|
|
setProgressMessage(message)
|
|
})
|
|
clearMarketplacesCache()
|
|
clearAllCaches()
|
|
logForDebugging(`Marketplace ${marketplaceName} refreshed`)
|
|
}
|
|
|
|
if (!pluginAlreadyInstalled) {
|
|
// Install the plugin
|
|
setState({ phase: 'installing-plugin' })
|
|
logForDebugging(`Installing plugin ${pluginId}`)
|
|
|
|
const result = await installSelectedPlugins([pluginId])
|
|
|
|
if (result.failed.length > 0) {
|
|
const errorMsg = result.failed
|
|
.map(f => `${f.name}: ${f.error}`)
|
|
.join(', ')
|
|
throw new Error(`Failed to install plugin: ${errorMsg}`)
|
|
}
|
|
|
|
clearAllCaches()
|
|
logForDebugging(`Plugin ${pluginId} installed`)
|
|
} else {
|
|
// Plugin is installed, check if it's enabled
|
|
const { disabled } = await loadAllPlugins()
|
|
const isDisabled = disabled.some(
|
|
p => p.name === 'thinkback' || p.source?.includes(pluginId),
|
|
)
|
|
|
|
if (isDisabled) {
|
|
// Enable the plugin
|
|
setState({ phase: 'enabling-plugin' })
|
|
logForDebugging(`Enabling plugin ${pluginId}`)
|
|
|
|
const enableResult = await enablePluginOp(pluginId)
|
|
if (!enableResult.success) {
|
|
throw new Error(
|
|
`Failed to enable plugin: ${enableResult.message}`,
|
|
)
|
|
}
|
|
|
|
clearAllCaches()
|
|
logForDebugging(`Plugin ${pluginId} enabled`)
|
|
}
|
|
}
|
|
|
|
setState({ phase: 'ready' })
|
|
onReady()
|
|
} catch (error) {
|
|
const err = toError(error)
|
|
logError(err)
|
|
setState({ phase: 'error', message: err.message })
|
|
onError(err.message)
|
|
}
|
|
}
|
|
|
|
void checkAndInstall()
|
|
}, [onReady, onError])
|
|
|
|
if (state.phase === 'error') {
|
|
return (
|
|
<Box flexDirection="column">
|
|
<Text color="error">Error: {state.message}</Text>
|
|
</Box>
|
|
)
|
|
}
|
|
|
|
if (state.phase === 'ready') {
|
|
return null
|
|
}
|
|
|
|
const statusMessage =
|
|
state.phase === 'checking'
|
|
? 'Checking thinkback installation…'
|
|
: state.phase === 'installing-marketplace'
|
|
? 'Installing marketplace…'
|
|
: state.phase === 'enabling-plugin'
|
|
? 'Enabling thinkback plugin…'
|
|
: 'Installing thinkback plugin…'
|
|
|
|
return (
|
|
<Box flexDirection="column">
|
|
<Box>
|
|
<Spinner />
|
|
<Text>{progressMessage || statusMessage}</Text>
|
|
</Box>
|
|
</Box>
|
|
)
|
|
}
|
|
|
|
type MenuAction = 'play' | 'edit' | 'fix' | 'regenerate'
|
|
type GenerativeAction = Exclude<MenuAction, 'play'>
|
|
|
|
function ThinkbackMenu({
|
|
onDone,
|
|
onAction,
|
|
skillDir,
|
|
hasGenerated,
|
|
}: {
|
|
onDone: (
|
|
result?: string,
|
|
options?: { display?: CommandResultDisplay; shouldQuery?: boolean },
|
|
) => void
|
|
onAction: (action: GenerativeAction) => void
|
|
skillDir: string
|
|
hasGenerated: boolean
|
|
}): React.ReactNode {
|
|
const [hasSelected, setHasSelected] = useState(false)
|
|
|
|
const options = hasGenerated
|
|
? [
|
|
{
|
|
label: 'Play animation',
|
|
value: 'play' as const,
|
|
description: 'Watch your year in review',
|
|
},
|
|
{
|
|
label: 'Edit content',
|
|
value: 'edit' as const,
|
|
description: 'Modify the animation',
|
|
},
|
|
{
|
|
label: 'Fix errors',
|
|
value: 'fix' as const,
|
|
description: 'Fix validation or rendering issues',
|
|
},
|
|
{
|
|
label: 'Regenerate',
|
|
value: 'regenerate' as const,
|
|
description: 'Create a new animation from scratch',
|
|
},
|
|
]
|
|
: [
|
|
{
|
|
label: "Let's go!",
|
|
value: 'regenerate' as const,
|
|
description: 'Generate your personalized animation',
|
|
},
|
|
]
|
|
|
|
function handleSelect(value: MenuAction): void {
|
|
setHasSelected(true)
|
|
if (value === 'play') {
|
|
// Play runs the terminal-takeover animation, then signal done with skip
|
|
void playAnimation(skillDir).then(() => {
|
|
onDone(undefined, { display: 'skip' })
|
|
})
|
|
} else {
|
|
onAction(value)
|
|
}
|
|
}
|
|
|
|
function handleCancel(): void {
|
|
onDone(undefined, { display: 'skip' })
|
|
}
|
|
|
|
if (hasSelected) {
|
|
return null
|
|
}
|
|
|
|
return (
|
|
<Dialog
|
|
title="Think Back on 2025 with Claude Code"
|
|
subtitle="Generate your 2025 Claude Code Think Back (takes a few minutes to run)"
|
|
onCancel={handleCancel}
|
|
color="claude"
|
|
>
|
|
<Box flexDirection="column" gap={1}>
|
|
{/* Description for first-time users */}
|
|
{!hasGenerated && (
|
|
<Box flexDirection="column">
|
|
<Text>Relive your year of coding with Claude.</Text>
|
|
<Text dimColor>
|
|
{
|
|
"We'll create a personalized ASCII animation celebrating your journey."
|
|
}
|
|
</Text>
|
|
</Box>
|
|
)}
|
|
|
|
{/* Menu */}
|
|
<Select
|
|
options={options}
|
|
onChange={handleSelect}
|
|
visibleOptionCount={5}
|
|
/>
|
|
</Box>
|
|
</Dialog>
|
|
)
|
|
}
|
|
|
|
const EDIT_PROMPT =
|
|
'Use the Skill tool to invoke the "thinkback" skill with mode=edit to modify my existing Claude Code year in review animation. Ask me what I want to change. When the animation is ready, tell the user to run /think-back again to play it.'
|
|
|
|
const FIX_PROMPT =
|
|
'Use the Skill tool to invoke the "thinkback" skill with mode=fix to fix validation or rendering errors in my existing Claude Code year in review animation. Run the validator, identify errors, and fix them. When the animation is ready, tell the user to run /think-back again to play it.'
|
|
|
|
const REGENERATE_PROMPT =
|
|
'Use the Skill tool to invoke the "thinkback" skill with mode=regenerate to create a completely new Claude Code year in review animation from scratch. Delete the existing animation and start fresh. When the animation is ready, tell the user to run /think-back again to play it.'
|
|
|
|
function ThinkbackFlow({
|
|
onDone,
|
|
}: {
|
|
onDone: (
|
|
result?: string,
|
|
options?: { display?: CommandResultDisplay; shouldQuery?: boolean },
|
|
) => void
|
|
}): React.ReactNode {
|
|
const [installComplete, setInstallComplete] = useState(false)
|
|
const [installError, setInstallError] = useState<string | null>(null)
|
|
const [skillDir, setSkillDir] = useState<string | null>(null)
|
|
const [hasGenerated, setHasGenerated] = useState<boolean | null>(null)
|
|
|
|
function handleReady(): void {
|
|
setInstallComplete(true)
|
|
}
|
|
|
|
const handleError = useCallback(
|
|
(message: string): void => {
|
|
setInstallError(message)
|
|
// Call onDone with the error message so the model can continue
|
|
onDone(
|
|
`Error with thinkback: ${message}. Try running /plugin to manually install the think-back plugin.`,
|
|
{ display: 'system' },
|
|
)
|
|
},
|
|
[onDone],
|
|
)
|
|
|
|
useEffect(() => {
|
|
if (installComplete && !skillDir && !installError) {
|
|
// Get the skill directory after installation
|
|
void getThinkbackSkillDir().then(dir => {
|
|
if (dir) {
|
|
logForDebugging(`Thinkback skill directory: ${dir}`)
|
|
setSkillDir(dir)
|
|
} else {
|
|
handleError('Could not find thinkback skill directory')
|
|
}
|
|
})
|
|
}
|
|
}, [installComplete, skillDir, installError, handleError])
|
|
|
|
// Check for generated file once we have skillDir
|
|
useEffect(() => {
|
|
if (!skillDir) {
|
|
return
|
|
}
|
|
|
|
const dataPath = join(skillDir, 'year_in_review.js')
|
|
void pathExists(dataPath).then(exists => {
|
|
logForDebugging(
|
|
`Checking for ${dataPath}: ${exists ? 'found' : 'not found'}`,
|
|
)
|
|
setHasGenerated(exists)
|
|
})
|
|
}, [skillDir])
|
|
|
|
function handleAction(action: GenerativeAction): void {
|
|
// Send prompt to model based on action
|
|
const prompts: Record<GenerativeAction, string> = {
|
|
edit: EDIT_PROMPT,
|
|
fix: FIX_PROMPT,
|
|
regenerate: REGENERATE_PROMPT,
|
|
}
|
|
onDone(prompts[action], { display: 'user', shouldQuery: true })
|
|
}
|
|
|
|
if (installError) {
|
|
return (
|
|
<Box flexDirection="column">
|
|
<Text color="error">Error: {installError}</Text>
|
|
<Text dimColor>
|
|
Try running /plugin to manually install the think-back plugin.
|
|
</Text>
|
|
</Box>
|
|
)
|
|
}
|
|
|
|
if (!installComplete) {
|
|
return <ThinkbackInstaller onReady={handleReady} onError={handleError} />
|
|
}
|
|
|
|
if (!skillDir || hasGenerated === null) {
|
|
return (
|
|
<Box>
|
|
<Spinner />
|
|
<Text>Loading thinkback skill…</Text>
|
|
</Box>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<ThinkbackMenu
|
|
onDone={onDone}
|
|
onAction={handleAction}
|
|
skillDir={skillDir}
|
|
hasGenerated={hasGenerated}
|
|
/>
|
|
)
|
|
}
|
|
|
|
export async function call(
|
|
onDone: (
|
|
result?: string,
|
|
options?: { display?: CommandResultDisplay; shouldQuery?: boolean },
|
|
) => void,
|
|
): Promise<React.ReactNode> {
|
|
return <ThinkbackFlow onDone={onDone} />
|
|
}
|