feat(ui): use Tab to switch focus between shell and input (#14332)

This commit is contained in:
Jacob Richman
2026-01-12 15:30:12 -08:00
committed by GitHub
parent 2e8c6cfdbb
commit ca6786a28b
11 changed files with 180 additions and 114 deletions

View File

@@ -2418,6 +2418,84 @@ describe('InputPrompt', () => {
});
});
describe('Tab focus toggle', () => {
it.each([
{
name: 'should toggle focus in on Tab when no suggestions or ghost text',
showSuggestions: false,
ghostText: '',
suggestions: [],
expectedFocusToggle: true,
},
{
name: 'should accept ghost text and NOT toggle focus on Tab',
showSuggestions: false,
ghostText: 'ghost text',
suggestions: [],
expectedFocusToggle: false,
expectedAcceptCall: true,
},
{
name: 'should NOT toggle focus on Tab when suggestions are present',
showSuggestions: true,
ghostText: '',
suggestions: [{ label: 'test', value: 'test' }],
expectedFocusToggle: false,
},
])(
'$name',
async ({
showSuggestions,
ghostText,
suggestions,
expectedFocusToggle,
expectedAcceptCall,
}) => {
const mockAccept = vi.fn();
mockedUseCommandCompletion.mockReturnValue({
...mockCommandCompletion,
showSuggestions,
suggestions,
promptCompletion: {
text: ghostText,
accept: mockAccept,
clear: vi.fn(),
isLoading: false,
isActive: ghostText !== '',
markSelected: vi.fn(),
},
});
const { stdin, unmount } = renderWithProviders(
<InputPrompt {...props} />,
{
uiActions,
uiState: { activePtyId: 1 },
},
);
await act(async () => {
stdin.write('\t');
});
await waitFor(() => {
if (expectedFocusToggle) {
expect(uiActions.setEmbeddedShellFocused).toHaveBeenCalledWith(
true,
);
} else {
expect(uiActions.setEmbeddedShellFocused).not.toHaveBeenCalled();
}
if (expectedAcceptCall) {
expect(mockAccept).toHaveBeenCalled();
}
});
unmount();
},
);
});
describe('mouse interaction', () => {
it.each([
{

View File

@@ -135,7 +135,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
const kittyProtocol = useKittyKeyboardProtocol();
const isShellFocused = useShellFocusState();
const { setEmbeddedShellFocused } = useUIActions();
const { mainAreaWidth } = useUIState();
const { mainAreaWidth, activePtyId } = useUIState();
const [justNavigatedHistory, setJustNavigatedHistory] = useState(false);
const escPressCount = useRef(0);
const [showEscapePrompt, setShowEscapePrompt] = useState(false);
@@ -829,6 +829,14 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
return;
}
if (keyMatchers[Command.TOGGLE_SHELL_INPUT_FOCUS_IN](key)) {
// If we got here, Autocomplete didn't handle the key (e.g. no suggestions).
if (activePtyId) {
setEmbeddedShellFocused(true);
}
return;
}
// Fall back to the text buffer's default input handling for all other keys
buffer.handleInput(key);
@@ -870,6 +878,8 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
kittyProtocol.enabled,
tryLoadQueuedMessages,
setBannerVisible,
activePtyId,
setEmbeddedShellFocused,
],
);

View File

@@ -140,7 +140,7 @@ export const ShellToolMessage: React.FC<ShellToolMessageProps> = ({
{shouldShowFocusHint && (
<Box marginLeft={1} flexShrink={0}>
<Text color={theme.text.accent}>
{isThisShellFocused ? '(Focused)' : '(ctrl+f to focus)'}
{isThisShellFocused ? '(Focused)' : '(tab to focus)'}
</Text>
</Box>
)}

View File

@@ -112,7 +112,7 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
{shouldShowFocusHint && (
<Box marginLeft={1} flexShrink={0}>
<Text color={theme.text.accent}>
{isThisShellFocused ? '(Focused)' : '(ctrl+f to focus)'}
{isThisShellFocused ? '(Focused)' : '(tab to focus)'}
</Text>
</Box>
)}