Skip to content

Commit d622f5c

Browse files
committed
fix(workspaces): auto-prefill adopt from same-path cross-machine bindings
Detect sibling bindings when folder names match but machine scope differs, seed label/icon/space/FS on panel open, and load workspace appearance icons. Signed-off-by: crimsonsunset <jsangio1@gmail.com>
1 parent 4ab677b commit d622f5c

2 files changed

Lines changed: 80 additions & 20 deletions

File tree

apps/desktop/src/features/workspaces/workspace-binding-form.component.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,41 @@ export function folderName(root: string): string {
102102
return segments[segments.length - 1] ?? root;
103103
}
104104

105+
/**
106+
* Bindings on other machines (or scopes) that can seed a new create-from-live row.
107+
* Same folder name is enough; identical absolute paths count when machine differs.
108+
*/
109+
export function findAdoptableSiblingBindings(
110+
allBindings: WorkspaceBinding[],
111+
workspaceRoot: string,
112+
targetMachineId: string | null,
113+
): WorkspaceBinding[] {
114+
const currentFolder = folderName(workspaceRoot).toLowerCase();
115+
const normalizedRoot = workspaceRoot.toLowerCase();
116+
return allBindings.filter((binding) => {
117+
if (folderName(binding.workspace_root).toLowerCase() !== currentFolder) return false;
118+
const samePath = binding.workspace_root.toLowerCase() === normalizedRoot;
119+
if (!samePath) return true;
120+
return (binding.machine_id ?? null) !== targetMachineId;
121+
});
122+
}
123+
124+
/**
125+
* Space, feature sets, label, and icon to copy from an adopt source binding.
126+
*/
127+
export function adoptBindingSeed(
128+
source: WorkspaceBinding,
129+
workspaceRoot: string,
130+
): Pick<WorkspaceBinding, 'space_id' | 'feature_set_ids' | 'label' | 'icon'> {
131+
const trimmedLabel = source.label?.trim() ?? '';
132+
return {
133+
space_id: source.space_id,
134+
feature_set_ids: source.feature_set_ids,
135+
label: trimmedLabel.length > 0 ? trimmedLabel : folderName(workspaceRoot),
136+
icon: source.icon,
137+
};
138+
}
139+
105140
/** True when the icon value is an uploaded file ref or URL, not a plain emoji. */
106141
function isWorkspaceFileIcon(icon: string): boolean {
107142
const trimmed = icon.trim();

apps/desktop/src/features/workspaces/workspace-binding-panel.component.tsx

Lines changed: 45 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
} from '@/lib/api/machines';
3737
import {
3838
deleteWorkspaceAppearance,
39+
listWorkspaceAppearances,
3940
upsertWorkspaceAppearance,
4041
} from '@/lib/api/workspaceAppearances';
4142
import { listFeatureSets, type FeatureSet } from '@/lib/api/featureSets';
@@ -50,6 +51,8 @@ import {
5051
bindingMachineId,
5152
bindingScopeConflicts,
5253
buildBindingPayload,
54+
adoptBindingSeed,
55+
findAdoptableSiblingBindings,
5356
folderName,
5457
normalizeIcon,
5558
sameBindingInput,
@@ -267,6 +270,7 @@ export function WorkspaceBindingPanel() {
267270
const [clientMachineId, setClientMachineIdState] = useState<string | null>(null);
268271
const [showMachineCallout, setShowMachineCallout] = useState(false);
269272
const [adoptDismissed, setAdoptDismissed] = useState(false);
273+
const [appearanceIcon, setAppearanceIcon] = useState<string | null>(null);
270274
const [assignMachineId, setAssignMachineId] = useState('');
271275
const [creatingMachine, setCreatingMachine] = useState(false);
272276
const [newMachineName, setNewMachineName] = useState('');
@@ -326,15 +330,17 @@ export function WorkspaceBindingPanel() {
326330
setCreatingMachine(false);
327331
setNewMachineName('');
328332
setAdoptDismissed(false);
333+
setAppearanceIcon(null);
329334

330335
void (async () => {
331336
try {
332-
const [loadedSpaces, loadedFs, loadedMachines, loadedBindings, loadedLocalId] =
337+
const [loadedSpaces, loadedFs, loadedMachines, loadedBindings, loadedAppearances, loadedLocalId] =
333338
await Promise.all([
334339
listSpaces(),
335340
listFeatureSets(),
336341
listMachines().catch(() => [] as Machine[]),
337342
listWorkspaceBindings().catch(() => [] as WorkspaceBinding[]),
343+
listWorkspaceAppearances().catch(() => []),
338344
getLocalMachineId().catch(() => null),
339345
]);
340346
if (cancelled) return;
@@ -344,6 +350,14 @@ export function WorkspaceBindingPanel() {
344350
setAllBindings(loadedBindings);
345351
setLocalMachineId(loadedLocalId);
346352

353+
const rootForAppearance = payload.workspaceRoot ?? payload.binding?.workspace_root;
354+
if (rootForAppearance) {
355+
const appearance = loadedAppearances.find(
356+
(entry) => entry.workspace_root.toLowerCase() === rootForAppearance.toLowerCase(),
357+
);
358+
setAppearanceIcon(appearance?.icon ?? null);
359+
}
360+
347361
if (payload.mode === 'create-from-live' && payload.clientId) {
348362
const existingClientMachine = await getClientMachineId(payload.clientId).catch(
349363
() => null,
@@ -396,20 +410,18 @@ export function WorkspaceBindingPanel() {
396410
const isEdit = mode === 'edit';
397411
const rootEditable = mode !== 'create-from-live';
398412
const spaceLocked = payload?.spaceLocked ?? false;
399-
const panelKey = `${mode}:${payload?.binding?.id ?? workspaceRoot ?? 'new'}:${spaces.length}`;
413+
const panelKey = `${mode}:${payload?.binding?.id ?? workspaceRoot ?? 'new'}:${spaces.length}:${allBindings.length}`;
400414

401415
const defaultTargetMachineId =
402416
clientMachineId ?? viewerMachineId ?? localMachineId ?? null;
403417

404418
const siblingBindings = useMemo(() => {
405419
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]);
420+
return findAdoptableSiblingBindings(allBindings, workspaceRoot, defaultTargetMachineId);
421+
}, [mode, workspaceRoot, allBindings, defaultTargetMachineId]);
422+
423+
const adoptSource =
424+
mode === 'create-from-live' && !adoptDismissed ? siblingBindings[0] ?? null : null;
413425

414426
const effectiveMachineId = isEdit
415427
? bindingMachineId(machineId)
@@ -418,11 +430,21 @@ export function WorkspaceBindingPanel() {
418430
useEffect(() => {
419431
if (!isOpen || !payload || loadingData) return;
420432
const initial = formInitial;
421-
setRoot(initial?.workspace_root ?? payload.workspaceRoot ?? '');
422-
setLabel(initial?.label ?? '');
423-
setIcon(initial?.icon ?? payload.appearanceIcon ?? '');
424-
setSpaceId(initial?.space_id ?? defaultSpaceId);
425-
setFsIds(initial?.feature_set_ids ?? []);
433+
const rootValue = initial?.workspace_root ?? payload.workspaceRoot ?? '';
434+
const adopted = adoptSource ? adoptBindingSeed(adoptSource, rootValue) : null;
435+
const resolvedIcon =
436+
adopted?.icon ??
437+
initial?.icon ??
438+
payload.appearanceIcon ??
439+
appearanceIcon ??
440+
'';
441+
const resolvedLabel = adopted?.label ?? initial?.label ?? '';
442+
443+
setRoot(rootValue);
444+
setLabel(resolvedLabel);
445+
setIcon(resolvedIcon);
446+
setSpaceId(adopted?.space_id ?? initial?.space_id ?? defaultSpaceId);
447+
setFsIds(adopted?.feature_set_ids ?? initial?.feature_set_ids ?? []);
426448
setMachineId(initial?.machine_id ?? '');
427449
setMachineIds(
428450
mode === 'edit' ? [] : defaultTargetMachineId ? [defaultTargetMachineId] : [],
@@ -432,7 +454,7 @@ export function WorkspaceBindingPanel() {
432454
lastSavedRef.current = null;
433455
pendingPayloadRef.current = null;
434456
lastSavedAppearanceRef.current =
435-
mode === 'create-from-live' ? normalizeIcon(initial?.icon) : null;
457+
mode === 'create-from-live' ? normalizeIcon(resolvedIcon) : null;
436458
}, [
437459
panelKey,
438460
loadingData,
@@ -442,6 +464,8 @@ export function WorkspaceBindingPanel() {
442464
defaultSpaceId,
443465
mode,
444466
defaultTargetMachineId,
467+
adoptSource,
468+
appearanceIcon,
445469
]);
446470

447471
useEffect(() => {
@@ -1039,7 +1063,7 @@ export function WorkspaceBindingPanel() {
10391063
</div>
10401064
)}
10411065

1042-
{mode === 'create-from-live' && siblingBindings.length > 0 && !adoptDismissed && (
1066+
{mode === 'create-from-live' && siblingBindings.length > 1 && !adoptDismissed && (
10431067
<div
10441068
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"
10451069
data-testid="workspace-binding-adopt-card"
@@ -1105,10 +1129,11 @@ export function WorkspaceBindingPanel() {
11051129
variant="secondary"
11061130
size="sm"
11071131
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);
1132+
const seed = adoptBindingSeed(sibling, root);
1133+
setSpaceId(seed.space_id);
1134+
setFsIds(seed.feature_set_ids);
1135+
setLabel(seed.label ?? '');
1136+
setIcon(seed.icon ?? '');
11121137
setAdoptDismissed(true);
11131138
}}
11141139
data-testid={`workspace-binding-adopt-use-${sibling.id}`}

0 commit comments

Comments
 (0)