@@ -16,13 +16,16 @@ import { createResizeObserver } from "@solid-primitives/resize-observer"
1616import { Dynamic } from "solid-js/web"
1717import { useLocal } from "@/context/local"
1818import { selectionFromLines , useFile , type FileSelection , type SelectedLineRange } from "@/context/file"
19- import { createStore } from "solid-js/store"
19+ import { createStore , produce } from "solid-js/store"
2020import { PromptInput } from "@/components/prompt-input"
2121import { SessionContextUsage } from "@/components/session-context-usage"
2222import { IconButton } from "@opencode-ai/ui/icon-button"
2323import { Button } from "@opencode-ai/ui/button"
2424import { Icon } from "@opencode-ai/ui/icon"
2525import { Tooltip , TooltipKeybind } from "@opencode-ai/ui/tooltip"
26+ import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
27+ import { Dialog } from "@opencode-ai/ui/dialog"
28+ import { TextField } from "@opencode-ai/ui/text-field"
2629import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
2730import { Tabs } from "@opencode-ai/ui/tabs"
2831import { useCodeComponent } from "@opencode-ai/ui/context/code"
@@ -436,6 +439,218 @@ export default function Page() {
436439 if ( ! id ) return false
437440 return sync . session . history . loading ( id )
438441 } )
442+
443+ const errorMessage = ( err : unknown ) => {
444+ if ( err && typeof err === "object" && "data" in err ) {
445+ const data = ( err as { data ?: { message ?: string } } ) . data
446+ if ( data ?. message ) return data . message
447+ }
448+ if ( err instanceof Error ) return err . message
449+ return language . t ( "common.requestFailed" )
450+ }
451+
452+ async function archiveSession ( sessionID : string ) {
453+ const session = sync . session . get ( sessionID )
454+ if ( ! session ) return
455+
456+ const sessions = sync . data . session ?? [ ]
457+ const index = sessions . findIndex ( ( s ) => s . id === sessionID )
458+ const nextSession = index === - 1 ? undefined : ( sessions [ index + 1 ] ?? sessions [ index - 1 ] )
459+
460+ await sdk . client . session
461+ . update ( { sessionID, time : { archived : Date . now ( ) } } )
462+ . then ( ( ) => {
463+ sync . set (
464+ produce ( ( draft ) => {
465+ const index = draft . session . findIndex ( ( s ) => s . id === sessionID )
466+ if ( index !== - 1 ) draft . session . splice ( index , 1 )
467+ } ) ,
468+ )
469+
470+ if ( params . id !== sessionID ) return
471+ if ( session . parentID ) {
472+ navigate ( `/${ params . dir } /session/${ session . parentID } ` )
473+ return
474+ }
475+ if ( nextSession ) {
476+ navigate ( `/${ params . dir } /session/${ nextSession . id } ` )
477+ return
478+ }
479+ navigate ( `/${ params . dir } /session` )
480+ } )
481+ . catch ( ( err ) => {
482+ showToast ( {
483+ title : language . t ( "common.requestFailed" ) ,
484+ description : errorMessage ( err ) ,
485+ } )
486+ } )
487+ }
488+
489+ async function deleteSession ( sessionID : string ) {
490+ const session = sync . session . get ( sessionID )
491+ if ( ! session ) return false
492+
493+ const sessions = ( sync . data . session ?? [ ] ) . filter ( ( s ) => ! s . parentID && ! s . time ?. archived )
494+ const index = sessions . findIndex ( ( s ) => s . id === sessionID )
495+ const nextSession = index === - 1 ? undefined : ( sessions [ index + 1 ] ?? sessions [ index - 1 ] )
496+
497+ const result = await sdk . client . session
498+ . delete ( { sessionID } )
499+ . then ( ( x ) => x . data )
500+ . catch ( ( err ) => {
501+ showToast ( {
502+ title : language . t ( "session.delete.failed.title" ) ,
503+ description : errorMessage ( err ) ,
504+ } )
505+ return false
506+ } )
507+
508+ if ( ! result ) return false
509+
510+ sync . set (
511+ produce ( ( draft ) => {
512+ const removed = new Set < string > ( [ sessionID ] )
513+
514+ const byParent = new Map < string , string [ ] > ( )
515+ for ( const item of draft . session ) {
516+ const parentID = item . parentID
517+ if ( ! parentID ) continue
518+ const existing = byParent . get ( parentID )
519+ if ( existing ) {
520+ existing . push ( item . id )
521+ continue
522+ }
523+ byParent . set ( parentID , [ item . id ] )
524+ }
525+
526+ const stack = [ sessionID ]
527+ while ( stack . length ) {
528+ const parentID = stack . pop ( )
529+ if ( ! parentID ) continue
530+
531+ const children = byParent . get ( parentID )
532+ if ( ! children ) continue
533+
534+ for ( const child of children ) {
535+ if ( removed . has ( child ) ) continue
536+ removed . add ( child )
537+ stack . push ( child )
538+ }
539+ }
540+
541+ draft . session = draft . session . filter ( ( s ) => ! removed . has ( s . id ) )
542+ } ) ,
543+ )
544+
545+ if ( params . id !== sessionID ) return true
546+ if ( session . parentID ) {
547+ navigate ( `/${ params . dir } /session/${ session . parentID } ` )
548+ return true
549+ }
550+ if ( nextSession ) {
551+ navigate ( `/${ params . dir } /session/${ nextSession . id } ` )
552+ return true
553+ }
554+ navigate ( `/${ params . dir } /session` )
555+ return true
556+ }
557+
558+ function DialogRenameSession ( props : { sessionID : string } ) {
559+ const [ data , setData ] = createStore ( {
560+ title : sync . session . get ( props . sessionID ) ?. title ?? "" ,
561+ saving : false ,
562+ } )
563+
564+ const submit = ( event : Event ) => {
565+ event . preventDefault ( )
566+ if ( data . saving ) return
567+
568+ const title = data . title . trim ( )
569+ if ( ! title ) {
570+ dialog . close ( )
571+ return
572+ }
573+
574+ const current = sync . session . get ( props . sessionID ) ?. title ?? ""
575+ if ( title === current ) {
576+ dialog . close ( )
577+ return
578+ }
579+
580+ setData ( "saving" , true )
581+ void sdk . client . session
582+ . update ( { sessionID : props . sessionID , title } )
583+ . then ( ( ) => {
584+ sync . set (
585+ produce ( ( draft ) => {
586+ const index = draft . session . findIndex ( ( s ) => s . id === props . sessionID )
587+ if ( index !== - 1 ) draft . session [ index ] . title = title
588+ } ) ,
589+ )
590+ dialog . close ( )
591+ } )
592+ . catch ( ( err ) => {
593+ showToast ( {
594+ title : language . t ( "common.requestFailed" ) ,
595+ description : errorMessage ( err ) ,
596+ } )
597+ } )
598+ . finally ( ( ) => {
599+ setData ( "saving" , false )
600+ } )
601+ }
602+
603+ return (
604+ < Dialog title = { language . t ( "common.rename" ) } fit >
605+ < form onSubmit = { submit } class = "flex flex-col gap-4 pl-6 pr-2.5 pb-3" >
606+ < TextField
607+ autofocus
608+ type = "text"
609+ label = { language . t ( "common.rename" ) }
610+ value = { data . title }
611+ onChange = { ( value ) => setData ( "title" , value ) }
612+ />
613+ < div class = "flex justify-end gap-2" >
614+ < Button type = "button" variant = "ghost" size = "large" disabled = { data . saving } onClick = { ( ) => dialog . close ( ) } >
615+ { language . t ( "common.cancel" ) }
616+ </ Button >
617+ < Button type = "submit" variant = "primary" size = "large" disabled = { data . saving || ! data . title . trim ( ) } >
618+ { language . t ( "common.save" ) }
619+ </ Button >
620+ </ div >
621+ </ form >
622+ </ Dialog >
623+ )
624+ }
625+
626+ function DialogDeleteSession ( props : { sessionID : string } ) {
627+ const title = createMemo ( ( ) => sync . session . get ( props . sessionID ) ?. title ?? language . t ( "command.session.new" ) )
628+ const handleDelete = async ( ) => {
629+ await deleteSession ( props . sessionID )
630+ dialog . close ( )
631+ }
632+
633+ return (
634+ < Dialog title = { language . t ( "session.delete.title" ) } fit >
635+ < div class = "flex flex-col gap-4 pl-6 pr-2.5 pb-3" >
636+ < div class = "flex flex-col gap-1" >
637+ < span class = "text-14-regular text-text-strong" >
638+ { language . t ( "session.delete.confirm" , { name : title ( ) } ) }
639+ </ span >
640+ </ div >
641+ < div class = "flex justify-end gap-2" >
642+ < Button variant = "ghost" size = "large" onClick = { ( ) => dialog . close ( ) } >
643+ { language . t ( "common.cancel" ) }
644+ </ Button >
645+ < Button variant = "primary" size = "large" onClick = { handleDelete } >
646+ { language . t ( "session.delete.button" ) }
647+ </ Button >
648+ </ div >
649+ </ div >
650+ </ Dialog >
651+ )
652+ }
653+
439654 const emptyUserMessages : UserMessage [ ] = [ ]
440655 const userMessages = createMemo (
441656 ( ) => messages ( ) . filter ( ( m ) => m . role === "user" ) as UserMessage [ ] ,
@@ -1992,20 +2207,63 @@ export default function Page() {
19922207 centered ( ) ,
19932208 } }
19942209 >
1995- < div class = "h-10 flex items-center gap-1" >
1996- < Show when = { info ( ) ?. parentID } >
1997- < IconButton
1998- tabIndex = { - 1 }
1999- icon = "arrow-left"
2000- variant = "ghost"
2001- onClick = { ( ) => {
2002- navigate ( `/${ params . dir } /session/${ info ( ) ?. parentID } ` )
2003- } }
2004- aria-label = { language . t ( "common.goBack" ) }
2005- />
2006- </ Show >
2007- < Show when = { info ( ) ?. title } >
2008- < h1 class = "text-16-medium text-text-strong truncate" > { info ( ) ?. title } </ h1 >
2210+ < div class = "h-10 w-full flex items-center justify-between gap-2" >
2211+ < div class = "flex items-center gap-1 min-w-0" >
2212+ < Show when = { info ( ) ?. parentID } >
2213+ < IconButton
2214+ tabIndex = { - 1 }
2215+ icon = "arrow-left"
2216+ variant = "ghost"
2217+ onClick = { ( ) => {
2218+ navigate ( `/${ params . dir } /session/${ info ( ) ?. parentID } ` )
2219+ } }
2220+ aria-label = { language . t ( "common.goBack" ) }
2221+ />
2222+ </ Show >
2223+ < Show when = { info ( ) ?. title } >
2224+ < h1 class = "text-16-medium text-text-strong truncate min-w-0" > { info ( ) ?. title } </ h1 >
2225+ </ Show >
2226+ </ div >
2227+ < Show when = { params . id } >
2228+ { ( id ) => (
2229+ < div class = "shrink-0 flex items-center" >
2230+ < DropdownMenu >
2231+ < Tooltip value = { language . t ( "common.moreOptions" ) } placement = "top" >
2232+ < DropdownMenu . Trigger
2233+ as = { IconButton }
2234+ icon = "dot-grid"
2235+ variant = "ghost"
2236+ class = "size-6 rounded-md data-[expanded]:bg-surface-base-active"
2237+ aria-label = { language . t ( "common.moreOptions" ) }
2238+ />
2239+ </ Tooltip >
2240+ < DropdownMenu . Portal >
2241+ < DropdownMenu . Content >
2242+ < DropdownMenu . Item
2243+ onSelect = { ( ) => dialog . show ( ( ) => < DialogRenameSession sessionID = { id ( ) } /> ) }
2244+ >
2245+ < DropdownMenu . ItemLabel >
2246+ { language . t ( "common.rename" ) }
2247+ </ DropdownMenu . ItemLabel >
2248+ </ DropdownMenu . Item >
2249+ < DropdownMenu . Item onSelect = { ( ) => void archiveSession ( id ( ) ) } >
2250+ < DropdownMenu . ItemLabel >
2251+ { language . t ( "common.archive" ) }
2252+ </ DropdownMenu . ItemLabel >
2253+ </ DropdownMenu . Item >
2254+ < DropdownMenu . Separator />
2255+ < DropdownMenu . Item
2256+ onSelect = { ( ) => dialog . show ( ( ) => < DialogDeleteSession sessionID = { id ( ) } /> ) }
2257+ >
2258+ < DropdownMenu . ItemLabel >
2259+ { language . t ( "common.delete" ) }
2260+ </ DropdownMenu . ItemLabel >
2261+ </ DropdownMenu . Item >
2262+ </ DropdownMenu . Content >
2263+ </ DropdownMenu . Portal >
2264+ </ DropdownMenu >
2265+ </ div >
2266+ ) }
20092267 </ Show >
20102268 </ div >
20112269 </ div >
0 commit comments