11import { Icon } from "@opencode/ui/icon"
22import { Menu } from "@opencode/ui/menu"
3- import { createMemo , For , Show , type Accessor , type JSX } from "solid-js"
3+ import { Popover } from "@kobalte/core/popover"
4+ import { createMemo , For , Match , Show , Switch , untrack , type Accessor , type JSX } from "solid-js"
5+ import { createStore } from "solid-js/store"
46import {
57 ComposerPill ,
68 type ComposerPillProps ,
@@ -25,7 +27,8 @@ const tones: Record<ComposerPillTone, string> = {
2527 danger : "text-v2-state-fg-danger" ,
2628}
2729
28- // Plugin pills in the composer lane, after the app's own: on the side away from the lane's aligned edge.
30+ // Plugin pills in the composer lane, after the app's own: on the side away from the lane's aligned edge. A pill with
31+ // nothing to show renders no element, so the row hides while it is empty and takes no gap in the lane.
2932export function SessionPluginPills ( props : {
3033 session : MountedSession
3134 screen : SessionScreen
@@ -52,7 +55,7 @@ export function SessionPluginPills(props: {
5255 < Show when = { items ( ) . length > 0 } >
5356 < div
5457 data-component = "session-plugin-pills"
55- class = "flex min-w-0 shrink items-center gap-2"
58+ class = "flex min-w-0 shrink items-center gap-2 [&:not(:has(*))]:hidden "
5659 classList = { { "-order-2" : alignment ( ) === "end" , "order-2" : alignment ( ) !== "end" } }
5760 >
5861 < For each = { items ( ) } >
@@ -71,12 +74,12 @@ function PluginPill(props: { pill: ComposerPill; input: ComposerPillProps; align
7174 const alignment = props . alignment
7275 const view = createMemo ( ( ) => props . pill . show ( props . input ) )
7376 const placement = ( ) => ( ( { center : "top" , start : "top-start" , end : "top-end" } ) as const ) [ alignment ( ) ]
77+ const [ state , setState ] = createStore ( { open : false } )
7478
7579 return (
7680 < Show when = { view ( ) } >
7781 { ( current ) => (
78- < Show
79- when = { current ( ) . rows }
82+ < Switch
8083 fallback = {
8184 < button
8285 type = "button"
@@ -89,42 +92,93 @@ function PluginPill(props: { pill: ComposerPill; input: ComposerPillProps; align
8992 </ button >
9093 }
9194 >
92- { ( rows ) => (
93- < Menu gutter = { 8 } placement = { placement ( ) } >
94- < Menu . Trigger
95- as = "button"
96- type = "button"
97- data-slot = "session-plugin-pill"
98- aria-label = { current ( ) . title }
99- class = { capsule }
95+ < Match when = { props . pill . menu } >
96+ { ( menu ) => (
97+ < Popover
98+ open = { state . open }
99+ onOpenChange = { ( open ) => setState ( "open" , open ) }
100+ placement = { placement ( ) }
101+ gutter = { 8 }
100102 >
101- < Content view = { current ( ) } />
102- </ Menu . Trigger >
103- < Menu . Portal >
104- < Menu . Content data-slot = "session-plugin-pill-menu" aria-label = { current ( ) . title ?? current ( ) . label } >
105- < Menu . Group >
106- < Menu . GroupLabel > { current ( ) . title ?? current ( ) . label } </ Menu . GroupLabel >
107- < For each = { rows ( ) } >
108- { ( row ) => (
109- < Menu . Item
110- data-slot = "session-plugin-pill-row"
111- disabled = { ! row . run }
112- onSelect = { ( ) => row . run ?.( ) }
113- badge = { row . detail }
114- >
115- < PillIcon name = { row . icon } tone = { row . tone } />
116- < span dir = "auto" class = "min-w-0 flex-1 truncate" >
117- { row . label }
118- </ span >
119- </ Menu . Item >
120- ) }
121- </ For >
122- </ Menu . Group >
123- </ Menu . Content >
124- </ Menu . Portal >
125- </ Menu >
126- ) }
127- </ Show >
103+ < Popover . Trigger
104+ as = "button"
105+ type = "button"
106+ data-slot = "session-plugin-pill"
107+ aria-label = { current ( ) . title }
108+ class = { capsule }
109+ >
110+ < Content view = { current ( ) } />
111+ </ Popover . Trigger >
112+ < Popover . Portal >
113+ < Popover . Content
114+ data-slot = "session-plugin-pill-popover"
115+ aria-label = { current ( ) . title ?? current ( ) . label }
116+ class = "w-80 max-w-[calc(100vw-56px)] rounded-[10px] border-[0.5px] border-v2-border-border-muted bg-v2-background-bg-layer-01 p-1 shadow-[var(--v2-elevation-floating)] outline-none"
117+ >
118+ < div class = "px-2 pt-1 pb-1.5 text-[11px] font-[530] tracking-[0.05px] leading-[var(--line-height-tight)] text-v2-text-text-faint" >
119+ < span dir = "auto" class = "line-clamp-3 break-words" >
120+ { current ( ) . title ?? current ( ) . label }
121+ </ span >
122+ </ div >
123+ { /* Untracked: the content renders once per open; its own reactivity updates it in place. */ }
124+ { untrack ( ( ) =>
125+ menu ( ) ( {
126+ get session ( ) {
127+ return props . input . session
128+ } ,
129+ get screen ( ) {
130+ return props . input . screen
131+ } ,
132+ close : ( ) => setState ( "open" , false ) ,
133+ } ) ,
134+ ) }
135+ </ Popover . Content >
136+ </ Popover . Portal >
137+ </ Popover >
138+ ) }
139+ </ Match >
140+ < Match when = { current ( ) . rows } >
141+ { ( rows ) => (
142+ < Menu gutter = { 8 } placement = { placement ( ) } >
143+ < Menu . Trigger
144+ as = "button"
145+ type = "button"
146+ data-slot = "session-plugin-pill"
147+ aria-label = { current ( ) . title }
148+ class = { capsule }
149+ >
150+ < Content view = { current ( ) } />
151+ </ Menu . Trigger >
152+ < Menu . Portal >
153+ < Menu . Content data-slot = "session-plugin-pill-menu" aria-label = { current ( ) . title ?? current ( ) . label } >
154+ < Menu . Group >
155+ < Menu . GroupLabel class = "!h-auto max-w-80 !py-1.5" >
156+ < span dir = "auto" class = "line-clamp-3 break-words" >
157+ { current ( ) . title ?? current ( ) . label }
158+ </ span >
159+ </ Menu . GroupLabel >
160+ < For each = { rows ( ) } >
161+ { ( row ) => (
162+ < Menu . Item
163+ data-slot = "session-plugin-pill-row"
164+ disabled = { ! row . run }
165+ onSelect = { ( ) => row . run ?.( ) }
166+ badge = { row . detail }
167+ >
168+ < PillIcon name = { row . icon } tone = { row . tone } />
169+ < span dir = "auto" class = "min-w-0 flex-1 truncate" >
170+ { row . label }
171+ </ span >
172+ </ Menu . Item >
173+ ) }
174+ </ For >
175+ </ Menu . Group >
176+ </ Menu . Content >
177+ </ Menu . Portal >
178+ </ Menu >
179+ ) }
180+ </ Match >
181+ </ Switch >
128182 ) }
129183 </ Show >
130184 )
0 commit comments