+
+
Layout Slots
+
+ Layouts are saved globally and can be applied to any workspace.
+
+
+ Slots 1–9 have default Ctrl/Cmd+Alt+1..9 hotkeys. Additional layouts can be added and
+ assigned custom hotkeys.
+
+ {selectedWorkspaceLabel ? null : (
+
+ Select a workspace to capture or apply layouts.
+
+ )}
+
+
+ {!loaded ?
Loading…
: null}
+ {loadFailed ? (
+
+ Failed to load layouts from config. Using defaults.
+
+ ) : null}
+ {actionError ?
{actionError}
: null}
+
+ {visibleSlots.length > 0 ? (
+
+ {visibleSlots.map((slotConfig) => {
+ const slot = slotConfig.slot;
+ const preset = slotConfig.preset;
+ const effectiveKeybind = getEffectiveSlotKeybind(layoutPresets, slot);
+
+ const isEditingName = editingName?.slot === slot;
+ const isCapturing = capturingSlot === slot;
+
+ const menuItems: KebabMenuItem[] = [
+ {
+ label: "Apply",
+ disabled: !workspaceId,
+ tooltip: workspaceId ? undefined : "Select a workspace to apply layouts.",
+ onClick: () => {
+ setActionError(null);
+ if (!workspaceId) return;
+ void applySlotToWorkspace(workspaceId, slot).catch(() => {
+ setActionError("Failed to apply layout.");
+ });
+ },
+ },
+ {
+ label: "Update from current workspace",
+ disabled: !workspaceId,
+ tooltip: workspaceId ? undefined : "Select a workspace to capture its layout.",
+ onClick: () => {
+ setActionError(null);
+ if (!workspaceId) {
+ setActionError("Select a workspace to capture its layout.");
+ return;
+ }
+
+ void saveCurrentWorkspaceToSlot(workspaceId, slot).catch(() => {
+ setActionError("Failed to update layout.");
+ });
+ },
+ },
+ {
+ label: "Delete layout",
+ onClick: () => {
+ const ok = confirm(`Delete layout "${preset.name}"?`);
+ if (!ok) return;
+
+ setActionError(null);
+
+ setEditingName(null);
+ setCapturingSlot(null);
+ setCaptureError(null);
+
+ void deleteSlot(slot).catch(() => {
+ setActionError("Failed to delete layout.");
+ });
+ },
+ },
+ ];
+
+ return (
+
+
+
+
Slot {slot}
+
+
+ {isEditingName ? (
+
+ setEditingName({ ...editingName, value: e.target.value })
+ }
+ onKeyDown={(e) => {
+ stopKeyboardPropagation(e);
+
+ if (e.key === "Enter") {
+ e.preventDefault();
+ void submitRename(slot, editingName.value);
+ } else if (e.key === "Escape") {
+ e.preventDefault();
+ setEditingName(null);
+ setNameError(null);
+ }
+ }}
+ onBlur={() => void submitRename(slot, editingName.value)}
+ autoFocus
+ aria-label={`Rename layout Slot ${slot}`}
+ />
+ ) : (
+
+
+ {
+ e.stopPropagation();
+ setActionError(null);
+ setCapturingSlot(null);
+ setCaptureError(null);
+ setEditingName({
+ slot,
+ value: preset.name,
+ original: preset.name,
+ });
+ setNameError(null);
+ }}
+ title="Double-click to rename"
+ >
+ {preset.name}
+
+
+ Double-click to rename
+
+ )}
+
+
+
+
+ {isCapturing ? (
+
+
+
+ Press keys…
+
+ handleCaptureKeyDown(slot, e)}
+ aria-label={`Set hotkey for Slot ${slot}`}
+ />
+
+
+ {slotConfig.keybindOverride ? (
+
+
+
+
+
+ {slot <= 9 ? "Reset to default" : "Clear hotkey"}
+
+
+ ) : null}
+
+ ) : (
+
+
+ {
+ e.preventDefault();
+ e.stopPropagation();
+
+ setActionError(null);
+ setEditingName(null);
+ setNameError(null);
+ setCapturingSlot(slot);
+ setCaptureError(null);
+ }}
+ >
+ {effectiveKeybind ? formatKeybind(effectiveKeybind) : "No hotkey"}
+
+
+ Double-click to change hotkey
+
+ )}
+
+
+
+
+
+ {isCapturing ? (
+
+ Press a key combo (Esc to cancel)
+ {captureError ?
{captureError}
: null}
+
+ ) : null}
+
+ {isEditingName && nameError ? (
+
{nameError}
+ ) : null}
+
+ );
+ })}
+
+ ) : null}
+
+
+
+ );
+}
diff --git a/src/browser/contexts/UILayoutsContext.tsx b/src/browser/contexts/UILayoutsContext.tsx
new file mode 100644
index 0000000000..f9b23bb09c
--- /dev/null
+++ b/src/browser/contexts/UILayoutsContext.tsx
@@ -0,0 +1,237 @@
+import React, {
+ useCallback,
+ createContext,
+ useContext,
+ useEffect,
+ useMemo,
+ useState,
+ type ReactNode,
+} from "react";
+import { useAPI } from "@/browser/contexts/API";
+import assert from "@/common/utils/assert";
+import {
+ DEFAULT_LAYOUT_PRESETS_CONFIG,
+ normalizeLayoutPresetsConfig,
+ type LayoutPreset,
+ type LayoutPresetsConfig,
+ type LayoutSlotNumber,
+} from "@/common/types/uiLayouts";
+import {
+ applyLayoutPresetToWorkspace,
+ createPresetFromCurrentWorkspace,
+ deleteSlotAndShiftFollowingSlots,
+ getLayoutsConfigOrDefault,
+ getPresetForSlot,
+ updateSlotKeybindOverride,
+ updateSlotPreset,
+} from "@/browser/utils/uiLayouts";
+import type { Keybind } from "@/common/types/keybind";
+
+interface UILayoutsContextValue {
+ layoutPresets: LayoutPresetsConfig;
+ loaded: boolean;
+ loadFailed: boolean;
+ refresh: () => Promise