@@ -115,9 +115,18 @@ const GROUPS: QuickKeyGroup[] = [
115115 } ,
116116] ;
117117
118+ interface TmuxSession {
119+ name : string ;
120+ windows : number ;
121+ attached : boolean ;
122+ }
123+
118124export default function InputBox ( ) {
119125 const [ text , setText ] = useState ( "" ) ;
120126 const [ activeGroup , setActiveGroup ] = useState < string | null > ( null ) ;
127+ const [ tmuxSessions , setTmuxSessions ] = useState < TmuxSession [ ] > ( [ ] ) ;
128+ const [ tmuxOpen , setTmuxOpen ] = useState ( false ) ;
129+ const [ tmuxLoading , setTmuxLoading ] = useState ( false ) ;
121130 const textareaRef = useRef < HTMLTextAreaElement > ( null ) ;
122131 const activeSessionId = useTerminalStore ( ( s ) => s . activeSessionId ) ;
123132 const sendInput = useTerminalStore ( ( s ) => s . sendInput ) ;
@@ -213,6 +222,33 @@ export default function InputBox() {
213222 onContextMenu : ( e : React . MouseEvent ) => e . preventDefault ( ) ,
214223 } ) ;
215224
225+ const fetchTmuxSessions = async ( ) => {
226+ setTmuxLoading ( true ) ;
227+ try {
228+ const res = await fetch ( "/api/tmux/sessions" ) ;
229+ const data = await res . json ( ) ;
230+ setTmuxSessions ( data . sessions || [ ] ) ;
231+ } catch {
232+ setTmuxSessions ( [ ] ) ;
233+ }
234+ setTmuxLoading ( false ) ;
235+ } ;
236+
237+ const handleTmuxAttach = ( sessionName : string ) => {
238+ if ( ! activeSessionId ) return ;
239+ sendInput ( activeSessionId , `tmux attach -t ${ sessionName } \n` ) ;
240+ setTmuxOpen ( false ) ;
241+ } ;
242+
243+ const [ tmuxNewName , setTmuxNewName ] = useState ( "" ) ;
244+
245+ const handleTmuxNew = ( ) => {
246+ if ( ! activeSessionId || ! tmuxNewName . trim ( ) ) return ;
247+ sendInput ( activeSessionId , `tmux new -s ${ tmuxNewName . trim ( ) } \n` ) ;
248+ setTmuxNewName ( "" ) ;
249+ setTmuxOpen ( false ) ;
250+ } ;
251+
216252 const group = activeGroup ? GROUPS . find ( ( g ) => g . label === activeGroup ) : null ;
217253
218254 return (
@@ -247,20 +283,90 @@ export default function InputBox() {
247283 ) ) }
248284 </ >
249285 ) : (
250- /* Top-level group buttons */
251- GROUPS . map ( ( g ) => (
286+ < >
287+ { /* Top-level group buttons */ }
288+ { GROUPS . map ( ( g ) => (
289+ < button
290+ key = { g . label }
291+ onClick = { ( ) => setActiveGroup ( g . label ) }
292+ title = { g . title }
293+ className = "px-2.5 py-0.5 text-[11px] bg-[#1a1a2e] text-gray-400 hover:text-white hover:bg-[#2a2a4a] rounded border border-gray-700 whitespace-nowrap transition-colors shrink-0 select-none touch-manipulation"
294+ >
295+ { g . label }
296+ </ button >
297+ ) ) }
298+ { /* Tmux sessions button */ }
252299 < button
253- key = { g . label }
254- onClick = { ( ) => setActiveGroup ( g . label ) }
255- title = { g . title }
256- className = "px-2.5 py-0.5 text-[11px] bg-[#1a1a2e] text-gray -400 hover:text-white hover:bg-[#2a2a4a] rounded border border-gray-700 whitespace-nowrap transition-colors shrink-0 select-none touch-manipulation"
300+ onClick = { ( ) => { setTmuxOpen ( ! tmuxOpen ) ; if ( ! tmuxOpen ) fetchTmuxSessions ( ) ; } }
301+ disabled = { ! activeSessionId }
302+ title = "tmux sessions"
303+ className = "px-2.5 py-0.5 text-[11px] bg-[#1a1a2e] text-green -400 hover:text-green-300 hover:bg-[#2a2a4a] disabled:text-gray-600 rounded border border-green-800 whitespace-nowrap transition-colors shrink-0 select-none touch-manipulation"
257304 >
258- { g . label }
305+ tmux ↗
259306 </ button >
260- ) )
307+ </ >
261308 ) }
262309 </ div >
263310 </ div >
311+ { /* Tmux session picker popup */ }
312+ { tmuxOpen && (
313+ < div className = "border-b border-gray-700/50 bg-[#12122a] px-3 py-2" >
314+ < div className = "flex items-center justify-between mb-2" >
315+ < span className = "text-[11px] text-gray-400 font-medium" > tmux sessions</ span >
316+ < button onClick = { ( ) => setTmuxOpen ( false ) } className = "text-gray-500 hover:text-white" >
317+ < svg className = "w-3 h-3" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" > < path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 2 } d = "M6 18L18 6M6 6l12 12" /> </ svg >
318+ </ button >
319+ </ div >
320+ { tmuxLoading ? (
321+ < span className = "text-[11px] text-gray-500" > Loading...</ span >
322+ ) : tmuxSessions . length === 0 ? (
323+ < span className = "text-[11px] text-gray-500" > No tmux sessions running</ span >
324+ ) : (
325+ < div className = "flex flex-col gap-1 mb-2" >
326+ { tmuxSessions . map ( ( s ) => (
327+ < div key = { s . name } className = "flex items-center gap-2" >
328+ < button
329+ onClick = { ( ) => handleTmuxAttach ( s . name ) }
330+ className = "flex-1 text-left px-2 py-1 text-[11px] bg-[#1a1a2e] text-gray-300 hover:text-white hover:bg-[#2a2a4a] rounded border border-gray-700 transition-colors"
331+ >
332+ < span className = "font-medium" > { s . name } </ span >
333+ < span className = "text-gray-500 ml-2" > { s . windows } w</ span >
334+ { s . attached && < span className = "text-green-500 ml-1" > (attached)</ span > }
335+ </ button >
336+ < button
337+ onClick = { ( ) => {
338+ if ( ! activeSessionId ) return ;
339+ sendInput ( activeSessionId , `tmux kill-session -t ${ s . name } \n` ) ;
340+ setTimeout ( fetchTmuxSessions , 500 ) ;
341+ } }
342+ className = "p-1 text-gray-500 hover:text-red-400 transition-colors"
343+ title = { `Kill session ${ s . name } ` }
344+ >
345+ < svg className = "w-3 h-3" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" > < path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 2 } d = "M6 18L18 6M6 6l12 12" /> </ svg >
346+ </ button >
347+ </ div >
348+ ) ) }
349+ </ div >
350+ ) }
351+ { /* New session */ }
352+ < div className = "flex items-center gap-2 mt-1" >
353+ < input
354+ value = { tmuxNewName }
355+ onChange = { ( e ) => setTmuxNewName ( e . target . value ) }
356+ onKeyDown = { ( e ) => e . key === "Enter" && handleTmuxNew ( ) }
357+ placeholder = "New session name..."
358+ className = "flex-1 bg-[#1a1a2e] text-white border border-gray-700 rounded px-2 py-1 text-[11px] focus:outline-none focus:ring-1 focus:ring-green-500"
359+ />
360+ < button
361+ onClick = { handleTmuxNew }
362+ disabled = { ! tmuxNewName . trim ( ) || ! activeSessionId }
363+ className = "px-2 py-1 text-[11px] bg-green-700 hover:bg-green-600 disabled:bg-gray-700 disabled:text-gray-500 text-white rounded transition-colors"
364+ >
365+ Create
366+ </ button >
367+ </ div >
368+ </ div >
369+ ) }
264370 { /* Text input */ }
265371 < div className = "flex gap-2 p-2" >
266372 < textarea
0 commit comments