Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion src/components/App/ReadinessCheck.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,29 @@ import { useRef } from 'react';

import { ReadinessCheck, ReadinessState } from './ReadinessCheck';

const mockSubmit = vi.hoisted(() => vi.fn());

vi.mock('@/components/Chat', () => ({
ChatInput: ({ onSubmit }: { onSubmit: (value: string) => void }) => {
ChatInput: ({
onSubmit,
}: {
onSubmit: (value: { content: string }) => void;
}) => {
const onSubmitRef = useRef(onSubmit);
onSubmitRef.current = onSubmit;
// Store the callback for tests to access
mockSubmit.mockImplementation((value: { content: string }) => {
onSubmitRef.current(value);
});
return null;
},
}));

describe('ReadinessCheck', () => {
beforeEach(() => {
mockSubmit.mockClear();
});

it('renders checking state', () => {
const { lastFrame } = render(
<ReadinessCheck
Expand Down Expand Up @@ -94,4 +108,17 @@ describe('ReadinessCheck', () => {
expect(lastFrame()).not.toContain('No models installed');
expect(lastFrame()).not.toContain('Unable to load models');
});

it('calls onCommand when ChatInput submits', () => {
const onCommand = vi.fn();
render(
<ReadinessCheck
setupState={ReadinessState.Ready}
onCommand={onCommand}
/>,
);
// The mock stores the callback in mockSubmit
mockSubmit({ content: '/model' });
expect(onCommand).toHaveBeenCalledWith('/model');
});
});
8 changes: 7 additions & 1 deletion src/components/App/ReadinessCheck.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,13 @@ export function ReadinessCheck({
{getMessage(setupState, errorMessage)}
</Box>

<ChatInput history={[]} onSubmit={onCommand} />
<ChatInput
history={[]}
onSubmit={({ content }) => {
onCommand(content);
}}
theme={theme}
/>
</Box>
);
}
69 changes: 64 additions & 5 deletions src/components/Chat/Chat.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import type { Decision } from '@/types';
import { ollama, time, tools } from '@/utils';

const mockState = vi.hoisted(() => ({
handler: undefined as ((value: string) => void) | undefined,
handler: undefined as
| ((value: { content: string; images?: string[] }) => void)
| undefined,
history: [] as string[],
testInput: '',
shouldReset: false,
Expand Down Expand Up @@ -97,7 +99,7 @@ vi.mock('@/utils', async () => ({
vi.mock('./ChatInput', () => ({
ChatInput: (props: {
history?: string[];
onSubmit?: (value: string) => void;
onSubmit?: (value: { content: string; images?: string[] }) => void;
onInterrupt?: () => void;
isDisabled?: boolean;
}) => {
Expand Down Expand Up @@ -140,8 +142,8 @@ async function typeText(
await time.tick();
}

function submitInput(value: string) {
mockState.handler?.(value);
function submitInput(value: string, images?: string[]) {
mockState.handler?.({ content: value, images });
mockState.clear();
}

Expand Down Expand Up @@ -972,6 +974,7 @@ describe('Chat with tool calls', () => {
submitInput('make a plan');
rerender(chat);
await waitForStream();
await time.tick(50);
rerender(chat);

expect(lastFrame()).toContain('Plan Generated');
Expand All @@ -987,7 +990,7 @@ describe('Chat with tool calls', () => {

choosePlanMode(MODE.AUTO);
await time.tick();
});
}, 20_000);

it('executes an approved plan immediately in auto mode', async () => {
const { streamChat } = ollama;
Expand Down Expand Up @@ -1434,4 +1437,60 @@ describe('Chat interrupt', () => {

expect(lastFrame()).not.toContain('Execution interrupted');
});

it('submits with empty images array without adding images property', async () => {
const chat = (
<Chat
model="gemma4"
onCommand={vi.fn()}
mode={MODE.SAFE}
onModeChange={vi.fn()}
sessionId="0"
/>
);
const { lastFrame, rerender } = render(chat);
submitInput('hello', []);
rerender(chat);
await waitForStream();

expect(lastFrame()).toContain('Mocked response');
});

it('submits without images parameter', async () => {
const chat = (
<Chat
model="gemma4"
onCommand={vi.fn()}
mode={MODE.SAFE}
onModeChange={vi.fn()}
sessionId="0"
/>
);
const { lastFrame, rerender } = render(chat);
// Call submitInput without the images parameter (undefined)
mockState.handler?.({ content: 'hello' });
mockState.clear();
rerender(chat);
await waitForStream();

expect(lastFrame()).toContain('Mocked response');
});

it('submits with images array containing items', async () => {
const chat = (
<Chat
model="gemma4"
onCommand={vi.fn()}
mode={MODE.SAFE}
onModeChange={vi.fn()}
sessionId="0"
/>
);
const { lastFrame, rerender } = render(chat);
submitInput('hello', ['/tmp/image.png']);
rerender(chat);
await waitForStream();

expect(lastFrame()).toContain('Mocked response');
});
});
10 changes: 6 additions & 4 deletions src/components/Chat/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import type {
} from '@/types';
import { agents, ollama, tools } from '@/utils';

import { ChatInput } from './ChatInput';
import { ChatInput, type SubmittedInput } from './ChatInput';
import {
ACTION_NOT_PERFORMED,
InterruptReason,
Expand Down Expand Up @@ -545,11 +545,11 @@ export function Chat({
);

const handleSubmit = useCallback(
async (value: string) => {
async ({ content, images }: SubmittedInput) => {
setInterruptReason(null);
const userContent = value.trim();
const userContent = content.trim();

if (!userContent) {
if (!userContent && !images?.length) {
return;
}

Expand All @@ -563,6 +563,7 @@ export function Chat({
const userMessage: ollama.Message = {
role: ROLE.USER,
content: userContent,
...(images?.length ? { images } : {}),
};

const updatedMessages = [...messages, userMessage];
Expand Down Expand Up @@ -624,6 +625,7 @@ export function Chat({
onInterrupt={handleInterrupt}
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onSubmit={handleSubmit}
theme={theme}
/>
</Box>
)}
Expand Down
Loading