+
{/* Back button */}
);
}
diff --git a/apps/desktop/src/renderer/screens/main/components/SettingsView/WorkspaceSettings/WorkspaceSettings.tsx b/apps/desktop/src/renderer/screens/main/components/SettingsView/WorkspaceSettings/WorkspaceSettings.tsx
new file mode 100644
index 00000000000..2ca324ef1ae
--- /dev/null
+++ b/apps/desktop/src/renderer/screens/main/components/SettingsView/WorkspaceSettings/WorkspaceSettings.tsx
@@ -0,0 +1,125 @@
+import { Input } from "@superset/ui/input";
+import {
+ HiOutlineCog6Tooth,
+ HiOutlineFolder,
+ HiOutlinePencilSquare,
+} from "react-icons/hi2";
+import { LuGitBranch } from "react-icons/lu";
+import { ConfigFilePreview } from "renderer/components/ConfigFilePreview";
+import { trpc } from "renderer/lib/trpc";
+import { useWorkspaceRename } from "renderer/screens/main/components/TopBar/WorkspaceTabs/useWorkspaceRename";
+
+export function WorkspaceSettings() {
+ const { data: activeWorkspace, isLoading } =
+ trpc.workspaces.getActive.useQuery();
+
+ const { data: configFilePath } = trpc.config.getConfigFilePath.useQuery(
+ { projectId: activeWorkspace?.projectId ?? "" },
+ { enabled: !!activeWorkspace?.projectId },
+ );
+
+ const rename = useWorkspaceRename(
+ activeWorkspace?.id ?? "",
+ activeWorkspace?.name ?? "",
+ );
+
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ if (!activeWorkspace) {
+ return (
+
+
+
Workspace
+
+ No active workspace selected
+
+
+
+ );
+ }
+
+ return (
+
+
+
Workspace
+
+
+
+
+
Name
+ {rename.isRenaming ? (
+ rename.setRenameValue(e.target.value)}
+ onBlur={rename.submitRename}
+ onKeyDown={rename.handleKeyDown}
+ className="text-base"
+ />
+ ) : (
+
+ {activeWorkspace.name}
+
+
+ )}
+
+
+ {activeWorkspace.worktree && (
+
+
+
+ Branch
+
+
+
{activeWorkspace.worktree.branch}
+ {activeWorkspace.worktree.gitStatus?.needsRebase && (
+
+ Needs Rebase
+
+ )}
+
+
+ )}
+
+
+
+
+ Path
+
+
+ {activeWorkspace.worktreePath}
+
+
+
+ {activeWorkspace.project && (
+
+ )}
+
+
+ );
+}
diff --git a/apps/desktop/src/renderer/screens/main/components/SettingsView/WorkspaceSettings/index.ts b/apps/desktop/src/renderer/screens/main/components/SettingsView/WorkspaceSettings/index.ts
new file mode 100644
index 00000000000..52c413ca2c9
--- /dev/null
+++ b/apps/desktop/src/renderer/screens/main/components/SettingsView/WorkspaceSettings/index.ts
@@ -0,0 +1 @@
+export { WorkspaceSettings } from "./WorkspaceSettings";
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/TopBar/WorkspaceTabs/WorkspaceGroupContextMenu.tsx b/apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceGroupContextMenu.tsx
index 2e299fe7393..d1cbb5326a1 100644
--- a/apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceGroupContextMenu.tsx
+++ b/apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceGroupContextMenu.tsx
@@ -118,7 +118,7 @@ export function WorkspaceGroupContextMenu({
-
+
{PROJECT_COLORS.map((color) => (
- 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 && (
-
-
- {folderName}
-
- )}
-
-
-
- Open
-
-
-
-
- {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/app-state.ts b/apps/desktop/src/renderer/stores/app-state.ts
index 9fe5264fbf9..b934b577631 100644
--- a/apps/desktop/src/renderer/stores/app-state.ts
+++ b/apps/desktop/src/renderer/stores/app-state.ts
@@ -2,7 +2,7 @@ import { create } from "zustand";
import { devtools } from "zustand/middleware";
export type AppView = "workspace" | "settings";
-export type SettingsSection = "appearance" | "keyboard";
+export type SettingsSection = "workspace" | "appearance" | "keyboard";
interface AppState {
currentView: AppView;
@@ -20,7 +20,7 @@ export const useAppStore = create
()(
(set) => ({
currentView: "workspace",
isSettingsTabOpen: false,
- settingsSection: "appearance",
+ settingsSection: "workspace",
setView: (view) => {
set({ currentView: view });
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..eb813f2540a 100644
--- a/apps/desktop/src/shared/constants.ts
+++ b/apps/desktop/src/shared/constants.ts
@@ -21,4 +21,16 @@ export const PORTS = {
export const SUPERSET_DIR_NAME = ENVIRONMENT.IS_DEV
? ".superset-dev"
: ".superset";
+// Project-level directory name (always .superset, not conditional)
+export const PROJECT_SUPERSET_DIR_NAME = ".superset";
export const WORKTREES_DIR_NAME = "worktrees";
+export const CONFIG_FILE_NAME = "config.json";
+
+// Website URL - defaults to production, can be overridden via env var for local dev
+export const WEBSITE_URL = process.env.WEBSITE_URL || "https://superset.sh";
+
+// Config file template
+export const CONFIG_TEMPLATE = `{
+ "setup": [],
+ "teardown": []
+}`;
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
+
+
+
+
+
+
+ >
+ );
+}