diff --git a/.env.example b/.env.example
index 862e3b59a7b..74557ad21f0 100644
--- a/.env.example
+++ b/.env.example
@@ -1,2 +1,3 @@
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/superset
VITE_DEV_SERVER_PORT=4927
+WEBSITE_URL=http://localhost:3001
diff --git a/.gitignore b/.gitignore
index a4df714cf5e..28cdb69a6ab 100644
--- a/.gitignore
+++ b/.gitignore
@@ -55,7 +55,7 @@ mise.toml
# Superset
# Ignore .superset directory except for config and scripts
.superset/*
-!.superset/setup.json
+!.superset/config.json
!.superset/setup.sh
!.superset/teardown.sh
diff --git a/.superset/setup.json b/.superset/config.json
similarity index 100%
rename from .superset/setup.json
rename to .superset/config.json
diff --git a/README.md b/README.md
index 47f32f22379..446b59e9871 100644
--- a/README.md
+++ b/README.md
@@ -50,7 +50,7 @@ open apps/desktop/release
### Usage
-For each parallel tasks, Superset uses git worktrees to clone a new branch on your machine. Automate copying env variables, installing dependencies, etc. through a setup script (`./superset/setup.json`)
+For each parallel tasks, Superset uses git worktrees to clone a new branch on your machine. Automate copying env variables, installing dependencies, etc. through a config file (`.superset/config.json`)

diff --git a/apps/desktop/src/lib/trpc/routers/config/config.ts b/apps/desktop/src/lib/trpc/routers/config/config.ts
new file mode 100644
index 00000000000..b52eb914cb8
--- /dev/null
+++ b/apps/desktop/src/lib/trpc/routers/config/config.ts
@@ -0,0 +1,83 @@
+import { existsSync, mkdirSync, writeFileSync } from "node:fs";
+import { join } from "node:path";
+import { db } from "main/lib/db";
+import { z } from "zod";
+import { publicProcedure, router } from "../..";
+
+function configExists(mainRepoPath: string): boolean {
+ const configPath = join(mainRepoPath, ".superset", "config.json");
+ return existsSync(configPath);
+}
+
+const CONFIG_TEMPLATE = `{
+ "setup": [],
+ "teardown": []
+}
+`;
+
+function getConfigPath(mainRepoPath: string): string {
+ return join(mainRepoPath, ".superset", "config.json");
+}
+
+function ensureConfigExists(mainRepoPath: string): string {
+ const configPath = getConfigPath(mainRepoPath);
+ const supersetDir = join(mainRepoPath, ".superset");
+
+ if (!existsSync(configPath)) {
+ // Create .superset directory if it doesn't exist
+ if (!existsSync(supersetDir)) {
+ mkdirSync(supersetDir, { recursive: true });
+ }
+ // Create config.json with template
+ writeFileSync(configPath, CONFIG_TEMPLATE, "utf-8");
+ }
+
+ return configPath;
+}
+
+export const createConfigRouter = () => {
+ return router({
+ // Check if we should show the config toast for a project
+ shouldShowConfigToast: publicProcedure
+ .input(z.object({ projectId: z.string() }))
+ .query(({ input }) => {
+ const project = db.data.projects.find((p) => p.id === input.projectId);
+ if (!project) {
+ return false;
+ }
+
+ // Don't show if already dismissed or if config exists
+ if (project.configToastDismissed) {
+ return false;
+ }
+
+ return !configExists(project.mainRepoPath);
+ }),
+
+ // Mark the config toast as dismissed for a project
+ dismissConfigToast: publicProcedure
+ .input(z.object({ projectId: z.string() }))
+ .mutation(async ({ input }) => {
+ await db.update((data) => {
+ const project = data.projects.find((p) => p.id === input.projectId);
+ if (project) {
+ project.configToastDismissed = true;
+ }
+ });
+ return { success: true };
+ }),
+
+ // Get the config file path (creates it if it doesn't exist)
+ getConfigFilePath: publicProcedure
+ .input(z.object({ projectId: z.string() }))
+ .query(({ input }) => {
+ const project = db.data.projects.find((p) => p.id === input.projectId);
+ if (!project) {
+ return null;
+ }
+ return ensureConfigExists(project.mainRepoPath);
+ }),
+ });
+};
+
+export type ConfigRouter = ReturnType
;
diff --git a/apps/desktop/src/lib/trpc/routers/config/index.ts b/apps/desktop/src/lib/trpc/routers/config/index.ts
new file mode 100644
index 00000000000..eca16092786
--- /dev/null
+++ b/apps/desktop/src/lib/trpc/routers/config/index.ts
@@ -0,0 +1,2 @@
+export type { ConfigRouter } from "./config";
+export { createConfigRouter } from "./config";
diff --git a/apps/desktop/src/lib/trpc/routers/index.ts b/apps/desktop/src/lib/trpc/routers/index.ts
index c35ac6e5b32..44817a87322 100644
--- a/apps/desktop/src/lib/trpc/routers/index.ts
+++ b/apps/desktop/src/lib/trpc/routers/index.ts
@@ -1,5 +1,6 @@
import type { BrowserWindow } from "electron";
import { router } from "..";
+import { createConfigRouter } from "./config";
import { createExternalRouter } from "./external";
import { createNotificationsRouter } from "./notifications";
import { createProjectsRouter } from "./projects";
@@ -21,6 +22,7 @@ export const createAppRouter = (window: BrowserWindow) => {
notifications: createNotificationsRouter(),
external: createExternalRouter(),
settings: createSettingsRouter(),
+ config: createConfigRouter(),
});
};
diff --git a/apps/desktop/src/lib/trpc/routers/projects/projects.ts b/apps/desktop/src/lib/trpc/routers/projects/projects.ts
index ca60519ef2e..e4cb22d1e25 100644
--- a/apps/desktop/src/lib/trpc/routers/projects/projects.ts
+++ b/apps/desktop/src/lib/trpc/routers/projects/projects.ts
@@ -82,6 +82,12 @@ function extractRepoName(urlInput: string): string | null {
export const createProjectsRouter = (window: BrowserWindow) => {
return router({
+ get: publicProcedure
+ .input(z.object({ id: z.string() }))
+ .query(({ input }): Project | null => {
+ return db.data.projects.find((p) => p.id === input.id) ?? null;
+ }),
+
getRecents: publicProcedure.query((): Project[] => {
return db.data.projects
.slice()
diff --git a/apps/desktop/src/lib/trpc/routers/workspaces/utils/setup.test.ts b/apps/desktop/src/lib/trpc/routers/workspaces/utils/setup.test.ts
index b3c63955d72..69af31182a7 100644
--- a/apps/desktop/src/lib/trpc/routers/workspaces/utils/setup.test.ts
+++ b/apps/desktop/src/lib/trpc/routers/workspaces/utils/setup.test.ts
@@ -19,7 +19,7 @@ describe("loadSetupConfig", () => {
}
});
- test("returns null when setup.json does not exist", () => {
+ test("returns null when config.json does not exist", () => {
const config = loadSetupConfig(MAIN_REPO);
expect(config).toBeNull();
});
@@ -30,7 +30,7 @@ describe("loadSetupConfig", () => {
};
writeFileSync(
- join(MAIN_REPO, ".superset", "setup.json"),
+ join(MAIN_REPO, ".superset", "config.json"),
JSON.stringify(setupConfig),
);
@@ -39,7 +39,10 @@ describe("loadSetupConfig", () => {
});
test("returns null for invalid JSON", () => {
- writeFileSync(join(MAIN_REPO, ".superset", "setup.json"), "{ invalid json");
+ writeFileSync(
+ join(MAIN_REPO, ".superset", "config.json"),
+ "{ invalid json",
+ );
const config = loadSetupConfig(MAIN_REPO);
expect(config).toBeNull();
@@ -47,7 +50,7 @@ describe("loadSetupConfig", () => {
test("validates setup field must be an array", () => {
writeFileSync(
- join(MAIN_REPO, ".superset", "setup.json"),
+ join(MAIN_REPO, ".superset", "config.json"),
JSON.stringify({ setup: "not-an-array" }),
);
diff --git a/apps/desktop/src/lib/trpc/routers/workspaces/utils/setup.ts b/apps/desktop/src/lib/trpc/routers/workspaces/utils/setup.ts
index d2cbfe6a8d5..573a118a6e5 100644
--- a/apps/desktop/src/lib/trpc/routers/workspaces/utils/setup.ts
+++ b/apps/desktop/src/lib/trpc/routers/workspaces/utils/setup.ts
@@ -3,7 +3,7 @@ import { join } from "node:path";
import type { SetupConfig } from "shared/types";
export function loadSetupConfig(mainRepoPath: string): SetupConfig | null {
- const configPath = join(mainRepoPath, ".superset", "setup.json");
+ const configPath = join(mainRepoPath, ".superset", "config.json");
if (!existsSync(configPath)) {
return null;
diff --git a/apps/desktop/src/lib/trpc/routers/workspaces/utils/teardown.ts b/apps/desktop/src/lib/trpc/routers/workspaces/utils/teardown.ts
index 26bef181460..170e533bf10 100644
--- a/apps/desktop/src/lib/trpc/routers/workspaces/utils/teardown.ts
+++ b/apps/desktop/src/lib/trpc/routers/workspaces/utils/teardown.ts
@@ -11,7 +11,7 @@ export interface TeardownResult {
}
function loadSetupConfig(mainRepoPath: string): SetupConfig | null {
- const configPath = join(mainRepoPath, ".superset", "setup.json");
+ const configPath = join(mainRepoPath, ".superset", "config.json");
if (!existsSync(configPath)) {
return null;
diff --git a/apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts b/apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
index 9e826a6cb70..84206e661bb 100644
--- a/apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
+++ b/apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
@@ -118,6 +118,7 @@ export const createWorkspacesRouter = () => {
workspace,
initialCommands: setupConfig?.setup || null,
worktreePath,
+ projectId: project.id,
};
}),
diff --git a/apps/desktop/src/main/lib/db/schemas.ts b/apps/desktop/src/main/lib/db/schemas.ts
index 00abfd7f852..ef0d6480734 100644
--- a/apps/desktop/src/main/lib/db/schemas.ts
+++ b/apps/desktop/src/main/lib/db/schemas.ts
@@ -6,6 +6,7 @@ export interface Project {
tabOrder: number | null;
lastOpenedAt: number;
createdAt: number;
+ configToastDismissed?: boolean;
}
export interface GitStatus {
diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceHeader/assets/cursor.svg b/apps/desktop/src/renderer/assets/app-icons/cursor.svg
similarity index 100%
rename from apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceHeader/assets/cursor.svg
rename to apps/desktop/src/renderer/assets/app-icons/cursor.svg
diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceHeader/assets/finder.png b/apps/desktop/src/renderer/assets/app-icons/finder.png
similarity index 100%
rename from apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceHeader/assets/finder.png
rename to apps/desktop/src/renderer/assets/app-icons/finder.png
diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceHeader/assets/iterm.png b/apps/desktop/src/renderer/assets/app-icons/iterm.png
similarity index 100%
rename from apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceHeader/assets/iterm.png
rename to apps/desktop/src/renderer/assets/app-icons/iterm.png
diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceHeader/assets/terminal.png b/apps/desktop/src/renderer/assets/app-icons/terminal.png
similarity index 100%
rename from apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceHeader/assets/terminal.png
rename to apps/desktop/src/renderer/assets/app-icons/terminal.png
diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceHeader/assets/vscode.svg b/apps/desktop/src/renderer/assets/app-icons/vscode.svg
similarity index 100%
rename from apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceHeader/assets/vscode.svg
rename to apps/desktop/src/renderer/assets/app-icons/vscode.svg
diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceHeader/assets/warp.png b/apps/desktop/src/renderer/assets/app-icons/warp.png
similarity index 100%
rename from apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceHeader/assets/warp.png
rename to apps/desktop/src/renderer/assets/app-icons/warp.png
diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceHeader/assets/xcode.svg b/apps/desktop/src/renderer/assets/app-icons/xcode.svg
similarity index 100%
rename from apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceHeader/assets/xcode.svg
rename to apps/desktop/src/renderer/assets/app-icons/xcode.svg
diff --git a/apps/desktop/src/renderer/components/OpenInButton/OpenInButton.tsx b/apps/desktop/src/renderer/components/OpenInButton/OpenInButton.tsx
new file mode 100644
index 00000000000..646cf645719
--- /dev/null
+++ b/apps/desktop/src/renderer/components/OpenInButton/OpenInButton.tsx
@@ -0,0 +1,149 @@
+import { Button } from "@superset/ui/button";
+import { ButtonGroup } from "@superset/ui/button-group";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@superset/ui/dropdown-menu";
+import type { ExternalApp } from "main/lib/db/schemas";
+import { useState } from "react";
+import { HiChevronDown } from "react-icons/hi2";
+import { LuCopy } from "react-icons/lu";
+import cursorIcon from "renderer/assets/app-icons/cursor.svg";
+import finderIcon from "renderer/assets/app-icons/finder.png";
+import itermIcon from "renderer/assets/app-icons/iterm.png";
+import terminalIcon from "renderer/assets/app-icons/terminal.png";
+import vscodeIcon from "renderer/assets/app-icons/vscode.svg";
+import warpIcon from "renderer/assets/app-icons/warp.png";
+import xcodeIcon from "renderer/assets/app-icons/xcode.svg";
+import { trpc } from "renderer/lib/trpc";
+
+interface AppOption {
+ id: ExternalApp;
+ label: string;
+ icon: string;
+}
+
+const APP_OPTIONS: AppOption[] = [
+ { id: "finder", label: "Finder", icon: finderIcon },
+ { id: "cursor", label: "Cursor", icon: cursorIcon },
+ { id: "vscode", label: "VS Code", icon: vscodeIcon },
+ { id: "xcode", label: "Xcode", icon: xcodeIcon },
+ { id: "iterm", label: "iTerm", icon: itermIcon },
+ { id: "warp", label: "Warp", icon: warpIcon },
+ { id: "terminal", label: "Terminal", icon: terminalIcon },
+];
+
+const getAppOption = (id: ExternalApp) =>
+ APP_OPTIONS.find((app) => app.id === id) ?? APP_OPTIONS[1];
+
+export interface OpenInButtonProps {
+ path: string | undefined;
+ /** Optional label to show next to the icon (e.g., folder name) */
+ label?: string;
+ /** Show keyboard shortcut hints */
+ showShortcuts?: boolean;
+}
+
+export function OpenInButton({
+ path,
+ label,
+ showShortcuts = false,
+}: OpenInButtonProps) {
+ const [isOpen, setIsOpen] = useState(false);
+ const utils = trpc.useUtils();
+
+ const { data: lastUsedApp = "cursor" } =
+ trpc.settings.getLastUsedApp.useQuery();
+
+ const openInApp = trpc.external.openInApp.useMutation({
+ onSuccess: () => utils.settings.getLastUsedApp.invalidate(),
+ });
+ const copyPath = trpc.external.copyPath.useMutation();
+
+ const currentApp = getAppOption(lastUsedApp);
+
+ const handleOpenIn = (app: ExternalApp) => {
+ if (!path) return;
+ openInApp.mutate({ path, app });
+ setIsOpen(false);
+ };
+
+ const handleCopyPath = () => {
+ if (!path) return;
+ copyPath.mutate(path);
+ setIsOpen(false);
+ };
+
+ const handleOpenLastUsed = () => {
+ if (!path) return;
+ openInApp.mutate({ path, app: lastUsedApp });
+ };
+
+ return (
+
+ {label && (
+
+ )}
+
+
+
+
+
+ {APP_OPTIONS.map((app) => (
+ handleOpenIn(app.id)}
+ className="flex items-center justify-between"
+ >
+
+

+
{app.label}
+
+ {showShortcuts && app.id === lastUsedApp && (
+ ⌘O
+ )}
+
+ ))}
+
+
+
+
+ Copy path
+
+ {showShortcuts && (
+ ⌘⇧C
+ )}
+
+
+
+
+ );
+}
diff --git a/apps/desktop/src/renderer/components/OpenInButton/index.ts b/apps/desktop/src/renderer/components/OpenInButton/index.ts
new file mode 100644
index 00000000000..a0fffc24752
--- /dev/null
+++ b/apps/desktop/src/renderer/components/OpenInButton/index.ts
@@ -0,0 +1,2 @@
+export type { OpenInButtonProps } from "./OpenInButton";
+export { OpenInButton } from "./OpenInButton";
diff --git a/apps/desktop/src/renderer/components/SetupConfigModal/SetupConfigModal.tsx b/apps/desktop/src/renderer/components/SetupConfigModal/SetupConfigModal.tsx
new file mode 100644
index 00000000000..47ae4f04950
--- /dev/null
+++ b/apps/desktop/src/renderer/components/SetupConfigModal/SetupConfigModal.tsx
@@ -0,0 +1,90 @@
+import { Button } from "@superset/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+} from "@superset/ui/dialog";
+import { HiArrowTopRightOnSquare } from "react-icons/hi2";
+import { OpenInButton } from "renderer/components/OpenInButton";
+import { trpc } from "renderer/lib/trpc";
+import {
+ useCloseConfigModal,
+ useConfigModalOpen,
+ useConfigModalProjectId,
+} from "renderer/stores/config-modal";
+import { WEBSITE_URL } from "shared/constants";
+
+const CONFIG_TEMPLATE = `{
+ "setup": [],
+ "teardown": []
+}`;
+
+export function SetupConfigModal() {
+ const isOpen = useConfigModalOpen();
+ const projectId = useConfigModalProjectId();
+ const closeModal = useCloseConfigModal();
+
+ const { data: project } = trpc.projects.get.useQuery(
+ { id: projectId ?? "" },
+ { enabled: !!projectId },
+ );
+
+ const { data: configFilePath } = trpc.config.getConfigFilePath.useQuery(
+ { projectId: projectId ?? "" },
+ { enabled: !!projectId },
+ );
+
+ const projectName = project?.name ?? "your-project";
+
+ const handleLearnMore = () => {
+ window.open(`${WEBSITE_URL}/scripts`, "_blank");
+ };
+
+ return (
+
+ );
+}
diff --git a/apps/desktop/src/renderer/components/SetupConfigModal/index.ts b/apps/desktop/src/renderer/components/SetupConfigModal/index.ts
new file mode 100644
index 00000000000..cb7f67a024b
--- /dev/null
+++ b/apps/desktop/src/renderer/components/SetupConfigModal/index.ts
@@ -0,0 +1 @@
+export { SetupConfigModal } from "./SetupConfigModal";
diff --git a/apps/desktop/src/renderer/react-query/workspaces/useCreateWorkspace.ts b/apps/desktop/src/renderer/react-query/workspaces/useCreateWorkspace.ts
index 5db3af70a30..171f98c04ed 100644
--- a/apps/desktop/src/renderer/react-query/workspaces/useCreateWorkspace.ts
+++ b/apps/desktop/src/renderer/react-query/workspaces/useCreateWorkspace.ts
@@ -1,10 +1,13 @@
+import { toast } from "@superset/ui/sonner";
import { trpc } from "renderer/lib/trpc";
+import { useOpenConfigModal } from "renderer/stores/config-modal";
import { useTabsStore } from "renderer/stores/tabs/store";
/**
* Mutation hook for creating a new workspace
* Automatically invalidates all workspace queries on success
* Creates a terminal tab with setup commands if present
+ * Shows config toast if no setup commands are configured
*/
export function useCreateWorkspace(
options?: Parameters[0],
@@ -12,6 +15,8 @@ export function useCreateWorkspace(
const utils = trpc.useUtils();
const addTab = useTabsStore((state) => state.addTab);
const createOrAttach = trpc.terminal.createOrAttach.useMutation();
+ const openConfigModal = useOpenConfigModal();
+ const dismissConfigToast = trpc.config.dismissConfigToast.useMutation();
return trpc.workspaces.create.useMutation({
...options,
@@ -33,6 +38,18 @@ export function useCreateWorkspace(
tabTitle: "Terminal",
initialCommands: data.initialCommands,
});
+ } else {
+ // Show config toast if no setup commands
+ toast.info("No setup script configured", {
+ description: "Automate workspace setup with a config.json file",
+ action: {
+ label: "Configure",
+ onClick: () => openConfigModal(data.projectId),
+ },
+ onDismiss: () => {
+ dismissConfigToast.mutate({ projectId: data.projectId });
+ },
+ });
}
// Call user's onSuccess if provided
diff --git a/apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceDropdown.tsx b/apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceDropdown.tsx
index 518b422d02f..0ec504a01a1 100644
--- a/apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceDropdown.tsx
+++ b/apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceDropdown.tsx
@@ -22,7 +22,7 @@ export function WorkspaceDropdown({ className }: WorkspaceDropdownProps) {
const createWorkspace = useCreateWorkspace();
const openNew = useOpenNew();
- const handleCreateWorkspace = (projectId: string) => {
+ const handleCreateWorkspace = async (projectId: string) => {
toast.promise(createWorkspace.mutateAsync({ projectId }), {
loading: "Creating workspace...",
success: () => {
diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceHeader/WorkspaceHeader.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceHeader/WorkspaceHeader.tsx
index 627a255f637..a872351f713 100644
--- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceHeader/WorkspaceHeader.tsx
+++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceHeader/WorkspaceHeader.tsx
@@ -1,148 +1,21 @@
-import { Button } from "@superset/ui/button";
-import { ButtonGroup } from "@superset/ui/button-group";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuSeparator,
- DropdownMenuTrigger,
-} from "@superset/ui/dropdown-menu";
-import type { ExternalApp } from "main/lib/db/schemas";
-import { useState } from "react";
-import { HiChevronDown } from "react-icons/hi2";
-import { LuCopy } from "react-icons/lu";
-import { trpc } from "renderer/lib/trpc";
-
-import cursorIcon from "./assets/cursor.svg";
-import finderIcon from "./assets/finder.png";
-import itermIcon from "./assets/iterm.png";
-import terminalIcon from "./assets/terminal.png";
-import vscodeIcon from "./assets/vscode.svg";
-import warpIcon from "./assets/warp.png";
-import xcodeIcon from "./assets/xcode.svg";
-
-interface AppOption {
- id: ExternalApp;
- label: string;
- icon: string;
-}
-
-const APP_OPTIONS: AppOption[] = [
- { id: "finder", label: "Finder", icon: finderIcon },
- { id: "cursor", label: "Cursor", icon: cursorIcon },
- { id: "vscode", label: "VS Code", icon: vscodeIcon },
- { id: "xcode", label: "Xcode", icon: xcodeIcon },
- { id: "iterm", label: "iTerm", icon: itermIcon },
- { id: "warp", label: "Warp", icon: warpIcon },
- { id: "terminal", label: "Terminal", icon: terminalIcon },
-];
-
-const getAppOption = (id: ExternalApp) =>
- APP_OPTIONS.find((app) => app.id === id) ?? APP_OPTIONS[1];
+import { OpenInButton } from "renderer/components/OpenInButton";
interface WorkspaceHeaderProps {
worktreePath: string | undefined;
}
export function WorkspaceHeader({ worktreePath }: WorkspaceHeaderProps) {
- const [isOpen, setIsOpen] = useState(false);
- const utils = trpc.useUtils();
-
- const { data: lastUsedApp = "cursor" } =
- trpc.settings.getLastUsedApp.useQuery();
-
- const openInApp = trpc.external.openInApp.useMutation({
- onSuccess: () => utils.settings.getLastUsedApp.invalidate(),
- });
- const copyPath = trpc.external.copyPath.useMutation();
-
const folderName = worktreePath
? worktreePath.split("/").filter(Boolean).pop() || worktreePath
: null;
- const currentApp = getAppOption(lastUsedApp);
-
- const handleOpenIn = (app: ExternalApp) => {
- if (!worktreePath) return;
- openInApp.mutate({ path: worktreePath, app });
- setIsOpen(false);
- };
-
- const handleCopyPath = () => {
- if (!worktreePath) return;
- copyPath.mutate(worktreePath);
- setIsOpen(false);
- };
-
- const handleOpenLastUsed = () => {
- if (!worktreePath) return;
- openInApp.mutate({ path: worktreePath, app: lastUsedApp });
- };
return (
-
- {folderName && (
-
- )}
-
-
-
-
-
- {APP_OPTIONS.map((app) => (
- handleOpenIn(app.id)}
- className="flex items-center justify-between"
- >
-
-

-
{app.label}
-
- {app.id === lastUsedApp && (
- ⌘O
- )}
-
- ))}
-
-
-
-
- Copy path
-
- ⌘⇧C
-
-
-
-
+
);
}
diff --git a/apps/desktop/src/renderer/screens/main/index.tsx b/apps/desktop/src/renderer/screens/main/index.tsx
index 9bf8c70fdb5..196d6a8702f 100644
--- a/apps/desktop/src/renderer/screens/main/index.tsx
+++ b/apps/desktop/src/renderer/screens/main/index.tsx
@@ -3,6 +3,7 @@ import { useState } from "react";
import { DndProvider } from "react-dnd";
import { useHotkeys } from "react-hotkeys-hook";
import { HiArrowPath } from "react-icons/hi2";
+import { SetupConfigModal } from "renderer/components/SetupConfigModal";
import { trpc } from "renderer/lib/trpc";
import { useCurrentView, useOpenSettings } from "renderer/stores/app-state";
import { useSidebarStore } from "renderer/stores/sidebar-state";
@@ -153,6 +154,7 @@ export function MainScreen() {
)}
+
);
}
diff --git a/apps/desktop/src/renderer/stores/config-modal.ts b/apps/desktop/src/renderer/stores/config-modal.ts
new file mode 100644
index 00000000000..e383435faba
--- /dev/null
+++ b/apps/desktop/src/renderer/stores/config-modal.ts
@@ -0,0 +1,37 @@
+import { create } from "zustand";
+import { devtools } from "zustand/middleware";
+
+interface ConfigModalState {
+ isOpen: boolean;
+ projectId: string | null;
+ openModal: (projectId: string) => void;
+ closeModal: () => void;
+}
+
+export const useConfigModalStore = create()(
+ devtools(
+ (set) => ({
+ isOpen: false,
+ projectId: null,
+
+ openModal: (projectId) => {
+ set({ isOpen: true, projectId });
+ },
+
+ closeModal: () => {
+ set({ isOpen: false, projectId: null });
+ },
+ }),
+ { name: "ConfigModalStore" },
+ ),
+);
+
+// Convenience hooks
+export const useConfigModalOpen = () =>
+ useConfigModalStore((state) => state.isOpen);
+export const useConfigModalProjectId = () =>
+ useConfigModalStore((state) => state.projectId);
+export const useOpenConfigModal = () =>
+ useConfigModalStore((state) => state.openModal);
+export const useCloseConfigModal = () =>
+ useConfigModalStore((state) => state.closeModal);
diff --git a/apps/desktop/src/shared/constants.ts b/apps/desktop/src/shared/constants.ts
index a5fecadd589..4021a83dbf8 100644
--- a/apps/desktop/src/shared/constants.ts
+++ b/apps/desktop/src/shared/constants.ts
@@ -22,3 +22,6 @@ export const SUPERSET_DIR_NAME = ENVIRONMENT.IS_DEV
? ".superset-dev"
: ".superset";
export const WORKTREES_DIR_NAME = "worktrees";
+
+// Website URL - defaults to production, can be overridden via env var for local dev
+export const WEBSITE_URL = process.env.WEBSITE_URL || "https://superset.dev";
diff --git a/apps/website/src/app/scripts/page.tsx b/apps/website/src/app/scripts/page.tsx
new file mode 100644
index 00000000000..55646fda475
--- /dev/null
+++ b/apps/website/src/app/scripts/page.tsx
@@ -0,0 +1,176 @@
+"use client";
+
+import { motion } from "framer-motion";
+import { Footer } from "../components/Footer";
+import { Header } from "../components/Header";
+
+const CONFIG_EXAMPLE = `{
+ "setup": [
+ "bun install",
+ "bun run db:migrate"
+ ],
+ "teardown": [
+ "docker-compose down"
+ ]
+}`;
+
+const SHELL_SCRIPT_EXAMPLE = `{
+ "setup": ["./.superset/setup.sh"],
+ "teardown": ["./.superset/teardown.sh"]
+}`;
+
+export default function ScriptsPage() {
+ return (
+ <>
+
+
+
+
+
+ Setup & Teardown Scripts
+
+
+ Automate workspace initialization and cleanup with config.json
+
+
+
+
+ Overview
+
+
+ Superset can automatically run commands when creating or
+ deleting workspaces. This is useful for:
+
+
+ - Installing dependencies
+ - Running database migrations
+ - Starting background services
+ - Cleaning up resources when done
+
+
+
+
+
+ Configuration
+
+
+ Create a config.json{" "}
+ file in your project's{" "}
+ .superset directory:
+
+
+
+ your-project/.superset/config.json
+
+
+
+
+
+ Schema
+
+ The config file has two optional arrays:
+
+
+ {CONFIG_EXAMPLE}
+
+
+
+
+
+ setup
+
+
+ Array of shell commands to run when a new workspace is
+ created. Commands run sequentially in the workspace's
+ worktree directory.
+
+
+
+
+
+ teardown
+
+
+ Array of shell commands to run when a workspace is deleted.
+ Useful for cleaning up resources like Docker containers or
+ temporary files.
+
+
+
+
+
+
+
+ Using Shell Scripts
+
+
+ For complex setup logic, reference shell scripts instead of
+ inline commands:
+
+
+
+ {SHELL_SCRIPT_EXAMPLE}
+
+
+
+ Make sure your scripts are executable:{" "}
+
+ chmod +x .superset/setup.sh
+
+
+
+
+
+
+ How It Works
+
+
+ -
+ When you create a new workspace, Superset creates a git
+ worktree for your branch
+
+ -
+ If
config.json exists
+ with setup commands, they run automatically in the new
+ worktree
+
+ -
+ Commands execute in a terminal tab so you can see output
+
+ -
+ When deleting a workspace, teardown commands run before the
+ worktree is removed
+
+
+
+
+
+ Tips
+
+ -
+ Keep setup scripts fast - they run every time you create a
+ workspace
+
+ -
+ Use
&& to chain
+ commands that depend on each other
+
+ -
+ Add
.superset/ to your{" "}
+ .gitignore if you
+ don't want to share configs
+
+ - Or commit it to share workspace setup with your team
+
+
+
+
+
+
+ >
+ );
+}