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() {
)}