Skip to content

Commit 0ab2e9e

Browse files
Merge pull request #12 from ai-action/feat/plan
2 parents 4f1a419 + 8d47f61 commit 0ab2e9e

23 files changed

Lines changed: 1989 additions & 796 deletions

src/components/App.test.tsx

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ vi.mock('../utils', () => ({
1515

1616
const capturedCallbacks = vi.hoisted(() => ({
1717
onCommand: null as ((command: string) => void) | null,
18+
onModeChange: null as ((mode: string) => void) | null,
1819
onSelect: null as ((model: string) => void) | null,
1920
onCancel: null as (() => void) | null,
2021
onToggleMode: null as (() => void) | null,
@@ -29,12 +30,15 @@ vi.mock('./Header', () => ({
2930
vi.mock('./Chat', () => ({
3031
Chat: ({
3132
onCommand,
33+
onModeChange,
3234
}: {
3335
model: string;
3436
onCommand: (command: string) => void;
35-
autoExecute: boolean;
37+
mode: string;
38+
onModeChange: (mode: string) => void;
3639
}) => {
3740
capturedCallbacks.onCommand = onCommand;
41+
capturedCallbacks.onModeChange = onModeChange;
3842
return <Text>{'>'}</Text>;
3943
},
4044
}));
@@ -56,14 +60,15 @@ vi.mock('./ModelPicker', () => ({
5660

5761
vi.mock('./Footer', () => ({
5862
Footer: ({
59-
autoExecute,
63+
mode,
6064
onToggleMode,
6165
}: {
62-
autoExecute: boolean;
66+
mode: string;
6367
onToggleMode: () => void;
6468
}) => {
6569
capturedCallbacks.onToggleMode = onToggleMode;
66-
return <Text>Mode: {autoExecute ? 'Auto' : 'Safe'}</Text>;
70+
const modeLabel = mode.charAt(0).toUpperCase() + mode.slice(1);
71+
return <Text>Mode: {modeLabel}</Text>;
6772
},
6873
}));
6974

@@ -72,6 +77,7 @@ import { App } from './App';
7277
describe('App', () => {
7378
beforeEach(() => {
7479
capturedCallbacks.onCommand = null;
80+
capturedCallbacks.onModeChange = null;
7581
capturedCallbacks.onSelect = null;
7682
capturedCallbacks.onCancel = null;
7783
capturedCallbacks.onToggleMode = null;
@@ -127,25 +133,44 @@ describe('App', () => {
127133
expect(lastFrame()).not.toContain('ModelPicker');
128134
});
129135

130-
it('toggles autoExecute via Footer onToggleMode callback', async () => {
136+
it('toggles mode via Footer onToggleMode callback (3-state cycle)', async () => {
131137
const { lastFrame, rerender } = render(<App />);
132138

133-
// Initial state
139+
// Initial state: Safe
134140
expect(lastFrame()).toContain('Mode: Safe');
135141

136-
// Call the callback passed to Footer
142+
// Call the callback passed to Footer - cycles to Auto
137143
capturedCallbacks.onToggleMode?.();
138144
rerender(<App />);
139145
await tick();
140-
141-
// Should show Auto mode
142146
expect(lastFrame()).toContain('Mode: Auto');
143147

144-
// Call again to toggle back
148+
// Call again - cycles to Plan
149+
capturedCallbacks.onToggleMode?.();
150+
rerender(<App />);
151+
await tick();
152+
expect(lastFrame()).toContain('Mode: Plan');
153+
154+
// Call again - cycles back to Safe
145155
capturedCallbacks.onToggleMode?.();
146156
rerender(<App />);
147157
await tick();
158+
expect(lastFrame()).toContain('Mode: Safe');
159+
});
160+
161+
it('updates footer mode when Chat changes execution mode', async () => {
162+
const { lastFrame, rerender } = render(<App />);
148163

149164
expect(lastFrame()).toContain('Mode: Safe');
165+
166+
capturedCallbacks.onModeChange?.('auto');
167+
rerender(<App />);
168+
await tick();
169+
expect(lastFrame()).toContain('Mode: Auto');
170+
171+
capturedCallbacks.onModeChange?.('safe');
172+
rerender(<App />);
173+
await tick();
174+
expect(lastFrame()).toContain('Mode: Safe');
150175
});
151176
});

src/components/App.tsx

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Box } from 'ink';
22
import { useCallback, useState } from 'react';
33

4+
import { MODE } from '../constants';
45
import { config } from '../utils';
56
import { Chat } from './Chat';
67
import { Footer } from './Footer';
@@ -10,7 +11,7 @@ import { ModelPicker } from './ModelPicker';
1011
export function App() {
1112
const [model, setModel] = useState(() => config.loadConfig().model);
1213
const [picking, setPicking] = useState(false);
13-
const [autoExecute, setAutoExecute] = useState(false);
14+
const [mode, setMode] = useState<MODE.Name>(MODE.NAME.SAFE);
1415

1516
const handleCommand = useCallback((command: string) => {
1617
if (command === '/model') {
@@ -42,14 +43,26 @@ export function App() {
4243
<Chat
4344
model={model}
4445
onCommand={handleCommand}
45-
autoExecute={autoExecute}
46+
mode={mode}
47+
onModeChange={setMode}
4648
/>
4749
)}
4850

4951
<Footer
50-
autoExecute={autoExecute}
52+
mode={mode}
5153
onToggleMode={() => {
52-
setAutoExecute((isAutoExecute) => !isAutoExecute);
54+
setMode((mode) => {
55+
// Cycle: safe -> auto -> plan -> safe
56+
switch (mode) {
57+
case MODE.NAME.SAFE:
58+
return MODE.NAME.AUTO;
59+
case MODE.NAME.AUTO:
60+
return MODE.NAME.PLAN;
61+
case MODE.NAME.PLAN:
62+
default:
63+
return MODE.NAME.SAFE;
64+
}
65+
});
5366
}}
5467
/>
5568
</Box>

0 commit comments

Comments
 (0)