diff --git a/src/components/App.test.tsx b/src/components/App.test.tsx index 40f4649f..13d48e1c 100644 --- a/src/components/App.test.tsx +++ b/src/components/App.test.tsx @@ -15,6 +15,7 @@ vi.mock('../utils', () => ({ const capturedCallbacks = vi.hoisted(() => ({ onCommand: null as ((command: string) => void) | null, + onModeChange: null as ((mode: string) => void) | null, onSelect: null as ((model: string) => void) | null, onCancel: null as (() => void) | null, onToggleMode: null as (() => void) | null, @@ -29,12 +30,15 @@ vi.mock('./Header', () => ({ vi.mock('./Chat', () => ({ Chat: ({ onCommand, + onModeChange, }: { model: string; onCommand: (command: string) => void; - autoExecute: boolean; + mode: string; + onModeChange: (mode: string) => void; }) => { capturedCallbacks.onCommand = onCommand; + capturedCallbacks.onModeChange = onModeChange; return {'>'}; }, })); @@ -56,14 +60,15 @@ vi.mock('./ModelPicker', () => ({ vi.mock('./Footer', () => ({ Footer: ({ - autoExecute, + mode, onToggleMode, }: { - autoExecute: boolean; + mode: string; onToggleMode: () => void; }) => { capturedCallbacks.onToggleMode = onToggleMode; - return Mode: {autoExecute ? 'Auto' : 'Safe'}; + const modeLabel = mode.charAt(0).toUpperCase() + mode.slice(1); + return Mode: {modeLabel}; }, })); @@ -72,6 +77,7 @@ import { App } from './App'; describe('App', () => { beforeEach(() => { capturedCallbacks.onCommand = null; + capturedCallbacks.onModeChange = null; capturedCallbacks.onSelect = null; capturedCallbacks.onCancel = null; capturedCallbacks.onToggleMode = null; @@ -127,25 +133,44 @@ describe('App', () => { expect(lastFrame()).not.toContain('ModelPicker'); }); - it('toggles autoExecute via Footer onToggleMode callback', async () => { + it('toggles mode via Footer onToggleMode callback (3-state cycle)', async () => { const { lastFrame, rerender } = render(); - // Initial state + // Initial state: Safe expect(lastFrame()).toContain('Mode: Safe'); - // Call the callback passed to Footer + // Call the callback passed to Footer - cycles to Auto capturedCallbacks.onToggleMode?.(); rerender(); await tick(); - - // Should show Auto mode expect(lastFrame()).toContain('Mode: Auto'); - // Call again to toggle back + // Call again - cycles to Plan + capturedCallbacks.onToggleMode?.(); + rerender(); + await tick(); + expect(lastFrame()).toContain('Mode: Plan'); + + // Call again - cycles back to Safe capturedCallbacks.onToggleMode?.(); rerender(); await tick(); + expect(lastFrame()).toContain('Mode: Safe'); + }); + + it('updates footer mode when Chat changes execution mode', async () => { + const { lastFrame, rerender } = render(); expect(lastFrame()).toContain('Mode: Safe'); + + capturedCallbacks.onModeChange?.('auto'); + rerender(); + await tick(); + expect(lastFrame()).toContain('Mode: Auto'); + + capturedCallbacks.onModeChange?.('safe'); + rerender(); + await tick(); + expect(lastFrame()).toContain('Mode: Safe'); }); }); diff --git a/src/components/App.tsx b/src/components/App.tsx index 6f198b4a..74d9d877 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -1,6 +1,7 @@ import { Box } from 'ink'; import { useCallback, useState } from 'react'; +import { MODE } from '../constants'; import { config } from '../utils'; import { Chat } from './Chat'; import { Footer } from './Footer'; @@ -10,7 +11,7 @@ import { ModelPicker } from './ModelPicker'; export function App() { const [model, setModel] = useState(() => config.loadConfig().model); const [picking, setPicking] = useState(false); - const [autoExecute, setAutoExecute] = useState(false); + const [mode, setMode] = useState(MODE.NAME.SAFE); const handleCommand = useCallback((command: string) => { if (command === '/model') { @@ -42,14 +43,26 @@ export function App() { )}