@@ -9,7 +9,8 @@ import { Tooltip } from "@opencode-ai/ui/tooltip"
99import { base64Encode } from "@opencode-ai/util/encode"
1010import { getFilename } from "@opencode-ai/util/path"
1111import { A , useNavigate , useParams } from "@solidjs/router"
12- import { type Accessor , createMemo , For , type JSX , Match , onCleanup , Show , Switch } from "solid-js"
12+ import { type Accessor , createEffect , createMemo , For , type JSX , on , onCleanup , Show } from "solid-js"
13+ import { createStore } from "solid-js/store"
1314import { useGlobalSync } from "@/context/global-sync"
1415import { useLanguage } from "@/context/language"
1516import { getAvatarColors , type LocalProject , useLayout } from "@/context/layout"
@@ -101,46 +102,94 @@ const SessionRow = (props: {
101102 warmPress : ( ) => void
102103 warmFocus : ( ) => void
103104 cancelHoverPrefetch : ( ) => void
104- } ) : JSX . Element => (
105- < A
106- href = { `/${ props . slug } /session/${ props . session . id } ` }
107- class = { `flex items-center justify-between gap-3 min-w-0 text-left w-full focus:outline-none transition-[padding] ${ props . mobile ? "pr-7" : "" } group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7 ${ props . dense ? "py-0.5" : "py-1" } ` }
108- onPointerDown = { props . warmPress }
109- onPointerEnter = { props . warmHover }
110- onPointerLeave = { props . cancelHoverPrefetch }
111- onFocus = { props . warmFocus }
112- onClick = { ( ) => {
113- props . setHoverSession ( undefined )
114- if ( props . sidebarOpened ( ) ) return
115- props . clearHoverProjectSoon ( )
116- } }
117- >
118- < div class = "flex items-center gap-1 w-full" >
119- < div
120- class = "shrink-0 size-6 flex items-center justify-center"
121- style = { { color : props . tint ( ) ?? "var(--icon-interactive-base)" } }
122- >
123- < Switch fallback = { < Icon name = "dash" size = "small" class = "text-icon-weak" /> } >
124- < Match when = { props . isWorking ( ) } >
125- < Spinner class = "size-[15px]" />
126- </ Match >
127- < Match when = { props . hasPermissions ( ) } >
128- < div class = "size-1.5 rounded-full bg-surface-warning-strong" />
129- </ Match >
130- < Match when = { props . hasError ( ) } >
131- < div class = "size-1.5 rounded-full bg-text-diff-delete-base" />
132- </ Match >
133- < Match when = { props . unseenCount ( ) > 0 } >
134- < div class = "size-1.5 rounded-full bg-text-interactive-base" />
135- </ Match >
136- </ Switch >
105+ } ) : JSX . Element => {
106+ const [ slot , setSlot ] = createStore ( {
107+ open : false ,
108+ show : false ,
109+ fade : false ,
110+ } )
111+
112+ let f : number | undefined
113+ const clear = ( ) => {
114+ if ( f !== undefined ) window . clearTimeout ( f )
115+ f = undefined
116+ }
117+
118+ onCleanup ( clear )
119+ createEffect (
120+ on (
121+ ( ) => props . isWorking ( ) ,
122+ ( on , prev ) => {
123+ clear ( )
124+ if ( on ) {
125+ setSlot ( { open : true , show : true , fade : false } )
126+ return
127+ }
128+ if ( prev ) {
129+ setSlot ( { open : false , show : true , fade : true } )
130+ f = window . setTimeout ( ( ) => setSlot ( { show : false , fade : false } ) , 260 )
131+ return
132+ }
133+ setSlot ( { open : false , show : false , fade : false } )
134+ } ,
135+ { defer : true } ,
136+ ) ,
137+ )
138+
139+ return (
140+ < A
141+ href = { `/${ props . slug } /session/${ props . session . id } ` }
142+ class = { `relative flex items-center min-w-0 text-left w-full focus:outline-none transition-[padding] ${ props . mobile ? "pr-7" : "" } group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7 ${ props . dense ? "py-0.5" : "py-1" } ` }
143+ onPointerDown = { props . warmPress }
144+ onPointerEnter = { props . warmHover }
145+ onPointerLeave = { props . cancelHoverPrefetch }
146+ onFocus = { props . warmFocus }
147+ onClick = { ( ) => {
148+ props . setHoverSession ( undefined )
149+ if ( props . sidebarOpened ( ) ) return
150+ props . clearHoverProjectSoon ( )
151+ } }
152+ >
153+ < Show when = { ! props . isWorking ( ) && ( props . hasPermissions ( ) || props . hasError ( ) || props . unseenCount ( ) > 0 ) } >
154+ < div
155+ classList = { {
156+ "absolute left-0 top-1/2 -translate-y-1/2 size-1.5 rounded-full" : true ,
157+ "bg-surface-warning-strong" : props . hasPermissions ( ) ,
158+ "bg-text-diff-delete-base" : ! props . hasPermissions ( ) && props . hasError ( ) ,
159+ "bg-text-interactive-base" : ! props . hasPermissions ( ) && ! props . hasError ( ) && props . unseenCount ( ) > 0 ,
160+ } }
161+ aria-hidden = "true"
162+ />
163+ </ Show >
164+
165+ < div class = "flex items-center min-w-0 grow-1" >
166+ < div
167+ class = "shrink-0 flex items-center justify-center overflow-hidden transition-[width,margin] duration-300 ease-[cubic-bezier(0.22,1,0.36,1)]"
168+ style = { {
169+ width : slot . open ? "16px" : "0px" ,
170+ "margin-right" : slot . open ? "8px" : "0px" ,
171+ } }
172+ aria-hidden = "true"
173+ >
174+ < Show when = { slot . show } >
175+ < div
176+ class = "transition-opacity duration-200 ease-out"
177+ classList = { {
178+ "opacity-0" : slot . fade ,
179+ } }
180+ >
181+ < Spinner class = "size-4" style = { { color : props . tint ( ) ?? "var(--icon-interactive-base)" } } />
182+ </ div >
183+ </ Show >
184+ </ div >
185+
186+ < span class = "text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate" >
187+ { props . session . title }
188+ </ span >
137189 </ div >
138- < span class = "text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate" >
139- { props . session . title }
140- </ span >
141- </ div >
142- </ A >
143- )
190+ </ A >
191+ )
192+ }
144193
145194const SessionHoverPreview = ( props : {
146195 mobile ?: boolean
@@ -204,8 +253,18 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
204253 } )
205254 const isWorking = createMemo ( ( ) => {
206255 if ( hasPermissions ( ) ) return false
256+ const pending = ( sessionStore . message [ props . session . id ] ?? [ ] ) . findLast (
257+ ( message ) =>
258+ message . role === "assistant" &&
259+ typeof ( message as { time ?: { completed ?: unknown } } ) . time ?. completed !== "number" ,
260+ )
207261 const status = sessionStore . session_status [ props . session . id ]
208- return status ?. type === "busy" || status ?. type === "retry"
262+ return (
263+ pending !== undefined ||
264+ status ?. type === "busy" ||
265+ status ?. type === "retry" ||
266+ ( status !== undefined && status . type !== "idle" )
267+ )
209268 } )
210269
211270 const tint = createMemo ( ( ) => {
@@ -300,7 +359,7 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
300359 return (
301360 < div
302361 data-session-id = { props . session . id }
303- class = "group/session relative w-full rounded-md cursor-default transition-colors pl-2 pr-3
362+ class = "group/session relative w-full rounded-md cursor-default pl-3 pr-3 transition-colors
304363 hover:bg-surface-raised-base-hover [&:has(:focus-visible)]:bg-surface-raised-base-hover has-[[data-expanded]]:bg-surface-raised-base-hover has-[.active]:bg-surface-base-active"
305364 >
306365 < Show
0 commit comments