@@ -25,10 +25,22 @@ export interface TerminalEvent {
2525 type : "state" | "reset" ;
2626}
2727
28+ /** Options for {@link MockTerminalHandle.runCommand}. */
29+ export interface RunCommandOptions {
30+ /** Re-focus the terminal input after the command completes. Default true. */
31+ focus ?: boolean ;
32+ }
33+
2834/** Imperative API for driving the terminal from outside (Run / Save buttons). */
2935export interface MockTerminalHandle {
30- /** Runs a command line as if it were typed at the prompt. */
31- runCommand : ( text : string ) => void ;
36+ /**
37+ * Runs a command line as if it were typed at the prompt. By default the
38+ * input is re-focused once the command completes, as it is after a typed
39+ * command. Pass `{ focus: false }` when the caller is a Run button: focus
40+ * stays where it is, so on a touch device the keyboard does not open and
41+ * the page does not zoom or jump away from the button that was pressed.
42+ */
43+ runCommand : ( text : string , options ?: RunCommandOptions ) => void ;
3244 /** Writes content to a path in the virtual filesystem. */
3345 saveFile : ( path : string , content : string ) => void ;
3446}
@@ -217,6 +229,8 @@ export const MockTerminal = forwardRef<MockTerminalHandle, MockTerminalProps>(
217229 const scrollRef = useRef < HTMLDivElement > ( null ) ;
218230 const inputRef = useRef < HTMLInputElement > ( null ) ;
219231 const prevBusyRef = useRef ( false ) ;
232+ // Set for the one run that must leave focus where it is (see runCommand).
233+ const skipRefocusRef = useRef ( false ) ;
220234 const completionRef = useRef < {
221235 candidates : string [ ] ;
222236 index : number ;
@@ -316,9 +330,9 @@ export const MockTerminal = forwardRef<MockTerminalHandle, MockTerminalProps>(
316330 // in the submit() finally block because setBusy(false) only queues a render —
317331 // the input is still disabled when finally runs, so focus() would be a no-op.
318332 // This effect fires after React commits the busy=false update, when the input
319- // is already re-enabled.
333+ // is already re-enabled. A run started with `focus: false` skips it.
320334 useEffect ( ( ) => {
321- if ( prevBusyRef . current && ! busy ) {
335+ if ( prevBusyRef . current && ! busy && ! skipRefocusRef . current ) {
322336 inputRef . current ?. focus ( ) ;
323337 }
324338 prevBusyRef . current = busy ;
@@ -585,7 +599,7 @@ export const MockTerminal = forwardRef<MockTerminalHandle, MockTerminalProps>(
585599 // Runs a raw line through the terminal exactly as if it had been typed and
586600 // submitted. Shared by the Enter handler and the imperative runCommand().
587601 const runLine = useCallback (
588- async ( raw : string ) => {
602+ async ( raw : string , { focus = true } : RunCommandOptions = { } ) => {
589603 if ( busy || ! simulator ) return ;
590604 const line = raw . trim ( ) ;
591605
@@ -609,6 +623,7 @@ export const MockTerminal = forwardRef<MockTerminalHandle, MockTerminalProps>(
609623 // While collecting input, every line is a value (empty included); only
610624 // the abort words are special. Dispatch to the input handler.
611625 if ( inInput ) {
626+ skipRefocusRef . current = ! focus ;
612627 setBusy ( true ) ;
613628 try {
614629 await runInputTurn ( line , currentMode ) ;
@@ -627,6 +642,7 @@ export const MockTerminal = forwardRef<MockTerminalHandle, MockTerminalProps>(
627642 return ;
628643 }
629644
645+ skipRefocusRef . current = ! focus ;
630646 setBusy ( true ) ;
631647 try {
632648 if ( currentMode . kind === "session" ) {
@@ -787,7 +803,8 @@ export const MockTerminal = forwardRef<MockTerminalHandle, MockTerminalProps>(
787803 useImperativeHandle (
788804 ref ,
789805 ( ) => ( {
790- runCommand : ( text : string ) => void runLine ( text ) ,
806+ runCommand : ( text : string , options ?: RunCommandOptions ) =>
807+ void runLine ( text , options ) ,
791808 saveFile,
792809 } ) ,
793810 [ runLine , saveFile ] ,
0 commit comments