mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-17 05:45: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>
232 lines
7.3 KiB
TypeScript
232 lines
7.3 KiB
TypeScript
import * as React from 'react'
|
|
import { useEffect, useRef, useState } from 'react'
|
|
import { logEvent } from 'src/services/analytics/index.js'
|
|
import { logForDebugging } from 'src/utils/debug.js'
|
|
import { logError } from 'src/utils/log.js'
|
|
import { useInterval } from 'usehooks-ts'
|
|
import { useUpdateNotification } from '../hooks/useUpdateNotification.js'
|
|
import { Box, Text } from '../ink.js'
|
|
import type { AutoUpdaterResult } from '../utils/autoUpdater.js'
|
|
import { getMaxVersion, getMaxVersionMessage } from '../utils/autoUpdater.js'
|
|
import { isAutoUpdaterDisabled } from '../utils/config.js'
|
|
import { installLatest } from '../utils/nativeInstaller/index.js'
|
|
import { gt } from '../utils/semver.js'
|
|
import { getInitialSettings } from '../utils/settings/settings.js'
|
|
|
|
/**
|
|
* Categorize error messages for analytics
|
|
*/
|
|
function getErrorType(errorMessage: string): string {
|
|
if (errorMessage.includes('timeout')) {
|
|
return 'timeout'
|
|
}
|
|
if (errorMessage.includes('Checksum mismatch')) {
|
|
return 'checksum_mismatch'
|
|
}
|
|
if (errorMessage.includes('ENOENT') || errorMessage.includes('not found')) {
|
|
return 'not_found'
|
|
}
|
|
if (errorMessage.includes('EACCES') || errorMessage.includes('permission')) {
|
|
return 'permission_denied'
|
|
}
|
|
if (errorMessage.includes('ENOSPC')) {
|
|
return 'disk_full'
|
|
}
|
|
if (errorMessage.includes('npm')) {
|
|
return 'npm_error'
|
|
}
|
|
if (
|
|
errorMessage.includes('network') ||
|
|
errorMessage.includes('ECONNREFUSED') ||
|
|
errorMessage.includes('ENOTFOUND')
|
|
) {
|
|
return 'network_error'
|
|
}
|
|
return 'unknown'
|
|
}
|
|
|
|
type Props = {
|
|
isUpdating: boolean
|
|
onChangeIsUpdating: (isUpdating: boolean) => void
|
|
onAutoUpdaterResult: (autoUpdaterResult: AutoUpdaterResult) => void
|
|
autoUpdaterResult: AutoUpdaterResult | null
|
|
showSuccessMessage: boolean
|
|
verbose: boolean
|
|
}
|
|
|
|
export function NativeAutoUpdater({
|
|
isUpdating,
|
|
onChangeIsUpdating,
|
|
onAutoUpdaterResult,
|
|
autoUpdaterResult,
|
|
showSuccessMessage,
|
|
verbose,
|
|
}: Props): React.ReactNode {
|
|
const [versions, setVersions] = useState<{
|
|
current?: string | null
|
|
latest?: string | null
|
|
}>({})
|
|
const [maxVersionIssue, setMaxVersionIssue] = useState<string | null>(null)
|
|
const updateSemver = useUpdateNotification(autoUpdaterResult?.version)
|
|
const channel = getInitialSettings()?.autoUpdatesChannel ?? 'latest'
|
|
|
|
// Track latest isUpdating value in a ref so the memoized checkForUpdates
|
|
// callback always sees the current value without changing callback identity
|
|
// (which would re-trigger the initial-check useEffect below and cause
|
|
// repeated downloads on remount — the upstream trigger for #22413).
|
|
const isUpdatingRef = useRef(isUpdating)
|
|
isUpdatingRef.current = isUpdating
|
|
|
|
const checkForUpdates = React.useCallback(async () => {
|
|
if (isUpdatingRef.current) {
|
|
return
|
|
}
|
|
|
|
if (
|
|
"production" === 'test' ||
|
|
"production" === 'development'
|
|
) {
|
|
logForDebugging(
|
|
'NativeAutoUpdater: Skipping update check in test/dev environment',
|
|
)
|
|
return
|
|
}
|
|
|
|
if (isAutoUpdaterDisabled()) {
|
|
return
|
|
}
|
|
|
|
onChangeIsUpdating(true)
|
|
const startTime = Date.now()
|
|
|
|
// Log the start of an auto-update check for funnel analysis
|
|
logEvent('tengu_native_auto_updater_start', {})
|
|
|
|
try {
|
|
// Check if current version is above the max allowed version
|
|
const maxVersion = await getMaxVersion()
|
|
if (maxVersion && gt(MACRO.VERSION, maxVersion)) {
|
|
const msg = await getMaxVersionMessage()
|
|
setMaxVersionIssue(msg ?? 'affects your version')
|
|
}
|
|
|
|
const result = await installLatest(channel)
|
|
const currentVersion = MACRO.VERSION
|
|
const latencyMs = Date.now() - startTime
|
|
|
|
// Handle lock contention gracefully - just return without treating as error
|
|
if (result.lockFailed) {
|
|
logEvent('tengu_native_auto_updater_lock_contention', {
|
|
latency_ms: latencyMs,
|
|
})
|
|
return // Silently skip this update check, will try again later
|
|
}
|
|
|
|
// Update versions for display
|
|
setVersions({ current: currentVersion, latest: result.latestVersion })
|
|
|
|
if (result.wasUpdated) {
|
|
logEvent('tengu_native_auto_updater_success', {
|
|
latency_ms: latencyMs,
|
|
})
|
|
|
|
onAutoUpdaterResult({
|
|
version: result.latestVersion,
|
|
status: 'success',
|
|
})
|
|
} else {
|
|
// Already up to date
|
|
logEvent('tengu_native_auto_updater_up_to_date', {
|
|
latency_ms: latencyMs,
|
|
})
|
|
}
|
|
} catch (error) {
|
|
const latencyMs = Date.now() - startTime
|
|
const errorMessage =
|
|
error instanceof Error ? error.message : String(error)
|
|
logError(error)
|
|
|
|
const errorType = getErrorType(errorMessage)
|
|
logEvent('tengu_native_auto_updater_fail', {
|
|
latency_ms: latencyMs,
|
|
error_timeout: errorType === 'timeout',
|
|
error_checksum: errorType === 'checksum_mismatch',
|
|
error_not_found: errorType === 'not_found',
|
|
error_permission: errorType === 'permission_denied',
|
|
error_disk_full: errorType === 'disk_full',
|
|
error_npm: errorType === 'npm_error',
|
|
error_network: errorType === 'network_error',
|
|
})
|
|
|
|
onAutoUpdaterResult({
|
|
version: null,
|
|
status: 'install_failed',
|
|
})
|
|
} finally {
|
|
onChangeIsUpdating(false)
|
|
}
|
|
// isUpdating intentionally omitted from deps; we read isUpdatingRef
|
|
// instead so the guard is always current without changing callback
|
|
// identity (which would re-trigger the initial-check useEffect below).
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
// biome-ignore lint/correctness/useExhaustiveDependencies: isUpdating read via ref
|
|
}, [onAutoUpdaterResult, channel])
|
|
|
|
// Initial check
|
|
useEffect(() => {
|
|
void checkForUpdates()
|
|
}, [checkForUpdates])
|
|
|
|
// Check every 30 minutes
|
|
useInterval(checkForUpdates, 30 * 60 * 1000)
|
|
|
|
const hasUpdateResult = !!autoUpdaterResult?.version
|
|
const hasVersionInfo = !!versions.current && !!versions.latest
|
|
// Show the component when:
|
|
// - warning banner needed (above max version), or
|
|
// - there's an update result to display (success/error), or
|
|
// - actively checking and we have version info to show
|
|
const shouldRender =
|
|
!!maxVersionIssue || hasUpdateResult || (isUpdating && hasVersionInfo)
|
|
|
|
if (!shouldRender) {
|
|
return null
|
|
}
|
|
|
|
return (
|
|
<Box flexDirection="row" gap={1}>
|
|
{verbose && (
|
|
<Text dimColor wrap="truncate">
|
|
current: {versions.current} · {channel}: {versions.latest}
|
|
</Text>
|
|
)}
|
|
{isUpdating ? (
|
|
<Box>
|
|
<Text dimColor wrap="truncate">
|
|
Checking for updates
|
|
</Text>
|
|
</Box>
|
|
) : (
|
|
autoUpdaterResult?.status === 'success' &&
|
|
showSuccessMessage &&
|
|
updateSemver && (
|
|
<Text color="success" wrap="truncate">
|
|
✓ Update installed · Restart to update
|
|
</Text>
|
|
)
|
|
)}
|
|
{autoUpdaterResult?.status === 'install_failed' && (
|
|
<Text color="error" wrap="truncate">
|
|
✗ Auto-update failed · Try <Text bold>/status</Text>
|
|
</Text>
|
|
)}
|
|
{maxVersionIssue && process.env.USER_TYPE === 'ant' && (
|
|
<Text color="warning">
|
|
⚠ Known issue: {maxVersionIssue} · Run{' '}
|
|
<Text bold>claude rollback --safe</Text> to downgrade
|
|
</Text>
|
|
)}
|
|
</Box>
|
|
)
|
|
}
|