diff --git a/packages/cli/src/test-utils/render.tsx b/packages/cli/src/test-utils/render.tsx index 9dd0f96758..8894226a81 100644 --- a/packages/cli/src/test-utils/render.tsx +++ b/packages/cli/src/test-utils/render.tsx @@ -16,7 +16,7 @@ import { vi } from 'vitest'; import stripAnsi from 'strip-ansi'; import type React from 'react'; import { act, useState } from 'react'; -import type { LoadedSettings } from '../config/settings.js'; +import { LoadedSettings } from '../config/settings.js'; import { KeypressProvider } from '../ui/contexts/KeypressContext.js'; import { SettingsContext } from '../ui/contexts/SettingsContext.js'; import { ShellFocusContext } from '../ui/contexts/ShellFocusContext.js'; @@ -608,16 +608,18 @@ export const renderWithProviders = async ( uiState: providedUiState, width, mouseEventsEnabled = false, + useAlternateBuffer: explicitUseAlternateBuffer, config, uiActions, persistentState, appState = mockAppState, }: { shellFocus?: boolean; - settings?: LoadedSettings; + settings?: LoadedSettings | Partial; uiState?: Partial; width?: number; mouseEventsEnabled?: boolean; + useAlternateBuffer?: boolean; config?: Config; uiActions?: Partial; persistentState?: { @@ -656,15 +658,34 @@ export const renderWithProviders = async ( const terminalWidth = width ?? baseState.terminalWidth; + const finalSettings = + settings instanceof LoadedSettings + ? settings + : createMockSettings(settings || {}); + if (!config) { config = await loadCliConfig( - settings.merged, + finalSettings.merged, 'random-session-id', - {} as unknown as CliArgs, + {} as CliArgs, { cwd: '/' }, ); } + const useAlternateBuffer = + explicitUseAlternateBuffer ?? + finalSettings.merged.ui?.useAlternateBuffer ?? + false; + + const finalConfig = new Proxy(config, { + get(target, prop) { + if (prop === 'getUseAlternateBuffer') { + return () => useAlternateBuffer; + } + return Reflect.get(target, prop); + }, + }); + const mainAreaWidth = providedUiState?.mainAreaWidth ?? terminalWidth; const finalUiState = { @@ -693,8 +714,8 @@ export const renderWithProviders = async ( const wrapWithProviders = (comp: React.ReactElement) => ( - - + + @@ -705,7 +726,7 @@ export const renderWithProviders = async ( ( wrapper?: React.ComponentType<{ children: React.ReactNode }>; // Options for renderWithProviders shellFocus?: boolean; - settings?: LoadedSettings; + settings?: LoadedSettings | Partial; uiState?: Partial; width?: number; mouseEventsEnabled?: boolean; diff --git a/packages/cli/src/ui/components/MainContent.tsx b/packages/cli/src/ui/components/MainContent.tsx index d8656a879c..98b99d384f 100644 --- a/packages/cli/src/ui/components/MainContent.tsx +++ b/packages/cli/src/ui/components/MainContent.tsx @@ -88,6 +88,7 @@ export const MainContent = () => { () => augmentedHistory.map( ({ item, isExpandable, isFirstThinking, isFirstAfterThinking }) => ( + // ({ item, isExpandable, isFirstThinking, /* isFirstAfterThinking */ }) => ( { beforeEach(() => { vi.clearAllMocks(); + // Default to a pending promise to avoid unwanted async state updates in tests + // that don't specifically test the IdeClient initialization. + vi.mocked(IdeClient.getInstance).mockReturnValue(new Promise(() => {})); }); - const wrapper = ({ children }: { children: React.ReactNode }) => ( - - {children} - - ); + const wrapper = ({ children }: { children: React.ReactNode }) => { + return ( + + {children} + + ); + }; it('publishes to MessageBus for tools with correlationId', async () => { const { result } = await renderHook(() => useToolActions(), { wrapper }); diff --git a/packages/cli/src/ui/contexts/ToolActionsContext.tsx b/packages/cli/src/ui/contexts/ToolActionsContext.tsx index 10e063e098..4168853cbb 100644 --- a/packages/cli/src/ui/contexts/ToolActionsContext.tsx +++ b/packages/cli/src/ui/contexts/ToolActionsContext.tsx @@ -5,13 +5,7 @@ */ import type React from 'react'; -import { - createContext, - useContext, - useCallback, - useState, - useEffect, -} from 'react'; +import { createContext, useContext, useCallback, useState, useEffect } from 'react'; import { IdeClient, ToolConfirmationOutcome, @@ -52,7 +46,7 @@ interface ToolActionsContextValue { const ToolActionsContext = createContext(null); -export const useToolActions = () => { +export const useToolActions = (): ToolActionsContextValue => { const context = useContext(ToolActionsContext); if (!context) { throw new Error('useToolActions must be used within a ToolActionsProvider'); @@ -77,24 +71,23 @@ export const ToolActionsProvider: React.FC = ( useEffect(() => { let isMounted = true; + let activeClient: IdeClient | null = null; + + const handleStatusChange = () => { + if (isMounted && activeClient) { + setIsDiffingEnabled(activeClient.isDiffingEnabled()); + } + }; + if (config.getIdeMode()) { IdeClient.getInstance() .then((client) => { if (!isMounted) return; + activeClient = client; setIdeClient(client); setIsDiffingEnabled(client.isDiffingEnabled()); - const handleStatusChange = () => { - if (isMounted) { - setIsDiffingEnabled(client.isDiffingEnabled()); - } - }; - client.addStatusChangeListener(handleStatusChange); - // Return a cleanup function for the listener - return () => { - client.removeStatusChangeListener(handleStatusChange); - }; }) .catch((error) => { debugLogger.error('Failed to get IdeClient instance:', error); @@ -102,6 +95,9 @@ export const ToolActionsProvider: React.FC = ( } return () => { isMounted = false; + if (activeClient) { + activeClient.removeStatusChangeListener(handleStatusChange); + } }; }, [config]); @@ -164,7 +160,13 @@ export const ToolActionsProvider: React.FC = ( ); return ( - + {children} ); diff --git a/packages/cli/src/ui/types.ts b/packages/cli/src/ui/types.ts index 3760575a6f..ccab529192 100644 --- a/packages/cli/src/ui/types.ts +++ b/packages/cli/src/ui/types.ts @@ -93,16 +93,19 @@ export function mapCoreStatusToDisplayStatus( } } + /** * --- TYPE GUARDS --- */ + export const isTodoList = (res: unknown): res is { todos: unknown[] } => typeof res === 'object' && res !== null && 'todos' in res; export const isAnsiOutput = (res: unknown): res is AnsiOutput => Array.isArray(res) && (res.length === 0 || Array.isArray(res[0])); + export interface ToolCallEvent { type: 'tool_call'; status: CoreToolCallStatus; diff --git a/packages/core/src/tools/tools.ts b/packages/core/src/tools/tools.ts index 23e88b608b..0ec4da977b 100644 --- a/packages/core/src/tools/tools.ts +++ b/packages/core/src/tools/tools.ts @@ -920,7 +920,6 @@ export const isListResult = ( res: unknown, ): res is ListDirectoryResult | ReadManyFilesResult => isStructuredToolResult(res) && 'files' in res && Array.isArray(res.files); - export type ToolResultDisplay = | string | FileDiff