@@ -158,6 +158,7 @@ export function WorkspacesPage() {
158158 const { toasts, success, error : showError , dismiss } = useToast ( ) ;
159159 const { confirm, ConfirmDialogElement } = useConfirm ( ) ;
160160 const openBindingPanel = useBindingPanelStore ( ( state ) => state . open ) ;
161+ const isPanelOpen = useBindingPanelStore ( ( state ) => state . isOpen ) ;
161162
162163 const [ searchQuery , setSearchQuery ] = useState ( '' ) ;
163164 const [ filter , setFilter ] = useState < 'all' | 'live' | 'mapped' | 'unmapped' > ( 'all' ) ;
@@ -384,6 +385,21 @@ export function WorkspacesPage() {
384385 [ appearancesByRoot ]
385386 ) ;
386387
388+ // Auto-open binding panel for the first unmapped-live entry on page load.
389+ // Catches `workspace-needs-binding` events that fired before the listener was
390+ // registered (e.g. Cursor was already connected when this page first rendered).
391+ useEffect ( ( ) => {
392+ if ( isLoading || isPanelOpen ) return ;
393+ const firstUnmapped = entries . find ( ( e ) => e . kind === 'unmapped-live' ) ;
394+ if ( ! firstUnmapped ) return ;
395+ openBindingPanel ( {
396+ mode : 'create-from-live' ,
397+ workspaceRoot : firstUnmapped . root ,
398+ appearanceIcon : resolveEntryIcon ( firstUnmapped ) ?? undefined ,
399+ } ) ;
400+ // eslint-disable-next-line react-hooks/exhaustive-deps
401+ } , [ isLoading ] ) ;
402+
387403 const handleRegisterMachine = async ( input : {
388404 name : string ;
389405 icon : string | null ;
@@ -755,43 +771,39 @@ interface EntryCardRoutingRow {
755771 clickable : boolean ;
756772}
757773
774+ const ROUTING_GRID_COLS =
775+ 'grid grid-cols-[minmax(0,5.5rem)_minmax(0,1fr)_minmax(0,3.5rem)] gap-x-2' ;
776+
758777/**
759- * Compact routing table for EntryCard footer — machine, feature set, space.
760- * Uses semantic HTML table (no Table primitive in @mcpmux/ui). Feature set
761- * names wrap to additional lines when needed.
778+ * Routing footer for EntryCard — fixed 3-column headers with each binding row
779+ * rendered as an aligned chip pill (solid for real bindings, dashed for ghosts).
762780 */
763781function EntryCardRoutingTable ( {
764782 rows,
765- showMachineColumn,
766783 onRowClick,
767784 onCreateForCurrentMachine,
768785 t,
769786} : {
770787 rows : EntryCardRoutingRow [ ] ;
771- showMachineColumn : boolean ;
772788 onRowClick ?: ( bindingId : string ) => void ;
773789 onCreateForCurrentMachine ?: ( ) => void ;
774790 t : TFunction < [ 'workspaces' , 'common' ] > ;
775791} ) {
776792 const headCls =
777- 'pb-1 pr-2 text-left text-[10px] font-semibold uppercase tracking-wider text-[rgb(var(--muted))] last:pr-0 ' ;
778- const cellCls = 'py-0.5 pr-2 align-top text-[11px] text-[rgb(var(--foreground))] last:pr-0 ' ;
793+ 'text-left text-[10px] font-semibold uppercase tracking-wider text-[rgb(var(--muted))]' ;
794+ const cellCls = 'min-w-0 text-[11px] text-[rgb(var(--foreground))]' ;
779795
780796 return (
781- < table className = "w-full border-collapse text-xs" >
782- < colgroup >
783- { showMachineColumn ? < col className = "w-px" /> : null }
784- < col />
785- < col className = "w-px" />
786- </ colgroup >
787- < thead >
788- < tr className = "border-b border-[rgb(var(--border-subtle))]" >
789- { showMachineColumn ? < th className = { headCls } > { t ( 'card.machine' ) } </ th > : null }
790- < th className = { headCls } > { t ( 'card.routesTo' ) } </ th >
791- < th className = { `${ headCls } whitespace-nowrap` } > { t ( 'card.in' ) } </ th >
792- </ tr >
793- </ thead >
794- < tbody >
797+ < div className = "text-xs" >
798+ < div
799+ className = { `${ ROUTING_GRID_COLS } border-b border-[rgb(var(--border-subtle))] pb-1` }
800+ aria-hidden
801+ >
802+ < span className = { headCls } > { t ( 'card.machine' ) } </ span >
803+ < span className = { headCls } > { t ( 'card.routesTo' ) } </ span >
804+ < span className = { `${ headCls } whitespace-nowrap` } > { t ( 'card.in' ) } </ span >
805+ </ div >
806+ < div className = "mt-1.5 flex flex-col gap-1" >
795807 { rows . map ( ( row ) => {
796808 const fsDisplay = row . fsName || '—' ;
797809 const spaceDisplay = row . spaceName ?? '—' ;
@@ -804,18 +816,14 @@ function EntryCardRoutingTable({
804816 ? {
805817 role : 'button' as const ,
806818 tabIndex : 0 ,
807- className : [
808- 'cursor-pointer transition-colors hover:bg-[rgb(var(--surface-hover,var(--background)))]' ,
809- row . ghost ? 'opacity-70' : '' ,
810- ] . join ( ' ' ) ,
811819 'aria-label' : row . createForCurrentMachine
812820 ? t ( 'card.addBindingForMachine' , { machine : row . machineLabel } )
813821 : t ( 'card.machineRow' , { machine : row . machineLabel } ) ,
814- onClick : ( event : ReactMouseEvent < HTMLTableRowElement > ) => {
822+ onClick : ( event : ReactMouseEvent < HTMLDivElement > ) => {
815823 event . stopPropagation ( ) ;
816824 rowAction ( ) ;
817825 } ,
818- onKeyDown : ( event : ReactKeyboardEvent < HTMLTableRowElement > ) => {
826+ onKeyDown : ( event : ReactKeyboardEvent < HTMLDivElement > ) => {
819827 if ( event . key === 'Enter' || event . key === ' ' ) {
820828 event . preventDefault ( ) ;
821829 event . stopPropagation ( ) ;
@@ -826,37 +834,48 @@ function EntryCardRoutingTable({
826834 : { } ;
827835
828836 return (
829- < tr key = { row . key } { ...rowProps } >
830- { showMachineColumn ? (
831- < td className = { `${ cellCls } whitespace-nowrap` } title = { row . machineLabel } >
832- < span className = "inline-flex max-w-[7rem] items-center gap-1" >
833- { row . machine ?. icon ? (
834- < span className = "shrink-0 text-[11px] leading-none" > { row . machine . icon } </ span >
835- ) : null }
836- < span className = "truncate" > { row . machineLabel } </ span >
837- </ span >
838- </ td >
839- ) : null }
840- < td className = { cellCls } >
841- < span
842- className = { [
843- 'block break-words font-medium leading-snug' ,
844- row . ghost
845- ? 'italic text-[rgb(var(--muted))]'
846- : 'text-primary-700 dark:text-primary-300' ,
847- ] . join ( ' ' ) }
848- >
849- { fsDisplay }
837+ < div
838+ key = { row . key }
839+ className = { [
840+ ROUTING_GRID_COLS ,
841+ 'items-center rounded-md border px-1.5 py-1' ,
842+ row . ghost
843+ ? 'border-dashed border-[rgb(var(--border-subtle))] opacity-70'
844+ : 'border-[rgb(var(--border-subtle))] bg-[rgb(var(--background))]' ,
845+ row . clickable && rowAction
846+ ? 'cursor-pointer transition-colors hover:bg-[rgb(var(--surface-hover,var(--background)))]'
847+ : '' ,
848+ ] . join ( ' ' ) }
849+ { ...rowProps }
850+ >
851+ < span className = { `${ cellCls } truncate whitespace-nowrap` } title = { row . machineLabel } >
852+ < span className = "inline-flex max-w-full items-center gap-1" >
853+ { row . machine ?. icon ? (
854+ < span className = "shrink-0 text-[11px] leading-none" > { row . machine . icon } </ span >
855+ ) : null }
856+ < span className = "truncate" > { row . machineLabel } </ span >
850857 </ span >
851- </ td >
852- < td className = { `${ cellCls } whitespace-nowrap` } title = { spaceDisplay } >
858+ </ span >
859+ < span
860+ className = { [
861+ cellCls ,
862+ 'truncate font-medium' ,
863+ row . ghost
864+ ? 'italic text-[rgb(var(--muted))]'
865+ : 'text-primary-700 dark:text-primary-300' ,
866+ ] . join ( ' ' ) }
867+ title = { fsDisplay }
868+ >
869+ { fsDisplay }
870+ </ span >
871+ < span className = { `${ cellCls } truncate whitespace-nowrap` } title = { spaceDisplay } >
853872 { spaceDisplay }
854- </ td >
855- </ tr >
873+ </ span >
874+ </ div >
856875 ) ;
857876 } ) }
858- </ tbody >
859- </ table >
877+ </ div >
878+ </ div >
860879 ) ;
861880}
862881
@@ -911,6 +930,22 @@ function buildEntryRoutingRows(
911930 } ) ;
912931 }
913932
933+ if ( entry . kind === 'unmapped-live' && bindings . length === 0 ) {
934+ const currentMachine = currentMachineId
935+ ? machinesById . get ( currentMachineId )
936+ : undefined ;
937+ rows . push ( {
938+ key : 'ghost:unmapped' ,
939+ createForCurrentMachine : true ,
940+ ghost : true ,
941+ machine : currentMachine ,
942+ machineLabel : currentMachine ?. name ?? t ( 'card.addBinding' ) ,
943+ fsName : '—' ,
944+ spaceName : undefined ,
945+ clickable : true ,
946+ } ) ;
947+ }
948+
914949 return rows ;
915950}
916951
@@ -965,11 +1000,6 @@ function EntryCard({
9651000 fsById ,
9661001 t ,
9671002 ) ;
968- const showMachineColumn =
969- bindings . some ( ( b ) => b . machine_id != null ) ||
970- routingRows . some ( ( row ) => row . ghost ) ||
971- bindings . length > 1 ;
972-
9731003 return (
9741004 < Card
9751005 className = "group relative h-full cursor-pointer transition-all hover:shadow-lg hover:scale-[1.01]"
@@ -1054,19 +1084,10 @@ function EntryCard({
10541084 < div className = "mt-auto -mx-6 -mb-6 rounded-b-xl bg-[rgb(var(--surface))] px-5 py-3 text-xs text-[rgb(var(--muted))]" >
10551085 < EntryCardRoutingTable
10561086 rows = { routingRows }
1057- showMachineColumn = { showMachineColumn }
10581087 onRowClick = { onMachineRowClick }
10591088 onCreateForCurrentMachine = { onCreateForCurrentMachine }
10601089 t = { t }
10611090 />
1062- { ! binding && (
1063- < span
1064- className = "mt-2 inline-flex items-center px-1.5 py-0.5 rounded-md text-[10px] font-medium uppercase tracking-wider bg-amber-50 dark:bg-amber-900/20 text-amber-700 dark:text-amber-400 border border-amber-200/70 dark:border-amber-800/60"
1065- title = { t ( 'card.unboundTooltip' ) }
1066- >
1067- { t ( 'card.unbound' ) }
1068- </ span >
1069- ) }
10701091 </ div >
10711092 </ CardContent >
10721093 </ Card >
0 commit comments