From 90be7fa17c7f318d27e6f6127dc0b4e39cb58d51 Mon Sep 17 00:00:00 2001 From: AviPeltz Date: Thu, 27 Nov 2025 00:29:58 -0800 Subject: [PATCH 1/8] update themes and cards --- .../src/lib/trpc/routers/projects/projects.ts | 72 +++++++- .../main/components/StartView/ActionCard.tsx | 39 +++++ .../components/StartView/CloneRepoDialog.tsx | 102 ++++++++++++ .../main/components/StartView/StartTopBar.tsx | 28 ++++ .../main/components/StartView/index.tsx | 156 ++++++++++++++++++ .../src/renderer/screens/main/index.tsx | 21 ++- .../src/shared/themes/built-in/dark.ts | 4 +- .../src/shared/themes/built-in/ember.ts | 97 +++++++++++ .../src/shared/themes/built-in/index.ts | 4 +- .../src/shared/themes/built-in/light.ts | 6 +- .../src/shared/themes/built-in/monokai.ts | 2 +- .../src/shared/themes/built-in/one-dark.ts | 4 +- 12 files changed, 522 insertions(+), 13 deletions(-) create mode 100644 apps/desktop/src/renderer/screens/main/components/StartView/ActionCard.tsx create mode 100644 apps/desktop/src/renderer/screens/main/components/StartView/CloneRepoDialog.tsx create mode 100644 apps/desktop/src/renderer/screens/main/components/StartView/StartTopBar.tsx create mode 100644 apps/desktop/src/renderer/screens/main/components/StartView/index.tsx create mode 100644 apps/desktop/src/shared/themes/built-in/ember.ts diff --git a/apps/desktop/src/lib/trpc/routers/projects/projects.ts b/apps/desktop/src/lib/trpc/routers/projects/projects.ts index 45c7ac98e53..a7905de763e 100644 --- a/apps/desktop/src/lib/trpc/routers/projects/projects.ts +++ b/apps/desktop/src/lib/trpc/routers/projects/projects.ts @@ -1,4 +1,5 @@ -import { basename } from "node:path"; +import { basename, join } from "node:path"; +import simpleGit from "simple-git"; import type { BrowserWindow } from "electron"; import { dialog } from "electron"; import { db } from "main/lib/db"; @@ -75,6 +76,75 @@ export const createProjectsRouter = (window: BrowserWindow) => { }; }), + cloneRepo: publicProcedure + .input( + z.object({ + url: z.string().url(), + targetDirectory: z.string().optional(), + }), + ) + .mutation(async ({ input }) => { + try { + let targetDir = input.targetDirectory; + + if (!targetDir) { + const result = await dialog.showOpenDialog(window, { + properties: ["openDirectory", "createDirectory"], + title: "Select Clone Destination", + }); + + if (result.canceled || result.filePaths.length === 0) { + return { success: false as const, error: "No directory selected" }; + } + + targetDir = result.filePaths[0]; + } + + const repoName = input.url + .split("/") + .pop() + ?.replace(/\.git$/, ""); + if (!repoName) { + return { + success: false as const, + error: "Invalid repository URL", + }; + } + + const clonePath = join(targetDir, repoName); + + const git = simpleGit(); + await git.clone(input.url, clonePath); + + const name = basename(clonePath); + const project: Project = { + id: nanoid(), + mainRepoPath: clonePath, + name, + color: assignRandomColor(), + tabOrder: null, + lastOpenedAt: Date.now(), + createdAt: Date.now(), + }; + + await db.update((data) => { + data.projects.push(project); + }); + + return { + success: true as const, + project, + }; + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + return { + success: false as const, + error: `Failed to clone repository: ${errorMessage}`, + }; + } + }), + update: publicProcedure .input( z.object({ diff --git a/apps/desktop/src/renderer/screens/main/components/StartView/ActionCard.tsx b/apps/desktop/src/renderer/screens/main/components/StartView/ActionCard.tsx new file mode 100644 index 00000000000..0510eb65dc5 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/StartView/ActionCard.tsx @@ -0,0 +1,39 @@ +import type { LucideIcon } from "lucide-react"; + +interface ActionCardProps { + icon: LucideIcon; + label: string; + onClick?: () => void; + disabled?: boolean; + isLoading?: boolean; +} + +export function ActionCard({ + icon: Icon, + label, + onClick, + disabled = false, + isLoading = false, +}: ActionCardProps) { + return ( + + ); +} diff --git a/apps/desktop/src/renderer/screens/main/components/StartView/CloneRepoDialog.tsx b/apps/desktop/src/renderer/screens/main/components/StartView/CloneRepoDialog.tsx new file mode 100644 index 00000000000..1491a285439 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/StartView/CloneRepoDialog.tsx @@ -0,0 +1,102 @@ +import { useState } from "react"; +import { trpc } from "renderer/lib/trpc"; +import { useCreateWorkspace } from "renderer/react-query/workspaces"; + +interface CloneRepoDialogProps { + isOpen: boolean; + onClose: () => void; + onError: (error: string) => void; +} + +export function CloneRepoDialog({ + isOpen, + onClose, + onError, +}: CloneRepoDialogProps) { + const [url, setUrl] = useState(""); + const cloneRepo = trpc.projects.cloneRepo.useMutation(); + const createWorkspace = useCreateWorkspace(); + + const handleClone = async () => { + if (!url.trim()) { + onError("Please enter a repository URL"); + return; + } + + cloneRepo.mutate( + { url: url.trim() }, + { + onSuccess: (result) => { + if (result.success && result.project) { + createWorkspace.mutate({ projectId: result.project.id }); + onClose(); + setUrl(""); + } else if (!result.success && result.error) { + onError(result.error); + } + }, + onError: (err) => { + onError(err.message || "Failed to clone repository"); + }, + }, + ); + }; + + if (!isOpen) return null; + + const isLoading = cloneRepo.isPending || createWorkspace.isPending; + + return ( +
+
+

+ Clone Repository +

+ +
+
+ + setUrl(e.target.value)} + placeholder="https://github.com/user/repo.git" + className="w-full px-3 py-2.5 bg-background border border-border rounded-md text-foreground placeholder:text-muted-foreground/50 focus:outline-none focus:border-ring transition-colors" + disabled={isLoading} + onKeyDown={(e) => { + if (e.key === "Enter" && !isLoading) { + handleClone(); + } + }} + /> +
+ +
+ + +
+
+
+
+ ); +} diff --git a/apps/desktop/src/renderer/screens/main/components/StartView/StartTopBar.tsx b/apps/desktop/src/renderer/screens/main/components/StartView/StartTopBar.tsx new file mode 100644 index 00000000000..40e863b15a0 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/StartView/StartTopBar.tsx @@ -0,0 +1,28 @@ +import { trpc } from "renderer/lib/trpc"; +import { SettingsButton } from "../TopBar/SettingsButton"; +import { WindowControls } from "../TopBar/WindowControls"; + +export function StartTopBar() { + const { data: platform } = trpc.window.getPlatform.useQuery(); + const isMac = platform === "darwin"; + + return ( +
+
+ {/* Empty space on left for symmetry */} +
+
+ {/* Empty middle section - no tabs */} +
+
+ + {!isMac && } +
+
+ ); +} diff --git a/apps/desktop/src/renderer/screens/main/components/StartView/index.tsx b/apps/desktop/src/renderer/screens/main/components/StartView/index.tsx new file mode 100644 index 00000000000..2b2aab05fa8 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/StartView/index.tsx @@ -0,0 +1,156 @@ +import { FolderGit, FolderOpen } from "lucide-react"; +import { useState } from "react"; +import { trpc } from "renderer/lib/trpc"; +import { useOpenNew } from "renderer/react-query/projects"; +import { useCreateWorkspace } from "renderer/react-query/workspaces"; +import { ActionCard } from "./ActionCard"; +import { CloneRepoDialog } from "./CloneRepoDialog"; +import { StartTopBar } from "./StartTopBar"; + +export function StartView() { + const { data: recentProjects = [] } = trpc.projects.getRecents.useQuery(); + const openNew = useOpenNew(); + const createWorkspace = useCreateWorkspace(); + const [error, setError] = useState(null); + const [isCloneDialogOpen, setIsCloneDialogOpen] = useState(false); + + const handleOpenProject = () => { + setError(null); + openNew.mutate(undefined, { + onSuccess: (result) => { + if (result.success && result.project) { + createWorkspace.mutate({ projectId: result.project.id }); + } else if (!result.success && result.error) { + setError(result.error); + } + }, + onError: (err) => { + setError(err.message || "Failed to open project"); + }, + }); + }; + + const handleOpenRecentProject = (projectId: string) => { + setError(null); + createWorkspace.mutate( + { projectId }, + { + onError: (err) => { + setError(err.message || "Failed to create workspace"); + }, + }, + ); + }; + + const displayedProjects = recentProjects.slice(0, 5); + const isLoading = openNew.isPending || createWorkspace.isPending; + + return ( +
+ +
+
+ {/* Logo */} +
+ + Superset + + +
+ + {/* Error Display */} + {error && ( +
+

{error}

+
+ )} + + {/* Action Cards and Recent Projects Container */} +
+ {/* Action Cards */} +
+ + + { + setError(null); + setIsCloneDialogOpen(true); + }} + isLoading={isLoading} + /> +
+ + {/* Recent Projects */} + {displayedProjects.length > 0 && ( +
+
+
+
+
+ Recent projects +
+
+ {recentProjects.length > 5 && ( +
+
+ View all ({recentProjects.length}) +
+
+ )} +
+ + {displayedProjects.map((project) => ( + + ))} +
+
+ )} +
+
+
+ + {/* Dialogs */} + setIsCloneDialogOpen(false)} + onError={setError} + /> +
+ ); +} diff --git a/apps/desktop/src/renderer/screens/main/index.tsx b/apps/desktop/src/renderer/screens/main/index.tsx index 46c38ff384b..754deaa2af2 100644 --- a/apps/desktop/src/renderer/screens/main/index.tsx +++ b/apps/desktop/src/renderer/screens/main/index.tsx @@ -12,6 +12,7 @@ import { dragDropManager } from "../../lib/dnd"; import { AppFrame } from "./components/AppFrame"; import { Background } from "./components/Background"; import { SettingsView } from "./components/SettingsView"; +import { StartView } from "./components/StartView"; import { TopBar } from "./components/TopBar"; import { WorkspaceView } from "./components/WorkspaceView"; @@ -58,15 +59,29 @@ export function MainScreen() { [activeWorkspaceId, splitTabHorizontal, isWorkspaceView], ); + // Show start screen when no active workspace and not in settings + if (!activeWorkspace && currentView !== "settings") { + return ; + } + + // Determine which content view to show + const renderContent = () => { + if (currentView === "settings") { + return ; + } + if (!activeWorkspace) { + return ; + } + return ; + }; + return (
-
- {currentView === "settings" ? : } -
+
{renderContent()}
diff --git a/apps/desktop/src/shared/themes/built-in/dark.ts b/apps/desktop/src/shared/themes/built-in/dark.ts index ee29a4648f0..e7ca2764008 100644 --- a/apps/desktop/src/shared/themes/built-in/dark.ts +++ b/apps/desktop/src/shared/themes/built-in/dark.ts @@ -13,9 +13,9 @@ export const darkTheme: Theme = { ui: { background: "oklch(0.145 0 0)", foreground: "oklch(0.985 0 0)", - card: "oklch(0.145 0 0)", + card: "oklch(0.205 0 0)", cardForeground: "oklch(0.985 0 0)", - popover: "oklch(0.145 0 0)", + popover: "oklch(0.205 0 0)", popoverForeground: "oklch(0.985 0 0)", primary: "oklch(0.985 0 0)", primaryForeground: "oklch(0.205 0 0)", diff --git a/apps/desktop/src/shared/themes/built-in/ember.ts b/apps/desktop/src/shared/themes/built-in/ember.ts new file mode 100644 index 00000000000..122606a631f --- /dev/null +++ b/apps/desktop/src/shared/themes/built-in/ember.ts @@ -0,0 +1,97 @@ +import type { Theme } from "../types"; + +/** + * Ember theme - Warm dark theme inspired by the Figma start screen design + * Features a warm, slightly reddish dark background (#151110) + */ +export const emberTheme: Theme = { + id: "ember", + name: "Ember", + author: "Superset", + type: "dark", + isBuiltIn: true, + + ui: { + // Core - warm dark tones + background: "#151110", + foreground: "#eae8e6", + card: "#201E1C", + cardForeground: "#eae8e6", + popover: "#201E1C", + popoverForeground: "#eae8e6", + + // Primary - light foreground for contrast + primary: "#eae8e6", + primaryForeground: "#151110", + + // Secondary - warm grays + secondary: "#2a2827", + secondaryForeground: "#eae8e6", + + // Muted - subtle warm grays + muted: "#2a2827", + mutedForeground: "#a8a5a3", + + // Accent - warm highlight + accent: "#2a2827", + accentForeground: "#eae8e6", + + // Tertiary - panel backgrounds + tertiary: "#1a1716", + tertiaryActive: "#252220", + + // Destructive - warm red + destructive: "#cc4444", + destructiveForeground: "#ffcccc", + + // Borders - subtle warm gray + border: "#2a2827", + input: "#2a2827", + ring: "#3a3837", + + // Sidebar - slightly lighter than background + sidebar: "#1a1716", + sidebarForeground: "#eae8e6", + sidebarPrimary: "#e07850", + sidebarPrimaryForeground: "#151110", + sidebarAccent: "#252220", + sidebarAccentForeground: "#eae8e6", + sidebarBorder: "#2a2827", + sidebarRing: "#3a3837", + + // Charts - warm palette + chart1: "#e07850", + chart2: "#50a878", + chart3: "#d4a84b", + chart4: "#7b68ee", + chart5: "#dc6b6b", + }, + + terminal: { + background: "#151110", + foreground: "#eae8e6", + cursor: "#e07850", + cursorAccent: "#151110", + selectionBackground: "rgba(224, 120, 80, 0.25)", + + // Standard ANSI colors - warm tinted + black: "#151110", + red: "#dc6b6b", + green: "#7ec699", + yellow: "#e5c07b", + blue: "#61afef", + magenta: "#c678dd", + cyan: "#56b6c2", + white: "#eae8e6", + + // Bright ANSI colors + brightBlack: "#5c5856", + brightRed: "#e88888", + brightGreen: "#98d1a8", + brightYellow: "#ecd08f", + brightBlue: "#7ec0f5", + brightMagenta: "#d494e6", + brightCyan: "#73c7d3", + brightWhite: "#ffffff", + }, +}; diff --git a/apps/desktop/src/shared/themes/built-in/index.ts b/apps/desktop/src/shared/themes/built-in/index.ts index 0e67cdc5811..e2772389d19 100644 --- a/apps/desktop/src/shared/themes/built-in/index.ts +++ b/apps/desktop/src/shared/themes/built-in/index.ts @@ -1,5 +1,6 @@ import type { Theme } from "../types"; import { darkTheme } from "./dark"; +import { emberTheme } from "./ember"; import { lightTheme } from "./light"; import { monokaiTheme } from "./monokai"; import { oneDarkTheme } from "./one-dark"; @@ -10,6 +11,7 @@ import { oneDarkTheme } from "./one-dark"; export const builtInThemes: Theme[] = [ darkTheme, lightTheme, + emberTheme, monokaiTheme, oneDarkTheme, ]; @@ -27,4 +29,4 @@ export function getBuiltInTheme(id: string): Theme | undefined { } // Re-export individual themes -export { darkTheme, lightTheme, monokaiTheme, oneDarkTheme }; +export { darkTheme, emberTheme, lightTheme, monokaiTheme, oneDarkTheme }; diff --git a/apps/desktop/src/shared/themes/built-in/light.ts b/apps/desktop/src/shared/themes/built-in/light.ts index 00c4b879207..0c1144de911 100644 --- a/apps/desktop/src/shared/themes/built-in/light.ts +++ b/apps/desktop/src/shared/themes/built-in/light.ts @@ -13,9 +13,9 @@ export const lightTheme: Theme = { ui: { background: "oklch(1 0 0)", foreground: "oklch(0.145 0 0)", - card: "oklch(1 0 0)", + card: "oklch(0.97 0 0)", cardForeground: "oklch(0.145 0 0)", - popover: "oklch(1 0 0)", + popover: "oklch(0.97 0 0)", popoverForeground: "oklch(0.145 0 0)", primary: "oklch(0.205 0 0)", primaryForeground: "oklch(0.985 0 0)", @@ -23,7 +23,7 @@ export const lightTheme: Theme = { secondaryForeground: "oklch(0.205 0 0)", muted: "oklch(0.97 0 0)", mutedForeground: "oklch(0.556 0 0)", - accent: "oklch(0.97 0 0)", + accent: "oklch(0.93 0 0)", accentForeground: "oklch(0.205 0 0)", tertiary: "oklch(0.95 0.003 40)", tertiaryActive: "oklch(0.90 0.003 40)", diff --git a/apps/desktop/src/shared/themes/built-in/monokai.ts b/apps/desktop/src/shared/themes/built-in/monokai.ts index 40eb0a8bbc7..a3f2a284933 100644 --- a/apps/desktop/src/shared/themes/built-in/monokai.ts +++ b/apps/desktop/src/shared/themes/built-in/monokai.ts @@ -14,7 +14,7 @@ export const monokaiTheme: Theme = { ui: { background: "#272822", foreground: "#f8f8f2", - card: "#272822", + card: "#3e3d32", cardForeground: "#f8f8f2", popover: "#3e3d32", popoverForeground: "#f8f8f2", diff --git a/apps/desktop/src/shared/themes/built-in/one-dark.ts b/apps/desktop/src/shared/themes/built-in/one-dark.ts index 815685ec43c..7173e890e3a 100644 --- a/apps/desktop/src/shared/themes/built-in/one-dark.ts +++ b/apps/desktop/src/shared/themes/built-in/one-dark.ts @@ -14,9 +14,9 @@ export const oneDarkTheme: Theme = { ui: { background: "#282c34", foreground: "#abb2bf", - card: "#282c34", + card: "#2c313c", cardForeground: "#abb2bf", - popover: "#21252b", + popover: "#2c313c", popoverForeground: "#abb2bf", primary: "#61afef", primaryForeground: "#282c34", From 46cadf320b29642186e370336ee0c4375da48e34 Mon Sep 17 00:00:00 2001 From: AviPeltz Date: Sun, 30 Nov 2025 17:11:08 -0800 Subject: [PATCH 2/8] start screen start --- apps/desktop/package.json | 1 + .../src/lib/trpc/routers/projects/projects.ts | 7 +- .../KeyboardShortcutsSettings.tsx | 139 ++++++++++++++++++ .../SettingsView/SettingsContent.tsx | 6 +- .../SettingsView/SettingsSidebar.tsx | 14 +- .../main/components/SettingsView/index.tsx | 11 +- .../main/components/StartView/index.tsx | 6 +- .../TopBar/SettingsButton/SettingsButton.tsx | 2 +- .../src/renderer/screens/main/index.tsx | 31 ++-- apps/desktop/src/renderer/stores/app-state.ts | 21 ++- apps/desktop/src/shared/hotkeys.ts | 4 +- bun.lock | 3 + 12 files changed, 206 insertions(+), 39 deletions(-) create mode 100644 apps/desktop/src/renderer/screens/main/components/SettingsView/KeyboardShortcutsSettings.tsx diff --git a/apps/desktop/package.json b/apps/desktop/package.json index f64eb142d6f..10f550ccfe0 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -64,6 +64,7 @@ "line-column-path": "^3.0.0", "lodash": "^4.17.21", "lowdb": "^7.0.1", + "lucide-react": "^0.555.0", "nanoid": "^5.1.6", "node-pty": "1.1.0-beta30", "react": "^19.1.1", diff --git a/apps/desktop/src/lib/trpc/routers/projects/projects.ts b/apps/desktop/src/lib/trpc/routers/projects/projects.ts index 5fd9f2c42db..bb8bb726e2e 100644 --- a/apps/desktop/src/lib/trpc/routers/projects/projects.ts +++ b/apps/desktop/src/lib/trpc/routers/projects/projects.ts @@ -1,11 +1,11 @@ import { basename, join } from "node:path"; -import simpleGit from "simple-git"; import type { BrowserWindow } from "electron"; import { dialog } from "electron"; import { db } from "main/lib/db"; import type { Project } from "main/lib/db/schemas"; import { nanoid } from "nanoid"; import { PROJECT_COLOR_VALUES } from "shared/constants/project-colors"; +import simpleGit from "simple-git"; import { z } from "zod"; import { publicProcedure, router } from "../.."; import { getGitRoot } from "../workspaces/utils/git"; @@ -92,7 +92,10 @@ export const createProjectsRouter = (window: BrowserWindow) => { }); if (result.canceled || result.filePaths.length === 0) { - return { success: false as const, error: "No directory selected" }; + return { + success: false as const, + error: "No directory selected", + }; } targetDir = result.filePaths[0]; diff --git a/apps/desktop/src/renderer/screens/main/components/SettingsView/KeyboardShortcutsSettings.tsx b/apps/desktop/src/renderer/screens/main/components/SettingsView/KeyboardShortcutsSettings.tsx new file mode 100644 index 00000000000..4cf561279fd --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/SettingsView/KeyboardShortcutsSettings.tsx @@ -0,0 +1,139 @@ +import { Input } from "@superset/ui/input"; +import { Kbd, KbdGroup } from "@superset/ui/kbd"; +import { useState } from "react"; +import { HiMagnifyingGlass } from "react-icons/hi2"; +import { + formatKeysForDisplay, + getHotkeysByCategory, + type HotkeyCategory, + type HotkeyDefinition, +} from "shared/hotkeys"; + +const CATEGORY_ORDER: HotkeyCategory[] = [ + "Workspace", + "Terminal", + "Layout", + "Window", + "Help", +]; + +function HotkeyRow({ + hotkey, + isEven, +}: { + hotkey: HotkeyDefinition; + isEven: boolean; +}) { + const keys = formatKeysForDisplay(hotkey.keys); + + return ( +
+ {hotkey.label} + + {keys.map((key) => ( + {key} + ))} + +
+ ); +} + +/** + * Consolidate individual workspace jump shortcuts (1-9) into a single entry + */ +function consolidateWorkspaceJumps( + hotkeys: HotkeyDefinition[], +): HotkeyDefinition[] { + const workspaceJumpPattern = /^Switch to Workspace \d$/; + const hasWorkspaceJumps = hotkeys.some((h) => + workspaceJumpPattern.test(h.label), + ); + + if (!hasWorkspaceJumps) return hotkeys; + + const filtered = hotkeys.filter((h) => !workspaceJumpPattern.test(h.label)); + filtered.unshift({ + keys: "meta+1-9", + label: "Switch to Workspace 1-9", + category: "Workspace", + }); + + return filtered; +} + +export function KeyboardShortcutsSettings() { + const [searchQuery, setSearchQuery] = useState(""); + const hotkeysByCategory = getHotkeysByCategory(); + + // Flatten and consolidate all hotkeys + const allHotkeys = CATEGORY_ORDER.flatMap((category) => + consolidateWorkspaceJumps(hotkeysByCategory[category]), + ); + + // Filter based on search query + const filteredHotkeys = searchQuery + ? allHotkeys.filter((hotkey) => + hotkey.label.toLowerCase().includes(searchQuery.toLowerCase()), + ) + : allHotkeys; + + return ( +
+ {/* Header */} +
+

Keyboard Shortcuts

+

+ View all available keyboard shortcuts. Press{" "} + + ? to open this page anytime. +

+
+ + {/* Search */} +
+ + setSearchQuery(e.target.value)} + className="pl-9 bg-accent/30 border-transparent focus:border-accent" + /> +
+ + {/* Table */} +
+ {/* Table Header */} +
+ + Command + + + Shortcut + +
+ + {/* Table Body */} +
+ {filteredHotkeys.length > 0 ? ( + filteredHotkeys.map((hotkey, index) => ( + + )) + ) : ( +
+ No shortcuts found matching "{searchQuery}" +
+ )} +
+
+
+ ); +} diff --git a/apps/desktop/src/renderer/screens/main/components/SettingsView/SettingsContent.tsx b/apps/desktop/src/renderer/screens/main/components/SettingsView/SettingsContent.tsx index a3d4e4f4700..812e0b84eeb 100644 --- a/apps/desktop/src/renderer/screens/main/components/SettingsView/SettingsContent.tsx +++ b/apps/desktop/src/renderer/screens/main/components/SettingsView/SettingsContent.tsx @@ -1,5 +1,6 @@ +import type { SettingsSection } from "renderer/stores"; import { AppearanceSettings } from "./AppearanceSettings"; -import type { SettingsSection } from "./index"; +import { KeyboardShortcutsSettings } from "./KeyboardShortcutsSettings"; interface SettingsContentProps { activeSection: SettingsSection; @@ -7,8 +8,9 @@ interface SettingsContentProps { export function SettingsContent({ activeSection }: SettingsContentProps) { return ( -
+
{activeSection === "appearance" && } + {activeSection === "keyboard" && }
); } diff --git a/apps/desktop/src/renderer/screens/main/components/SettingsView/SettingsSidebar.tsx b/apps/desktop/src/renderer/screens/main/components/SettingsView/SettingsSidebar.tsx index b660e168f78..0bde8d42e11 100644 --- a/apps/desktop/src/renderer/screens/main/components/SettingsView/SettingsSidebar.tsx +++ b/apps/desktop/src/renderer/screens/main/components/SettingsView/SettingsSidebar.tsx @@ -1,7 +1,10 @@ import { cn } from "@superset/ui/utils"; -import { HiArrowLeft, HiOutlinePaintBrush } from "react-icons/hi2"; -import { useCloseSettings } from "renderer/stores"; -import type { SettingsSection } from "./index"; +import { + HiArrowLeft, + HiOutlineCommandLine, + HiOutlinePaintBrush, +} from "react-icons/hi2"; +import { type SettingsSection, useCloseSettings } from "renderer/stores"; interface SettingsSidebarProps { activeSection: SettingsSection; @@ -18,6 +21,11 @@ const SECTIONS: { label: "Appearance", icon: , }, + { + id: "keyboard", + label: "Keyboard Shortcuts", + icon: , + }, ]; export function SettingsSidebar({ diff --git a/apps/desktop/src/renderer/screens/main/components/SettingsView/index.tsx b/apps/desktop/src/renderer/screens/main/components/SettingsView/index.tsx index 1e176c03f44..69f02869f33 100644 --- a/apps/desktop/src/renderer/screens/main/components/SettingsView/index.tsx +++ b/apps/desktop/src/renderer/screens/main/components/SettingsView/index.tsx @@ -1,12 +1,13 @@ -import { useState } from "react"; +import { + useSetSettingsSection, + useSettingsSection, +} from "renderer/stores/app-state"; import { SettingsContent } from "./SettingsContent"; import { SettingsSidebar } from "./SettingsSidebar"; -export type SettingsSection = "appearance"; - export function SettingsView() { - const [activeSection, setActiveSection] = - useState("appearance"); + const activeSection = useSettingsSection(); + const setActiveSection = useSetSettingsSection(); return (
diff --git a/apps/desktop/src/renderer/screens/main/components/StartView/index.tsx b/apps/desktop/src/renderer/screens/main/components/StartView/index.tsx index 2b2aab05fa8..7d08a849d99 100644 --- a/apps/desktop/src/renderer/screens/main/components/StartView/index.tsx +++ b/apps/desktop/src/renderer/screens/main/components/StartView/index.tsx @@ -18,10 +18,8 @@ export function StartView() { setError(null); openNew.mutate(undefined, { onSuccess: (result) => { - if (result.success && result.project) { + if (!result.canceled && result.project) { createWorkspace.mutate({ projectId: result.project.id }); - } else if (!result.success && result.error) { - setError(result.error); } }, onError: (err) => { @@ -46,7 +44,7 @@ export function StartView() { const isLoading = openNew.isPending || createWorkspace.isPending; return ( -
+
diff --git a/apps/desktop/src/renderer/screens/main/components/TopBar/SettingsButton/SettingsButton.tsx b/apps/desktop/src/renderer/screens/main/components/TopBar/SettingsButton/SettingsButton.tsx index 86265ae163b..d3dbacd3ff4 100644 --- a/apps/desktop/src/renderer/screens/main/components/TopBar/SettingsButton/SettingsButton.tsx +++ b/apps/desktop/src/renderer/screens/main/components/TopBar/SettingsButton/SettingsButton.tsx @@ -7,7 +7,7 @@ export function SettingsButton() { return ( +
+ + + ); + } + return ( From 33c9816c8b4d609c680f964ab25a63bbeb8fb53f Mon Sep 17 00:00:00 2001 From: AviPeltz Date: Sun, 30 Nov 2025 17:30:04 -0800 Subject: [PATCH 4/8] fix: refetch and up'ed failure count to 5 --- apps/desktop/src/renderer/screens/main/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/renderer/screens/main/index.tsx b/apps/desktop/src/renderer/screens/main/index.tsx index cc90ca94a9d..9bf8c70fdb5 100644 --- a/apps/desktop/src/renderer/screens/main/index.tsx +++ b/apps/desktop/src/renderer/screens/main/index.tsx @@ -94,8 +94,9 @@ export function MainScreen() { } // Show error state with retry option + // Note: failureCount resets automatically on successful query if (isError) { - const hasRepeatedFailures = failureCount >= 3; + const hasRepeatedFailures = failureCount >= 5; const handleRetry = async () => { setIsRetrying(true); From 5aafcd7ee61561ae7b5a972bb85ffcca17f7c0f9 Mon Sep 17 00:00:00 2001 From: AviPeltz Date: Sun, 30 Nov 2025 18:41:22 -0800 Subject: [PATCH 5/8] fix: dedupe and cancel clone repo stuff --- .../src/lib/trpc/routers/projects/projects.ts | 41 +++++++++++++++++-- .../components/StartView/CloneRepoDialog.tsx | 13 +++++- .../main/components/StartView/index.tsx | 20 +++++++-- 3 files changed, 65 insertions(+), 9 deletions(-) diff --git a/apps/desktop/src/lib/trpc/routers/projects/projects.ts b/apps/desktop/src/lib/trpc/routers/projects/projects.ts index bb8bb726e2e..155dad71e97 100644 --- a/apps/desktop/src/lib/trpc/routers/projects/projects.ts +++ b/apps/desktop/src/lib/trpc/routers/projects/projects.ts @@ -1,3 +1,4 @@ +import { existsSync } from "node:fs"; import { basename, join } from "node:path"; import type { BrowserWindow } from "electron"; import { dialog } from "electron"; @@ -91,11 +92,9 @@ export const createProjectsRouter = (window: BrowserWindow) => { title: "Select Clone Destination", }); + // User canceled - return canceled state (not an error) if (result.canceled || result.filePaths.length === 0) { - return { - success: false as const, - error: "No directory selected", - }; + return { canceled: true as const, success: false as const }; } targetDir = result.filePaths[0]; @@ -107,6 +106,7 @@ export const createProjectsRouter = (window: BrowserWindow) => { ?.replace(/\.git$/, ""); if (!repoName) { return { + canceled: false as const, success: false as const, error: "Invalid repository URL", }; @@ -114,9 +114,40 @@ export const createProjectsRouter = (window: BrowserWindow) => { const clonePath = join(targetDir, repoName); + // Check if we already have a project for this path + const existingProject = db.data.projects.find( + (p) => p.mainRepoPath === clonePath, + ); + + if (existingProject) { + // Update lastOpenedAt and return existing project + await db.update((data) => { + const p = data.projects.find((p) => p.id === existingProject.id); + if (p) { + p.lastOpenedAt = Date.now(); + } + }); + return { + canceled: false as const, + success: true as const, + project: existingProject, + }; + } + + // Check if target directory already exists (but not our project) + if (existsSync(clonePath)) { + return { + canceled: false as const, + success: false as const, + error: `A folder named "${repoName}" already exists at this location. Please choose a different destination.`, + }; + } + + // Clone the repository const git = simpleGit(); await git.clone(input.url, clonePath); + // Create new project const name = basename(clonePath); const project: Project = { id: nanoid(), @@ -133,6 +164,7 @@ export const createProjectsRouter = (window: BrowserWindow) => { }); return { + canceled: false as const, success: true as const, project, }; @@ -140,6 +172,7 @@ export const createProjectsRouter = (window: BrowserWindow) => { const errorMessage = error instanceof Error ? error.message : String(error); return { + canceled: false as const, success: false as const, error: `Failed to clone repository: ${errorMessage}`, }; diff --git a/apps/desktop/src/renderer/screens/main/components/StartView/CloneRepoDialog.tsx b/apps/desktop/src/renderer/screens/main/components/StartView/CloneRepoDialog.tsx index 1491a285439..2c0deffab63 100644 --- a/apps/desktop/src/renderer/screens/main/components/StartView/CloneRepoDialog.tsx +++ b/apps/desktop/src/renderer/screens/main/components/StartView/CloneRepoDialog.tsx @@ -14,6 +14,7 @@ export function CloneRepoDialog({ onError, }: CloneRepoDialogProps) { const [url, setUrl] = useState(""); + const utils = trpc.useUtils(); const cloneRepo = trpc.projects.cloneRepo.useMutation(); const createWorkspace = useCreateWorkspace(); @@ -27,12 +28,20 @@ export function CloneRepoDialog({ { url: url.trim() }, { onSuccess: (result) => { + // User canceled the directory picker - silent no-op + if (result.canceled) { + return; + } + if (result.success && result.project) { + // Invalidate recents so the new/updated project appears + utils.projects.getRecents.invalidate(); createWorkspace.mutate({ projectId: result.project.id }); onClose(); setUrl(""); - } else if (!result.success && result.error) { - onError(result.error); + } else if (!result.success) { + // Show user-friendly error message + onError(result.error ?? "Failed to clone repository"); } }, onError: (err) => { diff --git a/apps/desktop/src/renderer/screens/main/components/StartView/index.tsx b/apps/desktop/src/renderer/screens/main/components/StartView/index.tsx index 7d08a849d99..ef213ab5893 100644 --- a/apps/desktop/src/renderer/screens/main/components/StartView/index.tsx +++ b/apps/desktop/src/renderer/screens/main/components/StartView/index.tsx @@ -1,5 +1,6 @@ -import { FolderGit, FolderOpen } from "lucide-react"; +import { FolderGit, FolderOpen, X } from "lucide-react"; import { useState } from "react"; +import { HiExclamationTriangle } from "react-icons/hi2"; import { trpc } from "renderer/lib/trpc"; import { useOpenNew } from "renderer/react-query/projects"; import { useCreateWorkspace } from "renderer/react-query/workspaces"; @@ -70,8 +71,21 @@ export function StartView() { {/* Error Display */} {error && ( -
-

{error}

+
+
+
+ +
+

{error}

+ +
)} From 42178733d95d2e6c6cbfebef2d5110df101a9a32 Mon Sep 17 00:00:00 2001 From: AviPeltz Date: Sun, 30 Nov 2025 19:08:16 -0800 Subject: [PATCH 6/8] update view all and path name stuff --- .../main/components/StartView/index.tsx | 131 +++++++++++++----- packages/ui/src/components/tooltip.tsx | 23 ++- 2 files changed, 114 insertions(+), 40 deletions(-) diff --git a/apps/desktop/src/renderer/screens/main/components/StartView/index.tsx b/apps/desktop/src/renderer/screens/main/components/StartView/index.tsx index ef213ab5893..3d6cbb1b442 100644 --- a/apps/desktop/src/renderer/screens/main/components/StartView/index.tsx +++ b/apps/desktop/src/renderer/screens/main/components/StartView/index.tsx @@ -1,4 +1,5 @@ -import { FolderGit, FolderOpen, X } from "lucide-react"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; +import { ChevronUp, FolderGit, FolderOpen, X } from "lucide-react"; import { useState } from "react"; import { HiExclamationTriangle } from "react-icons/hi2"; import { trpc } from "renderer/lib/trpc"; @@ -8,12 +9,30 @@ import { ActionCard } from "./ActionCard"; import { CloneRepoDialog } from "./CloneRepoDialog"; import { StartTopBar } from "./StartTopBar"; +function formatPath( + path: string, + projectName: string, +): { display: string; full: string } { + // Replace home directory patterns with ~ + const fullPath = path.replace(/^\/(?:Users|home)\/[^/]+/, "~"); + + // Remove trailing project name directory if it matches + const suffix = `/${projectName}`; + const displayPath = fullPath.endsWith(suffix) + ? fullPath.slice(0, -suffix.length) + : fullPath; + + return { display: displayPath, full: fullPath }; +} + export function StartView() { const { data: recentProjects = [] } = trpc.projects.getRecents.useQuery(); const openNew = useOpenNew(); const createWorkspace = useCreateWorkspace(); const [error, setError] = useState(null); const [isCloneDialogOpen, setIsCloneDialogOpen] = useState(false); + const [showAllProjects, setShowAllProjects] = useState(false); + const [visibleCount, setVisibleCount] = useState(50); const handleOpenProject = () => { setError(null); @@ -41,7 +60,11 @@ export function StartView() { ); }; - const displayedProjects = recentProjects.slice(0, 5); + const hasMoreProjects = recentProjects.length > 5; + const displayedProjects = showAllProjects + ? recentProjects.slice(0, visibleCount) + : recentProjects.slice(0, 5); + const hasMoreToLoad = showAllProjects && recentProjects.length > visibleCount; const isLoading = openNew.isPending || createWorkspace.isPending; return ( @@ -113,43 +136,79 @@ export function StartView() { {/* Recent Projects */} {displayedProjects.length > 0 && ( -
-
-
-
-
- Recent projects -
-
- {recentProjects.length > 5 && ( -
-
- View all ({recentProjects.length}) -
-
+
+
+
+ + Recent projects + + {hasMoreProjects && ( + )}
- {displayedProjects.map((project) => ( - - ))} +
+ {displayedProjects.map((project) => { + const pathInfo = formatPath( + project.mainRepoPath, + project.name, + ); + return ( + + ); + })} + + {hasMoreToLoad && ( + + )} +
)} diff --git a/packages/ui/src/components/tooltip.tsx b/packages/ui/src/components/tooltip.tsx index f032c0b83ae..2000312c2be 100644 --- a/packages/ui/src/components/tooltip.tsx +++ b/packages/ui/src/components/tooltip.tsx @@ -17,10 +17,13 @@ function TooltipProvider({ } function Tooltip({ + delayDuration, ...props -}: React.ComponentProps) { +}: React.ComponentProps & { + delayDuration?: number; +}) { return ( - + ); @@ -36,8 +39,13 @@ function TooltipContent({ className, sideOffset = 0, children, + showArrow = true, + arrowClassName, ...props -}: React.ComponentProps) { +}: React.ComponentProps & { + showArrow?: boolean; + arrowClassName?: string; +}) { return ( {children} - + {showArrow && ( + + )} ); From dbf8ae555d4d9e7c6aa9e9128cd2b5ba012a08ee Mon Sep 17 00:00:00 2001 From: AviPeltz Date: Sun, 30 Nov 2025 19:40:27 -0800 Subject: [PATCH 7/8] added min width and some fixes --- .../src/lib/trpc/routers/projects/projects.ts | 121 +++++++++++++++--- apps/desktop/src/lib/trpc/routers/window.ts | 5 + apps/desktop/src/main/windows/main.ts | 2 + .../KeyboardShortcutsSettings.tsx | 14 +- .../main/components/StartView/StartTopBar.tsx | 7 +- .../main/components/StartView/index.tsx | 40 +++++- 6 files changed, 160 insertions(+), 29 deletions(-) diff --git a/apps/desktop/src/lib/trpc/routers/projects/projects.ts b/apps/desktop/src/lib/trpc/routers/projects/projects.ts index 155dad71e97..ca60519ef2e 100644 --- a/apps/desktop/src/lib/trpc/routers/projects/projects.ts +++ b/apps/desktop/src/lib/trpc/routers/projects/projects.ts @@ -1,4 +1,5 @@ import { existsSync } from "node:fs"; +import { access } from "node:fs/promises"; import { basename, join } from "node:path"; import type { BrowserWindow } from "electron"; import { dialog } from "electron"; @@ -12,6 +13,73 @@ import { publicProcedure, router } from "../.."; import { getGitRoot } from "../workspaces/utils/git"; import { assignRandomColor } from "./utils/colors"; +// Safe filename regex: letters, numbers, dots, underscores, hyphens, spaces, and common unicode +// Allows most valid Git repo names while avoiding path traversal characters +const SAFE_REPO_NAME_REGEX = /^[a-zA-Z0-9._\- ]+$/; + +/** + * Extracts and validates a repository name from a git URL. + * Handles HTTP/HTTPS URLs, SSH-style URLs (git@host:user/repo), and edge cases. + */ +function extractRepoName(urlInput: string): string | null { + // Normalize: trim whitespace and strip trailing slashes + let normalized = urlInput.trim().replace(/\/+$/, ""); + + if (!normalized) return null; + + let repoSegment: string | undefined; + + // Try parsing as HTTP/HTTPS URL first + try { + const parsed = new URL(normalized); + if (parsed.protocol === "http:" || parsed.protocol === "https:") { + // Get pathname and strip query/hash (URL constructor handles this) + const pathname = parsed.pathname; + // Get the last segment of the path + repoSegment = pathname.split("/").filter(Boolean).pop(); + } + } catch { + // Not a valid URL, try SSH-style parsing + } + + // Fallback to SSH-style parsing (git@github.com:user/repo.git) + if (!repoSegment) { + // Handle SSH format: git@host:path or just path segments + const colonIndex = normalized.indexOf(":"); + if (colonIndex !== -1 && !normalized.includes("://")) { + // SSH-style: take everything after the colon + normalized = normalized.slice(colonIndex + 1); + } + // Split by '/' and get the last segment + repoSegment = normalized.split("/").filter(Boolean).pop(); + } + + if (!repoSegment) return null; + + // Strip query string and hash if present (for edge cases) + repoSegment = repoSegment.split("?")[0].split("#")[0]; + + // Remove trailing .git extension + repoSegment = repoSegment.replace(/\.git$/, ""); + + // Decode percent-encoded characters + try { + repoSegment = decodeURIComponent(repoSegment); + } catch { + // Invalid encoding, continue with raw value + } + + // Trim any remaining whitespace or special characters at boundaries + repoSegment = repoSegment.trim(); + + // Validate against safe filename regex + if (!repoSegment || !SAFE_REPO_NAME_REGEX.test(repoSegment)) { + return null; + } + + return repoSegment; +} + export const createProjectsRouter = (window: BrowserWindow) => { return router({ getRecents: publicProcedure.query((): Project[] => { @@ -79,7 +147,12 @@ export const createProjectsRouter = (window: BrowserWindow) => { .input( z.object({ url: z.string().url(), - targetDirectory: z.string().optional(), + // Trim and convert empty/whitespace strings to undefined + targetDirectory: z + .string() + .trim() + .optional() + .transform((v) => (v && v.length > 0 ? v : undefined)), }), ) .mutation(async ({ input }) => { @@ -100,10 +173,7 @@ export const createProjectsRouter = (window: BrowserWindow) => { targetDir = result.filePaths[0]; } - const repoName = input.url - .split("/") - .pop() - ?.replace(/\.git$/, ""); + const repoName = extractRepoName(input.url); if (!repoName) { return { canceled: false as const, @@ -120,18 +190,35 @@ export const createProjectsRouter = (window: BrowserWindow) => { ); if (existingProject) { - // Update lastOpenedAt and return existing project - await db.update((data) => { - const p = data.projects.find((p) => p.id === existingProject.id); - if (p) { - p.lastOpenedAt = Date.now(); - } - }); - return { - canceled: false as const, - success: true as const, - project: existingProject, - }; + // Verify the filesystem path still exists + try { + await access(clonePath); + // Directory exists - update lastOpenedAt and return existing project + await db.update((data) => { + const p = data.projects.find( + (p) => p.id === existingProject.id, + ); + if (p) { + p.lastOpenedAt = Date.now(); + } + }); + return { + canceled: false as const, + success: true as const, + project: existingProject, + }; + } catch { + // Directory is missing - remove the stale project record and continue with clone + await db.update((data) => { + const index = data.projects.findIndex( + (p) => p.id === existingProject.id, + ); + if (index !== -1) { + data.projects.splice(index, 1); + } + }); + // Continue to normal creation flow below + } } // Check if target directory already exists (but not our project) diff --git a/apps/desktop/src/lib/trpc/routers/window.ts b/apps/desktop/src/lib/trpc/routers/window.ts index dc19ec73965..2c4d4fe5ee2 100644 --- a/apps/desktop/src/lib/trpc/routers/window.ts +++ b/apps/desktop/src/lib/trpc/routers/window.ts @@ -1,3 +1,4 @@ +import { homedir } from "node:os"; import type { BrowserWindow } from "electron"; import { publicProcedure, router } from ".."; @@ -33,6 +34,10 @@ export const createWindowRouter = (window: BrowserWindow) => { getPlatform: publicProcedure.query(() => { return process.platform; }), + + getHomeDir: publicProcedure.query(() => { + return homedir(); + }), }); }; diff --git a/apps/desktop/src/main/windows/main.ts b/apps/desktop/src/main/windows/main.ts index 867494fdc06..43c760ab6ec 100644 --- a/apps/desktop/src/main/windows/main.ts +++ b/apps/desktop/src/main/windows/main.ts @@ -20,6 +20,8 @@ export async function MainWindow() { title: productName, width, height, + minWidth: 400, + minHeight: 400, show: false, center: true, movable: true, diff --git a/apps/desktop/src/renderer/screens/main/components/SettingsView/KeyboardShortcutsSettings.tsx b/apps/desktop/src/renderer/screens/main/components/SettingsView/KeyboardShortcutsSettings.tsx index 4cf561279fd..fc9fa2c6168 100644 --- a/apps/desktop/src/renderer/screens/main/components/SettingsView/KeyboardShortcutsSettings.tsx +++ b/apps/desktop/src/renderer/screens/main/components/SettingsView/KeyboardShortcutsSettings.tsx @@ -1,6 +1,6 @@ import { Input } from "@superset/ui/input"; import { Kbd, KbdGroup } from "@superset/ui/kbd"; -import { useState } from "react"; +import { useMemo, useState } from "react"; import { HiMagnifyingGlass } from "react-icons/hi2"; import { formatKeysForDisplay, @@ -9,6 +9,14 @@ import { type HotkeyDefinition, } from "shared/hotkeys"; +function useIsMac(): boolean { + return useMemo(() => { + const platform = navigator.platform?.toUpperCase() ?? ""; + const userAgent = navigator.userAgent?.toUpperCase() ?? ""; + return platform.includes("MAC") || userAgent.includes("MAC"); + }, []); +} + const CATEGORY_ORDER: HotkeyCategory[] = [ "Workspace", "Terminal", @@ -68,6 +76,8 @@ function consolidateWorkspaceJumps( export function KeyboardShortcutsSettings() { const [searchQuery, setSearchQuery] = useState(""); const hotkeysByCategory = getHotkeysByCategory(); + const isMac = useIsMac(); + const modifierKey = isMac ? "⌘" : "Ctrl"; // Flatten and consolidate all hotkeys const allHotkeys = CATEGORY_ORDER.flatMap((category) => @@ -88,7 +98,7 @@ export function KeyboardShortcutsSettings() {

Keyboard Shortcuts

View all available keyboard shortcuts. Press{" "} - + {modifierKey} ? to open this page anytime.

diff --git a/apps/desktop/src/renderer/screens/main/components/StartView/StartTopBar.tsx b/apps/desktop/src/renderer/screens/main/components/StartView/StartTopBar.tsx index 40e863b15a0..140c1ad564c 100644 --- a/apps/desktop/src/renderer/screens/main/components/StartView/StartTopBar.tsx +++ b/apps/desktop/src/renderer/screens/main/components/StartView/StartTopBar.tsx @@ -3,8 +3,9 @@ import { SettingsButton } from "../TopBar/SettingsButton"; import { WindowControls } from "../TopBar/WindowControls"; export function StartTopBar() { - const { data: platform } = trpc.window.getPlatform.useQuery(); - const isMac = platform === "darwin"; + const { data: platform, isLoading } = trpc.window.getPlatform.useQuery(); + const isMac = !isLoading && platform === "darwin"; + const showWindowControls = !isLoading && !isMac; return (
@@ -21,7 +22,7 @@ export function StartTopBar() {
- {!isMac && } + {showWindowControls && }
); diff --git a/apps/desktop/src/renderer/screens/main/components/StartView/index.tsx b/apps/desktop/src/renderer/screens/main/components/StartView/index.tsx index 3d6cbb1b442..79cb91d63af 100644 --- a/apps/desktop/src/renderer/screens/main/components/StartView/index.tsx +++ b/apps/desktop/src/renderer/screens/main/components/StartView/index.tsx @@ -9,24 +9,49 @@ import { ActionCard } from "./ActionCard"; import { CloneRepoDialog } from "./CloneRepoDialog"; import { StartTopBar } from "./StartTopBar"; +/** + * Normalizes path separators to forward slashes for consistent handling + */ +function normalizeSeparators(path: string): string { + return path.replace(/\\/g, "/"); +} + +/** + * Formats a path for display, replacing the home directory with ~ and optionally + * removing the trailing project name directory. + * Handles both Unix and Windows paths. + */ function formatPath( path: string, projectName: string, + homeDir: string | undefined, ): { display: string; full: string } { - // Replace home directory patterns with ~ - const fullPath = path.replace(/^\/(?:Users|home)\/[^/]+/, "~"); + // Normalize both path and homeDir to use forward slashes + const normalizedPath = normalizeSeparators(path); + const normalizedHome = homeDir ? normalizeSeparators(homeDir) : null; + + // Replace home directory with ~ if we know the home dir + let fullPath = normalizedPath; + if (normalizedHome && normalizedPath.startsWith(normalizedHome)) { + fullPath = "~" + normalizedPath.slice(normalizedHome.length); + } else { + // Fallback: try common Unix patterns if home dir not available + fullPath = normalizedPath.replace(/^\/(?:Users|home)\/[^/]+/, "~"); + } + + // Escape special regex characters in project name + const escapedProjectName = projectName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const suffixPattern = new RegExp(`/${escapedProjectName}$`); // Remove trailing project name directory if it matches - const suffix = `/${projectName}`; - const displayPath = fullPath.endsWith(suffix) - ? fullPath.slice(0, -suffix.length) - : fullPath; + const displayPath = fullPath.replace(suffixPattern, ""); return { display: displayPath, full: fullPath }; } export function StartView() { const { data: recentProjects = [] } = trpc.projects.getRecents.useQuery(); + const { data: homeDir } = trpc.window.getHomeDir.useQuery(); const openNew = useOpenNew(); const createWorkspace = useCreateWorkspace(); const [error, setError] = useState(null); @@ -115,7 +140,7 @@ export function StartView() { {/* Action Cards and Recent Projects Container */}
{/* Action Cards */} -
+
Date: Sun, 30 Nov 2025 19:42:15 -0800 Subject: [PATCH 8/8] fix: lint error --- .../src/renderer/screens/main/components/StartView/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/src/renderer/screens/main/components/StartView/index.tsx b/apps/desktop/src/renderer/screens/main/components/StartView/index.tsx index 79cb91d63af..d5ea4d96271 100644 --- a/apps/desktop/src/renderer/screens/main/components/StartView/index.tsx +++ b/apps/desktop/src/renderer/screens/main/components/StartView/index.tsx @@ -33,7 +33,7 @@ function formatPath( // Replace home directory with ~ if we know the home dir let fullPath = normalizedPath; if (normalizedHome && normalizedPath.startsWith(normalizedHome)) { - fullPath = "~" + normalizedPath.slice(normalizedHome.length); + fullPath = `~${normalizedPath.slice(normalizedHome.length)}`; } else { // Fallback: try common Unix patterns if home dir not available fullPath = normalizedPath.replace(/^\/(?:Users|home)\/[^/]+/, "~");