@@ -14,7 +14,7 @@ import {
1414 Trash2 ,
1515 X ,
1616} from 'lucide-react' ;
17- import { Button , useConfirm , useToast } from '@mcpmux/ui' ;
17+ import { Button , useConfirm , useToast , DropdownMenu , DropdownMenuContent , DropdownMenuTrigger } from '@mcpmux/ui' ;
1818import { useWorkspaceEvents } from '@/lib/backend/events' ;
1919import { apiCall } from '@/lib/api/transport' ;
2020import {
@@ -58,7 +58,6 @@ import {
5858import {
5959 CollapsibleSection ,
6060 EffectiveFeaturesContent ,
61- type CollapsibleSectionRef ,
6261} from './WorkspacesPage' ;
6362
6463/**
@@ -137,6 +136,132 @@ function Pill({
137136 ) ;
138137}
139138
139+ /**
140+ * Compact machine quick-switch dropdown in the panel header (mirrors Scope machine picker).
141+ */
142+ function HeaderMachineDropdown ( {
143+ mode,
144+ machines,
145+ machineId,
146+ setMachineId,
147+ machineIds,
148+ setMachineIds,
149+ machineBadgeLabel,
150+ machineBadgeIcon,
151+ t,
152+ } : {
153+ mode : 'create' | 'edit' | 'create-from-live' ;
154+ machines : Machine [ ] ;
155+ machineId : string ;
156+ setMachineId : ( value : string ) => void ;
157+ machineIds : string [ ] ;
158+ setMachineIds : ( value : string [ ] ) => void ;
159+ machineBadgeLabel : string ;
160+ machineBadgeIcon : string | null ;
161+ t : TFunction < [ 'workspaces' , 'common' ] > ;
162+ } ) {
163+ const [ menuOpen , setMenuOpen ] = useState ( false ) ;
164+ const usesSingleMachine = mode === 'edit' ;
165+
166+ /** Apply a machine selection from the header dropdown. */
167+ const handleSelect = ( nextId : string ) => {
168+ if ( usesSingleMachine ) {
169+ setMachineId ( nextId ) ;
170+ } else {
171+ setMachineIds ( nextId ? [ nextId ] : [ ] ) ;
172+ }
173+ setMenuOpen ( false ) ;
174+ } ;
175+
176+ /** Whether the dropdown row matches the current machine scope. */
177+ const isSelected = ( candidateId : string | null ) : boolean => {
178+ if ( usesSingleMachine ) {
179+ if ( ! candidateId ) return ! machineId ;
180+ return machineId === candidateId ;
181+ }
182+ if ( ! candidateId ) return machineIds . length === 0 ;
183+ return machineIds . length === 1 && machineIds [ 0 ] === candidateId ;
184+ } ;
185+
186+ return (
187+ < DropdownMenu open = { menuOpen } onOpenChange = { setMenuOpen } >
188+ < DropdownMenuTrigger
189+ className = "flex-shrink-0 rounded-md transition-colors hover:bg-[rgb(var(--surface-hover))] focus:outline-none focus:ring-2 focus:ring-primary-500 cursor-pointer"
190+ data-testid = "workspace-binding-header-machine-badge"
191+ title = { t ( 'panel.machineQuickSwitch' ) }
192+ >
193+ < Pill tone = "neutral" >
194+ < span className = "inline-flex items-center gap-1 normal-case tracking-normal" >
195+ { machineBadgeIcon ? (
196+ < span className = "text-xs leading-none" > { machineBadgeIcon } </ span >
197+ ) : null }
198+ { machineBadgeLabel }
199+ < ChevronDown className = "h-3 w-3 opacity-70" />
200+ </ span >
201+ </ Pill >
202+ </ DropdownMenuTrigger >
203+ < DropdownMenuContent
204+ align = "end"
205+ className = "w-52 py-1 px-1 bg-[rgb(var(--surface))] border border-[rgb(var(--border))] rounded-lg shadow-lg"
206+ data-testid = "workspace-binding-header-machine-menu"
207+ >
208+ < HeaderMachineOption
209+ label = { t ( 'panel.machineGlobal' ) }
210+ selected = { isSelected ( null ) }
211+ onSelect = { ( ) => handleSelect ( '' ) }
212+ testId = "workspace-binding-header-machine-global"
213+ />
214+ { machines . map ( ( machine ) => (
215+ < HeaderMachineOption
216+ key = { machine . id }
217+ label = { machine . name }
218+ icon = { machine . icon }
219+ selected = { isSelected ( machine . id ) }
220+ onSelect = { ( ) => handleSelect ( machine . id ) }
221+ testId = { `workspace-binding-header-machine-${ machine . id } ` }
222+ />
223+ ) ) }
224+ </ DropdownMenuContent >
225+ </ DropdownMenu >
226+ ) ;
227+ }
228+
229+ /**
230+ * Single row in the header machine quick-switch menu.
231+ */
232+ function HeaderMachineOption ( {
233+ label,
234+ icon,
235+ selected,
236+ onSelect,
237+ testId,
238+ } : {
239+ label : string ;
240+ icon ?: string | null ;
241+ selected : boolean ;
242+ onSelect : ( ) => void ;
243+ testId ?: string ;
244+ } ) {
245+ return (
246+ < button
247+ type = "button"
248+ role = "menuitem"
249+ onClick = { onSelect }
250+ className = { [
251+ 'w-full flex items-center gap-2 px-2.5 py-2 rounded-md text-left text-sm transition-colors' ,
252+ selected
253+ ? 'bg-primary-500/10 text-primary-700 dark:text-primary-300'
254+ : 'hover:bg-[rgb(var(--surface-hover))] text-[rgb(var(--foreground))]' ,
255+ ] . join ( ' ' ) }
256+ data-testid = { testId }
257+ >
258+ { icon ? < span className = "text-sm leading-none flex-shrink-0" > { icon } </ span > : null }
259+ < span className = "flex-1 min-w-0 truncate" > { label } </ span >
260+ { selected ? < Check className = "h-4 w-4 flex-shrink-0 text-primary-600" /> : null }
261+ </ button >
262+ ) ;
263+ }
264+
140265/**
141266 * Inline-editable workspace identity in the panel header: icon, label, and machine badge.
142267 */
@@ -147,9 +272,13 @@ function PanelIdentityHeader({
147272 icon,
148273 setIcon,
149274 workspaceRoot,
275+ machines,
276+ machineId,
277+ setMachineId,
278+ machineIds,
279+ setMachineIds,
150280 machineBadgeLabel,
151281 machineBadgeIcon,
152- onMachineBadgeClick,
153282 onIconClear,
154283 badges,
155284 footer,
@@ -161,9 +290,13 @@ function PanelIdentityHeader({
161290 icon : string ;
162291 setIcon : ( value : string ) => void ;
163292 workspaceRoot ?: string ;
293+ machines : Machine [ ] ;
294+ machineId : string ;
295+ setMachineId : ( value : string ) => void ;
296+ machineIds : string [ ] ;
297+ setMachineIds : ( value : string [ ] ) => void ;
164298 machineBadgeLabel : string ;
165299 machineBadgeIcon : string | null ;
166- onMachineBadgeClick : ( ) => void ;
167300 onIconClear : ( ) => void ;
168301 badges ?: ReactNode ;
169302 footer ?: ReactNode ;
@@ -219,22 +352,17 @@ function PanelIdentityHeader({
219352 { footer }
220353 </ div >
221354
222- < button
223- type = "button"
224- onClick = { onMachineBadgeClick }
225- className = "flex-shrink-0 rounded-md transition-colors hover:bg-[rgb(var(--surface-hover))] focus:outline-none focus:ring-2 focus:ring-primary-500"
226- data-testid = "workspace-binding-header-machine-badge"
227- title = { machineBadgeLabel }
228- >
229- < Pill tone = "neutral" >
230- < span className = "inline-flex items-center gap-1 normal-case tracking-normal" >
231- { machineBadgeIcon ? (
232- < span className = "text-xs leading-none" > { machineBadgeIcon } </ span >
233- ) : null }
234- { machineBadgeLabel }
235- </ span >
236- </ Pill >
237- </ button >
355+ < HeaderMachineDropdown
356+ mode = { mode }
357+ machines = { machines }
358+ machineId = { machineId }
359+ setMachineId = { setMachineId }
360+ machineIds = { machineIds }
361+ setMachineIds = { setMachineIds }
362+ machineBadgeLabel = { machineBadgeLabel }
363+ machineBadgeIcon = { machineBadgeIcon }
364+ t = { t }
365+ />
238366 </ div >
239367 ) ;
240368}
@@ -303,7 +431,6 @@ export function WorkspaceBindingPanel() {
303431 const lastSavedRef = useRef < WorkspaceBindingInput | null > ( null ) ;
304432 const lastSavedAppearanceRef = useRef < string | null > ( null ) ;
305433 const pendingPayloadRef = useRef < WorkspaceBindingInput | null > ( null ) ;
306- const scopeSectionRef = useRef < CollapsibleSectionRef > ( null ) ;
307434 const onSubmitRef = useRef < ( input : WorkspaceBindingInput ) => Promise < void > > ( async ( ) => undefined ) ;
308435 const onSaveStatusChangeRef = useRef ( setSaveStatus ) ;
309436
@@ -331,19 +458,6 @@ export function WorkspaceBindingPanel() {
331458 } ) ;
332459 } , [ subscribe , open ] ) ;
333460
334- useEffect ( ( ) => {
335- return subscribe ( 'workspace-binding-changed' , ( changedPayload ) => {
336- const state = useBindingPanelStore . getState ( ) ;
337- if ( ! state . isOpen || ! state . payload ) return ;
338- const changed = changedPayload . workspace_root . toLowerCase ( ) ;
339- const payloadRoot = state . payload . workspaceRoot ?. toLowerCase ( ) ;
340- const bindingRoot = state . payload . binding ?. workspace_root . toLowerCase ( ) ;
341- if ( changed === payloadRoot || changed === bindingRoot ) {
342- close ( ) ;
343- }
344- } ) ;
345- } , [ subscribe , close ] ) ;
346-
347461 useEffect ( ( ) => {
348462 if ( ! isOpen || ! payload ) return ;
349463 let cancelled = false ;
@@ -842,10 +956,6 @@ export function WorkspaceBindingPanel() {
842956 }
843957 } , [ isEdit , formInitial , canSubmit , handlePersistIcon , showError , t ] ) ;
844958
845- const handleMachineBadgeClick = useCallback ( ( ) => {
846- scopeSectionRef . current ?. expand ( ) ;
847- } , [ ] ) ;
848-
849959 if ( ! isOpen || ! payload ) return null ;
850960
851961 const binding = payload . binding ?? null ;
@@ -894,9 +1004,13 @@ export function WorkspaceBindingPanel() {
8941004 icon = { icon }
8951005 setIcon = { setIcon }
8961006 workspaceRoot = { workspaceRoot }
1007+ machines = { machines }
1008+ machineId = { machineId }
1009+ setMachineId = { setMachineId }
1010+ machineIds = { machineIds }
1011+ setMachineIds = { setMachineIds }
8971012 machineBadgeLabel = { machineBadgeLabel }
8981013 machineBadgeIcon = { machineBadgeIcon }
899- onMachineBadgeClick = { handleMachineBadgeClick }
9001014 onIconClear = { handleHeaderIconClear }
9011015 badges = {
9021016 < >
@@ -1032,7 +1146,6 @@ export function WorkspaceBindingPanel() {
10321146 ) }
10331147
10341148 < CollapsibleSection
1035- ref = { scopeSectionRef }
10361149 icon = { < Monitor className = "h-5 w-5" /> }
10371150 tone = "primary"
10381151 title = { t ( 'panel.scope' ) }
0 commit comments