@@ -123,11 +123,70 @@ const TMUX_KEYS: QuickKey[] = [
123123 { label : ": cmd" , key : "\x02:" , title : "Command prompt" } ,
124124] ;
125125
126+ const CLAUDE_KEYS : QuickKey [ ] = [
127+ { label : "Shift+Tab" , key : "\x1b[Z" , title : "Change permission mode" } ,
128+ { label : "Ctrl+O logs" , key : "\x0f" , title : "Show logs" } ,
129+ { label : "Ctrl+C cancel" , key : "\x03" , title : "Cancel / interrupt" } ,
130+ { label : "Esc" , key : "\x1b" , title : "Escape / go back" } ,
131+ { label : "Enter" , key : "\r" , title : "Confirm / send" } ,
132+ { label : "Up" , key : "\x1b[A" , title : "Previous" } ,
133+ { label : "Down" , key : "\x1b[B" , title : "Next" } ,
134+ { label : "y" , key : "y" , title : "Yes (approve)" } ,
135+ { label : "n" , key : "n" , title : "No (deny)" } ,
136+ ] ;
137+
138+ const CODEX_KEYS : QuickKey [ ] = [
139+ { label : "Shift+Tab" , key : "\x1b[Z" , title : "Change permission mode" } ,
140+ { label : "Ctrl+C cancel" , key : "\x03" , title : "Cancel / interrupt" } ,
141+ { label : "Esc" , key : "\x1b" , title : "Escape / go back" } ,
142+ { label : "Enter" , key : "\r" , title : "Confirm / send" } ,
143+ { label : "Up" , key : "\x1b[A" , title : "Previous" } ,
144+ { label : "Down" , key : "\x1b[B" , title : "Next" } ,
145+ { label : "y" , key : "y" , title : "Yes (approve)" } ,
146+ { label : "n" , key : "n" , title : "No (deny)" } ,
147+ ] ;
148+
149+ const OPENCODE_KEYS : QuickKey [ ] = [
150+ { label : "Ctrl+C cancel" , key : "\x03" , title : "Cancel / interrupt" } ,
151+ { label : "Esc" , key : "\x1b" , title : "Escape / go back" } ,
152+ { label : "Enter" , key : "\r" , title : "Confirm / send" } ,
153+ { label : "Tab" , key : "\t" , title : "Autocomplete / cycle" } ,
154+ { label : "Up" , key : "\x1b[A" , title : "Previous" } ,
155+ { label : "Down" , key : "\x1b[B" , title : "Next" } ,
156+ { label : "y" , key : "y" , title : "Yes (approve)" } ,
157+ { label : "n" , key : "n" , title : "No (deny)" } ,
158+ ] ;
159+
160+ interface CliLaunchOption {
161+ label : string ;
162+ title : string ;
163+ command : string ;
164+ }
165+
166+ const CLAUDE_LAUNCH_OPTIONS : CliLaunchOption [ ] = [
167+ { label : "default" , title : "Interactive mode (asks for permission)" , command : "claude" } ,
168+ { label : "yolo" , title : "Skip all permission checks" , command : "claude --dangerously-skip-permissions" } ,
169+ { label : "plan" , title : "Plan mode (read-only, no edits)" , command : "claude --allowedTools Read,Glob,Grep,WebSearch,WebFetch" } ,
170+ ] ;
171+
172+ const CODEX_LAUNCH_OPTIONS : CliLaunchOption [ ] = [
173+ { label : "suggest" , title : "Suggest mode (proposes changes for approval)" , command : "codex --approval-mode suggest" } ,
174+ { label : "auto-edit" , title : "Auto-edit mode (applies file changes, asks for commands)" , command : "codex --approval-mode auto-edit" } ,
175+ { label : "full-auto" , title : "Full auto mode (runs without confirmation)" , command : "codex --approval-mode full-auto" } ,
176+ ] ;
177+
178+ const OPENCODE_LAUNCH_OPTIONS : CliLaunchOption [ ] = [
179+ { label : "default" , title : "Interactive mode" , command : "opencode" } ,
180+ ] ;
181+
126182// Tab IDs
127183const STICKY_TAB = "__sticky__" ;
128184const TMUX_TAB = "__tmux__" ;
129185const NANO_TAB = "__nano__" ;
130186const VIM_TAB = "__vim__" ;
187+ const CLAUDE_TAB = "__claude__" ;
188+ const CODEX_TAB = "__codex__" ;
189+ const OPENCODE_TAB = "__opencode__" ;
131190
132191type StickyMode = "ctrl" | "ctrl+shift" | "alt" | "alt+shift" ;
133192
@@ -166,7 +225,7 @@ export default function InputBox() {
166225 const [ activeGroup , setActiveGroup ] = useState < string | null > ( null ) ;
167226 const [ tmuxSessions , setTmuxSessions ] = useState < TmuxSession [ ] > ( [ ] ) ;
168227 const [ tmuxLoading , setTmuxLoading ] = useState ( false ) ;
169- const [ toolVersions , setToolVersions ] = useState < { tmux : string | null ; nano : string | null ; vim : string | null } > ( { tmux : null , nano : null , vim : null } ) ;
228+ const [ toolVersions , setToolVersions ] = useState < { tmux : string | null ; nano : string | null ; vim : string | null ; claude : string | null ; codex : string | null ; opencode : string | null } > ( { tmux : null , nano : null , vim : null , claude : null , codex : null , opencode : null } ) ;
170229 const toolVersionsFetched = useRef ( false ) ;
171230 const [ tmuxNewName , setTmuxNewName ] = useState ( "" ) ;
172231 const [ editorFileName , setEditorFileName ] = useState ( "" ) ;
@@ -283,7 +342,7 @@ export default function InputBox() {
283342 setActiveGroup ( id ) ;
284343 if ( id === TMUX_TAB && ! inTmux ) fetchTmuxSessions ( ) ;
285344 if ( id === "cmds" ) fetchDirs ( ) ;
286- if ( [ TMUX_TAB , NANO_TAB , VIM_TAB ] . includes ( id ) ) fetchToolVersions ( ) ;
345+ if ( [ TMUX_TAB , NANO_TAB , VIM_TAB , CLAUDE_TAB , CODEX_TAB , OPENCODE_TAB ] . includes ( id ) ) fetchToolVersions ( ) ;
287346 }
288347 } ;
289348
@@ -408,6 +467,24 @@ export default function InputBox() {
408467 } , 50 ) ;
409468 } ;
410469
470+ // CLI apps (Claude Code, Codex, OpenCode)
471+ const handleLaunchCli = ( app : "claude" | "codex" | "opencode" , command : string ) => {
472+ if ( ! activeSessionId ) return ;
473+ sendInput ( activeSessionId , command + "\n" ) ;
474+ setInEditor ( activeSessionId , app ) ;
475+ setActiveGroup ( app === "claude" ? CLAUDE_TAB : app === "codex" ? CODEX_TAB : OPENCODE_TAB ) ;
476+ } ;
477+
478+ const handleExitCli = ( ) => {
479+ if ( ! activeSessionId || ! inEditor ) return ;
480+ sendInput ( activeSessionId , "\x03" ) ;
481+ setTimeout ( ( ) => {
482+ sendInput ( activeSessionId , "/exit\n" ) ;
483+ setInEditor ( activeSessionId , null ) ;
484+ setActiveGroup ( null ) ;
485+ } , 100 ) ;
486+ } ;
487+
411488 // --- Styles ---
412489 // Tab buttons: teal tint to distinguish from popup action buttons
413490 const tabBase = "px-2.5 py-0.5 text-[11px] rounded border whitespace-nowrap transition-colors shrink-0 select-none touch-manipulation" ;
@@ -438,7 +515,7 @@ export default function InputBox() {
438515 ) ;
439516
440517 // Resolve which key group to show in the popup
441- const activeStandardGroup = activeGroup && ! [ STICKY_TAB , TMUX_TAB , NANO_TAB , VIM_TAB ] . includes ( activeGroup )
518+ const activeStandardGroup = activeGroup && ! [ STICKY_TAB , TMUX_TAB , NANO_TAB , VIM_TAB , CLAUDE_TAB , CODEX_TAB , OPENCODE_TAB ] . includes ( activeGroup )
442519 ? ( isEditorMode
443520 ? ( activeGroup === "nav" ? EDITOR_NAV : null )
444521 : TERMINAL_GROUPS . find ( ( g ) => g . label === activeGroup ) )
@@ -451,9 +528,12 @@ export default function InputBox() {
451528 < div className = "flex items-center gap-1 px-2 py-1 w-max" >
452529 { isEditorMode ? (
453530 < >
454- { /* Editor mode tabs */ }
531+ { /* Editor/app mode tabs */ }
455532 { inEditor === "nano" && tabBtn ( NANO_TAB , "nano \u270f" , "nano commands" ) }
456533 { inEditor === "vim" && tabBtn ( VIM_TAB , "vim \u270f" , "vim commands" ) }
534+ { inEditor === "claude" && tabBtn ( CLAUDE_TAB , "claude \u2728" , "Claude Code actions" ) }
535+ { inEditor === "codex" && tabBtn ( CODEX_TAB , "codex \u26a1" , "Codex CLI actions" ) }
536+ { inEditor === "opencode" && tabBtn ( OPENCODE_TAB , "opencode \u2318" , "OpenCode actions" ) }
457537 { tabBtn ( "nav" , "nav" , "Navigation keys" ) }
458538 { tabBtn ( STICKY_TAB , STICKY_MODES . find ( ( m ) => m . id === stickyMode ) ?. label || "Ctrl+" , "Modifier combos" ) }
459539 </ >
@@ -465,6 +545,9 @@ export default function InputBox() {
465545 { tabBtn ( NANO_TAB , "nano" , "Open file in nano" , ! activeSessionId ) }
466546 { tabBtn ( VIM_TAB , "vim" , "Open file in vim" , ! activeSessionId ) }
467547 { tabBtn ( TMUX_TAB , inTmux ? "tmux \u2318" : "tmux" , inTmux ? "tmux commands" : "tmux sessions" , ! activeSessionId ) }
548+ { tabBtn ( CLAUDE_TAB , "claude" , "Launch Claude Code" , ! activeSessionId ) }
549+ { tabBtn ( CODEX_TAB , "codex" , "Launch Codex CLI" , ! activeSessionId ) }
550+ { tabBtn ( OPENCODE_TAB , "opencode" , "Launch OpenCode" , ! activeSessionId ) }
468551 </ >
469552 ) }
470553 </ div >
@@ -727,6 +810,126 @@ export default function InputBox() {
727810 </ div >
728811 ) }
729812
813+ { /* Claude Code popup: actions when inside, launcher when outside */ }
814+ { activeGroup === CLAUDE_TAB && inEditor === "claude" && (
815+ < div className = "border-b border-gray-700/50 bg-[#12122a] px-2 py-1.5" >
816+ < div className = "flex flex-wrap gap-1" >
817+ { CLAUDE_KEYS . map ( ( qk ) => (
818+ < button key = { qk . label } { ...repeatProps ( qk . key ) } disabled = { ! activeSessionId } title = { qk . title } className = { keyBtn } >
819+ { qk . label }
820+ </ button >
821+ ) ) }
822+ < button onClick = { handleExitCli } disabled = { ! activeSessionId } title = "Exit Claude Code" className = { exitBtn } >
823+ exit
824+ </ button >
825+ </ div >
826+ </ div >
827+ ) }
828+ { activeGroup === CLAUDE_TAB && inEditor !== "claude" && (
829+ < div className = "border-b border-gray-700/50 bg-[#12122a] px-3 py-2" >
830+ { ! toolVersions . claude ? (
831+ < span className = "text-[11px] text-yellow-400" > Claude Code is not installed. Install via: npm install -g @anthropic-ai/claude-code</ span >
832+ ) : (
833+ < div className = "flex flex-col gap-1.5" >
834+ < span className = "text-[11px] text-gray-500" > Claude Code < span className = "text-gray-600" > v{ toolVersions . claude } </ span > </ span >
835+ < div className = "flex flex-wrap gap-1.5" >
836+ { CLAUDE_LAUNCH_OPTIONS . map ( ( opt ) => (
837+ < button
838+ key = { opt . label }
839+ onClick = { ( ) => handleLaunchCli ( "claude" , opt . command ) }
840+ disabled = { ! activeSessionId }
841+ title = { opt . title }
842+ className = "px-2.5 py-1 text-[11px] bg-[#1a1a2e] text-purple-400 hover:text-purple-200 hover:bg-[#2a2a4a] disabled:text-gray-600 rounded border border-purple-800 whitespace-nowrap transition-colors select-none"
843+ >
844+ { opt . label }
845+ </ button >
846+ ) ) }
847+ </ div >
848+ </ div >
849+ ) }
850+ </ div >
851+ ) }
852+
853+ { /* Codex CLI popup: actions when inside, launcher when outside */ }
854+ { activeGroup === CODEX_TAB && inEditor === "codex" && (
855+ < div className = "border-b border-gray-700/50 bg-[#12122a] px-2 py-1.5" >
856+ < div className = "flex flex-wrap gap-1" >
857+ { CODEX_KEYS . map ( ( qk ) => (
858+ < button key = { qk . label } { ...repeatProps ( qk . key ) } disabled = { ! activeSessionId } title = { qk . title } className = { keyBtn } >
859+ { qk . label }
860+ </ button >
861+ ) ) }
862+ < button onClick = { handleExitCli } disabled = { ! activeSessionId } title = "Exit Codex CLI" className = { exitBtn } >
863+ exit
864+ </ button >
865+ </ div >
866+ </ div >
867+ ) }
868+ { activeGroup === CODEX_TAB && inEditor !== "codex" && (
869+ < div className = "border-b border-gray-700/50 bg-[#12122a] px-3 py-2" >
870+ { ! toolVersions . codex ? (
871+ < span className = "text-[11px] text-yellow-400" > Codex CLI is not installed. Install via: npm install -g @openai/codex</ span >
872+ ) : (
873+ < div className = "flex flex-col gap-1.5" >
874+ < span className = "text-[11px] text-gray-500" > Codex CLI < span className = "text-gray-600" > v{ toolVersions . codex } </ span > </ span >
875+ < div className = "flex flex-wrap gap-1.5" >
876+ { CODEX_LAUNCH_OPTIONS . map ( ( opt ) => (
877+ < button
878+ key = { opt . label }
879+ onClick = { ( ) => handleLaunchCli ( "codex" , opt . command ) }
880+ disabled = { ! activeSessionId }
881+ title = { opt . title }
882+ className = "px-2.5 py-1 text-[11px] bg-[#1a1a2e] text-orange-400 hover:text-orange-200 hover:bg-[#2a2a4a] disabled:text-gray-600 rounded border border-orange-800 whitespace-nowrap transition-colors select-none"
883+ >
884+ { opt . label }
885+ </ button >
886+ ) ) }
887+ </ div >
888+ </ div >
889+ ) }
890+ </ div >
891+ ) }
892+
893+ { /* OpenCode popup: actions when inside, launcher when outside */ }
894+ { activeGroup === OPENCODE_TAB && inEditor === "opencode" && (
895+ < div className = "border-b border-gray-700/50 bg-[#12122a] px-2 py-1.5" >
896+ < div className = "flex flex-wrap gap-1" >
897+ { OPENCODE_KEYS . map ( ( qk ) => (
898+ < button key = { qk . label } { ...repeatProps ( qk . key ) } disabled = { ! activeSessionId } title = { qk . title } className = { keyBtn } >
899+ { qk . label }
900+ </ button >
901+ ) ) }
902+ < button onClick = { handleExitCli } disabled = { ! activeSessionId } title = "Exit OpenCode" className = { exitBtn } >
903+ exit
904+ </ button >
905+ </ div >
906+ </ div >
907+ ) }
908+ { activeGroup === OPENCODE_TAB && inEditor !== "opencode" && (
909+ < div className = "border-b border-gray-700/50 bg-[#12122a] px-3 py-2" >
910+ { ! toolVersions . opencode ? (
911+ < span className = "text-[11px] text-yellow-400" > OpenCode is not installed. See: https://github.com/opencode-ai/opencode</ span >
912+ ) : (
913+ < div className = "flex flex-col gap-1.5" >
914+ < span className = "text-[11px] text-gray-500" > OpenCode < span className = "text-gray-600" > v{ toolVersions . opencode } </ span > </ span >
915+ < div className = "flex flex-wrap gap-1.5" >
916+ { OPENCODE_LAUNCH_OPTIONS . map ( ( opt ) => (
917+ < button
918+ key = { opt . label }
919+ onClick = { ( ) => handleLaunchCli ( "opencode" , opt . command ) }
920+ disabled = { ! activeSessionId }
921+ title = { opt . title }
922+ className = "px-2.5 py-1 text-[11px] bg-[#1a1a2e] text-cyan-400 hover:text-cyan-200 hover:bg-[#2a2a4a] disabled:text-gray-600 rounded border border-cyan-800 whitespace-nowrap transition-colors select-none"
923+ >
924+ { opt . label }
925+ </ button >
926+ ) ) }
927+ </ div >
928+ </ div >
929+ ) }
930+ </ div >
931+ ) }
932+
730933 { /* Text input */ }
731934 < div className = "flex gap-2 p-2" >
732935 < textarea
0 commit comments