claude-code-best 3 тижнів тому
батько
коміт
2c759fe6fa

+ 3 - 3
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,

+ 1 - 0
src/components/Spinner.tsx

@@ -46,6 +46,7 @@ type Props = {
   pauseStartTimeRef: React.RefObject<number | null>;
   spinnerTip?: string;
   responseLengthRef: React.RefObject<number>;
+  apiMetricsRef?: React.RefObject<Array<{ ttftMs: number; firstTokenTime: number; lastTokenTime: number; responseLengthBaseline: number; endResponseLength: number }>>;
   overrideColor?: keyof Theme | null;
   overrideShimmerColor?: keyof Theme | null;
   overrideMessage?: string | null;

+ 12 - 8
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<unknown>).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<unknown>).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 → <command-message>), local-command headers
       // (/help → <command-name>), and bash-mode (!cmd → <bash-input>).
@@ -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<ImageBlockParam> = message.message.content.filter(block => block.type === 'image');
+      const imageBlocks = message.message.content.filter(block => block.type === 'image') as Array<ImageBlockParam>;
       if (imageBlocks.length > 0) {
         const newPastedContents: Record<number, PastedContent> = {};
         imageBlocks.forEach((block, index) => {

+ 3 - 3
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<string, unknown>): JSX.Element | null
+declare function UltraplanLaunchDialog(props: Record<string, unknown>): JSX.Element | null
+declare function launchUltraplan(...args: unknown[]): Promise<string>
 
 // T — Generic type parameter leaked from React compiler output
 // (react/compiler-runtime emits compiled JSX that loses generic type params)

+ 17 - 4
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<string, unknown>
     [key: string]: unknown
   }
   [key: string]: unknown
 }
 export type AssistantMessage = Message & { type: 'assistant' };
-export type AttachmentMessage<T = unknown> = Message & { type: 'attachment' };
-export type ProgressMessage<T = unknown> = Message & { type: 'progress' };
+export type AttachmentMessage<T = unknown> = Message & { type: 'attachment'; attachment: { type: string; [key: string]: unknown } };
+export type ProgressMessage<T = unknown> = 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;

+ 2 - 1
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