From 4b28a47afea34c1ff58f390e5ec4e886a5db9f3e Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Sun, 8 Feb 2026 13:02:35 -0800 Subject: [PATCH 1/9] feat(desktop): split auto-apply preset into workspace creation and new tab settings Give users independent control over when the default preset is applied: one toggle for workspace/worktree creation and another for new tabs, panes, and splits. Explicit preset launches via hotkey/menu are never gated. Refactors TerminalSettings from a monolithic component into isolated sub-components (PresetsSection, SessionsSection, individual setting components), each owning their own queries. This prevents query resolutions in one section from re-rendering others, fixing the "Maximum update depth exceeded" error caused by Radix UI ref composition cascading through react-dnd connectors on re-renders. --- .../src/lib/trpc/routers/settings/index.ts | 21 + .../TerminalSettings/TerminalSettings.tsx | 1046 +--------------- .../components/ApplyPresetOnNewTabSetting.tsx | 57 + .../components/AutoApplyPresetSetting.tsx | 53 + .../components/LinkBehaviorSetting.tsx | 68 ++ .../components/PresetRow/PresetRow.tsx | 8 +- .../components/PresetsSection.tsx | 420 +++++++ .../components/SessionsSection.tsx | 442 +++++++ .../utils/settings-search/settings-search.ts | 25 +- .../stores/tabs/useTabsWithPresets.ts | 17 +- apps/desktop/src/shared/constants.ts | 1 + .../0020_add_apply_preset_on_new_tab.sql | 1 + .../local-db/drizzle/meta/0020_snapshot.json | 1078 +++++++++++++++++ packages/local-db/drizzle/meta/_journal.json | 7 + packages/local-db/src/schema/schema.ts | 3 + 15 files changed, 2236 insertions(+), 1011 deletions(-) create mode 100644 apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/ApplyPresetOnNewTabSetting.tsx create mode 100644 apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/AutoApplyPresetSetting.tsx create mode 100644 apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/LinkBehaviorSetting.tsx create mode 100644 apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/PresetsSection.tsx create mode 100644 apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/SessionsSection.tsx create mode 100644 packages/local-db/drizzle/0020_add_apply_preset_on_new_tab.sql create mode 100644 packages/local-db/drizzle/meta/0020_snapshot.json diff --git a/apps/desktop/src/lib/trpc/routers/settings/index.ts b/apps/desktop/src/lib/trpc/routers/settings/index.ts index ba0dcfa465e..e9451a677e1 100644 --- a/apps/desktop/src/lib/trpc/routers/settings/index.ts +++ b/apps/desktop/src/lib/trpc/routers/settings/index.ts @@ -10,6 +10,7 @@ import { app } from "electron"; import { quitWithoutConfirmation } from "main/index"; import { localDb } from "main/lib/local-db"; import { + DEFAULT_APPLY_PRESET_ON_NEW_TAB, DEFAULT_AUTO_APPLY_DEFAULT_PRESET, DEFAULT_CONFIRM_ON_QUIT, DEFAULT_TERMINAL_LINK_BEHAVIOR, @@ -314,6 +315,26 @@ export const createSettingsRouter = () => { return { success: true }; }), + getApplyPresetOnNewTab: publicProcedure.query(() => { + const row = getSettings(); + return row.applyPresetOnNewTab ?? DEFAULT_APPLY_PRESET_ON_NEW_TAB; + }), + + setApplyPresetOnNewTab: publicProcedure + .input(z.object({ enabled: z.boolean() })) + .mutation(({ input }) => { + localDb + .insert(settings) + .values({ id: 1, applyPresetOnNewTab: input.enabled }) + .onConflictDoUpdate({ + target: settings.id, + set: { applyPresetOnNewTab: input.enabled }, + }) + .run(); + + return { success: true }; + }), + restartApp: publicProcedure.mutation(() => { app.relaunch(); quitWithoutConfirmation(); diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/TerminalSettings.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/TerminalSettings.tsx index 8800cdab342..ae6a4ef6c8f 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/TerminalSettings.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/TerminalSettings.tsx @@ -1,116 +1,40 @@ -import type { - ExecutionMode, - TerminalLinkBehavior, - TerminalPreset, -} from "@superset/local-db"; -import { - AlertDialog, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from "@superset/ui/alert-dialog"; -import { Button } from "@superset/ui/button"; -import { Label } from "@superset/ui/label"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@superset/ui/select"; -import { toast } from "@superset/ui/sonner"; -import { Switch } from "@superset/ui/switch"; -import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { - HiOutlineCheck, - HiOutlinePlus, - HiOutlineQuestionMarkCircle, -} from "react-icons/hi2"; -import { - getPresetIcon, - useIsDarkTheme, -} from "renderer/assets/app-icons/preset-icons"; -import { electronTrpc } from "renderer/lib/electron-trpc"; -import { usePresets } from "renderer/react-query/presets"; -import { - PRESET_COLUMNS, - type PresetColumnKey, -} from "renderer/routes/_authenticated/settings/presets/types"; -import { DEFAULT_AUTO_APPLY_DEFAULT_PRESET } from "shared/constants"; +import type { ReactNode } from "react"; import { isItemVisible, SETTING_ITEM_ID, type SettingItemId, } from "../../../utils/settings-search"; -import { PresetRow } from "./components/PresetRow"; - -interface PresetTemplate { - name: string; - preset: { - name: string; - description: string; - cwd: string; - commands: string[]; - }; -} - -const PRESET_TEMPLATES: PresetTemplate[] = [ - { - name: "codex", - preset: { - name: "codex", - description: "Danger mode: All permissions auto-approved", - cwd: "", - commands: [ - 'codex -c model_reasoning_effort="high" --ask-for-approval never --sandbox danger-full-access -c model_reasoning_summary="detailed" -c model_supports_reasoning_summaries=true', - ], - }, - }, - { - name: "claude", - preset: { - name: "claude", - description: "Danger mode: All permissions auto-approved", - cwd: "", - commands: ["claude --dangerously-skip-permissions"], - }, - }, - { - name: "gemini", - preset: { - name: "gemini", - description: "Danger mode: All permissions auto-approved", - cwd: "", - commands: ["gemini --yolo"], - }, - }, - { - name: "cursor-agent", - preset: { - name: "cursor-agent", - description: "Cursor AI agent for terminal-based coding assistance", - cwd: "", - commands: ["cursor-agent"], - }, - }, - { - name: "opencode", - preset: { - name: "opencode", - description: "OpenCode: Open-source AI coding agent", - cwd: "", - commands: ["opencode"], - }, - }, -]; +import { ApplyPresetOnNewTabSetting } from "./components/ApplyPresetOnNewTabSetting"; +import { AutoApplyPresetSetting } from "./components/AutoApplyPresetSetting"; +import { LinkBehaviorSetting } from "./components/LinkBehaviorSetting"; +import { PresetsSection } from "./components/PresetsSection"; +import { SessionsSection } from "./components/SessionsSection"; interface TerminalSettingsProps { visibleItems?: SettingItemId[] | null; } +/** + * Renders a list of visible sections with automatic border separators. + * Each section is its own component that owns its data-fetching, + * so query resolutions in one section don't re-render others. + */ +function SectionList({ children }: { children: ReactNode[] }) { + const visibleChildren = children.filter(Boolean); + return ( +
+ {visibleChildren.map((child, i) => ( +
0 ? "pt-6 border-t mt-6" : ""} + > + {child} +
+ ))} +
+ ); +} + export function TerminalSettings({ visibleItems }: TerminalSettingsProps) { const showPresets = isItemVisible( SETTING_ITEM_ID.TERMINAL_PRESETS, @@ -124,385 +48,18 @@ export function TerminalSettings({ visibleItems }: TerminalSettingsProps) { SETTING_ITEM_ID.TERMINAL_AUTO_APPLY_PRESET, visibleItems, ); - const showSessions = isItemVisible( - SETTING_ITEM_ID.TERMINAL_SESSIONS, + const showApplyPresetOnNewTab = isItemVisible( + SETTING_ITEM_ID.TERMINAL_APPLY_PRESET_ON_NEW_TAB, visibleItems, ); const showLinkBehavior = isItemVisible( SETTING_ITEM_ID.TERMINAL_LINK_BEHAVIOR, visibleItems, ); - - const utils = electronTrpc.useUtils(); - const isDark = useIsDarkTheme(); - - // Presets - const { - presets: serverPresets, - isLoading: isLoadingPresets, - createPreset, - updatePreset, - deletePreset, - setDefaultPreset, - reorderPresets, - } = usePresets(); - const [localPresets, setLocalPresets] = - useState(serverPresets); - const presetsContainerRef = useRef(null); - const prevPresetsCountRef = useRef(serverPresets.length); - const serverPresetsRef = useRef(serverPresets); - - useEffect(() => { - serverPresetsRef.current = serverPresets; - }, [serverPresets]); - - useEffect(() => { - setLocalPresets(serverPresets); - - if (serverPresets.length > prevPresetsCountRef.current) { - requestAnimationFrame(() => { - presetsContainerRef.current?.scrollTo({ - top: presetsContainerRef.current.scrollHeight, - behavior: "smooth", - }); - }); - } - prevPresetsCountRef.current = serverPresets.length; - }, [serverPresets]); - - const existingPresetNames = useMemo( - () => new Set(serverPresets.map((p) => p.name)), - [serverPresets], - ); - - const isTemplateAdded = (template: PresetTemplate) => - existingPresetNames.has(template.preset.name); - - const handleCellChange = useCallback( - (rowIndex: number, column: PresetColumnKey, value: string) => { - setLocalPresets((prev) => - prev.map((p, i) => (i === rowIndex ? { ...p, [column]: value } : p)), - ); - }, - [], - ); - - const handleCellBlur = useCallback( - (rowIndex: number, column: PresetColumnKey) => { - setLocalPresets((currentLocal) => { - const preset = currentLocal[rowIndex]; - if (!preset) return currentLocal; - const serverPreset = serverPresetsRef.current.find( - (p) => p.id === preset.id, - ); - if (!serverPreset) return currentLocal; - if (preset[column] === serverPreset[column]) return currentLocal; - - updatePreset.mutate({ - id: preset.id, - patch: { [column]: preset[column] }, - }); - return currentLocal; - }); - }, - [updatePreset], - ); - - const handleCommandsChange = useCallback( - (rowIndex: number, commands: string[]) => { - setLocalPresets((prev) => { - const preset = prev[rowIndex]; - const isDelete = preset && commands.length < preset.commands.length; - const newPresets = prev.map((p, i) => - i === rowIndex ? { ...p, commands } : p, - ); - - // Save immediately on delete since onBlur won't have the updated state yet - if (isDelete && preset) { - updatePreset.mutate({ - id: preset.id, - patch: { commands }, - }); - } - return newPresets; - }); - }, - [updatePreset], - ); - - const handleCommandsBlur = useCallback( - (rowIndex: number) => { - setLocalPresets((currentLocal) => { - const preset = currentLocal[rowIndex]; - if (!preset) return currentLocal; - const serverPreset = serverPresetsRef.current.find( - (p) => p.id === preset.id, - ); - if (!serverPreset) return currentLocal; - if ( - JSON.stringify(preset.commands) === - JSON.stringify(serverPreset.commands) - ) - return currentLocal; - - updatePreset.mutate({ - id: preset.id, - patch: { commands: preset.commands }, - }); - return currentLocal; - }); - }, - [updatePreset], - ); - - const handleExecutionModeChange = useCallback( - (rowIndex: number, mode: ExecutionMode) => { - setLocalPresets((currentLocal) => { - const preset = currentLocal[rowIndex]; - if (!preset) return currentLocal; - - const newPresets = currentLocal.map((p, i) => - i === rowIndex ? { ...p, executionMode: mode } : p, - ); - - updatePreset.mutate({ - id: preset.id, - patch: { executionMode: mode }, - }); - - return newPresets; - }); - }, - [updatePreset], - ); - - const handleAddRow = useCallback(() => { - createPreset.mutate({ - name: "", - cwd: "", - commands: [""], - }); - }, [createPreset]); - - const handleAddTemplate = useCallback( - (template: PresetTemplate) => { - if (existingPresetNames.has(template.preset.name)) return; - createPreset.mutate(template.preset); - }, - [createPreset, existingPresetNames], - ); - - const handleDeleteRow = useCallback( - (rowIndex: number) => { - setLocalPresets((currentLocal) => { - const preset = currentLocal[rowIndex]; - if (preset) { - deletePreset.mutate({ id: preset.id }); - } - return currentLocal; - }); - }, - [deletePreset], - ); - - const handleSetDefault = useCallback( - (presetId: string | null) => { - setDefaultPreset.mutate({ id: presetId }); - }, - [setDefaultPreset], - ); - - const handleLocalReorder = useCallback( - (fromIndex: number, toIndex: number) => { - setLocalPresets((prev) => { - const newPresets = [...prev]; - const [removed] = newPresets.splice(fromIndex, 1); - newPresets.splice(toIndex, 0, removed); - return newPresets; - }); - }, - [], - ); - - const handlePersistReorder = useCallback( - (presetId: string, targetIndex: number) => { - reorderPresets.mutate({ presetId, targetIndex }); - }, - [reorderPresets], - ); - - const { data: daemonSessions } = - electronTrpc.terminal.listDaemonSessions.useQuery(); - const sessions = daemonSessions?.sessions ?? []; - const aliveSessions = useMemo( - () => sessions.filter((session) => session.isAlive), - [sessions], + const showSessions = isItemVisible( + SETTING_ITEM_ID.TERMINAL_SESSIONS, + visibleItems, ); - const sessionsSorted = useMemo(() => { - return [...aliveSessions].sort((a, b) => { - // Attached sessions first, then newest attach time. - if (a.attachedClients !== b.attachedClients) { - return b.attachedClients - a.attachedClients; - } - const aTime = a.lastAttachedAt ? Date.parse(a.lastAttachedAt) : 0; - const bTime = b.lastAttachedAt ? Date.parse(b.lastAttachedAt) : 0; - return bTime - aTime; - }); - }, [aliveSessions]); - - const [confirmKillAllOpen, setConfirmKillAllOpen] = useState(false); - const [confirmClearHistoryOpen, setConfirmClearHistoryOpen] = useState(false); - const [confirmRestartDaemonOpen, setConfirmRestartDaemonOpen] = - useState(false); - const [showSessionList, setShowSessionList] = useState(false); - const [pendingKillSession, setPendingKillSession] = useState<{ - sessionId: string; - workspaceId: string; - } | null>(null); - - // Terminal link behavior setting - const { data: terminalLinkBehavior, isLoading: isLoadingLinkBehavior } = - electronTrpc.settings.getTerminalLinkBehavior.useQuery(); - - const setTerminalLinkBehavior = - electronTrpc.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, - }); - }; - - // Auto-apply default preset setting - const { data: autoApplyDefaultPreset, isLoading: isLoadingAutoApply } = - electronTrpc.settings.getAutoApplyDefaultPreset.useQuery(); - - const setAutoApplyDefaultPreset = - electronTrpc.settings.setAutoApplyDefaultPreset.useMutation({ - onMutate: async ({ enabled }) => { - await utils.settings.getAutoApplyDefaultPreset.cancel(); - const previous = utils.settings.getAutoApplyDefaultPreset.getData(); - utils.settings.getAutoApplyDefaultPreset.setData(undefined, enabled); - return { previous }; - }, - onError: (_err, _vars, context) => { - if (context?.previous !== undefined) { - utils.settings.getAutoApplyDefaultPreset.setData( - undefined, - context.previous, - ); - } - }, - onSettled: () => { - utils.settings.getAutoApplyDefaultPreset.invalidate(); - }, - }); - - const handleAutoApplyToggle = (enabled: boolean) => { - setAutoApplyDefaultPreset.mutate({ enabled }); - }; - - const killAllDaemonSessions = - electronTrpc.terminal.killAllDaemonSessions.useMutation({ - onMutate: async () => { - await utils.terminal.listDaemonSessions.cancel(); - const previous = utils.terminal.listDaemonSessions.getData(); - utils.terminal.listDaemonSessions.setData(undefined, { - sessions: [], - }); - return { previous }; - }, - onSuccess: (result) => { - if (result.remainingCount > 0) { - toast.warning("Some sessions could not be killed", { - description: `${result.killedCount} terminated, ${result.remainingCount} remaining`, - }); - } else { - toast.success("Killed all terminal sessions", { - description: `${result.killedCount} sessions terminated`, - }); - } - }, - onError: (error, _vars, context) => { - if (context?.previous) { - utils.terminal.listDaemonSessions.setData( - undefined, - context.previous, - ); - } - toast.error("Failed to kill sessions", { - description: error.message, - }); - }, - onSettled: () => { - setTimeout(() => { - utils.terminal.listDaemonSessions.invalidate(); - }, 300); - }, - }); - - const clearTerminalHistory = - electronTrpc.terminal.clearTerminalHistory.useMutation({ - onSuccess: () => { - toast.success("Cleared terminal history"); - utils.terminal.listDaemonSessions.invalidate(); - }, - onError: (error) => { - toast.error("Failed to clear terminal history", { - description: error.message, - }); - }, - }); - - const killDaemonSession = electronTrpc.terminal.kill.useMutation({ - onSuccess: () => { - toast.success("Killed terminal session"); - utils.terminal.listDaemonSessions.invalidate(); - }, - onError: (error) => { - toast.error("Failed to kill session", { - description: error.message, - }); - }, - }); - - const restartDaemon = electronTrpc.terminal.restartDaemon.useMutation({ - onSuccess: () => { - toast.success("Daemon restarted", { - description: - "Terminal daemon has been restarted. Open a terminal to spawn a fresh daemon.", - }); - utils.terminal.listDaemonSessions.invalidate(); - }, - onError: (error) => { - toast.error("Failed to restart daemon", { - description: error.message, - }); - }, - }); - - const formatTimestamp = (value?: string) => { - if (!value) return "—"; - return value.replace("T", " ").replace(/\.\d+Z$/, "Z"); - }; return (
@@ -513,536 +70,21 @@ export function TerminalSettings({ visibleItems }: TerminalSettingsProps) {

-
- {/* Presets Section */} + {(showPresets || showQuickAdd) && ( -
-
-
- -

- Presets let you quickly launch terminals with pre-configured - commands. -

-
- {showPresets && ( - - )} -
- - {showQuickAdd && ( -
- - Quick add: - - {PRESET_TEMPLATES.map((template) => { - const alreadyAdded = isTemplateAdded(template); - const presetIcon = getPresetIcon(template.name, isDark); - return ( - - - - - - {alreadyAdded - ? "Already added" - : template.preset.description} - - - ); - })} -
- )} - - {showPresets && ( -
-
-
- {PRESET_COLUMNS.map((column) => ( -
- {column.label} -
- ))} - - -
- Mode - -
-
- -

Execution Mode

-

- Sequential: Commands run one after - another in a single terminal (joined with &&) -

-

- Parallel: Each command runs in its own - split pane within a single tab -

-
-
-
- Actions -
-
- -
- {isLoadingPresets ? ( -
- Loading presets... -
- ) : localPresets.length > 0 ? ( - localPresets.map((preset, index) => ( - - )) - ) : ( -
- No presets yet. Click "Add Preset" to create your first - preset. -
- )} -
-
- )} -
- )} - - {showAutoApplyPreset && ( -
-
- -

- Automatically apply your default preset when creating new - workspaces -

-
- -
+ )} - - {showLinkBehavior && ( -
-
- -

- Choose how to open file paths when Cmd+clicking in the terminal -

-
- -
- )} - - {showSessions && ( -
-
-
- - -
-

- Daemon sessions running: {aliveSessions.length} -

- {aliveSessions.length >= 20 && ( -

- Large numbers of persistent terminals can increase CPU/memory - usage. Consider killing old sessions if you notice slowdowns. -

- )} -
- -
- - - - -
- - {showSessionList && aliveSessions.length > 0 && ( -
-
- - - - - - - - - - - - - {sessionsSorted.map((session) => ( - - - - - - - - - ))} - -
- Workspace - - Session - - Clients - - PID - - Last attached - - Action -
- {session.workspaceId} - - {session.sessionId} - - {session.attachedClients} - - {session.pid ?? "—"} - - {formatTimestamp(session.lastAttachedAt)} - - -
-
-
- )} -
+ {showAutoApplyPreset && } + {showApplyPresetOnNewTab && ( + )} -
- - - - - - Kill all terminal sessions? - - -
- - This will terminate all persistent terminal processes (builds, - tests, agents, etc.). - - - You can't undo this action. Terminal panes will show "Process - exited" and can be restarted. - -
-
-
- - - - -
-
- - - - - - Clear terminal history? - - -
- - This deletes the saved scrollback used for reboot/crash - recovery. - - - Running terminal processes continue, but older output may no - longer be available after restarting the app. - -
-
-
- - - - -
-
- - { - if (!open) setPendingKillSession(null); - }} - > - - - - Kill terminal session? - - -
- - This will terminate the session and its underlying process. - - {pendingKillSession && ( - - {pendingKillSession.workspaceId} /{" "} - {pendingKillSession.sessionId} - - )} -
-
-
- - - - -
-
- - - - - - Restart terminal daemon? - - -
- - This will shut down the terminal daemon process and kill all - running sessions. Use this to fix terminals that are stuck or - unresponsive. - - - A fresh daemon will start automatically when you open a new - terminal. - -
-
-
- - - - -
-
+ {showLinkBehavior && } + {showSessions && } +
); } diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/ApplyPresetOnNewTabSetting.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/ApplyPresetOnNewTabSetting.tsx new file mode 100644 index 00000000000..82c187e17cc --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/ApplyPresetOnNewTabSetting.tsx @@ -0,0 +1,57 @@ +import { Label } from "@superset/ui/label"; +import { Switch } from "@superset/ui/switch"; +import { electronTrpc } from "renderer/lib/electron-trpc"; +import { DEFAULT_APPLY_PRESET_ON_NEW_TAB } from "shared/constants"; + +export function ApplyPresetOnNewTabSetting() { + const utils = electronTrpc.useUtils(); + + const { data: applyPresetOnNewTab, isLoading } = + electronTrpc.settings.getApplyPresetOnNewTab.useQuery(); + + const setApplyPresetOnNewTab = + electronTrpc.settings.setApplyPresetOnNewTab.useMutation({ + onMutate: async ({ enabled }) => { + await utils.settings.getApplyPresetOnNewTab.cancel(); + const previous = utils.settings.getApplyPresetOnNewTab.getData(); + utils.settings.getApplyPresetOnNewTab.setData(undefined, enabled); + return { previous }; + }, + onError: (_err, _vars, context) => { + if (context?.previous !== undefined) { + utils.settings.getApplyPresetOnNewTab.setData( + undefined, + context.previous, + ); + } + }, + onSettled: () => { + utils.settings.getApplyPresetOnNewTab.invalidate(); + }, + }); + + return ( +
+
+ +

+ Automatically apply your default preset when opening new tabs, panes, + or splits +

+
+ + setApplyPresetOnNewTab.mutate({ enabled }) + } + disabled={isLoading || setApplyPresetOnNewTab.isPending} + /> +
+ ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/AutoApplyPresetSetting.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/AutoApplyPresetSetting.tsx new file mode 100644 index 00000000000..cde96246f1c --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/AutoApplyPresetSetting.tsx @@ -0,0 +1,53 @@ +import { Label } from "@superset/ui/label"; +import { Switch } from "@superset/ui/switch"; +import { electronTrpc } from "renderer/lib/electron-trpc"; +import { DEFAULT_AUTO_APPLY_DEFAULT_PRESET } from "shared/constants"; + +export function AutoApplyPresetSetting() { + const utils = electronTrpc.useUtils(); + + const { data: autoApplyDefaultPreset, isLoading } = + electronTrpc.settings.getAutoApplyDefaultPreset.useQuery(); + + const setAutoApplyDefaultPreset = + electronTrpc.settings.setAutoApplyDefaultPreset.useMutation({ + onMutate: async ({ enabled }) => { + await utils.settings.getAutoApplyDefaultPreset.cancel(); + const previous = utils.settings.getAutoApplyDefaultPreset.getData(); + utils.settings.getAutoApplyDefaultPreset.setData(undefined, enabled); + return { previous }; + }, + onError: (_err, _vars, context) => { + if (context?.previous !== undefined) { + utils.settings.getAutoApplyDefaultPreset.setData( + undefined, + context.previous, + ); + } + }, + onSettled: () => { + utils.settings.getAutoApplyDefaultPreset.invalidate(); + }, + }); + + return ( +
+
+ +

+ Automatically apply your default preset when creating new workspaces +

+
+ + setAutoApplyDefaultPreset.mutate({ enabled }) + } + disabled={isLoading || setAutoApplyDefaultPreset.isPending} + /> +
+ ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/LinkBehaviorSetting.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/LinkBehaviorSetting.tsx new file mode 100644 index 00000000000..963a2157f40 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/LinkBehaviorSetting.tsx @@ -0,0 +1,68 @@ +import type { TerminalLinkBehavior } from "@superset/local-db"; +import { Label } from "@superset/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@superset/ui/select"; +import { electronTrpc } from "renderer/lib/electron-trpc"; + +export function LinkBehaviorSetting() { + const utils = electronTrpc.useUtils(); + + const { data: terminalLinkBehavior, isLoading } = + electronTrpc.settings.getTerminalLinkBehavior.useQuery(); + + const setTerminalLinkBehavior = + electronTrpc.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(); + }, + }); + + return ( +
+
+ +

+ Choose how to open file paths when Cmd+clicking in the terminal +

+
+ +
+ ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/PresetRow/PresetRow.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/PresetRow/PresetRow.tsx index 785d5e298db..7f83cb2f54e 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/PresetRow/PresetRow.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/PresetRow/PresetRow.tsx @@ -9,7 +9,7 @@ import { SelectValue, } from "@superset/ui/select"; import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; -import { useRef } from "react"; +import { useEffect, useRef } from "react"; import { useDrag, useDrop } from "react-dnd"; import { HiOutlineStar, HiStar } from "react-icons/hi2"; import { LuGripVertical, LuTrash } from "react-icons/lu"; @@ -125,8 +125,10 @@ export function PresetRow({ }, }); - preview(drop(rowRef)); - drag(dragHandleRef); + useEffect(() => { + preview(drop(rowRef)); + drag(dragHandleRef); + }, [preview, drop, drag]); const handleToggleDefault = () => { onSetDefault(preset.isDefault ? null : preset.id); diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/PresetsSection.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/PresetsSection.tsx new file mode 100644 index 00000000000..725743e9f62 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/PresetsSection.tsx @@ -0,0 +1,420 @@ +import type { ExecutionMode, TerminalPreset } from "@superset/local-db"; +import { Button } from "@superset/ui/button"; +import { Label } from "@superset/ui/label"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { + HiOutlineCheck, + HiOutlinePlus, + HiOutlineQuestionMarkCircle, +} from "react-icons/hi2"; +import { + getPresetIcon, + useIsDarkTheme, +} from "renderer/assets/app-icons/preset-icons"; +import { usePresets } from "renderer/react-query/presets"; +import { + PRESET_COLUMNS, + type PresetColumnKey, +} from "renderer/routes/_authenticated/settings/presets/types"; +import { PresetRow } from "./PresetRow"; + +interface PresetTemplate { + name: string; + preset: { + name: string; + description: string; + cwd: string; + commands: string[]; + }; +} + +const PRESET_TEMPLATES: PresetTemplate[] = [ + { + name: "codex", + preset: { + name: "codex", + description: "Danger mode: All permissions auto-approved", + cwd: "", + commands: [ + 'codex -c model_reasoning_effort="high" --ask-for-approval never --sandbox danger-full-access -c model_reasoning_summary="detailed" -c model_supports_reasoning_summaries=true', + ], + }, + }, + { + name: "claude", + preset: { + name: "claude", + description: "Danger mode: All permissions auto-approved", + cwd: "", + commands: ["claude --dangerously-skip-permissions"], + }, + }, + { + name: "gemini", + preset: { + name: "gemini", + description: "Danger mode: All permissions auto-approved", + cwd: "", + commands: ["gemini --yolo"], + }, + }, + { + name: "cursor-agent", + preset: { + name: "cursor-agent", + description: "Cursor AI agent for terminal-based coding assistance", + cwd: "", + commands: ["cursor-agent"], + }, + }, + { + name: "opencode", + preset: { + name: "opencode", + description: "OpenCode: Open-source AI coding agent", + cwd: "", + commands: ["opencode"], + }, + }, +]; + +interface PresetsSectionProps { + showPresets: boolean; + showQuickAdd: boolean; +} + +export function PresetsSection({ + showPresets, + showQuickAdd, +}: PresetsSectionProps) { + const isDark = useIsDarkTheme(); + const { + presets: serverPresets, + isLoading: isLoadingPresets, + createPreset, + updatePreset, + deletePreset, + setDefaultPreset, + reorderPresets, + } = usePresets(); + + const [localPresets, setLocalPresets] = + useState(serverPresets); + const presetsContainerRef = useRef(null); + const prevPresetsCountRef = useRef(serverPresets.length); + const serverPresetsRef = useRef(serverPresets); + + useEffect(() => { + serverPresetsRef.current = serverPresets; + }, [serverPresets]); + + useEffect(() => { + setLocalPresets(serverPresets); + + if (serverPresets.length > prevPresetsCountRef.current) { + requestAnimationFrame(() => { + presetsContainerRef.current?.scrollTo({ + top: presetsContainerRef.current.scrollHeight, + behavior: "smooth", + }); + }); + } + prevPresetsCountRef.current = serverPresets.length; + }, [serverPresets]); + + const existingPresetNames = useMemo( + () => new Set(serverPresets.map((p) => p.name)), + [serverPresets], + ); + + const isTemplateAdded = (template: PresetTemplate) => + existingPresetNames.has(template.preset.name); + + const handleCellChange = useCallback( + (rowIndex: number, column: PresetColumnKey, value: string) => { + setLocalPresets((prev) => + prev.map((p, i) => (i === rowIndex ? { ...p, [column]: value } : p)), + ); + }, + [], + ); + + const handleCellBlur = useCallback( + (rowIndex: number, column: PresetColumnKey) => { + setLocalPresets((currentLocal) => { + const preset = currentLocal[rowIndex]; + if (!preset) return currentLocal; + const serverPreset = serverPresetsRef.current.find( + (p) => p.id === preset.id, + ); + if (!serverPreset) return currentLocal; + if (preset[column] === serverPreset[column]) return currentLocal; + + updatePreset.mutate({ + id: preset.id, + patch: { [column]: preset[column] }, + }); + return currentLocal; + }); + }, + [updatePreset], + ); + + const handleCommandsChange = useCallback( + (rowIndex: number, commands: string[]) => { + setLocalPresets((prev) => { + const preset = prev[rowIndex]; + const isDelete = preset && commands.length < preset.commands.length; + const newPresets = prev.map((p, i) => + i === rowIndex ? { ...p, commands } : p, + ); + + if (isDelete && preset) { + updatePreset.mutate({ + id: preset.id, + patch: { commands }, + }); + } + return newPresets; + }); + }, + [updatePreset], + ); + + const handleCommandsBlur = useCallback( + (rowIndex: number) => { + setLocalPresets((currentLocal) => { + const preset = currentLocal[rowIndex]; + if (!preset) return currentLocal; + const serverPreset = serverPresetsRef.current.find( + (p) => p.id === preset.id, + ); + if (!serverPreset) return currentLocal; + if ( + JSON.stringify(preset.commands) === + JSON.stringify(serverPreset.commands) + ) + return currentLocal; + + updatePreset.mutate({ + id: preset.id, + patch: { commands: preset.commands }, + }); + return currentLocal; + }); + }, + [updatePreset], + ); + + const handleExecutionModeChange = useCallback( + (rowIndex: number, mode: ExecutionMode) => { + setLocalPresets((currentLocal) => { + const preset = currentLocal[rowIndex]; + if (!preset) return currentLocal; + + const newPresets = currentLocal.map((p, i) => + i === rowIndex ? { ...p, executionMode: mode } : p, + ); + + updatePreset.mutate({ + id: preset.id, + patch: { executionMode: mode }, + }); + + return newPresets; + }); + }, + [updatePreset], + ); + + const handleAddRow = useCallback(() => { + createPreset.mutate({ + name: "", + cwd: "", + commands: [""], + }); + }, [createPreset]); + + const handleAddTemplate = useCallback( + (template: PresetTemplate) => { + if (existingPresetNames.has(template.preset.name)) return; + createPreset.mutate(template.preset); + }, + [createPreset, existingPresetNames], + ); + + const handleDeleteRow = useCallback( + (rowIndex: number) => { + setLocalPresets((currentLocal) => { + const preset = currentLocal[rowIndex]; + if (preset) { + deletePreset.mutate({ id: preset.id }); + } + return currentLocal; + }); + }, + [deletePreset], + ); + + const handleSetDefault = useCallback( + (presetId: string | null) => { + setDefaultPreset.mutate({ id: presetId }); + }, + [setDefaultPreset], + ); + + const handleLocalReorder = useCallback( + (fromIndex: number, toIndex: number) => { + setLocalPresets((prev) => { + const newPresets = [...prev]; + const [removed] = newPresets.splice(fromIndex, 1); + newPresets.splice(toIndex, 0, removed); + return newPresets; + }); + }, + [], + ); + + const handlePersistReorder = useCallback( + (presetId: string, targetIndex: number) => { + reorderPresets.mutate({ presetId, targetIndex }); + }, + [reorderPresets], + ); + + return ( +
+
+
+ +

+ Presets let you quickly launch terminals with pre-configured + commands. +

+
+ {showPresets && ( + + )} +
+ + {showQuickAdd && ( +
+ + Quick add: + + {PRESET_TEMPLATES.map((template) => { + const alreadyAdded = isTemplateAdded(template); + const presetIcon = getPresetIcon(template.name, isDark); + return ( + + + + + + {alreadyAdded ? "Already added" : template.preset.description} + + + ); + })} +
+ )} + + {showPresets && ( +
+
+
+ {PRESET_COLUMNS.map((column) => ( +
+ {column.label} +
+ ))} + + +
+ Mode + +
+
+ +

Execution Mode

+

+ Sequential: Commands run one after another in + a single terminal (joined with &&) +

+

+ Parallel: Each command runs in its own split + pane within a single tab +

+
+
+
+ Actions +
+
+ +
+ {isLoadingPresets ? ( +
+ Loading presets... +
+ ) : localPresets.length > 0 ? ( + localPresets.map((preset, index) => ( + + )) + ) : ( +
+ No presets yet. Click "Add Preset" to create your first preset. +
+ )} +
+
+ )} +
+ ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/SessionsSection.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/SessionsSection.tsx new file mode 100644 index 00000000000..36dda370f0f --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/SessionsSection.tsx @@ -0,0 +1,442 @@ +import { + AlertDialog, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@superset/ui/alert-dialog"; +import { Button } from "@superset/ui/button"; +import { Label } from "@superset/ui/label"; +import { toast } from "@superset/ui/sonner"; +import { useMemo, useState } from "react"; +import { electronTrpc } from "renderer/lib/electron-trpc"; + +export function SessionsSection() { + const utils = electronTrpc.useUtils(); + + const { data: daemonSessions } = + electronTrpc.terminal.listDaemonSessions.useQuery(); + const sessions = daemonSessions?.sessions ?? []; + const aliveSessions = useMemo( + () => sessions.filter((session) => session.isAlive), + [sessions], + ); + const sessionsSorted = useMemo(() => { + return [...aliveSessions].sort((a, b) => { + if (a.attachedClients !== b.attachedClients) { + return b.attachedClients - a.attachedClients; + } + const aTime = a.lastAttachedAt ? Date.parse(a.lastAttachedAt) : 0; + const bTime = b.lastAttachedAt ? Date.parse(b.lastAttachedAt) : 0; + return bTime - aTime; + }); + }, [aliveSessions]); + + const [confirmKillAllOpen, setConfirmKillAllOpen] = useState(false); + const [confirmClearHistoryOpen, setConfirmClearHistoryOpen] = useState(false); + const [confirmRestartDaemonOpen, setConfirmRestartDaemonOpen] = + useState(false); + const [showSessionList, setShowSessionList] = useState(false); + const [pendingKillSession, setPendingKillSession] = useState<{ + sessionId: string; + workspaceId: string; + } | null>(null); + + const killAllDaemonSessions = + electronTrpc.terminal.killAllDaemonSessions.useMutation({ + onMutate: async () => { + await utils.terminal.listDaemonSessions.cancel(); + const previous = utils.terminal.listDaemonSessions.getData(); + utils.terminal.listDaemonSessions.setData(undefined, { + sessions: [], + }); + return { previous }; + }, + onSuccess: (result) => { + if (result.remainingCount > 0) { + toast.warning("Some sessions could not be killed", { + description: `${result.killedCount} terminated, ${result.remainingCount} remaining`, + }); + } else { + toast.success("Killed all terminal sessions", { + description: `${result.killedCount} sessions terminated`, + }); + } + }, + onError: (error, _vars, context) => { + if (context?.previous) { + utils.terminal.listDaemonSessions.setData( + undefined, + context.previous, + ); + } + toast.error("Failed to kill sessions", { + description: error.message, + }); + }, + onSettled: () => { + setTimeout(() => { + utils.terminal.listDaemonSessions.invalidate(); + }, 300); + }, + }); + + const clearTerminalHistory = + electronTrpc.terminal.clearTerminalHistory.useMutation({ + onSuccess: () => { + toast.success("Cleared terminal history"); + utils.terminal.listDaemonSessions.invalidate(); + }, + onError: (error) => { + toast.error("Failed to clear terminal history", { + description: error.message, + }); + }, + }); + + const killDaemonSession = electronTrpc.terminal.kill.useMutation({ + onSuccess: () => { + toast.success("Killed terminal session"); + utils.terminal.listDaemonSessions.invalidate(); + }, + onError: (error) => { + toast.error("Failed to kill session", { + description: error.message, + }); + }, + }); + + const restartDaemon = electronTrpc.terminal.restartDaemon.useMutation({ + onSuccess: () => { + toast.success("Daemon restarted", { + description: + "Terminal daemon has been restarted. Open a terminal to spawn a fresh daemon.", + }); + utils.terminal.listDaemonSessions.invalidate(); + }, + onError: (error) => { + toast.error("Failed to restart daemon", { + description: error.message, + }); + }, + }); + + const formatTimestamp = (value?: string) => { + if (!value) return "—"; + return value.replace("T", " ").replace(/\.\d+Z$/, "Z"); + }; + + return ( + <> +
+
+
+ + +
+

+ Daemon sessions running: {aliveSessions.length} +

+ {aliveSessions.length >= 20 && ( +

+ Large numbers of persistent terminals can increase CPU/memory + usage. Consider killing old sessions if you notice slowdowns. +

+ )} +
+ +
+ + + + +
+ + {showSessionList && aliveSessions.length > 0 && ( +
+
+ + + + + + + + + + + + + {sessionsSorted.map((session) => ( + + + + + + + + + ))} + +
+ Workspace + Session + Clients + PID + Last attached + Action
+ {session.workspaceId} + + {session.sessionId} + + {session.attachedClients} + + {session.pid ?? "—"} + + {formatTimestamp(session.lastAttachedAt)} + + +
+
+
+ )} +
+ + + + + + Kill all terminal sessions? + + +
+ + This will terminate all persistent terminal processes (builds, + tests, agents, etc.). + + + You can't undo this action. Terminal panes will show "Process + exited" and can be restarted. + +
+
+
+ + + + +
+
+ + + + + + Clear terminal history? + + +
+ + This deletes the saved scrollback used for reboot/crash + recovery. + + + Running terminal processes continue, but older output may no + longer be available after restarting the app. + +
+
+
+ + + + +
+
+ + { + if (!open) setPendingKillSession(null); + }} + > + + + + Kill terminal session? + + +
+ + This will terminate the session and its underlying process. + + {pendingKillSession && ( + + {pendingKillSession.workspaceId} /{" "} + {pendingKillSession.sessionId} + + )} +
+
+
+ + + + +
+
+ + + + + + Restart terminal daemon? + + +
+ + This will shut down the terminal daemon process and kill all + running sessions. Use this to fix terminals that are stuck or + unresponsive. + + + A fresh daemon will start automatically when you open a new + terminal. + +
+
+
+ + + + +
+
+ + ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/utils/settings-search/settings-search.ts b/apps/desktop/src/renderer/routes/_authenticated/settings/utils/settings-search/settings-search.ts index c9632ae229a..73ef7c22265 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/utils/settings-search/settings-search.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/utils/settings-search/settings-search.ts @@ -28,6 +28,7 @@ export const SETTING_ITEM_ID = { TERMINAL_PRESETS: "terminal-presets", TERMINAL_QUICK_ADD: "terminal-quick-add", TERMINAL_AUTO_APPLY_PRESET: "terminal-auto-apply-preset", + TERMINAL_APPLY_PRESET_ON_NEW_TAB: "terminal-apply-preset-on-new-tab", TERMINAL_SESSIONS: "terminal-sessions", TERMINAL_LINK_BEHAVIOR: "terminal-link-behavior", @@ -405,8 +406,9 @@ export const SETTINGS_ITEMS: SettingsItem[] = [ { id: SETTING_ITEM_ID.TERMINAL_AUTO_APPLY_PRESET, section: "terminal", - title: "Auto-apply Default Preset", - description: "Automatically apply default preset when creating workspaces", + title: "Apply Preset on Workspace Creation", + description: + "Automatically apply default preset when creating new workspaces", keywords: [ "terminal", "preset", @@ -420,6 +422,25 @@ export const SETTINGS_ITEMS: SettingsItem[] = [ "launch", ], }, + { + id: SETTING_ITEM_ID.TERMINAL_APPLY_PRESET_ON_NEW_TAB, + section: "terminal", + title: "Apply Preset on New Tab", + description: + "Automatically apply default preset when opening new tabs, panes, or splits", + keywords: [ + "terminal", + "preset", + "default", + "auto", + "apply", + "tab", + "pane", + "split", + "new", + "open", + ], + }, { id: SETTING_ITEM_ID.TERMINAL_SESSIONS, section: "terminal", diff --git a/apps/desktop/src/renderer/stores/tabs/useTabsWithPresets.ts b/apps/desktop/src/renderer/stores/tabs/useTabsWithPresets.ts index 2d082087402..4424a1da3cb 100644 --- a/apps/desktop/src/renderer/stores/tabs/useTabsWithPresets.ts +++ b/apps/desktop/src/renderer/stores/tabs/useTabsWithPresets.ts @@ -1,7 +1,9 @@ import type { TerminalPreset } from "@superset/local-db"; import { useCallback, useMemo } from "react"; import type { MosaicBranch } from "react-mosaic-component"; +import { electronTrpc } from "renderer/lib/electron-trpc"; import { usePresets } from "renderer/react-query/presets"; +import { DEFAULT_APPLY_PRESET_ON_NEW_TAB } from "shared/constants"; import { useTabsStore } from "./store"; import type { AddTabOptions } from "./types"; @@ -13,6 +15,11 @@ import type { AddTabOptions } from "./types"; export function useTabsWithPresets() { const { defaultPreset } = usePresets(); + const { data: applyPresetOnNewTab } = + electronTrpc.settings.getApplyPresetOnNewTab.useQuery(); + const isNewTabPresetEnabled = + applyPresetOnNewTab ?? DEFAULT_APPLY_PRESET_ON_NEW_TAB; + const storeAddTab = useTabsStore((s) => s.addTab); const storeAddTabWithMultiplePanes = useTabsStore( (s) => s.addTabWithMultiplePanes, @@ -24,19 +31,20 @@ export function useTabsWithPresets() { const renameTab = useTabsStore((s) => s.renameTab); const defaultPresetOptions: AddTabOptions | undefined = useMemo(() => { - if (!defaultPreset) return undefined; + if (!isNewTabPresetEnabled || !defaultPreset) return undefined; return { initialCommands: defaultPreset.commands, initialCwd: defaultPreset.cwd || undefined, }; - }, [defaultPreset]); + }, [isNewTabPresetEnabled, defaultPreset]); const shouldUseParallelMode = useMemo(() => { return ( + isNewTabPresetEnabled && defaultPreset?.executionMode === "parallel" && defaultPreset.commands.length > 1 ); - }, [defaultPreset]); + }, [isNewTabPresetEnabled, defaultPreset]); const addTab = useCallback( (workspaceId: string, options?: AddTabOptions) => { @@ -59,7 +67,7 @@ export function useTabsWithPresets() { const result = storeAddTab(workspaceId, defaultPresetOptions); - if (defaultPreset?.name) { + if (isNewTabPresetEnabled && defaultPreset?.name) { renameTab(result.tabId, defaultPreset.name); } @@ -71,6 +79,7 @@ export function useTabsWithPresets() { defaultPresetOptions, defaultPreset, shouldUseParallelMode, + isNewTabPresetEnabled, renameTab, ], ); diff --git a/apps/desktop/src/shared/constants.ts b/apps/desktop/src/shared/constants.ts index 2eadec60637..f588b40698a 100644 --- a/apps/desktop/src/shared/constants.ts +++ b/apps/desktop/src/shared/constants.ts @@ -54,6 +54,7 @@ export const MOCK_ORG_ID = "mock-org-id"; export const DEFAULT_CONFIRM_ON_QUIT = true; export const DEFAULT_TERMINAL_LINK_BEHAVIOR = "external-editor" as const; export const DEFAULT_AUTO_APPLY_DEFAULT_PRESET = true; +export const DEFAULT_APPLY_PRESET_ON_NEW_TAB = true; export const DEFAULT_TELEMETRY_ENABLED = true; // External links (documentation, help resources, etc.) diff --git a/packages/local-db/drizzle/0020_add_apply_preset_on_new_tab.sql b/packages/local-db/drizzle/0020_add_apply_preset_on_new_tab.sql new file mode 100644 index 00000000000..cf01395d1ee --- /dev/null +++ b/packages/local-db/drizzle/0020_add_apply_preset_on_new_tab.sql @@ -0,0 +1 @@ +ALTER TABLE `settings` ADD `apply_preset_on_new_tab` integer; \ No newline at end of file diff --git a/packages/local-db/drizzle/meta/0020_snapshot.json b/packages/local-db/drizzle/meta/0020_snapshot.json new file mode 100644 index 00000000000..885a1603cdb --- /dev/null +++ b/packages/local-db/drizzle/meta/0020_snapshot.json @@ -0,0 +1,1078 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "94b221c1-711b-4c2b-85ff-111e7133bd7d", + "prevId": "732c942c-5f01-451f-a6cf-92c38b434076", + "tables": { + "organization_members": { + "name": "organization_members", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "organization_members_organization_id_idx": { + "name": "organization_members_organization_id_idx", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "organization_members_user_id_idx": { + "name": "organization_members_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "organization_members_organization_id_organizations_id_fk": { + "name": "organization_members_organization_id_organizations_id_fk", + "tableFrom": "organization_members", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_members_user_id_users_id_fk": { + "name": "organization_members_user_id_users_id_fk", + "tableFrom": "organization_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "organizations": { + "name": "organizations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "clerk_org_id": { + "name": "clerk_org_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "github_org": { + "name": "github_org", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "organizations_clerk_org_id_unique": { + "name": "organizations_clerk_org_id_unique", + "columns": [ + "clerk_org_id" + ], + "isUnique": true + }, + "organizations_slug_unique": { + "name": "organizations_slug_unique", + "columns": [ + "slug" + ], + "isUnique": true + }, + "organizations_slug_idx": { + "name": "organizations_slug_idx", + "columns": [ + "slug" + ], + "isUnique": false + }, + "organizations_clerk_org_id_idx": { + "name": "organizations_clerk_org_id_idx", + "columns": [ + "clerk_org_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "projects": { + "name": "projects", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "main_repo_path": { + "name": "main_repo_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_opened_at": { + "name": "last_opened_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config_toast_dismissed": { + "name": "config_toast_dismissed", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_branch": { + "name": "default_branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "github_owner": { + "name": "github_owner", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "branch_prefix_mode": { + "name": "branch_prefix_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "branch_prefix_custom": { + "name": "branch_prefix_custom", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hide_image": { + "name": "hide_image", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "projects_main_repo_path_idx": { + "name": "projects_main_repo_path_idx", + "columns": [ + "main_repo_path" + ], + "isUnique": false + }, + "projects_last_opened_at_idx": { + "name": "projects_last_opened_at_idx", + "columns": [ + "last_opened_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "settings": { + "name": "settings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "last_active_workspace_id": { + "name": "last_active_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_app": { + "name": "last_used_app", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_presets": { + "name": "terminal_presets", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_presets_initialized": { + "name": "terminal_presets_initialized", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "selected_ringtone_id": { + "name": "selected_ringtone_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_on_quit": { + "name": "confirm_on_quit", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_link_behavior": { + "name": "terminal_link_behavior", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "persist_terminal": { + "name": "persist_terminal", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "auto_apply_default_preset": { + "name": "auto_apply_default_preset", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "apply_preset_on_new_tab": { + "name": "apply_preset_on_new_tab", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "branch_prefix_mode": { + "name": "branch_prefix_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "branch_prefix_custom": { + "name": "branch_prefix_custom", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notification_sounds_muted": { + "name": "notification_sounds_muted", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "delete_local_branch": { + "name": "delete_local_branch", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tasks": { + "name": "tasks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_color": { + "name": "status_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_type": { + "name": "status_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_position": { + "name": "status_position", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repository_id": { + "name": "repository_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "assignee_id": { + "name": "assignee_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "creator_id": { + "name": "creator_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "estimate": { + "name": "estimate", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "due_date": { + "name": "due_date", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "labels": { + "name": "labels", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pr_url": { + "name": "pr_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_provider": { + "name": "external_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_key": { + "name": "external_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_url": { + "name": "external_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "tasks_slug_unique": { + "name": "tasks_slug_unique", + "columns": [ + "slug" + ], + "isUnique": true + }, + "tasks_slug_idx": { + "name": "tasks_slug_idx", + "columns": [ + "slug" + ], + "isUnique": false + }, + "tasks_organization_id_idx": { + "name": "tasks_organization_id_idx", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "tasks_assignee_id_idx": { + "name": "tasks_assignee_id_idx", + "columns": [ + "assignee_id" + ], + "isUnique": false + }, + "tasks_status_idx": { + "name": "tasks_status_idx", + "columns": [ + "status" + ], + "isUnique": false + }, + "tasks_created_at_idx": { + "name": "tasks_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "tasks_organization_id_organizations_id_fk": { + "name": "tasks_organization_id_organizations_id_fk", + "tableFrom": "tasks", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tasks_assignee_id_users_id_fk": { + "name": "tasks_assignee_id_users_id_fk", + "tableFrom": "tasks", + "tableTo": "users", + "columnsFrom": [ + "assignee_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tasks_creator_id_users_id_fk": { + "name": "tasks_creator_id_users_id_fk", + "tableFrom": "tasks", + "tableTo": "users", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "clerk_id": { + "name": "clerk_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "users_clerk_id_unique": { + "name": "users_clerk_id_unique", + "columns": [ + "clerk_id" + ], + "isUnique": true + }, + "users_email_unique": { + "name": "users_email_unique", + "columns": [ + "email" + ], + "isUnique": true + }, + "users_email_idx": { + "name": "users_email_idx", + "columns": [ + "email" + ], + "isUnique": false + }, + "users_clerk_id_idx": { + "name": "users_clerk_id_idx", + "columns": [ + "clerk_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "workspaces": { + "name": "workspaces", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "worktree_id": { + "name": "worktree_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_opened_at": { + "name": "last_opened_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_unread": { + "name": "is_unread", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "is_unnamed": { + "name": "is_unnamed", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "deleting_at": { + "name": "deleting_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "workspaces_project_id_idx": { + "name": "workspaces_project_id_idx", + "columns": [ + "project_id" + ], + "isUnique": false + }, + "workspaces_worktree_id_idx": { + "name": "workspaces_worktree_id_idx", + "columns": [ + "worktree_id" + ], + "isUnique": false + }, + "workspaces_last_opened_at_idx": { + "name": "workspaces_last_opened_at_idx", + "columns": [ + "last_opened_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "workspaces_project_id_projects_id_fk": { + "name": "workspaces_project_id_projects_id_fk", + "tableFrom": "workspaces", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspaces_worktree_id_worktrees_id_fk": { + "name": "workspaces_worktree_id_worktrees_id_fk", + "tableFrom": "workspaces", + "tableTo": "worktrees", + "columnsFrom": [ + "worktree_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "worktrees": { + "name": "worktrees", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "base_branch": { + "name": "base_branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "git_status": { + "name": "git_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "github_status": { + "name": "github_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "worktrees_project_id_idx": { + "name": "worktrees_project_id_idx", + "columns": [ + "project_id" + ], + "isUnique": false + }, + "worktrees_branch_idx": { + "name": "worktrees_branch_idx", + "columns": [ + "branch" + ], + "isUnique": false + } + }, + "foreignKeys": { + "worktrees_project_id_projects_id_fk": { + "name": "worktrees_project_id_projects_id_fk", + "tableFrom": "worktrees", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/packages/local-db/drizzle/meta/_journal.json b/packages/local-db/drizzle/meta/_journal.json index d6ac4a2497b..93d72b999d9 100644 --- a/packages/local-db/drizzle/meta/_journal.json +++ b/packages/local-db/drizzle/meta/_journal.json @@ -141,6 +141,13 @@ "when": 1770438863796, "tag": "0019_add_hide_image_to_projects", "breakpoints": true + }, + { + "idx": 20, + "version": "6", + "when": 1770596618589, + "tag": "0020_add_apply_preset_on_new_tab", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/local-db/src/schema/schema.ts b/packages/local-db/src/schema/schema.ts index 74757311d4d..42fb268a93a 100644 --- a/packages/local-db/src/schema/schema.ts +++ b/packages/local-db/src/schema/schema.ts @@ -151,6 +151,9 @@ export const settings = sqliteTable("settings", { autoApplyDefaultPreset: integer("auto_apply_default_preset", { mode: "boolean", }), + applyPresetOnNewTab: integer("apply_preset_on_new_tab", { + mode: "boolean", + }), branchPrefixMode: text("branch_prefix_mode").$type(), branchPrefixCustom: text("branch_prefix_custom"), notificationSoundsMuted: integer("notification_sounds_muted", { From 93ca149a06c058e30fd48d396a9322a7a7b9429d Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Sun, 8 Feb 2026 17:29:26 -0800 Subject: [PATCH 2/9] feat(desktop): make preset auto-apply a per-preset setting instead of global toggle Replace the global "apply preset on new tab" setting with per-preset flags (applyOnWorkspaceCreated, applyOnNewTab) that give users granular control over when each preset auto-applies. Each preset now shows two toggle icons in the row: folder+ for workspace creation and document+ for new tab. --- .../src/lib/trpc/routers/settings/index.ts | 69 +- .../routers/workspaces/procedures/init.ts | 6 +- .../src/renderer/react-query/presets/index.ts | 20 + .../TerminalSettings/TerminalSettings.tsx | 8 - .../components/ApplyPresetOnNewTabSetting.tsx | 57 - .../components/AutoApplyPresetSetting.tsx | 5 +- .../components/PresetRow/PresetRow.tsx | 64 +- .../components/PresetsSection.tsx | 17 +- .../utils/settings-search/settings-search.ts | 24 +- .../stores/tabs/useTabsWithPresets.ts | 48 +- apps/desktop/src/shared/constants.ts | 1 - .../0020_add_apply_preset_on_new_tab.sql | 1 - .../local-db/drizzle/meta/0020_snapshot.json | 1078 ----------------- packages/local-db/drizzle/meta/_journal.json | 7 - packages/local-db/src/schema/schema.ts | 3 - packages/local-db/src/schema/zod.ts | 2 + 16 files changed, 156 insertions(+), 1254 deletions(-) delete mode 100644 apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/ApplyPresetOnNewTabSetting.tsx delete mode 100644 packages/local-db/drizzle/0020_add_apply_preset_on_new_tab.sql delete mode 100644 packages/local-db/drizzle/meta/0020_snapshot.json diff --git a/apps/desktop/src/lib/trpc/routers/settings/index.ts b/apps/desktop/src/lib/trpc/routers/settings/index.ts index e9451a677e1..cb11dcd4008 100644 --- a/apps/desktop/src/lib/trpc/routers/settings/index.ts +++ b/apps/desktop/src/lib/trpc/routers/settings/index.ts @@ -10,7 +10,6 @@ import { app } from "electron"; import { quitWithoutConfirmation } from "main/index"; import { localDb } from "main/lib/local-db"; import { - DEFAULT_APPLY_PRESET_ON_NEW_TAB, DEFAULT_AUTO_APPLY_DEFAULT_PRESET, DEFAULT_CONFIRM_ON_QUIT, DEFAULT_TERMINAL_LINK_BEHAVIOR, @@ -160,6 +159,34 @@ export const createSettingsRouter = () => { return { success: true }; }), + setPresetAutoApply: publicProcedure + .input( + z.object({ + id: z.string().nullable(), + field: z.enum(["applyOnWorkspaceCreated", "applyOnNewTab"]), + }), + ) + .mutation(({ input }) => { + const row = getSettings(); + const presets = row.terminalPresets ?? []; + + const updatedPresets = presets.map((p) => ({ + ...p, + [input.field]: input.id === p.id ? true : undefined, + })); + + localDb + .insert(settings) + .values({ id: 1, terminalPresets: updatedPresets }) + .onConflictDoUpdate({ + target: settings.id, + set: { terminalPresets: updatedPresets }, + }) + .run(); + + return { success: true }; + }), + reorderTerminalPresets: publicProcedure .input( z.object({ @@ -207,6 +234,26 @@ export const createSettingsRouter = () => { return presets.find((p) => p.isDefault) ?? null; }), + getWorkspaceCreationPreset: publicProcedure.query(() => { + const row = getSettings(); + const presets = row.terminalPresets ?? []; + return ( + presets.find((p) => p.applyOnWorkspaceCreated) ?? + presets.find((p) => p.isDefault) ?? + null + ); + }), + + getNewTabPreset: publicProcedure.query(() => { + const row = getSettings(); + const presets = row.terminalPresets ?? []; + return ( + presets.find((p) => p.applyOnNewTab) ?? + presets.find((p) => p.isDefault) ?? + null + ); + }), + getSelectedRingtoneId: publicProcedure.query(() => { const row = getSettings(); const storedId = row.selectedRingtoneId; @@ -315,26 +362,6 @@ export const createSettingsRouter = () => { return { success: true }; }), - getApplyPresetOnNewTab: publicProcedure.query(() => { - const row = getSettings(); - return row.applyPresetOnNewTab ?? DEFAULT_APPLY_PRESET_ON_NEW_TAB; - }), - - setApplyPresetOnNewTab: publicProcedure - .input(z.object({ enabled: z.boolean() })) - .mutation(({ input }) => { - localDb - .insert(settings) - .values({ id: 1, applyPresetOnNewTab: input.enabled }) - .onConflictDoUpdate({ - target: settings.id, - set: { applyPresetOnNewTab: input.enabled }, - }) - .run(); - - return { success: true }; - }), - restartApp: publicProcedure.mutation(() => { app.relaunch(); quitWithoutConfirmation(); diff --git a/apps/desktop/src/lib/trpc/routers/workspaces/procedures/init.ts b/apps/desktop/src/lib/trpc/routers/workspaces/procedures/init.ts index 0e380dc3e57..b467f4d0011 100644 --- a/apps/desktop/src/lib/trpc/routers/workspaces/procedures/init.ts +++ b/apps/desktop/src/lib/trpc/routers/workspaces/procedures/init.ts @@ -13,7 +13,11 @@ function getDefaultPreset() { const row = localDb.select().from(settings).get(); if (!row) return null; const presets = row.terminalPresets ?? []; - return presets.find((p) => p.isDefault) ?? null; + return ( + presets.find((p) => p.applyOnWorkspaceCreated) ?? + presets.find((p) => p.isDefault) ?? + null + ); } export const createInitProcedures = () => { diff --git a/apps/desktop/src/renderer/react-query/presets/index.ts b/apps/desktop/src/renderer/react-query/presets/index.ts index a2c2b888985..4100564f13c 100644 --- a/apps/desktop/src/renderer/react-query/presets/index.ts +++ b/apps/desktop/src/renderer/react-query/presets/index.ts @@ -65,6 +65,24 @@ function useSetDefaultPreset( }); } +function useSetPresetAutoApply( + options?: Parameters< + typeof electronTrpc.settings.setPresetAutoApply.useMutation + >[0], +) { + const utils = electronTrpc.useUtils(); + + return electronTrpc.settings.setPresetAutoApply.useMutation({ + ...options, + onSuccess: async (...args) => { + await utils.settings.getTerminalPresets.invalidate(); + await utils.settings.getWorkspaceCreationPreset.invalidate(); + await utils.settings.getNewTabPreset.invalidate(); + await options?.onSuccess?.(...args); + }, + }); +} + function useReorderTerminalPresets( options?: Parameters< typeof electronTrpc.settings.reorderTerminalPresets.useMutation @@ -96,6 +114,7 @@ export function usePresets() { const updatePreset = useUpdateTerminalPreset(); const deletePreset = useDeleteTerminalPreset(); const setDefaultPreset = useSetDefaultPreset(); + const setPresetAutoApply = useSetPresetAutoApply(); const reorderPresets = useReorderTerminalPresets(); return { @@ -106,6 +125,7 @@ export function usePresets() { updatePreset, deletePreset, setDefaultPreset, + setPresetAutoApply, reorderPresets, }; } diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/TerminalSettings.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/TerminalSettings.tsx index ae6a4ef6c8f..136ad182d72 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/TerminalSettings.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/TerminalSettings.tsx @@ -4,7 +4,6 @@ import { SETTING_ITEM_ID, type SettingItemId, } from "../../../utils/settings-search"; -import { ApplyPresetOnNewTabSetting } from "./components/ApplyPresetOnNewTabSetting"; import { AutoApplyPresetSetting } from "./components/AutoApplyPresetSetting"; import { LinkBehaviorSetting } from "./components/LinkBehaviorSetting"; import { PresetsSection } from "./components/PresetsSection"; @@ -48,10 +47,6 @@ export function TerminalSettings({ visibleItems }: TerminalSettingsProps) { SETTING_ITEM_ID.TERMINAL_AUTO_APPLY_PRESET, visibleItems, ); - const showApplyPresetOnNewTab = isItemVisible( - SETTING_ITEM_ID.TERMINAL_APPLY_PRESET_ON_NEW_TAB, - visibleItems, - ); const showLinkBehavior = isItemVisible( SETTING_ITEM_ID.TERMINAL_LINK_BEHAVIOR, visibleItems, @@ -79,9 +74,6 @@ export function TerminalSettings({ visibleItems }: TerminalSettingsProps) { /> )} {showAutoApplyPreset && } - {showApplyPresetOnNewTab && ( - - )} {showLinkBehavior && } {showSessions && } diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/ApplyPresetOnNewTabSetting.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/ApplyPresetOnNewTabSetting.tsx deleted file mode 100644 index 82c187e17cc..00000000000 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/ApplyPresetOnNewTabSetting.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { Label } from "@superset/ui/label"; -import { Switch } from "@superset/ui/switch"; -import { electronTrpc } from "renderer/lib/electron-trpc"; -import { DEFAULT_APPLY_PRESET_ON_NEW_TAB } from "shared/constants"; - -export function ApplyPresetOnNewTabSetting() { - const utils = electronTrpc.useUtils(); - - const { data: applyPresetOnNewTab, isLoading } = - electronTrpc.settings.getApplyPresetOnNewTab.useQuery(); - - const setApplyPresetOnNewTab = - electronTrpc.settings.setApplyPresetOnNewTab.useMutation({ - onMutate: async ({ enabled }) => { - await utils.settings.getApplyPresetOnNewTab.cancel(); - const previous = utils.settings.getApplyPresetOnNewTab.getData(); - utils.settings.getApplyPresetOnNewTab.setData(undefined, enabled); - return { previous }; - }, - onError: (_err, _vars, context) => { - if (context?.previous !== undefined) { - utils.settings.getApplyPresetOnNewTab.setData( - undefined, - context.previous, - ); - } - }, - onSettled: () => { - utils.settings.getApplyPresetOnNewTab.invalidate(); - }, - }); - - return ( -
-
- -

- Automatically apply your default preset when opening new tabs, panes, - or splits -

-
- - setApplyPresetOnNewTab.mutate({ enabled }) - } - disabled={isLoading || setApplyPresetOnNewTab.isPending} - /> -
- ); -} diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/AutoApplyPresetSetting.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/AutoApplyPresetSetting.tsx index cde96246f1c..4f69fe7142d 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/AutoApplyPresetSetting.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/AutoApplyPresetSetting.tsx @@ -34,10 +34,11 @@ export function AutoApplyPresetSetting() {

- Automatically apply your default preset when creating new workspaces + Automatically apply the workspace creation preset when creating new + workspaces

void; onExecutionModeChange: (rowIndex: number, mode: ExecutionMode) => void; onDelete: (rowIndex: number) => void; - onSetDefault: (presetId: string | null) => void; + onToggleAutoApply: (presetId: string | null, field: AutoApplyField) => void; onLocalReorder: (fromIndex: number, toIndex: number) => void; onPersistReorder: (presetId: string, targetIndex: number) => void; } @@ -92,7 +94,7 @@ export function PresetRow({ onCommandsBlur, onExecutionModeChange, onDelete, - onSetDefault, + onToggleAutoApply, onLocalReorder, onPersistReorder, }: PresetRowProps) { @@ -130,9 +132,12 @@ export function PresetRow({ drag(dragHandleRef); }, [preview, drop, drag]); - const handleToggleDefault = () => { - onSetDefault(preset.isDefault ? null : preset.id); - }; + const isWorkspaceCreation = + preset.applyOnWorkspaceCreated || + (!preset.applyOnNewTab && preset.isDefault); + const isNewTab = + preset.applyOnNewTab || + (!preset.applyOnWorkspaceCreated && preset.isDefault); return (
-
+
+ + + {isWorkspaceCreation + ? "Applied on workspace creation (click to remove)" + : "Apply on workspace creation"} + + + + + - {preset.isDefault - ? "Remove as default" - : "Set as default for new terminals"} + {isNewTab + ? "Applied on new tab (click to remove)" + : "Apply on new tab"} @@ -222,7 +231,11 @@ export function PresetRow({ className={`h-8 w-8 p-0 ${isNewTab ? "text-green-500 hover:text-green-600" : "text-muted-foreground hover:text-foreground"}`} aria-label={isNewTab ? "Remove from new tab" : "Apply on new tab"} > - + {isNewTab ? ( + + ) : ( + + )} From e96137aa94c21930a2a827d95efd4d462e46959e Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Sun, 8 Feb 2026 20:39:51 -0800 Subject: [PATCH 4/9] feat(desktop): use badge toggles and allow multiple presets per trigger Replace icon buttons with clickable WS/Tab badges that toggle independently. Multiple presets can now be tagged for the same trigger - all tagged presets fire when a workspace is created or a new tab opens. - setPresetAutoApply toggles per-preset (no longer mutually exclusive) - getWorkspaceCreationPresets/getNewTabPresets return arrays - WorkspaceInitEffects applies all workspace-tagged presets - useTabsWithPresets opens a tab for each new-tab-tagged preset --- .../src/lib/trpc/routers/settings/index.ts | 30 +++-- .../routers/workspaces/procedures/init.ts | 18 +-- .../src/renderer/react-query/presets/index.ts | 4 +- .../workspaces/useCreateBranchWorkspace.ts | 1 + .../tools/start-claude-session.ts | 1 + .../components/PresetRow/PresetRow.tsx | 81 +++++------- .../components/PresetsSection.tsx | 5 +- .../main/components/WorkspaceInitEffects.tsx | 36 +++--- .../stores/tabs/useTabsWithPresets.ts | 120 ++++++++---------- .../src/renderer/stores/workspace-init.ts | 1 + 10 files changed, 140 insertions(+), 157 deletions(-) diff --git a/apps/desktop/src/lib/trpc/routers/settings/index.ts b/apps/desktop/src/lib/trpc/routers/settings/index.ts index cb11dcd4008..3b5f28a2be7 100644 --- a/apps/desktop/src/lib/trpc/routers/settings/index.ts +++ b/apps/desktop/src/lib/trpc/routers/settings/index.ts @@ -162,8 +162,9 @@ export const createSettingsRouter = () => { setPresetAutoApply: publicProcedure .input( z.object({ - id: z.string().nullable(), + id: z.string(), field: z.enum(["applyOnWorkspaceCreated", "applyOnNewTab"]), + enabled: z.boolean(), }), ) .mutation(({ input }) => { @@ -172,7 +173,10 @@ export const createSettingsRouter = () => { const updatedPresets = presets.map((p) => ({ ...p, - [input.field]: input.id === p.id ? true : undefined, + [input.field]: + p.id === input.id + ? input.enabled || undefined + : p[input.field as keyof typeof p], })); localDb @@ -234,24 +238,22 @@ export const createSettingsRouter = () => { return presets.find((p) => p.isDefault) ?? null; }), - getWorkspaceCreationPreset: publicProcedure.query(() => { + getWorkspaceCreationPresets: publicProcedure.query(() => { const row = getSettings(); const presets = row.terminalPresets ?? []; - return ( - presets.find((p) => p.applyOnWorkspaceCreated) ?? - presets.find((p) => p.isDefault) ?? - null - ); + const tagged = presets.filter((p) => p.applyOnWorkspaceCreated); + if (tagged.length > 0) return tagged; + const defaultPreset = presets.find((p) => p.isDefault); + return defaultPreset ? [defaultPreset] : []; }), - getNewTabPreset: publicProcedure.query(() => { + getNewTabPresets: publicProcedure.query(() => { const row = getSettings(); const presets = row.terminalPresets ?? []; - return ( - presets.find((p) => p.applyOnNewTab) ?? - presets.find((p) => p.isDefault) ?? - null - ); + const tagged = presets.filter((p) => p.applyOnNewTab); + if (tagged.length > 0) return tagged; + const defaultPreset = presets.find((p) => p.isDefault); + return defaultPreset ? [defaultPreset] : []; }), getSelectedRingtoneId: publicProcedure.query(() => { diff --git a/apps/desktop/src/lib/trpc/routers/workspaces/procedures/init.ts b/apps/desktop/src/lib/trpc/routers/workspaces/procedures/init.ts index b467f4d0011..a573fadbc1e 100644 --- a/apps/desktop/src/lib/trpc/routers/workspaces/procedures/init.ts +++ b/apps/desktop/src/lib/trpc/routers/workspaces/procedures/init.ts @@ -9,15 +9,14 @@ import { getProject, getWorkspaceWithRelations } from "../utils/db-helpers"; import { loadSetupConfig } from "../utils/setup"; import { initializeWorkspaceWorktree } from "../utils/workspace-init"; -function getDefaultPreset() { +function getWorkspaceCreationPresets() { const row = localDb.select().from(settings).get(); - if (!row) return null; + if (!row) return []; const presets = row.terminalPresets ?? []; - return ( - presets.find((p) => p.applyOnWorkspaceCreated) ?? - presets.find((p) => p.isDefault) ?? - null - ); + const tagged = presets.filter((p) => p.applyOnWorkspaceCreated); + if (tagged.length > 0) return tagged; + const defaultPreset = presets.find((p) => p.isDefault); + return defaultPreset ? [defaultPreset] : []; } export const createInitProcedures = () => { @@ -124,12 +123,13 @@ export const createInitProcedures = () => { worktreePath: relations.worktree?.path, projectName: project.name, }); - const defaultPreset = getDefaultPreset(); + const defaultPresets = getWorkspaceCreationPresets(); return { projectId: project.id, initialCommands: setupConfig?.setup ?? null, - defaultPreset, + defaultPreset: defaultPresets[0] ?? null, + defaultPresets, }; }), }); diff --git a/apps/desktop/src/renderer/react-query/presets/index.ts b/apps/desktop/src/renderer/react-query/presets/index.ts index 4100564f13c..0f6b07d7563 100644 --- a/apps/desktop/src/renderer/react-query/presets/index.ts +++ b/apps/desktop/src/renderer/react-query/presets/index.ts @@ -76,8 +76,8 @@ function useSetPresetAutoApply( ...options, onSuccess: async (...args) => { await utils.settings.getTerminalPresets.invalidate(); - await utils.settings.getWorkspaceCreationPreset.invalidate(); - await utils.settings.getNewTabPreset.invalidate(); + await utils.settings.getWorkspaceCreationPresets.invalidate(); + await utils.settings.getNewTabPresets.invalidate(); await options?.onSuccess?.(...args); }, }); diff --git a/apps/desktop/src/renderer/react-query/workspaces/useCreateBranchWorkspace.ts b/apps/desktop/src/renderer/react-query/workspaces/useCreateBranchWorkspace.ts index 468761474a1..57531eaedde 100644 --- a/apps/desktop/src/renderer/react-query/workspaces/useCreateBranchWorkspace.ts +++ b/apps/desktop/src/renderer/react-query/workspaces/useCreateBranchWorkspace.ts @@ -39,6 +39,7 @@ export function useCreateBranchWorkspace( projectId: data.projectId, initialCommands: setupData?.initialCommands ?? null, defaultPreset: setupData?.defaultPreset ?? null, + defaultPresets: setupData?.defaultPresets ?? [], }); // Branch workspaces skip git init, so mark ready immediately to trigger terminal setup diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/start-claude-session.ts b/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/start-claude-session.ts index 50ca43bf59e..423a0b10e2a 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/start-claude-session.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/start-claude-session.ts @@ -35,6 +35,7 @@ async function execute( initialCommands: [...(pending?.initialCommands ?? []), params.command], // Preserve undefined (signals "fetch from backend") vs null (no preset needed) defaultPreset: pending ? pending.defaultPreset : null, + defaultPresets: pending?.defaultPresets, }); return { diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/PresetRow/PresetRow.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/PresetRow/PresetRow.tsx index 181906e0d3a..c0d4a070052 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/PresetRow/PresetRow.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/PresetRow/PresetRow.tsx @@ -1,5 +1,4 @@ import { EXECUTION_MODES, type ExecutionMode } from "@superset/local-db"; -import { Button } from "@superset/ui/button"; import { Input } from "@superset/ui/input"; import { Select, @@ -11,12 +10,6 @@ import { import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; import { useEffect, useRef } from "react"; import { useDrag, useDrop } from "react-dnd"; -import { - HiOutlineRectangleGroup, - HiOutlineSquare2Stack, - HiRectangleGroup, - HiSquare2Stack, -} from "react-icons/hi2"; import { LuGripVertical, LuTrash } from "react-icons/lu"; import { PRESET_COLUMNS, @@ -84,7 +77,11 @@ interface PresetRowProps { onCommandsBlur: (rowIndex: number) => void; onExecutionModeChange: (rowIndex: number, mode: ExecutionMode) => void; onDelete: (rowIndex: number) => void; - onToggleAutoApply: (presetId: string | null, field: AutoApplyField) => void; + onToggleAutoApply: ( + presetId: string, + field: AutoApplyField, + enabled: boolean, + ) => void; onLocalReorder: (fromIndex: number, toIndex: number) => void; onPersistReorder: (presetId: string, targetIndex: number) => void; } @@ -188,71 +185,61 @@ export function PresetRow({
-
+
- + WS + {isWorkspaceCreation - ? "Applied on workspace creation (click to remove)" - : "Apply on workspace creation"} + ? "Runs on workspace creation (click to remove)" + : "Run on workspace creation"} - + Tab + - {isNewTab - ? "Applied on new tab (click to remove)" - : "Apply on new tab"} + {isNewTab ? "Runs on new tab (click to remove)" : "Run on new tab"} - + +
); diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/PresetsSection.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/PresetsSection.tsx index 0a6d4f8cdbc..c97878ed060 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/PresetsSection.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/PresetsSection.tsx @@ -259,10 +259,11 @@ export function PresetsSection({ const handleToggleAutoApply = useCallback( ( - presetId: string | null, + presetId: string, field: "applyOnWorkspaceCreated" | "applyOnNewTab", + enabled: boolean, ) => { - setPresetAutoApply.mutate({ id: presetId, field }); + setPresetAutoApply.mutate({ id: presetId, field, enabled }); }, [setPresetAutoApply], ); diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceInitEffects.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceInitEffects.tsx index ff37c84f56d..f333a795e97 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceInitEffects.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceInitEffects.tsx @@ -88,21 +88,23 @@ export function WorkspaceInitEffects() { const hasSetupScript = Array.isArray(setup.initialCommands) && setup.initialCommands.length > 0; - const hasDefaultPreset = - shouldApplyPreset && - setup.defaultPreset != null && - setup.defaultPreset.commands.length > 0; + const presets = ( + setup.defaultPresets ?? + (setup.defaultPreset ? [setup.defaultPreset] : []) + ).filter((p) => p.commands.length > 0); + const hasPresets = shouldApplyPreset && presets.length > 0; - if (hasSetupScript && hasDefaultPreset && setup.defaultPreset) { + if (hasSetupScript && hasPresets) { const { tabId: setupTabId, paneId: setupPaneId } = addTab( setup.workspaceId, ); setTabAutoTitle(setupTabId, "Workspace Setup"); - createPresetTerminal( - setup.workspaceId, - setup.defaultPreset, - setupTabId, - ); + // Add first preset to the setup tab + createPresetTerminal(setup.workspaceId, presets[0], setupTabId); + // Additional presets get their own tabs + for (let i = 1; i < presets.length; i++) { + createPresetTerminal(setup.workspaceId, presets[i]); + } createOrAttach.mutate( { @@ -171,17 +173,15 @@ export function WorkspaceInitEffects() { return; } - if ( - shouldApplyPreset && - setup.defaultPreset && - setup.defaultPreset.commands.length > 0 - ) { - createPresetTerminal(setup.workspaceId, setup.defaultPreset); + if (hasPresets) { + for (const preset of presets) { + createPresetTerminal(setup.workspaceId, preset); + } onComplete(); return; } - // No setup script or default preset — sidebar card handles the prompt + // No setup script or presets — sidebar card handles the prompt onComplete(); }, [ @@ -223,6 +223,7 @@ export function WorkspaceInitEffects() { const completeSetup: PendingTerminalSetup = { ...setup, defaultPreset: setupData?.defaultPreset ?? null, + defaultPresets: setupData?.defaultPresets ?? [], }; handleTerminalSetup(completeSetup, () => { removePendingTerminalSetup(workspaceId); @@ -283,6 +284,7 @@ export function WorkspaceInitEffects() { projectId: setupData.projectId, initialCommands: setupData.initialCommands, defaultPreset: setupData.defaultPreset, + defaultPresets: setupData.defaultPresets ?? [], }; handleTerminalSetup(fetchedSetup, () => { diff --git a/apps/desktop/src/renderer/stores/tabs/useTabsWithPresets.ts b/apps/desktop/src/renderer/stores/tabs/useTabsWithPresets.ts index 10948e8364c..83997ff5922 100644 --- a/apps/desktop/src/renderer/stores/tabs/useTabsWithPresets.ts +++ b/apps/desktop/src/renderer/stores/tabs/useTabsWithPresets.ts @@ -7,12 +7,12 @@ import type { AddTabOptions } from "./types"; /** * Hook that wraps tab store actions with default preset support. - * When a preset with applyOnNewTab is configured, new terminals will - * automatically use that preset's commands and cwd. + * When presets are tagged with applyOnNewTab, new terminals will + * automatically use those presets' commands and cwd. */ export function useTabsWithPresets() { - const { data: newTabPreset } = - electronTrpc.settings.getNewTabPreset.useQuery(); + const { data: newTabPresets = [] } = + electronTrpc.settings.getNewTabPresets.useQuery(); const storeAddTab = useTabsStore((s) => s.addTab); const storeAddTabWithMultiplePanes = useTabsStore( @@ -24,20 +24,39 @@ export function useTabsWithPresets() { const storeSplitPaneAuto = useTabsStore((s) => s.splitPaneAuto); const renameTab = useTabsStore((s) => s.renameTab); - const defaultPresetOptions: AddTabOptions | undefined = useMemo(() => { - if (!newTabPreset) return undefined; + const firstPreset = newTabPresets[0] ?? null; + + const firstPresetOptions: AddTabOptions | undefined = useMemo(() => { + if (!firstPreset) return undefined; return { - initialCommands: newTabPreset.commands, - initialCwd: newTabPreset.cwd || undefined, + initialCommands: firstPreset.commands, + initialCwd: firstPreset.cwd || undefined, }; - }, [newTabPreset]); + }, [firstPreset]); + + const openPresetAsTab = useCallback( + (workspaceId: string, preset: TerminalPreset) => { + const isParallel = + preset.executionMode === "parallel" && preset.commands.length > 1; + + const { tabId } = isParallel + ? storeAddTabWithMultiplePanes(workspaceId, { + commands: preset.commands, + initialCwd: preset.cwd || undefined, + }) + : storeAddTab(workspaceId, { + initialCommands: preset.commands, + initialCwd: preset.cwd || undefined, + }); - const shouldUseParallelMode = useMemo(() => { - return ( - newTabPreset?.executionMode === "parallel" && - (newTabPreset?.commands.length ?? 0) > 1 - ); - }, [newTabPreset]); + if (preset.name) { + renameTab(tabId, preset.name); + } + + return { tabId }; + }, + [storeAddTab, storeAddTabWithMultiplePanes, renameTab], + ); const addTab = useCallback( (workspaceId: string, options?: AddTabOptions) => { @@ -45,43 +64,29 @@ export function useTabsWithPresets() { return storeAddTab(workspaceId, options); } - if (shouldUseParallelMode && newTabPreset) { - const { tabId, paneIds } = storeAddTabWithMultiplePanes(workspaceId, { - commands: newTabPreset.commands, - initialCwd: newTabPreset.cwd || undefined, - }); - - if (newTabPreset.name) { - renameTab(tabId, newTabPreset.name); - } - - return { tabId, paneId: paneIds[0] }; + if (newTabPresets.length === 0) { + return storeAddTab(workspaceId); } - const result = storeAddTab(workspaceId, defaultPresetOptions); + // Open the first preset using the normal addTab path + const firstResult = openPresetAsTab(workspaceId, newTabPresets[0]); - if (newTabPreset?.name) { - renameTab(result.tabId, newTabPreset.name); + // Open additional presets as separate tabs + for (let i = 1; i < newTabPresets.length; i++) { + openPresetAsTab(workspaceId, newTabPresets[i]); } - return result; + return { tabId: firstResult.tabId, paneId: firstResult.tabId }; }, - [ - storeAddTab, - storeAddTabWithMultiplePanes, - defaultPresetOptions, - newTabPreset, - shouldUseParallelMode, - renameTab, - ], + [storeAddTab, newTabPresets, openPresetAsTab], ); const addPane = useCallback( (tabId: string, options?: AddTabOptions) => { - const effectiveOptions = options ?? defaultPresetOptions; + const effectiveOptions = options ?? firstPresetOptions; return storeAddPane(tabId, effectiveOptions); }, - [storeAddPane, defaultPresetOptions], + [storeAddPane, firstPresetOptions], ); const splitPaneVertical = useCallback( @@ -91,7 +96,7 @@ export function useTabsWithPresets() { path?: MosaicBranch[], options?: AddTabOptions, ) => { - const effectiveOptions = options ?? defaultPresetOptions; + const effectiveOptions = options ?? firstPresetOptions; return storeSplitPaneVertical( tabId, sourcePaneId, @@ -99,7 +104,7 @@ export function useTabsWithPresets() { effectiveOptions, ); }, - [storeSplitPaneVertical, defaultPresetOptions], + [storeSplitPaneVertical, firstPresetOptions], ); const splitPaneHorizontal = useCallback( @@ -109,7 +114,7 @@ export function useTabsWithPresets() { path?: MosaicBranch[], options?: AddTabOptions, ) => { - const effectiveOptions = options ?? defaultPresetOptions; + const effectiveOptions = options ?? firstPresetOptions; return storeSplitPaneHorizontal( tabId, sourcePaneId, @@ -117,7 +122,7 @@ export function useTabsWithPresets() { effectiveOptions, ); }, - [storeSplitPaneHorizontal, defaultPresetOptions], + [storeSplitPaneHorizontal, firstPresetOptions], ); const splitPaneAuto = useCallback( @@ -128,7 +133,7 @@ export function useTabsWithPresets() { path?: MosaicBranch[], options?: AddTabOptions, ) => { - const effectiveOptions = options ?? defaultPresetOptions; + const effectiveOptions = options ?? firstPresetOptions; return storeSplitPaneAuto( tabId, sourcePaneId, @@ -137,31 +142,14 @@ export function useTabsWithPresets() { effectiveOptions, ); }, - [storeSplitPaneAuto, defaultPresetOptions], + [storeSplitPaneAuto, firstPresetOptions], ); const openPreset = useCallback( (workspaceId: string, preset: TerminalPreset) => { - const isParallel = - preset.executionMode === "parallel" && preset.commands.length > 1; - - const { tabId } = isParallel - ? storeAddTabWithMultiplePanes(workspaceId, { - commands: preset.commands, - initialCwd: preset.cwd || undefined, - }) - : storeAddTab(workspaceId, { - initialCommands: preset.commands, - initialCwd: preset.cwd || undefined, - }); - - if (preset.name) { - renameTab(tabId, preset.name); - } - - return { tabId }; + return openPresetAsTab(workspaceId, preset); }, - [storeAddTab, storeAddTabWithMultiplePanes, renameTab], + [openPresetAsTab], ); return { @@ -171,6 +159,6 @@ export function useTabsWithPresets() { splitPaneHorizontal, splitPaneAuto, openPreset, - defaultPreset: newTabPreset, + defaultPreset: firstPreset, }; } diff --git a/apps/desktop/src/renderer/stores/workspace-init.ts b/apps/desktop/src/renderer/stores/workspace-init.ts index 25fb88337e4..61c05095a64 100644 --- a/apps/desktop/src/renderer/stores/workspace-init.ts +++ b/apps/desktop/src/renderer/stores/workspace-init.ts @@ -8,6 +8,7 @@ export interface PendingTerminalSetup { projectId: string; initialCommands: string[] | null; defaultPreset?: TerminalPreset | null; + defaultPresets?: TerminalPreset[]; } interface WorkspaceInitState { From 23c5eadcafb58cc158fa218a66341397452c561a Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Sun, 8 Feb 2026 20:47:58 -0800 Subject: [PATCH 5/9] Use tabs --- .../_authenticated/settings/presets/types.ts | 5 +- .../components/PresetRow/PresetRow.tsx | 70 ++++++------------- .../components/PresetsSection.tsx | 56 ++++++++++++--- 3 files changed, 71 insertions(+), 60 deletions(-) diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/presets/types.ts b/apps/desktop/src/renderer/routes/_authenticated/settings/presets/types.ts index df42000bffd..11586832422 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/presets/types.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/presets/types.ts @@ -9,6 +9,7 @@ export interface PresetColumnConfig { label: string; placeholder: string; mono?: boolean; + tooltip?: string; } export const PRESET_COLUMNS: PresetColumnConfig[] = [ @@ -20,9 +21,11 @@ export const PRESET_COLUMNS: PresetColumnConfig[] = [ }, { key: "cwd", - label: "CWD", + label: "Directory", placeholder: "e.g. ./src (optional)", mono: true, + tooltip: + "Working directory for the terminal session (relative to workspace root)", }, { key: "commands", diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/PresetRow/PresetRow.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/PresetRow/PresetRow.tsx index c0d4a070052..d959a374295 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/PresetRow/PresetRow.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/PresetRow/PresetRow.tsx @@ -1,4 +1,5 @@ import { EXECUTION_MODES, type ExecutionMode } from "@superset/local-db"; +import { Checkbox } from "@superset/ui/checkbox"; import { Input } from "@superset/ui/input"; import { Select, @@ -7,7 +8,6 @@ import { SelectTrigger, SelectValue, } from "@superset/ui/select"; -import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; import { useEffect, useRef } from "react"; import { useDrag, useDrop } from "react-dnd"; import { LuGripVertical, LuTrash } from "react-icons/lu"; @@ -185,53 +185,27 @@ export function PresetRow({
-
- - - - - - {isWorkspaceCreation - ? "Runs on workspace creation (click to remove)" - : "Run on workspace creation"} - - - - - - - - {isNewTab ? "Runs on new tab (click to remove)" : "Run on new tab"} - - +
+ + onToggleAutoApply( + preset.id, + "applyOnWorkspaceCreated", + checked === true, + ) + } + /> +
+
+ + onToggleAutoApply(preset.id, "applyOnNewTab", checked === true) + } + /> +
+