Skip to content
Merged
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
78 changes: 76 additions & 2 deletions apps/desktop/src/lib/trpc/routers/workspaces/procedures/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,86 @@ export const createQueryProcedures = () => {
return router({
get: publicProcedure
.input(z.object({ id: z.string() }))
.query(({ input }) => {
.query(async ({ input }) => {
const workspace = getWorkspace(input.id);
if (!workspace) {
throw new Error(`Workspace ${input.id} not found`);
}
return workspace;

const project = localDb
.select()
.from(projects)
.where(eq(projects.id, workspace.projectId))
.get();
const worktree = workspace.worktreeId
? localDb
.select()
.from(worktrees)
.where(eq(worktrees.id, workspace.worktreeId))
.get()
: null;

// Detect and persist base branch for existing worktrees that don't have it
// We use undefined to mean "not yet attempted" and null to mean "attempted but not found"
let baseBranch = worktree?.baseBranch;
if (worktree && baseBranch === undefined && project) {
// Only attempt detection if there's a remote origin
const hasRemote = await hasOriginRemote(project.mainRepoPath);
if (hasRemote) {
try {
const defaultBranch = project.defaultBranch || "main";
const detected = await detectBaseBranch(
worktree.path,
worktree.branch,
defaultBranch,
);
if (detected) {
baseBranch = detected;
}
// Persist the result (detected branch or null sentinel)
localDb
.update(worktrees)
.set({ baseBranch: detected ?? null })
.where(eq(worktrees.id, worktree.id))
.run();
} catch {
// Detection failed, persist null to avoid retrying
localDb
.update(worktrees)
.set({ baseBranch: null })
.where(eq(worktrees.id, worktree.id))
.run();
}
} else {
// No remote - persist null to avoid retrying
localDb
.update(worktrees)
.set({ baseBranch: null })
.where(eq(worktrees.id, worktree.id))
.run();
}
}
Comment on lines +35 to +74
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Extract duplicated baseBranch detection logic.

This entire block (Lines 35-74) is duplicated nearly verbatim in getActive (Lines 232-271). Extract to a shared helper to improve maintainability:

♻️ Proposed refactor
// In ../utils/git.ts or a new helper file
async function detectAndPersistBaseBranch(params: {
  worktree: { id: string; path: string; branch: string; baseBranch?: string | null };
  project: { mainRepoPath: string; defaultBranch?: string | null } | null;
}): Promise<string | null | undefined> {
  const { worktree, project } = params;
  
  if (worktree.baseBranch !== undefined || !project) {
    return worktree.baseBranch;
  }
  
  const hasRemote = await hasOriginRemote(project.mainRepoPath);
  let baseBranch: string | null = null;
  
  if (hasRemote) {
    try {
      const defaultBranch = project.defaultBranch || "main";
      const detected = await detectBaseBranch(worktree.path, worktree.branch, defaultBranch);
      baseBranch = detected ?? null;
    } catch {
      baseBranch = null;
    }
  }
  
  localDb
    .update(worktrees)
    .set({ baseBranch })
    .where(eq(worktrees.id, worktree.id))
    .run();
    
  return baseBranch;
}
🤖 Prompt for AI Agents
In @apps/desktop/src/lib/trpc/routers/workspaces/procedures/query.ts around
lines 35 - 74, The duplicated baseBranch detection/persistence logic should be
extracted into a shared helper (e.g., detectAndPersistBaseBranch) and called
from both locations (the current block in query.ts and the getActive function)
to avoid duplication; implement a helper (in ../utils/git.ts or a new helper
file) that accepts the worktree and project objects, returns worktree.baseBranch
if already defined, otherwise runs hasOriginRemote(project.mainRepoPath), calls
detectBaseBranch(worktree.path, worktree.branch, project.defaultBranch ||
"main") when remote exists, converts undefined detection to null, persists the
result via localDb.update(worktrees).set({ baseBranch }).where(eq(worktrees.id,
worktree.id)).run(), and returns the final baseBranch (string | null |
undefined), then replace the duplicated blocks with calls to
detectAndPersistBaseBranch and use its return value.


return {
...workspace,
type: workspace.type as "worktree" | "branch",
worktreePath: getWorkspacePath(workspace) ?? "",
project: project
? {
id: project.id,
name: project.name,
mainRepoPath: project.mainRepoPath,
}
: null,
worktree: worktree
? {
branch: worktree.branch,
baseBranch,
// Normalize to null to ensure consistent "incomplete init" detection in UI
gitStatus: worktree.gitStatus ?? null,
}
: null,
};
}),

getAll: publicProcedure.query(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,69 @@
import { Avatar } from "@superset/ui/atoms/Avatar";
import { Button } from "@superset/ui/button";
import { Skeleton } from "@superset/ui/skeleton";
import { toast } from "@superset/ui/sonner";
import { createFileRoute } from "@tanstack/react-router";
import { trpc } from "renderer/lib/trpc";

export const Route = createFileRoute("/_authenticated/settings/account/")({
component: AccountSettingsPage,
});

function AccountSettingsPage() {
const { data: user, isLoading } = trpc.user.me.useQuery();
const signOutMutation = trpc.auth.signOut.useMutation({
onSuccess: () => toast.success("Signed out"),
});

const signOut = () => signOutMutation.mutate();
Comment on lines +14 to +18
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add error handling for sign-out mutation.

The sign-out mutation only handles onSuccess. If the sign-out fails (e.g., network error), the user won't receive any feedback.

🐛 Proposed fix
 const signOutMutation = trpc.auth.signOut.useMutation({
   onSuccess: () => toast.success("Signed out"),
+  onError: (error) => toast.error(`Sign out failed: ${error.message}`),
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const signOutMutation = trpc.auth.signOut.useMutation({
onSuccess: () => toast.success("Signed out"),
});
const signOut = () => signOutMutation.mutate();
const signOutMutation = trpc.auth.signOut.useMutation({
onSuccess: () => toast.success("Signed out"),
onError: (error) => toast.error(`Sign out failed: ${error.message}`),
});
const signOut = () => signOutMutation.mutate();
🤖 Prompt for AI Agents
In @apps/desktop/src/renderer/routes/_authenticated/settings/account/page.tsx
around lines 14 - 18, The sign-out mutation only handles success; update the
trpc.auth.signOut.useMutation call (signOutMutation) to include an onError
handler that surfaces failures (e.g., toast.error with the error message or a
generic "Sign out failed") and optionally logs the error, and ensure the signOut
wrapper (signOut function) still calls signOutMutation.mutate() so failures
trigger the new onError path; reference trpc.auth.signOut.useMutation,
signOutMutation, and signOut when applying the change.


return (
<div>
<h2>Account Settings</h2>
<p>Account settings placeholder</p>
<div className="p-6 max-w-4xl">
<div className="mb-8">
<h2 className="text-xl font-semibold">Account</h2>
<p className="text-sm text-muted-foreground mt-1">
Manage your account settings
</p>
</div>

<div className="space-y-8">
{/* Profile Section */}
<div>
<h3 className="text-sm font-medium mb-4">Profile</h3>
<div className="flex items-center gap-4 p-4 rounded-lg border bg-card">
{isLoading ? (
<>
<Skeleton className="h-16 w-16 rounded-full" />
<div className="space-y-2">
<Skeleton className="h-5 w-32" />
<Skeleton className="h-4 w-48" />
</div>
</>
) : user ? (
<>
<Avatar size="xl" fullName={user.name} image={user.image} />
<div>
<p className="font-medium text-lg">{user.name}</p>
<p className="text-sm text-muted-foreground">{user.email}</p>
</div>
</>
) : (
<p className="text-muted-foreground">Unable to load user info</p>
)}
</div>
</div>

{/* Sign Out Section */}
<div className="pt-6 border-t">
<h3 className="text-sm font-medium mb-2">Sign Out</h3>
<p className="text-sm text-muted-foreground mb-4">
Sign out of your Superset account on this device.
</p>
<Button variant="outline" onClick={() => signOut()}>
Sign Out
</Button>
</div>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ThemeCard } from "./ThemeCard";
Original file line number Diff line number Diff line change
@@ -1,14 +1,91 @@
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@superset/ui/select";
import { createFileRoute } from "@tanstack/react-router";
import {
type MarkdownStyle,
useMarkdownStyle,
useSetMarkdownStyle,
useSetTheme,
useThemeId,
useThemeStore,
} from "renderer/stores";
import { builtInThemes } from "shared/themes";
import { ThemeCard } from "./components/ThemeCard";

export const Route = createFileRoute("/_authenticated/settings/appearance/")({
component: AppearanceSettingsPage,
});

function AppearanceSettingsPage() {
const activeThemeId = useThemeId();
const setTheme = useSetTheme();
const customThemes = useThemeStore((state) => state.customThemes);
const markdownStyle = useMarkdownStyle();
const setMarkdownStyle = useSetMarkdownStyle();

const allThemes = [...builtInThemes, ...customThemes];

return (
<div>
<h2>Appearance Settings</h2>
<p>Appearance settings placeholder</p>
<div className="p-6 max-w-4xl">
<div className="mb-8">
<h2 className="text-xl font-semibold">Appearance</h2>
<p className="text-sm text-muted-foreground mt-1">
Customize how Superset looks on your device
</p>
</div>

<div className="space-y-8">
{/* Theme Section */}
<div>
<h3 className="text-sm font-medium mb-4">Theme</h3>
<div className="grid grid-cols-2 lg:grid-cols-3 gap-4">
{allThemes.map((theme) => (
<ThemeCard
key={theme.id}
theme={theme}
isSelected={activeThemeId === theme.id}
onSelect={() => setTheme(theme.id)}
/>
))}
</div>
</div>

<div className="pt-6 border-t">
<h3 className="text-sm font-medium mb-2">Markdown Style</h3>
<p className="text-sm text-muted-foreground mb-4">
Rendering style for markdown files when viewing rendered content
</p>
<Select
value={markdownStyle}
onValueChange={(value) => setMarkdownStyle(value as MarkdownStyle)}
>
<SelectTrigger className="w-[200px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="default">Default</SelectItem>
<SelectItem value="tufte">Tufte</SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground mt-2">
Tufte style uses elegant serif typography inspired by Edward Tufte's
books
</p>
</div>

<div className="pt-6 border-t">
<h3 className="text-sm font-medium mb-2">Custom Themes</h3>
<p className="text-sm text-muted-foreground">
Custom theme import coming soon. You'll be able to import JSON theme
files to create your own themes.
</p>
</div>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,135 @@
import type { TerminalLinkBehavior } from "@superset/local-db";
import { Label } from "@superset/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@superset/ui/select";
import { Switch } from "@superset/ui/switch";
import { createFileRoute } from "@tanstack/react-router";
import { trpc } from "renderer/lib/trpc";

export const Route = createFileRoute("/_authenticated/settings/behavior/")({
component: BehaviorSettingsPage,
});

function BehaviorSettingsPage() {
const utils = trpc.useUtils();

// Confirm on quit setting
const { data: confirmOnQuit, isLoading: isConfirmLoading } =
trpc.settings.getConfirmOnQuit.useQuery();
const setConfirmOnQuit = trpc.settings.setConfirmOnQuit.useMutation({
onMutate: async ({ enabled }) => {
await utils.settings.getConfirmOnQuit.cancel();
const previous = utils.settings.getConfirmOnQuit.getData();
utils.settings.getConfirmOnQuit.setData(undefined, enabled);
return { previous };
},
onError: (_err, _vars, context) => {
if (context?.previous !== undefined) {
utils.settings.getConfirmOnQuit.setData(undefined, context.previous);
}
},
onSettled: () => {
utils.settings.getConfirmOnQuit.invalidate();
},
});

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

// Terminal link behavior setting
const { data: terminalLinkBehavior, isLoading: isLoadingLinkBehavior } =
trpc.settings.getTerminalLinkBehavior.useQuery();

const setTerminalLinkBehavior =
trpc.settings.setTerminalLinkBehavior.useMutation({
onMutate: async ({ behavior }) => {
await utils.settings.getTerminalLinkBehavior.cancel();
const previous = utils.settings.getTerminalLinkBehavior.getData();
utils.settings.getTerminalLinkBehavior.setData(undefined, behavior);
return { previous };
},
onError: (_err, _vars, context) => {
if (context?.previous !== undefined) {
utils.settings.getTerminalLinkBehavior.setData(
undefined,
context.previous,
);
}
},
onSettled: () => {
utils.settings.getTerminalLinkBehavior.invalidate();
},
});

const handleLinkBehaviorChange = (value: string) => {
setTerminalLinkBehavior.mutate({
behavior: value as TerminalLinkBehavior,
});
};

return (
<div>
<h2>Behavior Settings</h2>
<p>Behavior settings placeholder</p>
<div className="p-6 max-w-4xl w-full">
<div className="mb-8">
<h2 className="text-xl font-semibold">Behavior</h2>
<p className="text-sm text-muted-foreground mt-1">
Configure app behavior and preferences
</p>
</div>

<div className="space-y-6">
{/* 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">
Confirm before quitting
</Label>
<p className="text-xs text-muted-foreground">
Show a confirmation dialog when quitting the app
</p>
</div>
<Switch
id="confirm-on-quit"
checked={confirmOnQuit ?? true}
onCheckedChange={handleConfirmToggle}
disabled={isConfirmLoading || setConfirmOnQuit.isPending}
/>
</div>

<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label
htmlFor="terminal-link-behavior"
className="text-sm font-medium"
>
Terminal file links
</Label>
<p className="text-xs text-muted-foreground">
Choose how to open file paths when Cmd+clicking in the terminal
</p>
</div>
<Select
value={terminalLinkBehavior ?? "external-editor"}
onValueChange={handleLinkBehaviorChange}
disabled={
isLoadingLinkBehavior || setTerminalLinkBehavior.isPending
}
>
<SelectTrigger className="w-[180px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="external-editor">External editor</SelectItem>
<SelectItem value="file-viewer">File viewer</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
);
}
Loading
Loading