Skip to content
Open
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
91 changes: 90 additions & 1 deletion source/components/user-input.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {render} from 'ink-testing-library';
import React from 'react';
import {themes} from '../config/themes';
import {ThemeContext} from '../hooks/useTheme';
import {UIStateProvider} from '../hooks/useUIState';
import {UIStateProvider, useUIStateContext} from '../hooks/useUIState';
import UserInput from './user-input';

console.log(`\nuser-input.spec.tsx – ${React.version}`);
Expand Down Expand Up @@ -393,6 +393,7 @@ test('UserInput does not show ctrl-o hint when onToggleCompactDisplay is not pro
unmount();
});


// ============================================================================
// Command Completion Navigation Tests
// ============================================================================
Expand Down Expand Up @@ -484,3 +485,91 @@ test('completion menu dismissal/reset after selection or escape', async t => {

unmount();
});

test('UserInput renders completions text when typing /', async t => {
const {stdin, lastFrame, unmount} = render(
<TestWrapper>
<UserInput customCommands={['help', 'model']} />
</TestWrapper>
);

await new Promise(resolve => setTimeout(resolve, 50));
stdin.write('/');
await new Promise(resolve => setTimeout(resolve, 150));

const output = lastFrame()!;
t.truthy(output);
t.regex(output, /Available commands:/);
unmount();
});

test('UserInput renders completions BEFORE the mode indicator (inside the input box)', async t => {
const {stdin, lastFrame, unmount} = render(
<TestWrapper>
<UserInput developmentMode="normal" customCommands={['help', 'model']} />
</TestWrapper>
);

await new Promise(resolve => setTimeout(resolve, 50));
stdin.write('/');
await new Promise(resolve => setTimeout(resolve, 150));

const output = lastFrame()!;
t.truthy(output);

const completionsIdx = output.indexOf('Available commands:');
const modeIdx = output.indexOf('normal mode');
t.true(completionsIdx > -1, 'Completions text should be present');
t.true(modeIdx > -1, 'Mode indicator should be present');
t.true(
completionsIdx < modeIdx,
'Completions must render before the mode indicator (inside the bordered input box)',
);
unmount();
});

test('UserInput completions appear on a line above the mode indicator', async t => {
const {stdin, lastFrame, unmount} = render(
<TestWrapper>
<UserInput developmentMode="normal" customCommands={['help', 'model']} />
</TestWrapper>
);

await new Promise(resolve => setTimeout(resolve, 50));
stdin.write('/');
await new Promise(resolve => setTimeout(resolve, 150));

const output = lastFrame()!;
const lines = output.split('\n');

let completionLine = -1;
let modeLine = -1;
for (let i = 0; i < lines.length; i++) {
if (lines[i].includes('Available commands:')) completionLine = i;
if (lines[i].includes('normal mode')) modeLine = i;
}

t.true(completionLine > -1, 'Should find completions line');
t.true(modeLine > -1, 'Should find mode indicator line');
t.true(
completionLine < modeLine,
`Completions (line ${completionLine}) must be above mode indicator (line ${modeLine})`,
);
unmount();
});

test('UserInput does not show completions when input is empty', t => {
const {lastFrame, unmount} = render(
<TestWrapper>
<UserInput />
</TestWrapper>
);

const output = lastFrame()!;
t.truthy(output);
t.notRegex(output, /Available commands:/);
unmount();
});



85 changes: 43 additions & 42 deletions source/components/user-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -651,47 +651,6 @@ export default function UserInput({
</Text>
)}

{showCompletions && completions.length > 0 && (
<Box flexDirection="column" marginTop={1}>
<Text color={colors.secondary}>Available commands:</Text>
{completions.map((completion, index) => {
const isSelected = index === selectedCompletionIndex;
return (
<Text
key={index}
color={
isSelected
? colors.info
: completion.isCustom
? colors.info
: colors.primary
}
bold={isSelected}
>
{isSelected ? '▸ ' : ' '}/{completion.name}
</Text>
);
})}
</Box>
)}
{isFileAutocompleteMode && fileCompletions.length > 0 && (
<Box flexDirection="column" marginTop={1}>
<Text color={colors.secondary}>
File suggestions (↑/↓ to navigate, Tab to select):
</Text>
{fileCompletions.slice(0, 5).map((file, index) => (
<Text
key={index}
color={index === selectedFileIndex ? colors.info : colors.primary}
bold={index === selectedFileIndex}
>
{index === selectedFileIndex ? '▸ ' : ' '}
{file.path}
</Text>
))}
</Box>
)}

<Box
flexDirection="column"
marginTop={1}
Expand Down Expand Up @@ -726,6 +685,49 @@ export default function UserInput({
{showClearMessage && (
<Text color={colors.secondary}>Press escape again to clear</Text>
)}

{showCompletions && completions.length > 0 && (
<Box flexDirection="column" marginTop={1}>
<Text color={colors.secondary}>Available commands:</Text>
{completions.map((completion, index) => {
const isSelected = index === selectedCompletionIndex;
return (
<Text
key={index}
color={
isSelected
? colors.info
: completion.isCustom
? colors.info
: colors.primary
}
bold={isSelected}
>
{isSelected ? '▸ ' : ' '}/{completion.name}
</Text>
);
})}
</Box>
)}
{isFileAutocompleteMode && fileCompletions.length > 0 && (
<Box flexDirection="column" marginTop={1}>
<Text color={colors.secondary}>
File suggestions (↑/↓ to navigate, Tab to select):
</Text>
{fileCompletions.slice(0, 5).map((file, index) => (
<Text
key={index}
color={
index === selectedFileIndex ? colors.info : colors.primary
}
bold={index === selectedFileIndex}
>
{index === selectedFileIndex ? '▸ ' : ' '}
{file.path}
</Text>
))}
</Box>
)}
</Box>

{attachments.length > 0 && (
Expand All @@ -738,7 +740,6 @@ export default function UserInput({
<Text color={colors.secondary}> · ctrl-x remove last</Text>
</Box>
)}

{/* Development mode indicator - always visible */}
<DevelopmentModeIndicator
developmentMode={developmentMode}
Expand Down
Loading