@@ -42,13 +42,15 @@ import { listFeatureSets, type FeatureSet } from '@/lib/api/featureSets';
4242import { listSpaces , type Space } from '@/lib/api/spaces' ;
4343import { ServerIcon } from '@/components/ServerIcon' ;
4444import { useBindingPanelStore } from '@/stores/bindingPanelStore' ;
45+ import { useViewerIdentity } from '@/hooks/use-viewer-identity.hook' ;
4546import {
4647 RoutingFields ,
4748 SaveStatusPill ,
4849 ScopeFields ,
4950 bindingMachineId ,
5051 bindingScopeConflicts ,
5152 buildBindingPayload ,
53+ folderName ,
5254 normalizeIcon ,
5355 sameBindingInput ,
5456 type RootValidationState ,
@@ -252,16 +254,19 @@ function buildFormInitial(
252254export function WorkspaceBindingPanel ( ) {
253255 const { t } = useTranslation ( [ 'workspaces' , 'common' ] ) ;
254256 const { isOpen, payload, open, close } = useBindingPanelStore ( ) ;
257+ const { machineId : viewerMachineId } = useViewerIdentity ( ) ;
255258 const { subscribe } = useWorkspaceEvents ( ) ;
256259 const { confirm, ConfirmDialogElement } = useConfirm ( ) ;
257260 const { error : showError } = useToast ( ) ;
258261
259262 const [ spaces , setSpaces ] = useState < Space [ ] > ( [ ] ) ;
260263 const [ featureSets , setFeatureSets ] = useState < FeatureSet [ ] > ( [ ] ) ;
261264 const [ machines , setMachines ] = useState < Machine [ ] > ( [ ] ) ;
265+ const [ allBindings , setAllBindings ] = useState < WorkspaceBinding [ ] > ( [ ] ) ;
262266 const [ localMachineId , setLocalMachineId ] = useState < string | null > ( null ) ;
263267 const [ clientMachineId , setClientMachineIdState ] = useState < string | null > ( null ) ;
264268 const [ showMachineCallout , setShowMachineCallout ] = useState ( false ) ;
269+ const [ adoptDismissed , setAdoptDismissed ] = useState ( false ) ;
265270 const [ assignMachineId , setAssignMachineId ] = useState ( '' ) ;
266271 const [ creatingMachine , setCreatingMachine ] = useState ( false ) ;
267272 const [ newMachineName , setNewMachineName ] = useState ( '' ) ;
@@ -320,19 +325,23 @@ export function WorkspaceBindingPanel() {
320325 setEffectiveTotal ( null ) ;
321326 setCreatingMachine ( false ) ;
322327 setNewMachineName ( '' ) ;
328+ setAdoptDismissed ( false ) ;
323329
324330 void ( async ( ) => {
325331 try {
326- const [ loadedSpaces , loadedFs , loadedMachines , loadedLocalId ] = await Promise . all ( [
332+ const [ loadedSpaces , loadedFs , loadedMachines , loadedBindings , loadedLocalId ] =
333+ await Promise . all ( [
327334 listSpaces ( ) ,
328335 listFeatureSets ( ) ,
329336 listMachines ( ) . catch ( ( ) => [ ] as Machine [ ] ) ,
337+ listWorkspaceBindings ( ) . catch ( ( ) => [ ] as WorkspaceBinding [ ] ) ,
330338 getLocalMachineId ( ) . catch ( ( ) => null ) ,
331339 ] ) ;
332340 if ( cancelled ) return ;
333341 setSpaces ( loadedSpaces ) ;
334342 setFeatureSets ( loadedFs ) ;
335343 setMachines ( loadedMachines ) ;
344+ setAllBindings ( loadedBindings ) ;
336345 setLocalMachineId ( loadedLocalId ) ;
337346
338347 if ( payload . mode === 'create-from-live' && payload . clientId ) {
@@ -389,6 +398,23 @@ export function WorkspaceBindingPanel() {
389398 const spaceLocked = payload ?. spaceLocked ?? false ;
390399 const panelKey = `${ mode } :${ payload ?. binding ?. id ?? workspaceRoot ?? 'new' } :${ spaces . length } ` ;
391400
401+ const defaultTargetMachineId =
402+ clientMachineId ?? viewerMachineId ?? localMachineId ?? null ;
403+
404+ const siblingBindings = useMemo ( ( ) => {
405+ if ( mode !== 'create-from-live' || ! workspaceRoot ) return [ ] ;
406+ const currentFolder = folderName ( workspaceRoot ) . toLowerCase ( ) ;
407+ return allBindings . filter (
408+ ( b ) =>
409+ b . workspace_root . toLowerCase ( ) !== workspaceRoot . toLowerCase ( ) &&
410+ folderName ( b . workspace_root ) . toLowerCase ( ) === currentFolder ,
411+ ) ;
412+ } , [ mode , workspaceRoot , allBindings ] ) ;
413+
414+ const effectiveMachineId = isEdit
415+ ? bindingMachineId ( machineId )
416+ : machineIds [ 0 ] ?? null ;
417+
392418 useEffect ( ( ) => {
393419 if ( ! isOpen || ! payload || loadingData ) return ;
394420 const initial = formInitial ;
@@ -399,15 +425,24 @@ export function WorkspaceBindingPanel() {
399425 setFsIds ( initial ?. feature_set_ids ?? [ ] ) ;
400426 setMachineId ( initial ?. machine_id ?? '' ) ;
401427 setMachineIds (
402- mode === 'edit' ? [ ] : localMachineId ? [ localMachineId ] : [ ] ,
428+ mode === 'edit' ? [ ] : defaultTargetMachineId ? [ defaultTargetMachineId ] : [ ] ,
403429 ) ;
404430 setRootValidation ( { state : 'idle' } ) ;
405431 setSubmitting ( false ) ;
406432 lastSavedRef . current = null ;
407433 pendingPayloadRef . current = null ;
408434 lastSavedAppearanceRef . current =
409435 mode === 'create-from-live' ? normalizeIcon ( initial ?. icon ) : null ;
410- } , [ panelKey , loadingData , isOpen , payload , formInitial , defaultSpaceId , mode , localMachineId ] ) ;
436+ } , [
437+ panelKey ,
438+ loadingData ,
439+ isOpen ,
440+ payload ,
441+ formInitial ,
442+ defaultSpaceId ,
443+ mode ,
444+ defaultTargetMachineId ,
445+ ] ) ;
411446
412447 useEffect ( ( ) => {
413448 if ( ! rootEditable ) {
@@ -727,9 +762,17 @@ export function WorkspaceBindingPanel() {
727762 const handleDelete = useCallback ( async ( ) => {
728763 if ( ! payload ?. binding ) return ;
729764 const binding = payload . binding ;
765+ const displayName = binding . label ?. trim ( ) || folderName ( binding . workspace_root ) ;
766+ const machineName = binding . machine_id
767+ ? machines . find ( ( m ) => m . id === binding . machine_id ) ?. name
768+ : null ;
769+ const message =
770+ machineName != null
771+ ? t ( 'confirm.removeMessageMachine' , { machine : machineName , name : displayName } )
772+ : t ( 'confirm.removeMessageGlobal' , { name : displayName } ) ;
730773 const ok = await confirm ( {
731774 title : t ( 'confirm.removeTitle' ) ,
732- message : t ( 'confirm.removeMessage' , { path : binding . workspace_root } ) ,
775+ message,
733776 confirmLabel : t ( 'confirm.removeLabel' ) ,
734777 cancelLabel : t ( 'common:actions.cancel' ) ,
735778 variant : 'danger' ,
@@ -741,7 +784,7 @@ export function WorkspaceBindingPanel() {
741784 } catch ( e ) {
742785 showError ( t ( 'toast.failedToRemove' ) , e instanceof Error ? e . message : String ( e ) ) ;
743786 }
744- } , [ payload , confirm , close , showError , t ] ) ;
787+ } , [ payload , machines , confirm , close , showError , t ] ) ;
745788
746789 const handleAssignMachine = async ( ) => {
747790 if ( ! payload ?. clientId || ! assignMachineId || assigningMachine ) return ;
@@ -864,6 +907,13 @@ export function WorkspaceBindingPanel() {
864907 { mode === 'edit' && binding && < Pill tone = "neutral" > { t ( 'card.offline' ) } </ Pill > }
865908 </ >
866909 }
910+ footer = {
911+ mode === 'create-from-live' ? (
912+ < p className = "text-xs text-[rgb(var(--muted))] mt-1" >
913+ { t ( 'panel.targeting' , { machine : machineBadgeLabel } ) }
914+ </ p >
915+ ) : null
916+ }
867917 t = { t }
868918 />
869919 < button
@@ -989,6 +1039,100 @@ export function WorkspaceBindingPanel() {
9891039 </ div >
9901040 ) }
9911041
1042+ { mode === 'create-from-live' && siblingBindings . length > 0 && ! adoptDismissed && (
1043+ < div
1044+ className = "rounded-xl border border-primary-200/80 dark:border-primary-800/50 bg-primary-50/50 dark:bg-primary-900/10 p-4 space-y-3"
1045+ data-testid = "workspace-binding-adopt-card"
1046+ >
1047+ < div >
1048+ < p className = "text-sm font-semibold text-[rgb(var(--foreground))]" >
1049+ { t ( 'panel.adoptTitle' ) }
1050+ </ p >
1051+ < p className = "text-xs text-[rgb(var(--muted))] mt-1" > { t ( 'panel.adoptDesc' ) } </ p >
1052+ </ div >
1053+ < div className = "overflow-x-auto rounded-lg border border-[rgb(var(--border-subtle))]" >
1054+ < table className = "w-full text-xs" >
1055+ < thead >
1056+ < tr className = "border-b border-[rgb(var(--border-subtle))] bg-[rgb(var(--surface))]" >
1057+ < th className = "px-2 py-1.5 text-left font-semibold text-[rgb(var(--muted))]" >
1058+ { t ( 'panel.adoptColMachine' ) }
1059+ </ th >
1060+ < th className = "px-2 py-1.5 text-left font-semibold text-[rgb(var(--muted))]" >
1061+ { t ( 'panel.adoptColPath' ) }
1062+ </ th >
1063+ < th className = "px-2 py-1.5 text-left font-semibold text-[rgb(var(--muted))]" >
1064+ { t ( 'panel.adoptColSpace' ) }
1065+ </ th >
1066+ < th className = "px-2 py-1.5 text-left font-semibold text-[rgb(var(--muted))]" >
1067+ { t ( 'panel.adoptColToolSet' ) }
1068+ </ th >
1069+ < th className = "px-2 py-1.5" />
1070+ </ tr >
1071+ </ thead >
1072+ < tbody >
1073+ { siblingBindings . map ( ( sibling ) => {
1074+ const machine = sibling . machine_id
1075+ ? machinesById . get ( sibling . machine_id )
1076+ : null ;
1077+ const machineLabel = machine ?. name ?? t ( 'panel.machineGlobal' ) ;
1078+ const spaceName =
1079+ spaces . find ( ( s ) => s . id === sibling . space_id ) ?. name ?? '—' ;
1080+ const fsNames = formatFsList (
1081+ sibling . feature_set_ids . map (
1082+ ( id ) => featureSets . find ( ( f ) => f . id === id ) ?. name ?? id ,
1083+ ) ,
1084+ ) ;
1085+ return (
1086+ < tr
1087+ key = { sibling . id }
1088+ className = "border-b border-[rgb(var(--border-subtle))] last:border-0"
1089+ >
1090+ < td className = "px-2 py-2 whitespace-nowrap" >
1091+ < span className = "inline-flex items-center gap-1" >
1092+ { machine ?. icon ? (
1093+ < span className = "text-sm leading-none" > { machine . icon } </ span >
1094+ ) : null }
1095+ { machineLabel }
1096+ </ span >
1097+ </ td >
1098+ < td className = "px-2 py-2 font-mono text-[10px] break-all max-w-[120px]" >
1099+ { sibling . workspace_root }
1100+ </ td >
1101+ < td className = "px-2 py-2 whitespace-nowrap" > { spaceName } </ td >
1102+ < td className = "px-2 py-2" > { fsNames || '—' } </ td >
1103+ < td className = "px-2 py-2 whitespace-nowrap" >
1104+ < Button
1105+ variant = "secondary"
1106+ size = "sm"
1107+ onClick = { ( ) => {
1108+ setSpaceId ( sibling . space_id ) ;
1109+ setFsIds ( sibling . feature_set_ids ) ;
1110+ if ( sibling . label ) setLabel ( sibling . label ) ;
1111+ if ( sibling . icon ) setIcon ( sibling . icon ) ;
1112+ setAdoptDismissed ( true ) ;
1113+ } }
1114+ data-testid = { `workspace-binding-adopt-use-${ sibling . id } ` }
1115+ >
1116+ { t ( 'panel.adoptUseThis' ) }
1117+ </ Button >
1118+ </ td >
1119+ </ tr >
1120+ ) ;
1121+ } ) }
1122+ </ tbody >
1123+ </ table >
1124+ </ div >
1125+ < button
1126+ type = "button"
1127+ onClick = { ( ) => setAdoptDismissed ( true ) }
1128+ className = "text-xs text-[rgb(var(--muted))] underline-offset-2 hover:text-[rgb(var(--foreground))] hover:underline"
1129+ data-testid = "workspace-binding-adopt-start-fresh"
1130+ >
1131+ { t ( 'panel.adoptStartFresh' ) }
1132+ </ button >
1133+ </ div >
1134+ ) }
1135+
9921136 < CollapsibleSection
9931137 ref = { scopeSectionRef }
9941138 icon = { < Monitor className = "h-5 w-5" /> }
@@ -1054,6 +1198,7 @@ export function WorkspaceBindingPanel() {
10541198 >
10551199 < EffectiveFeaturesContent
10561200 root = { workspaceRoot }
1201+ machineId = { effectiveMachineId }
10571202 onTotalChange = { setEffectiveTotal }
10581203 t = { t }
10591204 />
0 commit comments