Skip to content

Commit cc09441

Browse files
Merge pull request #20 from ai-action/feat/clear
2 parents 21bff87 + 0da7a75 commit cc09441

17 files changed

Lines changed: 595 additions & 155 deletions

src/cli.test.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,13 @@ import type { MockInstance } from 'vitest';
33
type RunAction = (model: string, prompt: string) => Promise<void>;
44

55
const {
6-
clearScreen,
76
createSystemMessage,
87
executeTool,
98
outputHelp,
109
parse,
1110
renderApp,
1211
streamChat,
1312
} = vi.hoisted(() => ({
14-
clearScreen: vi.fn(),
1513
createSystemMessage: vi.fn(() => ({
1614
role: 'system',
1715
content: 'system prompt',
@@ -30,7 +28,6 @@ const commandState = vi.hoisted(() => ({
3028
vi.mock('./utils', () => ({
3129
agents: { createSystemMessage },
3230
ollama: { streamChat },
33-
screen: { clear: clearScreen },
3431
tools: { TOOLS: ['mock-tool'], executeTool },
3532
}));
3633
vi.mock('./tui', () => ({ renderApp }));
@@ -73,7 +70,7 @@ describe('cli', () => {
7370

7471
it('renders TUI with no args', async () => {
7572
await main([]);
76-
expect(clearScreen).toHaveBeenCalledOnce();
73+
expect(stdoutSpy).toHaveBeenCalledWith('\x1Bc');
7774
expect(renderApp).toHaveBeenCalledOnce();
7875
expect(parse).not.toHaveBeenCalled();
7976
});

src/cli.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { realpathSync } from 'node:fs';
55
import cac from 'cac';
66

77
import { PACKAGE, ROLE } from './constants';
8-
import { agents, ollama, screen, tools } from './utils';
8+
import { agents, ollama, tools } from './utils';
99

1010
const cli = cac('code-ollama');
1111

@@ -79,15 +79,17 @@ export async function main(
7979
if (!args.length) {
8080
const { renderApp } = await import('./tui');
8181

82-
screen.clear();
82+
// clear the tui
83+
process.stdout.write('\x1Bc');
84+
8385
renderApp();
8486
return;
8587
}
8688

8789
cli.parse(['node', 'code-ollama', ...args]);
8890
}
8991

90-
/* v8 ignore start */
92+
// v8 ignore start
9193
function isEntrypoint(argv1 = process.argv[1]): boolean {
9294
if (!argv1) {
9395
return false;
@@ -103,4 +105,4 @@ function isEntrypoint(argv1 = process.argv[1]): boolean {
103105
if (isEntrypoint()) {
104106
void main();
105107
}
106-
/* v8 ignore stop */
108+
// v8 ignore stop

src/components/App.test.tsx

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,13 @@ import { render } from 'ink-testing-library';
33

44
import { test } from '../utils';
55

6+
const resetSystemMessage = vi.hoisted(() => vi.fn());
7+
68
vi.mock('../utils', async () => ({
79
...(await vi.importActual('../utils')),
10+
agents: {
11+
resetSystemMessage,
12+
},
813
config: {
914
loadConfig: vi.fn(() => ({
1015
host: 'http://localhost:11434',
@@ -32,15 +37,17 @@ vi.mock('./Chat', () => ({
3237
Chat: ({
3338
onCommand,
3439
onModeChange,
40+
sessionId,
3541
}: {
3642
model: string;
3743
onCommand: (command: string) => void;
3844
mode: string;
3945
onModeChange: (mode: string) => void;
46+
sessionId: number;
4047
}) => {
4148
capturedCallbacks.onCommand = onCommand;
4249
capturedCallbacks.onModeChange = onModeChange;
43-
return <Text>{'>'}</Text>;
50+
return <Text>{`> session:${String(sessionId)}`}</Text>;
4451
},
4552
}));
4653

@@ -82,6 +89,7 @@ describe('App', () => {
8289
capturedCallbacks.onSelect = null;
8390
capturedCallbacks.onClose = null;
8491
capturedCallbacks.onToggleMode = null;
92+
resetSystemMessage.mockClear();
8593
});
8694

8795
it('renders title', () => {
@@ -134,6 +142,20 @@ describe('App', () => {
134142
expect(lastFrame()).not.toContain('ModelPicker');
135143
});
136144

145+
it('resets the chat session when /clear is issued', async () => {
146+
const { lastFrame, rerender } = render(<App />);
147+
148+
expect(lastFrame()).toContain('session:0');
149+
150+
capturedCallbacks.onCommand?.('/clear');
151+
rerender(<App />);
152+
await test.tick();
153+
154+
expect(resetSystemMessage).toHaveBeenCalledOnce();
155+
expect(lastFrame()).toContain('session:1');
156+
expect(lastFrame()).not.toContain('ModelPicker');
157+
});
158+
137159
it('toggles mode via Footer onToggleMode callback (3-state cycle)', async () => {
138160
const { lastFrame, rerender } = render(<App />);
139161

src/components/App.tsx

Lines changed: 45 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Box } from 'ink';
22
import { useCallback, useState } from 'react';
33

44
import { MODE } from '../constants';
5-
import { config } from '../utils';
5+
import { agents, config } from '../utils';
66
import { Chat } from './Chat';
77
import { Footer } from './Footer';
88
import { Header } from './Header';
@@ -12,10 +12,19 @@ export function App() {
1212
const [model, setModel] = useState(() => config.loadConfig().model);
1313
const [picking, setPicking] = useState(false);
1414
const [mode, setMode] = useState<MODE.Name>(MODE.NAME.SAFE);
15+
const [sessionId, setSessionId] = useState(0);
1516

1617
const handleCommand = useCallback((command: string) => {
17-
if (command === '/model') {
18-
setPicking(true);
18+
switch (command) {
19+
case '/model':
20+
setPicking(true);
21+
break;
22+
23+
case '/clear':
24+
agents.resetSystemMessage();
25+
setPicking(false);
26+
setSessionId((sessionId) => sessionId + 1);
27+
break;
1928
}
2029
}, []);
2130

@@ -29,42 +38,52 @@ export function App() {
2938
setPicking(false);
3039
}, []);
3140

32-
return (
33-
<Box flexDirection="column">
34-
<Header model={model} />
41+
const handleToggleMode = useCallback(() => {
42+
setMode((mode) => {
43+
// Cycle: safe -> auto -> plan -> safe
44+
switch (mode) {
45+
case MODE.NAME.SAFE:
46+
return MODE.NAME.AUTO;
47+
case MODE.NAME.AUTO:
48+
return MODE.NAME.PLAN;
49+
case MODE.NAME.PLAN:
50+
default:
51+
return MODE.NAME.SAFE;
52+
}
53+
});
54+
}, []);
3555

36-
{picking ? (
56+
let body: React.ReactNode;
57+
58+
switch (true) {
59+
case picking:
60+
body = (
3761
<ModelPicker
3862
currentModel={model}
3963
onSelect={handleSelect}
4064
onClose={handleClose}
4165
/>
42-
) : (
66+
);
67+
break;
68+
69+
default:
70+
body = (
4371
<Chat
4472
model={model}
4573
onCommand={handleCommand}
4674
mode={mode}
4775
onModeChange={setMode}
76+
sessionId={sessionId}
4877
/>
49-
)}
78+
);
79+
break;
80+
}
5081

51-
<Footer
52-
mode={mode}
53-
onToggleMode={() => {
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-
});
66-
}}
67-
/>
82+
return (
83+
<Box flexDirection="column">
84+
<Header model={model} />
85+
{body}
86+
<Footer mode={mode} onToggleMode={handleToggleMode} />
6887
</Box>
6988
);
7089
}

0 commit comments

Comments
 (0)