fix(cli): Ctrl+C clears input, second Ctrl+C exits TUI
InputBar is now a controlled component — input state lives in app.tsx. Ctrl+C when input has text clears it; Ctrl+C with empty input exits, giving the standard two-press-to-quit behaviour expected in TUIs. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -73,6 +73,9 @@ export function TuiApp({
|
|||||||
|
|
||||||
const [sidebarSelectedIndex, setSidebarSelectedIndex] = useState(0);
|
const [sidebarSelectedIndex, setSidebarSelectedIndex] = useState(0);
|
||||||
|
|
||||||
|
// Controlled input state — held here so Ctrl+C can clear it
|
||||||
|
const [tuiInput, setTuiInput] = useState('');
|
||||||
|
|
||||||
const handleLocalCommand = useCallback(
|
const handleLocalCommand = useCallback(
|
||||||
(parsed: ParsedCommand) => {
|
(parsed: ParsedCommand) => {
|
||||||
switch (parsed.command) {
|
switch (parsed.command) {
|
||||||
@@ -153,9 +156,15 @@ export function TuiApp({
|
|||||||
);
|
);
|
||||||
|
|
||||||
useInput((ch, key) => {
|
useInput((ch, key) => {
|
||||||
|
// Ctrl+C: clear input if non-empty; exit if already empty (second press)
|
||||||
if (key.ctrl && ch === 'c') {
|
if (key.ctrl && ch === 'c') {
|
||||||
|
if (tuiInput) {
|
||||||
|
setTuiInput('');
|
||||||
|
} else {
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Ctrl+L: toggle sidebar (refresh on open)
|
// Ctrl+L: toggle sidebar (refresh on open)
|
||||||
if (key.ctrl && ch === 'l') {
|
if (key.ctrl && ch === 'l') {
|
||||||
const willOpen = !appMode.sidebarOpen;
|
const willOpen = !appMode.sidebarOpen;
|
||||||
@@ -260,6 +269,8 @@ export function TuiApp({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<InputBar
|
<InputBar
|
||||||
|
value={tuiInput}
|
||||||
|
onChange={setTuiInput}
|
||||||
onSubmit={socket.sendMessage}
|
onSubmit={socket.sendMessage}
|
||||||
onSystemMessage={socket.addSystemMessage}
|
onSystemMessage={socket.addSystemMessage}
|
||||||
onLocalCommand={handleLocalCommand}
|
onLocalCommand={handleLocalCommand}
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import React, { useState, useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { Box, Text, useInput } from 'ink';
|
import { Box, Text, useInput } from 'ink';
|
||||||
import TextInput from 'ink-text-input';
|
import TextInput from 'ink-text-input';
|
||||||
import type { ParsedCommand, CommandDef } from '@mosaic/types';
|
import type { ParsedCommand, CommandDef } from '@mosaic/types';
|
||||||
import { parseSlashCommand, commandRegistry } from '../commands/index.js';
|
import { parseSlashCommand, commandRegistry } from '../commands/index.js';
|
||||||
import { CommandAutocomplete } from './command-autocomplete.js';
|
import { CommandAutocomplete } from './command-autocomplete.js';
|
||||||
import { useInputHistory } from '../hooks/use-input-history.js';
|
import { useInputHistory } from '../hooks/use-input-history.js';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
export interface InputBarProps {
|
export interface InputBarProps {
|
||||||
|
/** Controlled input value — caller owns the state */
|
||||||
|
value: string;
|
||||||
|
onChange: (val: string) => void;
|
||||||
onSubmit: (value: string) => void;
|
onSubmit: (value: string) => void;
|
||||||
onSystemMessage?: (message: string) => void;
|
onSystemMessage?: (message: string) => void;
|
||||||
onLocalCommand?: (parsed: ParsedCommand) => void;
|
onLocalCommand?: (parsed: ParsedCommand) => void;
|
||||||
@@ -18,6 +22,8 @@ export interface InputBarProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function InputBar({
|
export function InputBar({
|
||||||
|
value: input,
|
||||||
|
onChange: setInput,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onSystemMessage,
|
onSystemMessage,
|
||||||
onLocalCommand,
|
onLocalCommand,
|
||||||
@@ -27,7 +33,6 @@ export function InputBar({
|
|||||||
placeholder: placeholderOverride,
|
placeholder: placeholderOverride,
|
||||||
allCommands,
|
allCommands,
|
||||||
}: InputBarProps) {
|
}: InputBarProps) {
|
||||||
const [input, setInput] = useState('');
|
|
||||||
const [showAutocomplete, setShowAutocomplete] = useState(false);
|
const [showAutocomplete, setShowAutocomplete] = useState(false);
|
||||||
const [autocompleteIndex, setAutocompleteIndex] = useState(0);
|
const [autocompleteIndex, setAutocompleteIndex] = useState(0);
|
||||||
|
|
||||||
@@ -36,7 +41,8 @@ export function InputBar({
|
|||||||
// Determine which commands to show in autocomplete
|
// Determine which commands to show in autocomplete
|
||||||
const availableCommands = allCommands ?? commandRegistry.getAll();
|
const availableCommands = allCommands ?? commandRegistry.getAll();
|
||||||
|
|
||||||
const handleChange = useCallback((value: string) => {
|
const handleChange = useCallback(
|
||||||
|
(value: string) => {
|
||||||
setInput(value);
|
setInput(value);
|
||||||
if (value.startsWith('/')) {
|
if (value.startsWith('/')) {
|
||||||
setShowAutocomplete(true);
|
setShowAutocomplete(true);
|
||||||
@@ -44,7 +50,9 @@ export function InputBar({
|
|||||||
} else {
|
} else {
|
||||||
setShowAutocomplete(false);
|
setShowAutocomplete(false);
|
||||||
}
|
}
|
||||||
}, []);
|
},
|
||||||
|
[setInput],
|
||||||
|
);
|
||||||
|
|
||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
(value: string) => {
|
(value: string) => {
|
||||||
@@ -92,6 +100,7 @@ export function InputBar({
|
|||||||
isStreaming,
|
isStreaming,
|
||||||
connected,
|
connected,
|
||||||
addToHistory,
|
addToHistory,
|
||||||
|
setInput,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -116,7 +125,7 @@ export function InputBar({
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}, [showAutocomplete, input, availableCommands, autocompleteIndex]);
|
}, [showAutocomplete, input, availableCommands, autocompleteIndex, setInput]);
|
||||||
|
|
||||||
useInput((ch, key) => {
|
useInput((ch, key) => {
|
||||||
// Escape: hide autocomplete
|
// Escape: hide autocomplete
|
||||||
|
|||||||
Reference in New Issue
Block a user