@@ -2,7 +2,6 @@ import React, { useRef, useEffect, useState, useMemo, useCallback } from "react"
22import { useAppState , useAppDispatch , type ChatMessage } from "../store" ;
33import type { WSMessage } from "../ws" ;
44import { MessageContent } from "./MessageContent" ;
5- import { ModelSelect } from "./ModelSelect" ;
65import { SessionTabs } from "./SessionTabs" ;
76import { useIsMobile } from "../hooks/useIsMobile" ;
87import { dlog } from "../debug-log" ;
@@ -165,10 +164,14 @@ export function ChatWindow({ sendMessage }: ChatWindowProps) {
165164 const [ imageUploading , setImageUploading ] = useState ( false ) ;
166165 const [ dragOver , setDragOver ] = useState ( false ) ;
167166 const [ quotedMessage , setQuotedMessage ] = useState < ChatMessage | null > ( null ) ;
167+ const [ modelOpen , setModelOpen ] = useState ( false ) ;
168168 const messagesEndRef = useRef < HTMLDivElement > ( null ) ;
169169 const inputRef = useRef < HTMLTextAreaElement > ( null ) ;
170170 const fileInputRef = useRef < HTMLInputElement > ( null ) ;
171171 const dropZoneRef = useRef < HTMLDivElement > ( null ) ;
172+ const modelRef = useRef < HTMLDivElement > ( null ) ;
173+ const tabBarRef = useRef < HTMLDivElement > ( null ) ;
174+ const [ tabBarWidth , setTabBarWidth ] = useState ( 0 ) ;
172175
173176 const sessionKey = state . selectedSessionKey ;
174177
@@ -202,6 +205,32 @@ export function ChatWindow({ sendMessage }: ChatWindowProps) {
202205 }
203206 } , [ sessionKey , isMobile ] ) ;
204207
208+ // Close model dropdown on outside click
209+ useEffect ( ( ) => {
210+ if ( ! modelOpen ) return ;
211+ const handler = ( e : MouseEvent ) => {
212+ if ( modelRef . current && ! modelRef . current . contains ( e . target as Node ) ) {
213+ setModelOpen ( false ) ;
214+ }
215+ } ;
216+ document . addEventListener ( "mousedown" , handler ) ;
217+ return ( ) => document . removeEventListener ( "mousedown" , handler ) ;
218+ } , [ modelOpen ] ) ;
219+
220+ // Measure tab bar width for adaptive model display
221+ useEffect ( ( ) => {
222+ const el = tabBarRef . current ;
223+ if ( ! el ) return ;
224+ const observer = new ResizeObserver ( ( entries ) => {
225+ for ( const entry of entries ) {
226+ setTabBarWidth ( entry . contentRect . width ) ;
227+ }
228+ } ) ;
229+ observer . observe ( el ) ;
230+ setTabBarWidth ( el . clientWidth ) ;
231+ return ( ) => observer . disconnect ( ) ;
232+ } , [ ] ) ;
233+
205234 // Restore per-session model from localStorage when session changes
206235 useEffect ( ( ) => {
207236 if ( ! sessionKey ) return ;
@@ -219,6 +248,15 @@ export function ChatWindow({ sendMessage }: ChatWindowProps) {
219248
220249 const currentModel = state . sessionModel ?? state . defaultModel ;
221250
251+ const modelDisplayText = useMemo ( ( ) => {
252+ if ( ! currentModel ) return null ;
253+ if ( tabBarWidth >= 500 ) {
254+ const slash = currentModel . lastIndexOf ( "/" ) ;
255+ return slash >= 0 ? currentModel . substring ( slash + 1 ) : currentModel ;
256+ }
257+ return null ;
258+ } , [ currentModel , tabBarWidth ] ) ;
259+
222260 const handleModelChange = useCallback ( ( modelId : string ) => {
223261 if ( ! modelId || ! sessionKey || modelId === currentModel ) return ;
224262
@@ -563,8 +601,6 @@ export function ChatWindow({ sendMessage }: ChatWindowProps) {
563601 } ) ;
564602 } , [ sessionKey , state . streamingRunId , state . streamingThreadId , state . user ?. id , sendMessage , dispatch ] ) ;
565603
566- const isStreaming = ! ! state . streamingRunId && ! state . streamingThreadId ;
567-
568604 const selectedAgent = state . agents . find ( ( a ) => a . id === state . selectedAgentId ) ;
569605 const channelName = selectedAgent ?. name ?? "channel" ;
570606 const channelId = selectedAgent ?. channelId ?? null ;
@@ -619,56 +655,97 @@ export function ChatWindow({ sendMessage }: ChatWindowProps) {
619655 { /* Channel header — hidden on mobile (MobileLayout already shows channel name) */ }
620656 { ! isMobile && (
621657 < div
622- className = "flex items-center justify-between px-3 sm:px-5 gap-2 flex-shrink-0"
658+ className = "flex items-center px-3 sm:px-5 gap-2 flex-shrink-0"
623659 style = { {
624660 height : 44 ,
625661 borderBottom : "1px solid var(--border)" ,
626662 } }
627663 >
628- < div className = "flex items-center gap-2 min-w-0" >
629- < span className = "text-h1 truncate" style = { { color : "var(--text-primary)" } } >
630- # { channelName }
664+ < span className = "text-h1 truncate" style = { { color : "var(--text-primary)" } } >
665+ # { channelName }
666+ </ span >
667+ { selectedAgent && ! selectedAgent . isDefault && (
668+ < span className = "text-caption hidden sm:inline flex-shrink-0" style = { { color : "var(--text-secondary)" } } >
669+ — custom channel
631670 </ span >
632- { selectedAgent && ! selectedAgent . isDefault && (
633- < span className = "text-caption hidden sm:inline flex-shrink-0" style = { { color : "var(--text-secondary)" } } >
634- — custom channel
635- </ span >
636- ) }
637- </ div >
638- < div className = "flex items-center gap-1.5 flex-shrink-0" >
639- < svg className = "w-3.5 h-3.5 hidden sm:block" fill = "none" viewBox = "0 0 24 24" stroke = "currentColor" strokeWidth = { 1.5 } style = { { color : "var(--text-muted)" } } >
640- < path strokeLinecap = "round" strokeLinejoin = "round" d = "M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09zM18.259 8.715L18 9.75l-.259-1.035a3.375 3.375 0 00-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 002.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 002.455 2.456L21.75 6l-1.036.259a3.375 3.375 0 00-2.455 2.456z" />
641- </ svg >
642- < ModelSelect
643- value = { currentModel ?? "" }
644- onChange = { handleModelChange }
645- models = { state . models }
646- disabled = { ! state . openclawConnected }
647- placeholder = "No model"
648- compact
649- />
650- </ div >
671+ ) }
651672 </ div >
652673 ) }
653674
654- { /* Session tabs + model selector (mobile: inline with tabs ) */ }
675+ { /* Session tabs + adaptive model selector (all screen sizes ) */ }
655676 { showSessionTabs && (
656- < div className = "flex items-center flex-shrink-0" >
677+ < div ref = { tabBarRef } className = "flex items-stretch flex-shrink-0" >
657678 < div className = "flex-1 min-w-0" >
658679 < SessionTabs channelId = { channelId } />
659680 </ div >
660- { isMobile && (
661- < div className = "flex-shrink-0 pr-2" style = { { borderBottom : "1px solid var(--border)" } } >
662- < ModelSelect
663- value = { currentModel ?? "" }
664- onChange = { handleModelChange }
665- models = { state . models }
666- disabled = { ! state . openclawConnected }
667- placeholder = "No model"
668- compact
669- />
670- </ div >
671- ) }
681+ < div
682+ ref = { modelRef }
683+ className = "relative flex-shrink-0 flex items-center pr-2"
684+ style = { { borderBottom : "1px solid var(--border)" } }
685+ >
686+ < button
687+ onClick = { ( ) => setModelOpen ( ( v ) => ! v ) }
688+ disabled = { ! state . openclawConnected }
689+ className = "flex items-center gap-1.5 px-2 h-8 rounded-md transition-colors text-caption"
690+ style = { {
691+ color : currentModel ? "var(--text-primary)" : "var(--text-muted)" ,
692+ opacity : ! state . openclawConnected ? 0.5 : 1 ,
693+ cursor : ! state . openclawConnected ? "not-allowed" : "pointer" ,
694+ fontFamily : "var(--font-mono)" ,
695+ } }
696+ title = { currentModel || "Select model" }
697+ >
698+ < svg className = "w-4 h-4 shrink-0" fill = "none" viewBox = "0 0 24 24" stroke = "currentColor" strokeWidth = { 1.5 } >
699+ < path strokeLinecap = "round" strokeLinejoin = "round" d = "M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09zM18.259 8.715L18 9.75l-.259-1.035a3.375 3.375 0 00-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 002.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 002.455 2.456L21.75 6l-1.036.259a3.375 3.375 0 00-2.455 2.456z" />
700+ </ svg >
701+ { modelDisplayText && (
702+ < span
703+ className = "block overflow-hidden whitespace-nowrap max-w-[200px]"
704+ style = { { direction : "rtl" , textOverflow : "ellipsis" } }
705+ > { modelDisplayText } </ span >
706+ ) }
707+ </ button >
708+ { modelOpen && (
709+ < div
710+ className = "absolute right-0 top-full mt-1 z-50 rounded-md shadow-lg py-1 min-w-[220px] max-h-[300px] overflow-y-auto"
711+ style = { {
712+ background : "var(--bg-surface)" ,
713+ border : "1px solid var(--border)" ,
714+ boxShadow : "0 4px 12px rgba(0,0,0,0.15)" ,
715+ } }
716+ >
717+ { state . models . map ( ( m ) => (
718+ < button
719+ key = { m . id }
720+ onClick = { ( ) => {
721+ handleModelChange ( m . id ) ;
722+ setModelOpen ( false ) ;
723+ } }
724+ className = "w-full text-left px-3 py-2 text-caption transition-colors"
725+ style = { {
726+ color : m . id === currentModel ? "var(--text-primary)" : "var(--text-secondary)" ,
727+ background : m . id === currentModel ? "var(--bg-hover)" : "transparent" ,
728+ fontFamily : "var(--font-mono)" ,
729+ } }
730+ onMouseEnter = { ( e ) => { e . currentTarget . style . background = "var(--bg-hover)" ; } }
731+ onMouseLeave = { ( e ) => {
732+ e . currentTarget . style . background = m . id === currentModel ? "var(--bg-hover)" : "transparent" ;
733+ } }
734+ >
735+ { m . id === currentModel && (
736+ < span className = "mr-1.5" style = { { color : "var(--accent)" } } > ✓</ span >
737+ ) }
738+ { m . id }
739+ </ button >
740+ ) ) }
741+ { state . models . length === 0 && (
742+ < div className = "px-3 py-2 text-caption" style = { { color : "var(--text-muted)" } } >
743+ No models available
744+ </ div >
745+ ) }
746+ </ div >
747+ ) }
748+ </ div >
672749 </ div >
673750 ) }
674751
@@ -865,38 +942,20 @@ export function ChatWindow({ sendMessage }: ChatWindowProps) {
865942 </ button >
866943 </ div >
867944
868- { /* Send / Stop button */ }
869- { isStreaming ? (
870- < button
871- onClick = { handleStop }
872- className = "px-3 py-1.5 rounded-sm text-caption font-bold text-white transition-colors"
873- style = { { background : "#e74c3c" } }
874- onMouseEnter = { ( e ) => { e . currentTarget . style . background = "#c0392b" ; } }
875- onMouseLeave = { ( e ) => { e . currentTarget . style . background = "#e74c3c" ; } }
876- title = "Stop generating"
877- >
878- < div className = "flex items-center gap-1.5" >
879- < svg className = "w-4 h-4" fill = "currentColor" viewBox = "0 0 24 24" >
880- < rect x = "6" y = "6" width = "12" height = "12" rx = "2" />
881- </ svg >
882- Stop
883- </ div >
884- </ button >
885- ) : (
886- < button
887- onClick = { handleSend }
888- disabled = { ! input . trim ( ) && ! pendingImage }
889- className = "px-3 py-1.5 rounded-sm text-caption font-bold text-white disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
890- style = { { background : "var(--bg-active)" } }
891- >
892- < div className = "flex items-center gap-1.5" >
893- < svg className = "w-4 h-4" fill = "none" viewBox = "0 0 24 24" stroke = "currentColor" strokeWidth = { 2 } >
894- < path strokeLinecap = "round" strokeLinejoin = "round" d = "M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5" />
895- </ svg >
896- Send
897- </ div >
898- </ button >
899- ) }
945+ { /* Send button */ }
946+ < button
947+ onClick = { handleSend }
948+ disabled = { ! input . trim ( ) && ! pendingImage }
949+ className = "px-3 py-1.5 rounded-sm text-caption font-bold text-white disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
950+ style = { { background : "var(--bg-active)" } }
951+ >
952+ < div className = "flex items-center gap-1.5" >
953+ < svg className = "w-4 h-4" fill = "none" viewBox = "0 0 24 24" stroke = "currentColor" strokeWidth = { 2 } >
954+ < path strokeLinecap = "round" strokeLinejoin = "round" d = "M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5" />
955+ </ svg >
956+ Send
957+ </ div >
958+ </ button >
900959 </ div >
901960 </ div >
902961 </ div >
0 commit comments