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,61 +1,413 @@
|
||||
import { feature } from 'bun:bundle';
|
||||
import * as React from 'react';
|
||||
import { memo } from 'react';
|
||||
import { useAppState } from 'src/state/AppState.js';
|
||||
import { getSdkBetas, getKairosActive } from '../bootstrap/state.js';
|
||||
import { getTotalCost, getTotalInputTokens, getTotalOutputTokens } from '../cost-tracker.js';
|
||||
import { useMainLoopModel } from '../hooks/useMainLoopModel.js';
|
||||
import { type ReadonlySettings } from '../hooks/useSettings.js';
|
||||
import { getRawUtilization } from '../services/claudeAiLimits.js';
|
||||
import type { Message } from '../types/message.js';
|
||||
import { calculateContextPercentages, getContextWindowForModel } from '../utils/context.js';
|
||||
import { getLastAssistantMessage } from '../utils/messages.js';
|
||||
import { getRuntimeMainLoopModel, renderModelName } from '../utils/model/model.js';
|
||||
import { doesMostRecentAssistantMessageExceed200k, getCurrentUsage } from '../utils/tokens.js';
|
||||
import { BuiltinStatusLine } from './BuiltinStatusLine.js';
|
||||
import { feature } from 'bun:bundle'
|
||||
import * as React from 'react'
|
||||
import { memo, useCallback, useEffect, useRef } from 'react'
|
||||
import { logEvent } from 'src/services/analytics/index.js'
|
||||
import { useAppState, useSetAppState } from 'src/state/AppState.js'
|
||||
import type { PermissionMode } from 'src/utils/permissions/PermissionMode.js'
|
||||
import {
|
||||
getIsRemoteMode,
|
||||
getKairosActive,
|
||||
getMainThreadAgentType,
|
||||
getOriginalCwd,
|
||||
getSdkBetas,
|
||||
getSessionId,
|
||||
} from '../bootstrap/state.js'
|
||||
import { DEFAULT_OUTPUT_STYLE_NAME } from '../constants/outputStyles.js'
|
||||
import { useNotifications } from '../context/notifications.js'
|
||||
import {
|
||||
getTotalAPIDuration,
|
||||
getTotalCost,
|
||||
getTotalDuration,
|
||||
getTotalInputTokens,
|
||||
getTotalLinesAdded,
|
||||
getTotalLinesRemoved,
|
||||
getTotalOutputTokens,
|
||||
} from '../cost-tracker.js'
|
||||
import { useMainLoopModel } from '../hooks/useMainLoopModel.js'
|
||||
import { type ReadonlySettings, useSettings } from '../hooks/useSettings.js'
|
||||
import { Ansi, Box, Text } from '../ink.js'
|
||||
import { getRawUtilization } from '../services/claudeAiLimits.js'
|
||||
import type { Message } from '../types/message.js'
|
||||
import type { StatusLineCommandInput } from '../types/statusLine.js'
|
||||
import type { VimMode } from '../types/textInputTypes.js'
|
||||
import { checkHasTrustDialogAccepted } from '../utils/config.js'
|
||||
import {
|
||||
calculateContextPercentages,
|
||||
getContextWindowForModel,
|
||||
} from '../utils/context.js'
|
||||
import { getCwd } from '../utils/cwd.js'
|
||||
import { logForDebugging } from '../utils/debug.js'
|
||||
import { isFullscreenEnvEnabled } from '../utils/fullscreen.js'
|
||||
import {
|
||||
createBaseHookInput,
|
||||
executeStatusLineCommand,
|
||||
} from '../utils/hooks.js'
|
||||
import { getLastAssistantMessage } from '../utils/messages.js'
|
||||
import {
|
||||
getRuntimeMainLoopModel,
|
||||
type ModelName,
|
||||
renderModelName,
|
||||
} from '../utils/model/model.js'
|
||||
import { getCurrentSessionTitle } from '../utils/sessionStorage.js'
|
||||
import {
|
||||
doesMostRecentAssistantMessageExceed200k,
|
||||
getCurrentUsage,
|
||||
} from '../utils/tokens.js'
|
||||
import { getCurrentWorktreeSession } from '../utils/worktree.js'
|
||||
import { isVimModeEnabled } from './PromptInput/utils.js'
|
||||
|
||||
export function statusLineShouldDisplay(settings: ReadonlySettings): boolean {
|
||||
if (feature('KAIROS') && getKairosActive()) return false;
|
||||
return true;
|
||||
// Assistant mode: statusline fields (model, permission mode, cwd) reflect the
|
||||
// REPL/daemon process, not what the agent child is actually running. Hide it.
|
||||
if (feature('KAIROS') && getKairosActive()) return false
|
||||
return settings?.statusLine !== undefined
|
||||
}
|
||||
|
||||
function buildStatusLineCommandInput(
|
||||
permissionMode: PermissionMode,
|
||||
exceeds200kTokens: boolean,
|
||||
settings: ReadonlySettings,
|
||||
messages: Message[],
|
||||
addedDirs: string[],
|
||||
mainLoopModel: ModelName,
|
||||
vimMode?: VimMode,
|
||||
): StatusLineCommandInput {
|
||||
const agentType = getMainThreadAgentType()
|
||||
const worktreeSession = getCurrentWorktreeSession()
|
||||
const runtimeModel = getRuntimeMainLoopModel({
|
||||
permissionMode,
|
||||
mainLoopModel,
|
||||
exceeds200kTokens,
|
||||
})
|
||||
const outputStyleName = settings?.outputStyle || DEFAULT_OUTPUT_STYLE_NAME
|
||||
|
||||
const currentUsage = getCurrentUsage(messages)
|
||||
const contextWindowSize = getContextWindowForModel(
|
||||
runtimeModel,
|
||||
getSdkBetas(),
|
||||
)
|
||||
const contextPercentages = calculateContextPercentages(
|
||||
currentUsage,
|
||||
contextWindowSize,
|
||||
)
|
||||
|
||||
const sessionId = getSessionId()
|
||||
const sessionName = getCurrentSessionTitle(sessionId)
|
||||
const rawUtil = getRawUtilization()
|
||||
const rateLimits: StatusLineCommandInput['rate_limits'] = {
|
||||
...(rawUtil.five_hour && {
|
||||
five_hour: {
|
||||
used_percentage: rawUtil.five_hour.utilization * 100,
|
||||
resets_at: rawUtil.five_hour.resets_at,
|
||||
},
|
||||
}),
|
||||
...(rawUtil.seven_day && {
|
||||
seven_day: {
|
||||
used_percentage: rawUtil.seven_day.utilization * 100,
|
||||
resets_at: rawUtil.seven_day.resets_at,
|
||||
},
|
||||
}),
|
||||
}
|
||||
return {
|
||||
...createBaseHookInput(),
|
||||
...(sessionName && { session_name: sessionName }),
|
||||
model: {
|
||||
id: runtimeModel,
|
||||
display_name: renderModelName(runtimeModel),
|
||||
},
|
||||
workspace: {
|
||||
current_dir: getCwd(),
|
||||
project_dir: getOriginalCwd(),
|
||||
added_dirs: addedDirs,
|
||||
},
|
||||
version: MACRO.VERSION,
|
||||
output_style: {
|
||||
name: outputStyleName,
|
||||
},
|
||||
cost: {
|
||||
total_cost_usd: getTotalCost(),
|
||||
total_duration_ms: getTotalDuration(),
|
||||
total_api_duration_ms: getTotalAPIDuration(),
|
||||
total_lines_added: getTotalLinesAdded(),
|
||||
total_lines_removed: getTotalLinesRemoved(),
|
||||
},
|
||||
context_window: {
|
||||
total_input_tokens: getTotalInputTokens(),
|
||||
total_output_tokens: getTotalOutputTokens(),
|
||||
context_window_size: contextWindowSize,
|
||||
current_usage: currentUsage,
|
||||
used_percentage: contextPercentages.used,
|
||||
remaining_percentage: contextPercentages.remaining,
|
||||
},
|
||||
exceeds_200k_tokens: exceeds200kTokens,
|
||||
...((rateLimits.five_hour || rateLimits.seven_day) && {
|
||||
rate_limits: rateLimits,
|
||||
}),
|
||||
...(isVimModeEnabled() && {
|
||||
vim: {
|
||||
mode: vimMode ?? 'INSERT',
|
||||
},
|
||||
}),
|
||||
...(agentType && {
|
||||
agent: {
|
||||
name: agentType,
|
||||
},
|
||||
}),
|
||||
...(getIsRemoteMode() && {
|
||||
remote: {
|
||||
session_id: getSessionId(),
|
||||
},
|
||||
}),
|
||||
...(worktreeSession && {
|
||||
worktree: {
|
||||
name: worktreeSession.worktreeName,
|
||||
path: worktreeSession.worktreePath,
|
||||
branch: worktreeSession.worktreeBranch,
|
||||
original_cwd: worktreeSession.originalCwd,
|
||||
original_branch: worktreeSession.originalBranch,
|
||||
},
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
type Props = {
|
||||
messagesRef: React.RefObject<Message[]>;
|
||||
lastAssistantMessageId: string | null;
|
||||
vimMode?: unknown;
|
||||
};
|
||||
// messages stays behind a ref (read only in the debounced callback);
|
||||
// lastAssistantMessageId is the actual re-render trigger.
|
||||
messagesRef: React.RefObject<Message[]>
|
||||
lastAssistantMessageId: string | null
|
||||
vimMode?: VimMode
|
||||
}
|
||||
|
||||
export function getLastAssistantMessageId(messages: Message[]): string | null {
|
||||
return getLastAssistantMessage(messages)?.uuid ?? null;
|
||||
return getLastAssistantMessage(messages)?.uuid ?? null
|
||||
}
|
||||
|
||||
function StatusLineInner({ messagesRef, lastAssistantMessageId }: Props): React.ReactNode {
|
||||
const mainLoopModel = useMainLoopModel();
|
||||
const permissionMode = useAppState(s => s.toolPermissionContext.mode);
|
||||
function StatusLineInner({
|
||||
messagesRef,
|
||||
lastAssistantMessageId,
|
||||
vimMode,
|
||||
}: Props): React.ReactNode {
|
||||
const abortControllerRef = useRef<AbortController | undefined>(undefined)
|
||||
const permissionMode = useAppState(s => s.toolPermissionContext.mode)
|
||||
const additionalWorkingDirectories = useAppState(
|
||||
s => s.toolPermissionContext.additionalWorkingDirectories,
|
||||
)
|
||||
const statusLineText = useAppState(s => s.statusLineText)
|
||||
const setAppState = useSetAppState()
|
||||
const settings = useSettings()
|
||||
const { addNotification } = useNotifications()
|
||||
// AppState-sourced model — same source as API requests. getMainLoopModel()
|
||||
// re-reads settings.json on every call, so another session's /model write
|
||||
// would leak into this session's statusline (anthropics/claude-code#37596).
|
||||
const mainLoopModel = useMainLoopModel()
|
||||
|
||||
const messages = messagesRef.current ?? [];
|
||||
// Keep latest values in refs for stable callback access
|
||||
const settingsRef = useRef(settings)
|
||||
settingsRef.current = settings
|
||||
const vimModeRef = useRef(vimMode)
|
||||
vimModeRef.current = vimMode
|
||||
const permissionModeRef = useRef(permissionMode)
|
||||
permissionModeRef.current = permissionMode
|
||||
const addedDirsRef = useRef(additionalWorkingDirectories)
|
||||
addedDirsRef.current = additionalWorkingDirectories
|
||||
const mainLoopModelRef = useRef(mainLoopModel)
|
||||
mainLoopModelRef.current = mainLoopModel
|
||||
|
||||
const exceeds200kTokens = lastAssistantMessageId ? doesMostRecentAssistantMessageExceed200k(messages) : false;
|
||||
// Track previous state to detect changes and cache expensive calculations
|
||||
const previousStateRef = useRef<{
|
||||
messageId: string | null
|
||||
exceeds200kTokens: boolean
|
||||
permissionMode: PermissionMode
|
||||
vimMode: VimMode | undefined
|
||||
mainLoopModel: ModelName
|
||||
}>({
|
||||
messageId: null,
|
||||
exceeds200kTokens: false,
|
||||
permissionMode,
|
||||
vimMode,
|
||||
mainLoopModel,
|
||||
})
|
||||
|
||||
const runtimeModel = getRuntimeMainLoopModel({ permissionMode, mainLoopModel, exceeds200kTokens });
|
||||
const modelDisplay = renderModelName(runtimeModel);
|
||||
const currentUsage = getCurrentUsage(messages);
|
||||
const contextWindowSize = getContextWindowForModel(runtimeModel, getSdkBetas());
|
||||
const contextPercentages = calculateContextPercentages(currentUsage, contextWindowSize);
|
||||
const rawUtil = getRawUtilization();
|
||||
const totalCost = getTotalCost();
|
||||
const usedTokens = getTotalInputTokens() + getTotalOutputTokens();
|
||||
// Debounce timer ref
|
||||
const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(
|
||||
undefined,
|
||||
)
|
||||
|
||||
// True when the next invocation should log its result (first run or after settings reload)
|
||||
const logNextResultRef = useRef(true)
|
||||
|
||||
// Stable update function — reads latest values from refs
|
||||
const doUpdate = useCallback(async () => {
|
||||
// Cancel any in-flight requests
|
||||
abortControllerRef.current?.abort()
|
||||
|
||||
const controller = new AbortController()
|
||||
abortControllerRef.current = controller
|
||||
|
||||
const msgs = messagesRef.current
|
||||
|
||||
const logResult = logNextResultRef.current
|
||||
logNextResultRef.current = false
|
||||
|
||||
try {
|
||||
let exceeds200kTokens = previousStateRef.current.exceeds200kTokens
|
||||
|
||||
// Only recalculate 200k check if messages changed
|
||||
const currentMessageId = getLastAssistantMessageId(msgs)
|
||||
if (currentMessageId !== previousStateRef.current.messageId) {
|
||||
exceeds200kTokens = doesMostRecentAssistantMessageExceed200k(msgs)
|
||||
previousStateRef.current.messageId = currentMessageId
|
||||
previousStateRef.current.exceeds200kTokens = exceeds200kTokens
|
||||
}
|
||||
|
||||
const statusInput = buildStatusLineCommandInput(
|
||||
permissionModeRef.current,
|
||||
exceeds200kTokens,
|
||||
settingsRef.current,
|
||||
msgs,
|
||||
Array.from(addedDirsRef.current.keys()),
|
||||
mainLoopModelRef.current,
|
||||
vimModeRef.current,
|
||||
)
|
||||
|
||||
const text = await executeStatusLineCommand(
|
||||
statusInput,
|
||||
controller.signal,
|
||||
undefined,
|
||||
logResult,
|
||||
)
|
||||
if (!controller.signal.aborted) {
|
||||
setAppState(prev => {
|
||||
if (prev.statusLineText === text) return prev
|
||||
return { ...prev, statusLineText: text }
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
// Silently ignore errors in status line updates
|
||||
}
|
||||
}, [messagesRef, setAppState])
|
||||
|
||||
// Stable debounced schedule function — no deps, uses refs
|
||||
const scheduleUpdate = useCallback(() => {
|
||||
if (debounceTimerRef.current !== undefined) {
|
||||
clearTimeout(debounceTimerRef.current)
|
||||
}
|
||||
debounceTimerRef.current = setTimeout(
|
||||
(ref, doUpdate) => {
|
||||
ref.current = undefined
|
||||
void doUpdate()
|
||||
},
|
||||
300,
|
||||
debounceTimerRef,
|
||||
doUpdate,
|
||||
)
|
||||
}, [doUpdate])
|
||||
|
||||
// Only trigger update when assistant message, permission mode, vim mode, or model actually changes
|
||||
useEffect(() => {
|
||||
if (
|
||||
lastAssistantMessageId !== previousStateRef.current.messageId ||
|
||||
permissionMode !== previousStateRef.current.permissionMode ||
|
||||
vimMode !== previousStateRef.current.vimMode ||
|
||||
mainLoopModel !== previousStateRef.current.mainLoopModel
|
||||
) {
|
||||
// Don't update messageId here — let doUpdate handle it so
|
||||
// exceeds200kTokens is recalculated with the latest messages
|
||||
previousStateRef.current.permissionMode = permissionMode
|
||||
previousStateRef.current.vimMode = vimMode
|
||||
previousStateRef.current.mainLoopModel = mainLoopModel
|
||||
scheduleUpdate()
|
||||
}
|
||||
}, [
|
||||
lastAssistantMessageId,
|
||||
permissionMode,
|
||||
vimMode,
|
||||
mainLoopModel,
|
||||
scheduleUpdate,
|
||||
])
|
||||
|
||||
// When the statusLine command changes (hot reload), log the next result
|
||||
const statusLineCommand = settings?.statusLine?.command
|
||||
const isFirstSettingsRender = useRef(true)
|
||||
useEffect(() => {
|
||||
if (isFirstSettingsRender.current) {
|
||||
isFirstSettingsRender.current = false
|
||||
return
|
||||
}
|
||||
logNextResultRef.current = true
|
||||
void doUpdate()
|
||||
}, [statusLineCommand, doUpdate])
|
||||
|
||||
// Separate effect for logging on mount
|
||||
useEffect(() => {
|
||||
const statusLine = settings?.statusLine
|
||||
if (statusLine) {
|
||||
logEvent('tengu_status_line_mount', {
|
||||
command_length: statusLine.command.length,
|
||||
padding: statusLine.padding,
|
||||
})
|
||||
// Log if status line is configured but disabled by disableAllHooks
|
||||
if (settings.disableAllHooks === true) {
|
||||
logForDebugging(
|
||||
'Status line is configured but disableAllHooks is true',
|
||||
{ level: 'warn' },
|
||||
)
|
||||
}
|
||||
// executeStatusLineCommand (hooks.ts) returns undefined when trust is
|
||||
// blocked — statusLineText stays undefined forever, user sees nothing,
|
||||
// and tengu_status_line_mount above fires anyway so telemetry looks fine.
|
||||
if (!checkHasTrustDialogAccepted()) {
|
||||
addNotification({
|
||||
key: 'statusline-trust-blocked',
|
||||
text: 'statusline skipped · restart to fix',
|
||||
color: 'warning',
|
||||
priority: 'low',
|
||||
})
|
||||
logForDebugging(
|
||||
'Status line command skipped: workspace trust not accepted',
|
||||
{ level: 'warn' },
|
||||
)
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: intentional
|
||||
}, []) // Only run once on mount - settings stable for initial logging
|
||||
|
||||
// Initial update on mount + cleanup on unmount
|
||||
useEffect(() => {
|
||||
void doUpdate()
|
||||
|
||||
return () => {
|
||||
abortControllerRef.current?.abort()
|
||||
if (debounceTimerRef.current !== undefined) {
|
||||
clearTimeout(debounceTimerRef.current)
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: intentional
|
||||
}, []) // Only run once on mount, not when doUpdate changes
|
||||
|
||||
// Get padding from settings or default to 0
|
||||
const paddingX = settings?.statusLine?.padding ?? 0
|
||||
|
||||
// StatusLine must have stable height in fullscreen — the footer is
|
||||
// flexShrink:0 so a 0→1 row change when the command finishes steals
|
||||
// a row from ScrollBox and shifts content. Reserve the row while loading
|
||||
// (same trick as PromptInputFooterLeftSide).
|
||||
return (
|
||||
<BuiltinStatusLine
|
||||
modelName={modelDisplay}
|
||||
contextUsedPct={contextPercentages.used}
|
||||
usedTokens={usedTokens}
|
||||
contextWindowSize={contextWindowSize}
|
||||
totalCostUsd={totalCost}
|
||||
rateLimits={rawUtil}
|
||||
/>
|
||||
);
|
||||
<Box paddingX={paddingX} gap={2}>
|
||||
{statusLineText ? (
|
||||
<Text dimColor wrap="truncate">
|
||||
<Ansi>{statusLineText}</Ansi>
|
||||
</Text>
|
||||
) : isFullscreenEnvEnabled() ? (
|
||||
<Text> </Text>
|
||||
) : null}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export const StatusLine = memo(StatusLineInner);
|
||||
// Parent (PromptInputFooter) re-renders on every setMessages, but StatusLine's
|
||||
// own props now only change when lastAssistantMessageId flips — memo keeps it
|
||||
// from being dragged along (previously ~18 no-prop-change renders per session).
|
||||
export const StatusLine = memo(StatusLineInner)
|
||||
|
||||
Reference in New Issue
Block a user