Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
271 changes: 271 additions & 0 deletions .agents/commands/create-plan-file.md

Large diffs are not rendered by default.

Large diffs are not rendered by default.

21 changes: 21 additions & 0 deletions apps/desktop/src/lib/trpc/routers/settings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
import { localDb } from "main/lib/local-db";
import {
DEFAULT_CONFIRM_ON_QUIT,
DEFAULT_NAVIGATION_STYLE,
DEFAULT_TERMINAL_LINK_BEHAVIOR,
} from "shared/constants";
import { DEFAULT_RINGTONE_ID, RINGTONES } from "shared/ringtones";
Expand Down Expand Up @@ -207,5 +208,25 @@ export const createSettingsRouter = () => {

return { success: true };
}),

getNavigationStyle: publicProcedure.query(() => {
const row = getSettings();
return row.navigationStyle ?? DEFAULT_NAVIGATION_STYLE;
}),

setNavigationStyle: publicProcedure
.input(z.object({ style: z.enum(["top-bar", "sidebar"]) }))
.mutation(({ input }) => {
localDb
.insert(settings)
.values({ id: 1, navigationStyle: input.style })
.onConflictDoUpdate({
target: settings.id,
set: { navigationStyle: input.style },
})
.run();

return { success: true };
}),
});
};
117 changes: 117 additions & 0 deletions apps/desktop/src/renderer/hooks/useWorkspaceShortcuts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useHotkeys } from "react-hotkeys-hook";
import { trpc } from "renderer/lib/trpc";
import {
useCreateBranchWorkspace,
useSetActiveWorkspace,
} from "renderer/react-query/workspaces";
import { HOTKEYS } from "shared/hotkeys";

/**
* Shared hook for workspace keyboard shortcuts and auto-creation logic.
* This hook should be used in both:
* - WorkspacesTabs (top-bar mode)
* - WorkspaceSidebar (sidebar mode)
*
* It handles:
* - ⌘1-9 workspace switching shortcuts
* - Previous/next workspace shortcuts
* - Auto-create main workspace for new projects
*/
export function useWorkspaceShortcuts() {
const { data: groups = [] } = trpc.workspaces.getAllGrouped.useQuery();
const { data: activeWorkspace } = trpc.workspaces.getActive.useQuery();
const activeWorkspaceId = activeWorkspace?.id || null;
const setActiveWorkspace = useSetActiveWorkspace();
const createBranchWorkspace = useCreateBranchWorkspace();

// Track projects we've attempted to create workspaces for (persists across renders)
const attemptedProjectsRef = useRef<Set<string>>(new Set());
const [isCreating, setIsCreating] = useState(false);

// Auto-create main workspace for new projects (one-time per project)
useEffect(() => {
if (isCreating) return;

for (const group of groups) {
const projectId = group.project.id;
const hasMainWorkspace = group.workspaces.some(
(w) => w.type === "branch",
);

// Skip if already has main workspace or we've already attempted this project
if (hasMainWorkspace || attemptedProjectsRef.current.has(projectId)) {
continue;
}

// Mark as attempted before creating (prevents retries)
attemptedProjectsRef.current.add(projectId);
setIsCreating(true);

// Auto-create fails silently - this is a background convenience feature
createBranchWorkspace.mutate(
{ projectId },
{
onSettled: () => {
setIsCreating(false);
},
},
);
// Only create one at a time
break;
}
}, [groups, isCreating, createBranchWorkspace]);

// Flatten workspaces for keyboard navigation
const allWorkspaces = groups.flatMap((group) => group.workspaces);

// Workspace switching shortcuts (⌘+1-9)
const workspaceKeys = Array.from(
{ length: 9 },
(_, i) => `meta+${i + 1}`,
).join(", ");

const handleWorkspaceSwitch = useCallback(
(event: KeyboardEvent) => {
const num = Number(event.key);
if (num >= 1 && num <= 9) {
const workspace = allWorkspaces[num - 1];
if (workspace) {
setActiveWorkspace.mutate({ id: workspace.id });
}
}
},
[allWorkspaces, setActiveWorkspace],
);

const handlePrevWorkspace = useCallback(() => {
if (!activeWorkspaceId) return;
const currentIndex = allWorkspaces.findIndex(
(w) => w.id === activeWorkspaceId,
);
if (currentIndex > 0) {
setActiveWorkspace.mutate({ id: allWorkspaces[currentIndex - 1].id });
}
}, [activeWorkspaceId, allWorkspaces, setActiveWorkspace]);

const handleNextWorkspace = useCallback(() => {
if (!activeWorkspaceId) return;
const currentIndex = allWorkspaces.findIndex(
(w) => w.id === activeWorkspaceId,
);
if (currentIndex < allWorkspaces.length - 1) {
setActiveWorkspace.mutate({ id: allWorkspaces[currentIndex + 1].id });
}
}, [activeWorkspaceId, allWorkspaces, setActiveWorkspace]);

useHotkeys(workspaceKeys, handleWorkspaceSwitch);
useHotkeys(HOTKEYS.PREV_WORKSPACE.keys, handlePrevWorkspace);
useHotkeys(HOTKEYS.NEXT_WORKSPACE.keys, handleNextWorkspace);

return {
groups,
allWorkspaces,
activeWorkspaceId,
setActiveWorkspace,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,33 +10,52 @@ import {
import { Switch } from "@superset/ui/switch";
import { trpc } from "renderer/lib/trpc";

type NavigationStyle = "top-bar" | "sidebar";

export function BehaviorSettings() {
const utils = trpc.useUtils();
const { data: confirmOnQuit, isLoading } =

// Confirm on quit setting
const { data: confirmOnQuit, isLoading: isConfirmLoading } =
trpc.settings.getConfirmOnQuit.useQuery();
const setConfirmOnQuit = trpc.settings.setConfirmOnQuit.useMutation({
onMutate: async ({ enabled }) => {
// Cancel outgoing fetches
await utils.settings.getConfirmOnQuit.cancel();
// Snapshot previous value
const previous = utils.settings.getConfirmOnQuit.getData();
// Optimistically update
utils.settings.getConfirmOnQuit.setData(undefined, enabled);
return { previous };
},
onError: (_err, _vars, context) => {
// Rollback on error
if (context?.previous !== undefined) {
utils.settings.getConfirmOnQuit.setData(undefined, context.previous);
}
},
onSettled: () => {
// Refetch to ensure sync with server
utils.settings.getConfirmOnQuit.invalidate();
},
});

const handleToggle = (enabled: boolean) => {
// Navigation style setting
const { data: navigationStyle, isLoading: isNavLoading } =
trpc.settings.getNavigationStyle.useQuery();
const setNavigationStyle = trpc.settings.setNavigationStyle.useMutation({
onMutate: async ({ style }) => {
await utils.settings.getNavigationStyle.cancel();
const previous = utils.settings.getNavigationStyle.getData();
utils.settings.getNavigationStyle.setData(undefined, style);
return { previous };
},
onError: (_err, _vars, context) => {
if (context?.previous !== undefined) {
utils.settings.getNavigationStyle.setData(undefined, context.previous);
}
},
onSettled: () => {
utils.settings.getNavigationStyle.invalidate();
},
});

const handleConfirmToggle = (enabled: boolean) => {
setConfirmOnQuit.mutate({ enabled });
};

Expand Down Expand Up @@ -71,6 +90,10 @@ export function BehaviorSettings() {
});
};

const handleNavigationStyleChange = (style: NavigationStyle) => {
setNavigationStyle.mutate({ style });
};

return (
<div className="p-6 max-w-4xl w-full">
<div className="mb-8">
Expand All @@ -81,6 +104,32 @@ export function BehaviorSettings() {
</div>

<div className="space-y-6">
{/* Navigation Style */}
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="navigation-style" className="text-sm font-medium">
Navigation style
</Label>
<p className="text-xs text-muted-foreground">
Choose how workspaces are displayed
</p>
</div>
<Select
value={navigationStyle ?? "top-bar"}
onValueChange={handleNavigationStyleChange}
disabled={isNavLoading || setNavigationStyle.isPending}
>
<SelectTrigger id="navigation-style" className="w-[140px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="top-bar">Top bar</SelectItem>
<SelectItem value="sidebar">Sidebar</SelectItem>
</SelectContent>
</Select>
</div>

{/* Confirm on Quit */}
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="confirm-on-quit" className="text-sm font-medium">
Expand All @@ -93,8 +142,8 @@ export function BehaviorSettings() {
<Switch
id="confirm-on-quit"
checked={confirmOnQuit ?? true}
onCheckedChange={handleToggle}
disabled={isLoading || setConfirmOnQuit.isPending}
onCheckedChange={handleConfirmToggle}
disabled={isConfirmLoading || setConfirmOnQuit.isPending}
/>
</div>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Button } from "@superset/ui/button";
import { Kbd, KbdGroup } from "@superset/ui/kbd";
import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip";
import { LuPanelLeft, LuPanelLeftClose } from "react-icons/lu";
import { useWorkspaceSidebarStore } from "renderer/stores";
import { HOTKEYS } from "shared/hotkeys";

export function WorkspaceSidebarControl() {
const { isOpen, toggleOpen } = useWorkspaceSidebarStore();

return (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={toggleOpen}
aria-label="Toggle workspace sidebar"
className="no-drag"
>
{isOpen ? (
<LuPanelLeftClose className="size-4" />
) : (
<LuPanelLeft className="size-4" />
)}
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" showArrow={false}>
<span className="flex items-center gap-2">
Toggle Workspaces
<KbdGroup>
{HOTKEYS.TOGGLE_WORKSPACE_SIDEBAR.display.map((key) => (
<Kbd key={key}>{key}</Kbd>
))}
</KbdGroup>
</span>
</TooltipContent>
</Tooltip>
);
}
Loading