From 2c759fe6fafab2575644e5404ba387af6050aaa6 Mon Sep 17 00:00:00 2001 From: claude-code-best Date: Tue, 31 Mar 2026 21:46:46 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=B1=BB=E5=9E=8B=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Settings/Config.tsx | 6 +++--- src/components/Spinner.tsx | 1 + src/screens/REPL.tsx | 20 ++++++++++++-------- src/types/global.d.ts | 6 +++--- src/types/message.ts | 21 +++++++++++++++++---- src/utils/sessionStorage.ts | 3 ++- 6 files changed, 38 insertions(+), 19 deletions(-) diff --git a/src/components/Settings/Config.tsx b/src/components/Settings/Config.tsx index 37ee93c..8034439 100644 --- a/src/components/Settings/Config.tsx +++ b/src/components/Settings/Config.tsx @@ -27,7 +27,7 @@ import { Dialog } from '../design-system/Dialog.js'; import { Select } from '../CustomSelect/index.js'; import { OutputStylePicker } from '../OutputStylePicker.js'; import { LanguagePicker } from '../LanguagePicker.js'; -import { getExternalClaudeMdIncludes, getMemoryFiles, hasExternalClaudeMdIncludes } from 'src/utils/claudemd.js'; +import { getExternalClaudeMdIncludes, getMemoryFiles, hasExternalClaudeMdIncludes, type MemoryFileInfo } from 'src/utils/claudemd.js'; import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'; import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'; import { Byline } from '../design-system/Byline.js'; @@ -197,7 +197,7 @@ export function Config({ }, [ownsEsc, onIsSearchModeChange]); const isConnectedToIde = hasAccessToIDEExtensionDiffFeature(context.options.mcpClients); const isFileCheckpointingAvailable = !isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_FILE_CHECKPOINTING); - const memoryFiles = React.use(getMemoryFiles(true)); + const memoryFiles = React.use(getMemoryFiles(true)) as MemoryFileInfo[]; const shouldShowExternalIncludesToggle = hasExternalClaudeMdIncludes(memoryFiles); const autoUpdaterDisabledReason = getAutoUpdaterDisabledReason(); function onChangeMainModelConfig(value: string | null): void { @@ -392,7 +392,7 @@ export function Config({ } }] : []), // Speculation toggle (ant-only) - ...("external" === 'ant' ? [{ + ...(("external" as string) === 'ant' ? [{ id: 'speculationEnabled', label: 'Speculative execution', value: globalConfig.speculationEnabled ?? true, diff --git a/src/components/Spinner.tsx b/src/components/Spinner.tsx index c170c86..78ad208 100644 --- a/src/components/Spinner.tsx +++ b/src/components/Spinner.tsx @@ -46,6 +46,7 @@ type Props = { pauseStartTimeRef: React.RefObject; spinnerTip?: string; responseLengthRef: React.RefObject; + apiMetricsRef?: React.RefObject>; overrideColor?: keyof Theme | null; overrideShimmerColor?: keyof Theme | null; overrideMessage?: string | null; diff --git a/src/screens/REPL.tsx b/src/screens/REPL.tsx index 8918dd8..1ebf37b 100644 --- a/src/screens/REPL.tsx +++ b/src/screens/REPL.tsx @@ -123,6 +123,7 @@ import type { ToolPermissionContext, Tool } from '../Tool.js'; import { applyPermissionUpdate, applyPermissionUpdates, persistPermissionUpdate } from '../utils/permissions/PermissionUpdate.js'; import { buildPermissionUpdates } from '../components/permissions/ExitPlanModePermissionRequest/ExitPlanModePermissionRequest.js'; import { stripDangerousPermissionsForAutoMode } from '../utils/permissions/permissionSetup.js'; +import type { PermissionMode } from '../types/permissions.js'; import { getScratchpadDir, isScratchpadEnabled } from '../utils/permissions/filesystem.js'; import { WEB_FETCH_TOOL_NAME } from '../tools/WebFetchTool/prompt.js'; import { SLEEP_TOOL_NAME } from '../tools/SleepTool/prompt.js'; @@ -1655,7 +1656,10 @@ export function REPL({ const onlySleepToolActive = useMemo(() => { const lastAssistant = messages.findLast(m => m.type === 'assistant'); if (lastAssistant?.type !== 'assistant') return false; - const inProgressToolUses = lastAssistant.message.content.filter(b => b.type === 'tool_use' && inProgressToolUseIDs.has(b.id)); + const content = lastAssistant.message.content; + if (typeof content === 'string') return false; + const contentArr = content as Array<{ type: string; id?: string; name?: string; [key: string]: unknown }>; + const inProgressToolUses = contentArr.filter(b => b.type === 'tool_use' && b.id && inProgressToolUseIDs.has(b.id)); return inProgressToolUses.length > 0 && inProgressToolUses.every(b => b.type === 'tool_use' && b.name === SLEEP_TOOL_NAME); }, [messages, inProgressToolUseIDs]); const { @@ -2605,7 +2609,7 @@ export function REPL({ if (feature('PROACTIVE') || feature('KAIROS')) { proactiveModule?.setContextBlocked(false); } - } else if ((newMessage as MessageType).type === 'progress' && isEphemeralToolProgress((newMessage as ProgressMessage).data.type)) { + } else if ((newMessage as MessageType).type === 'progress' && isEphemeralToolProgress(((newMessage as MessageType).data as { type: string }).type)) { // Replace the previous ephemeral progress tick for the same tool // call instead of appending. Sleep/Bash emit a tick per second and // only the last one is rendered; appending blows up the messages @@ -2618,7 +2622,7 @@ export function REPL({ // "Initializing…" because it renders the full progress trail. setMessages(oldMessages => { const last = oldMessages.at(-1); - if (last?.type === 'progress' && last.parentToolUseID === (newMessage as MessageType).parentToolUseID && last.data.type === (newMessage as ProgressMessage).data.type) { + if (last?.type === 'progress' && (last as MessageType).parentToolUseID === (newMessage as MessageType).parentToolUseID && ((last as MessageType).data as { type: string }).type === ((newMessage as MessageType).data as { type: string }).type) { const copy = oldMessages.slice(); copy[copy.length - 1] = newMessage; return copy; @@ -2683,7 +2687,7 @@ export function REPL({ // title silently fell through to the "Claude Code" default. if (!titleDisabled && !sessionTitle && !agentTitle && !haikuTitleAttemptedRef.current) { const firstUserMessage = newMessages.find(m => m.type === 'user' && !m.isMeta); - const text = firstUserMessage?.type === 'user' ? getContentText(firstUserMessage.message.content) : null; + const text = firstUserMessage?.type === 'user' ? getContentText(firstUserMessage.message.content as string | ContentBlockParam[]) : null; // Skip synthetic breadcrumbs — slash-command output, prompt-skill // expansions (/commit → ), local-command headers // (/help → ), and bash-mode (!cmd → ). @@ -2873,7 +2877,7 @@ export function REPL({ // Extract and enqueue user message text, skipping meta messages // (e.g. expanded skill content, tick prompts) that should not be // replayed as user-visible text. - newMessages.filter((m): m is UserMessage => m.type === 'user' && !m.isMeta).map(_ => getContentText(_.message.content)).filter(_ => _ !== null).forEach((msg, i) => { + newMessages.filter((m): m is UserMessage => m.type === 'user' && !m.isMeta).map(_ => getContentText(_.message.content as string | ContentBlockParam[])).filter(_ => _ !== null).forEach((msg, i) => { enqueue({ value: msg, mode: 'prompt' @@ -3081,7 +3085,7 @@ export function REPL({ toolPermissionContext: updatedToolPermissionContext, ...(shouldStorePlanForVerification && { pendingPlanVerification: { - plan: initialMsg.message.planContent!, + plan: initialMsg.message.planContent as string, verificationStarted: false, verificationCompleted: false } @@ -3693,7 +3697,7 @@ export function REPL({ // Restore permission mode from the message toolPermissionContext: message.permissionMode && prev.toolPermissionContext.mode !== message.permissionMode ? { ...prev.toolPermissionContext, - mode: message.permissionMode + mode: message.permissionMode as PermissionMode } : prev.toolPermissionContext, // Clear stale prompt suggestion from previous conversation state promptSuggestion: { @@ -3719,7 +3723,7 @@ export function REPL({ // Restore pasted images if (Array.isArray(message.message.content) && message.message.content.some(block => block.type === 'image')) { - const imageBlocks: Array = message.message.content.filter(block => block.type === 'image'); + const imageBlocks = message.message.content.filter(block => block.type === 'image') as Array; if (imageBlocks.length > 0) { const newPastedContents: Record = {}; imageBlocks.forEach((block, index) => { diff --git a/src/types/global.d.ts b/src/types/global.d.ts index 9a22a9f..c6778ba 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -49,9 +49,9 @@ declare function ExperimentEnrollmentNotice(): JSX.Element | null declare const HOOK_TIMING_DISPLAY_THRESHOLD_MS: number // Ultraplan (internal) -declare function UltraplanChoiceDialog(): JSX.Element | null -declare function UltraplanLaunchDialog(): JSX.Element | null -declare function launchUltraplan(...args: unknown[]): void +declare function UltraplanChoiceDialog(props: Record): JSX.Element | null +declare function UltraplanLaunchDialog(props: Record): JSX.Element | null +declare function launchUltraplan(...args: unknown[]): Promise // T — Generic type parameter leaked from React compiler output // (react/compiler-runtime emits compiled JSX that loses generic type params) diff --git a/src/types/message.ts b/src/types/message.ts index 73b40e2..9f0c6c8 100644 --- a/src/types/message.ts +++ b/src/types/message.ts @@ -14,24 +14,37 @@ export type Message = { isCompactSummary?: boolean toolUseResult?: unknown isVisibleInTranscriptOnly?: boolean + attachment?: { type: string; toolUseID?: string; [key: string]: unknown } message?: { role?: string - content?: string | Array<{ type: string; text?: string; [key: string]: unknown }> + id?: string + content?: string | Array<{ type: string; text?: string; id?: string; name?: string; tool_use_id?: string; [key: string]: unknown }> usage?: Record [key: string]: unknown } [key: string]: unknown } export type AssistantMessage = Message & { type: 'assistant' }; -export type AttachmentMessage = Message & { type: 'attachment' }; -export type ProgressMessage = Message & { type: 'progress' }; +export type AttachmentMessage = Message & { type: 'attachment'; attachment: { type: string; [key: string]: unknown } }; +export type ProgressMessage = Message & { type: 'progress'; data: T }; export type SystemLocalCommandMessage = Message & { type: 'system' }; export type SystemMessage = Message & { type: 'system' }; export type UserMessage = Message & { type: 'user' }; export type NormalizedUserMessage = UserMessage; export type RequestStartEvent = { type: string; [key: string]: unknown }; export type StreamEvent = { type: string; [key: string]: unknown }; -export type SystemCompactBoundaryMessage = Message & { type: 'system' }; +export type SystemCompactBoundaryMessage = Message & { + type: 'system' + compactMetadata: { + preservedSegment?: { + headUuid: UUID + tailUuid: UUID + anchorUuid: UUID + [key: string]: unknown + } + [key: string]: unknown + } +}; export type TombstoneMessage = Message; export type ToolUseSummaryMessage = Message; export type MessageOrigin = string; diff --git a/src/utils/sessionStorage.ts b/src/utils/sessionStorage.ts index b2a4a30..291aecb 100644 --- a/src/utils/sessionStorage.ts +++ b/src/utils/sessionStorage.ts @@ -1058,6 +1058,7 @@ class Project { entrypoint: getEntrypoint(), cwd: getCwd(), sessionId, + timestamp: new Date().toISOString(), version: VERSION, gitBranch, slug, @@ -2225,7 +2226,7 @@ export function checkResumeConsistency(chain: Message[]): void { for (let i = chain.length - 1; i >= 0; i--) { const m = chain[i]! if (m.type !== 'system' || m.subtype !== 'turn_duration') continue - const expected = m.messageCount + const expected = m.messageCount as number | undefined if (expected === undefined) return // `i` is the 0-based index of the checkpoint in the reconstructed chain. // The checkpoint was appended AFTER messageCount messages, so its own