diff --git a/apps/desktop/src/lib/trpc/routers/settings/index.ts b/apps/desktop/src/lib/trpc/routers/settings/index.ts index f5564a0774a..7871e480b96 100644 --- a/apps/desktop/src/lib/trpc/routers/settings/index.ts +++ b/apps/desktop/src/lib/trpc/routers/settings/index.ts @@ -152,6 +152,7 @@ export const createSettingsRouter = () => { description: z.string().optional(), cwd: z.string(), commands: z.array(z.string()), + pinnedToBar: z.boolean().optional(), executionMode: z.enum(EXECUTION_MODES).optional(), }), ) @@ -179,6 +180,7 @@ export const createSettingsRouter = () => { description: z.string().optional(), cwd: z.string().optional(), commands: z.array(z.string()).optional(), + pinnedToBar: z.boolean().optional(), executionMode: z.enum(EXECUTION_MODES).optional(), }), }), @@ -200,6 +202,8 @@ export const createSettingsRouter = () => { if (input.patch.cwd !== undefined) preset.cwd = input.patch.cwd; if (input.patch.commands !== undefined) preset.commands = input.patch.commands; + if (input.patch.pinnedToBar !== undefined) + preset.pinnedToBar = input.patch.pinnedToBar; if (input.patch.executionMode !== undefined) preset.executionMode = input.patch.executionMode; diff --git a/apps/desktop/src/renderer/components/HotkeyMenuShortcut/HotkeyMenuShortcut.tsx b/apps/desktop/src/renderer/components/HotkeyMenuShortcut/HotkeyMenuShortcut.tsx new file mode 100644 index 00000000000..3c8d918f986 --- /dev/null +++ b/apps/desktop/src/renderer/components/HotkeyMenuShortcut/HotkeyMenuShortcut.tsx @@ -0,0 +1,15 @@ +import { DropdownMenuShortcut } from "@superset/ui/dropdown-menu"; +import { useHotkeyText } from "renderer/stores/hotkeys"; +import type { HotkeyId } from "shared/hotkeys"; + +interface HotkeyMenuShortcutProps { + hotkeyId: HotkeyId; +} + +export function HotkeyMenuShortcut({ hotkeyId }: HotkeyMenuShortcutProps) { + const hotkeyText = useHotkeyText(hotkeyId); + if (hotkeyText === "Unassigned") { + return null; + } + return {hotkeyText}; +} diff --git a/apps/desktop/src/renderer/components/HotkeyMenuShortcut/index.ts b/apps/desktop/src/renderer/components/HotkeyMenuShortcut/index.ts new file mode 100644 index 00000000000..881e7168b7b --- /dev/null +++ b/apps/desktop/src/renderer/components/HotkeyMenuShortcut/index.ts @@ -0,0 +1 @@ +export { HotkeyMenuShortcut } from "./HotkeyMenuShortcut"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/TopBar.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/TopBar.tsx index 1163b4ce7ec..6e7160c205d 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/TopBar.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/TopBar.tsx @@ -21,7 +21,7 @@ export function TopBar() { const isMac = platform === undefined || platform === "darwin"; return ( -
+
addChatTab(workspaceId), undefined, [ + workspaceId, + addChatTab, + ]); useAppHotkey( "REOPEN_TAB", () => { - if (!reopenClosedTab(workspaceId)) { - addChatTab(workspaceId); - } + reopenClosedTab(workspaceId); }, undefined, - [workspaceId, reopenClosedTab, addChatTab], + [workspaceId, reopenClosedTab], ); useAppHotkey("NEW_BROWSER", () => addBrowserTab(workspaceId), undefined, [ workspaceId, diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebar.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebar.tsx index e7f8b78be79..8116429743f 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebar.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebar.tsx @@ -34,7 +34,7 @@ export function WorkspaceSidebar({ ); return ( - +
diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupStrip.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupStrip.tsx index f4bb71e037a..634d358d8fc 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupStrip.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupStrip.tsx @@ -8,26 +8,22 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@superset/ui/dropdown-menu"; -import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; import { useLiveQuery } from "@tanstack/react-db"; -import { useNavigate, useParams } from "@tanstack/react-router"; +import { useParams } from "@tanstack/react-router"; import { useFeatureFlagEnabled } from "posthog-js/react"; -import { useCallback, useEffect, useMemo, useState } from "react"; -import { BsTerminalPlus } from "react-icons/bs"; import { - HiMiniChevronDown, - HiMiniCog6Tooth, - HiMiniCommandLine, - HiStar, -} from "react-icons/hi2"; + useCallback, + useEffect, + useLayoutEffect, + useMemo, + useRef, + useState, +} from "react"; +import { BsTerminalPlus } from "react-icons/bs"; +import { LuPlus } from "react-icons/lu"; import { TbMessageCirclePlus, TbWorld } from "react-icons/tb"; -import { - getPresetIcon, - useIsDarkTheme, -} from "renderer/assets/app-icons/preset-icons"; -import { HotkeyTooltipContent } from "renderer/components/HotkeyTooltipContent"; +import { HotkeyMenuShortcut } from "renderer/components/HotkeyMenuShortcut"; import { electronTrpc } from "renderer/lib/electron-trpc"; -import { usePresets } from "renderer/react-query/presets"; import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; import { useTabsStore } from "renderer/stores/tabs/store"; import { useTabsWithPresets } from "renderer/stores/tabs/useTabsWithPresets"; @@ -36,7 +32,6 @@ import { resolveActiveTabIdForWorkspace, } from "renderer/stores/tabs/utils"; import { type ActivePaneStatus, pickHigherStatus } from "shared/tabs-types"; -import { PresetMenuItemShortcut } from "./components/PresetMenuItemShortcut"; import { GroupItem } from "./GroupItem"; import { NewTabDropZone } from "./NewTabDropZone"; @@ -47,7 +42,7 @@ export function GroupStrip() { const panes = useTabsStore((s) => s.panes); const activeTabIds = useTabsStore((s) => s.activeTabIds); const tabHistoryStacks = useTabsStore((s) => s.tabHistoryStacks); - const { addTab, openPreset } = useTabsWithPresets(); + const { addTab } = useTabsWithPresets(); const addChatTab = useTabsStore((s) => s.addChatTab); const addBrowserTab = useTabsStore((s) => s.addBrowserTab); const renameTab = useTabsStore((s) => s.renameTab); @@ -60,8 +55,9 @@ export function GroupStrip() { const setTabAutoTitle = useTabsStore((s) => s.setTabAutoTitle); const hasAiChat = useFeatureFlagEnabled(FEATURE_FLAGS.AI_CHAT); - const { presets } = usePresets(); - const isDark = useIsDarkTheme(); + const scrollContainerRef = useRef(null); + const tabsTrackRef = useRef(null); + const [hasHorizontalOverflow, setHasHorizontalOverflow] = useState(false); const utils = electronTrpc.useUtils(); const { data: showPresetsBar } = electronTrpc.settings.getShowPresetsBar.useQuery(); @@ -83,8 +79,6 @@ export function GroupStrip() { }, }, ); - const navigate = useNavigate(); - const [dropdownOpen, setDropdownOpen] = useState(false); const tabs = useMemo( () => @@ -166,17 +160,6 @@ export function GroupStrip() { addBrowserTab(activeWorkspaceId); }; - const handleSelectPreset = (preset: Parameters[1]) => { - if (!activeWorkspaceId) return; - openPreset(activeWorkspaceId, preset, { target: "active-tab" }); - setDropdownOpen(false); - }; - - const handleOpenPresetsSettings = () => { - navigate({ to: "/settings/presets" }); - setDropdownOpen(false); - }; - const handleSelectGroup = (tabId: string) => { if (activeWorkspaceId) { setActiveTab(activeWorkspaceId, tabId); @@ -208,162 +191,125 @@ export function GroupStrip() { return isLastPaneInTab(freshPanes, pane.tabId); }, []); - return ( -
{ + const container = scrollContainerRef.current; + const track = tabsTrackRef.current; + if (!container || !track) return; + setHasHorizontalOverflow(track.scrollWidth > container.clientWidth + 1); + }, []); + + useLayoutEffect(() => { + const container = scrollContainerRef.current; + const track = tabsTrackRef.current; + if (!container || !track) return; + + updateOverflow(); + const resizeObserver = new ResizeObserver(updateOverflow); + resizeObserver.observe(container); + resizeObserver.observe(track); + window.addEventListener("resize", updateOverflow); + + return () => { + resizeObserver.disconnect(); + window.removeEventListener("resize", updateOverflow); + }; + }, [updateOverflow]); + + useEffect(() => { + requestAnimationFrame(updateOverflow); + }, [updateOverflow]); + + const plusControl = ( + movePaneToNewTab(paneId)} + isLastPaneInTab={checkIsLastPaneInTab} > - {tabs.length > 0 && ( -
- {tabs.map((tab, index) => { - return ( -
- handleSelectGroup(tab.id)} - onClose={() => handleCloseGroup(tab.id)} - onRename={(newName) => handleRenameGroup(tab.id, newName)} - onPaneDrop={(paneId) => movePaneToTab(paneId, tab.id)} - onReorder={handleReorderTabs} - /> -
- ); - })} -
- )} - movePaneToNewTab(paneId)} - isLastPaneInTab={checkIsLastPaneInTab} + + + + + + + + Terminal + + + {hasAiChat && ( + + + Chat + + + )} + + + Browser + + + + + setShowPresetsBar.mutate({ enabled: checked }) + } + onSelect={(e) => e.preventDefault()} + > + Show Preset Bar + + + + + ); + + return ( +
+
- -
- - - - - - - - - {hasAiChat && ( - - - - - - - - - )} - - - - - - - - - - - -
- - {presets.length > 0 && ( - <> - {presets.map((preset, index) => { - const presetIcon = getPresetIcon(preset.name, isDark); - return ( - handleSelectPreset(preset)} - className="gap-2" - > - {presetIcon ? ( - - ) : ( - - )} - - {preset.name || "default"} - - {preset.isDefault && ( - - )} - - - ); - })} - - - )} - {presets.length > 0 && ( - - setShowPresetsBar.mutate({ enabled: checked }) - } - onSelect={(e) => e.preventDefault()} - > - Show Preset Bar - - )} - - - Configure Presets - - -
- + handleSelectGroup(tab.id)} + onClose={() => handleCloseGroup(tab.id)} + onRename={(newName) => handleRenameGroup(tab.id, newName)} + onPaneDrop={(paneId) => movePaneToTab(paneId, tab.id)} + onReorder={handleReorderTabs} + /> +
+ ); + })} +
+ )} + {hasHorizontalOverflow ? ( +
+ ) : ( +
{plusControl}
+ )} +
+
+ {hasHorizontalOverflow && ( +
{plusControl}
+ )}
); } diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/components/PresetMenuItemShortcut/PresetMenuItemShortcut.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/components/PresetMenuItemShortcut/PresetMenuItemShortcut.tsx deleted file mode 100644 index daac749f10b..00000000000 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/components/PresetMenuItemShortcut/PresetMenuItemShortcut.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { DropdownMenuShortcut } from "@superset/ui/dropdown-menu"; -import { PRESET_HOTKEY_IDS } from "renderer/routes/_authenticated/_dashboard/workspace/$workspaceId/hooks/usePresetHotkeys"; -import { useHotkeyText } from "renderer/stores/hotkeys"; -import type { HotkeyId } from "shared/hotkeys"; - -function PresetMenuItemShortcutInner({ hotkeyId }: { hotkeyId: HotkeyId }) { - const hotkeyText = useHotkeyText(hotkeyId); - - if (hotkeyText === "Unassigned") { - return null; - } - - return {hotkeyText}; -} - -export function PresetMenuItemShortcut({ index }: { index: number }) { - const hotkeyId = PRESET_HOTKEY_IDS[index]; - - if (!hotkeyId) { - return null; - } - - return ; -} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/components/PresetMenuItemShortcut/index.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/components/PresetMenuItemShortcut/index.ts deleted file mode 100644 index 5b93ed3229d..00000000000 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/components/PresetMenuItemShortcut/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { PresetMenuItemShortcut } from "./PresetMenuItemShortcut"; diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/PresetsBar/PresetsBar.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/PresetsBar/PresetsBar.tsx index 02c3de5e536..589a0c79400 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/PresetsBar/PresetsBar.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/PresetsBar/PresetsBar.tsx @@ -1,30 +1,243 @@ +import { + AGENT_PRESET_COMMANDS, + AGENT_PRESET_DESCRIPTIONS, + AGENT_TYPES, +} from "@superset/shared/agent-command"; import { Button } from "@superset/ui/button"; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@superset/ui/dropdown-menu"; import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; -import { useParams } from "@tanstack/react-router"; -import { HiMiniCommandLine } from "react-icons/hi2"; +import { useNavigate, useParams } from "@tanstack/react-router"; +import { useMemo } from "react"; +import { HiMiniCog6Tooth, HiMiniCommandLine } from "react-icons/hi2"; +import { LuCirclePlus, LuPin } from "react-icons/lu"; import { getPresetIcon, useIsDarkTheme, } from "renderer/assets/app-icons/preset-icons"; +import { HotkeyMenuShortcut } from "renderer/components/HotkeyMenuShortcut"; import { HotkeyTooltipContent } from "renderer/components/HotkeyTooltipContent/HotkeyTooltipContent"; +import { electronTrpc } from "renderer/lib/electron-trpc"; import { usePresets } from "renderer/react-query/presets"; import { PRESET_HOTKEY_IDS } from "renderer/routes/_authenticated/_dashboard/workspace/$workspaceId/hooks/usePresetHotkeys"; import { useTabsWithPresets } from "renderer/stores/tabs/useTabsWithPresets"; +interface PresetTemplate { + name: string; + preset: { + name: string; + description: string; + cwd: string; + commands: string[]; + }; +} + +const QUICK_ADD_PRESET_TEMPLATES: PresetTemplate[] = AGENT_TYPES.map( + (agent) => ({ + name: agent, + preset: { + name: agent, + description: AGENT_PRESET_DESCRIPTIONS[agent], + cwd: "", + commands: AGENT_PRESET_COMMANDS[agent], + }, + }), +); + +function isPresetPinnedToBar(pinnedToBar: boolean | undefined): boolean { + // Backward-compatibility rule: + // Existing presets created before `pinnedToBar` was introduced have + // `pinnedToBar === undefined` and should remain visible in the presets bar. + // Only an explicit `false` means "not pinned". + return pinnedToBar !== false; +} + export function PresetsBar() { const { workspaceId } = useParams({ strict: false }); - const { presets } = usePresets(); + const navigate = useNavigate(); + const { presets, createPreset, updatePreset } = usePresets(); const isDark = useIsDarkTheme(); const { openPreset } = useTabsWithPresets(); - - if (presets.length === 0) return null; + const utils = electronTrpc.useUtils(); + const { data: showPresetsBar } = + electronTrpc.settings.getShowPresetsBar.useQuery(); + const setShowPresetsBar = electronTrpc.settings.setShowPresetsBar.useMutation( + { + onMutate: async ({ enabled }) => { + await utils.settings.getShowPresetsBar.cancel(); + const previous = utils.settings.getShowPresetsBar.getData(); + utils.settings.getShowPresetsBar.setData(undefined, enabled); + return { previous }; + }, + onError: (_err, _vars, context) => { + if (context?.previous !== undefined) { + utils.settings.getShowPresetsBar.setData(undefined, context.previous); + } + }, + onSettled: () => { + utils.settings.getShowPresetsBar.invalidate(); + }, + }, + ); + const presetsByName = useMemo(() => { + const map = new Map(); + for (const preset of presets) { + const existing = map.get(preset.name); + if (existing) { + existing.push(preset); + continue; + } + map.set(preset.name, [preset]); + } + return map; + }, [presets]); + const pinnedPresets = useMemo( + () => + presets.flatMap((preset, index) => + isPresetPinnedToBar(preset.pinnedToBar) ? [{ preset, index }] : [], + ), + [presets], + ); + const presetIndexById = useMemo( + () => new Map(presets.map((preset, index) => [preset.id, index])), + [presets], + ); + const managedPresets = useMemo(() => { + const templateNames = new Set( + QUICK_ADD_PRESET_TEMPLATES.map((t) => t.name), + ); + const primaryTemplatePresetIds = new Set( + QUICK_ADD_PRESET_TEMPLATES.flatMap((template) => { + const match = presetsByName.get(template.name)?.[0]; + return match ? [match.id] : []; + }), + ); + const fromTemplates = QUICK_ADD_PRESET_TEMPLATES.map((template) => ({ + key: `template:${template.name}`, + name: template.name, + preset: presetsByName.get(template.name)?.[0], + template, + iconName: template.name, + })); + const customExisting = presets + .filter( + (preset) => + !templateNames.has(preset.name) || + !primaryTemplatePresetIds.has(preset.id), + ) + .map((preset) => ({ + key: `preset:${preset.id}`, + name: preset.name || "default", + preset, + template: null, + iconName: preset.name, + })); + return [...fromTemplates, ...customExisting]; + }, [presetsByName, presets]); return (
- {presets.map((preset, index) => { + + + + + + + + + Manage Presets + + + + {managedPresets.map((item) => { + const icon = getPresetIcon(item.iconName, isDark); + const isPinned = item.preset + ? isPresetPinnedToBar(item.preset.pinnedToBar) + : false; + const hasPreset = !!item.preset; + const presetIndex = item.preset + ? presetIndexById.get(item.preset.id) + : undefined; + const hotkeyId = + typeof presetIndex === "number" + ? PRESET_HOTKEY_IDS[presetIndex] + : undefined; + return ( + { + if (hasPreset && item.preset) { + updatePreset.mutate({ + id: item.preset.id, + patch: { pinnedToBar: !isPinned }, + }); + return; + } + if (!item.template) return; + createPreset.mutate({ + ...item.template.preset, + pinnedToBar: true, + }); + }} + > + {icon ? ( + + ) : ( + + )} + {item.name || "default"} +
+ {hotkeyId ? : null} + {hasPreset ? ( + + ) : ( + + )} +
+
+ ); + })} + + + setShowPresetsBar.mutate({ enabled: checked }) + } + onSelect={(e) => e.preventDefault()} + > + Show Preset Bar + + + navigate({ to: "/settings/presets" })} + > + + Manage Presets + +
+
+
+ {pinnedPresets.map(({ preset, index }) => { const icon = getPresetIcon(preset.name, isDark); const hotkeyId = PRESET_HOTKEY_IDS[index]; const label = preset.description || preset.name || "default"; diff --git a/apps/desktop/src/shared/constants.ts b/apps/desktop/src/shared/constants.ts index 39317c9ccaf..b74ff823f27 100644 --- a/apps/desktop/src/shared/constants.ts +++ b/apps/desktop/src/shared/constants.ts @@ -40,7 +40,7 @@ export const DEFAULT_CONFIRM_ON_QUIT = true; export const DEFAULT_TERMINAL_LINK_BEHAVIOR = "external-editor" as const; export const DEFAULT_FILE_OPEN_MODE = "split-pane" as const; export const DEFAULT_AUTO_APPLY_DEFAULT_PRESET = true; -export const DEFAULT_SHOW_PRESETS_BAR = false; +export const DEFAULT_SHOW_PRESETS_BAR = true; export const DEFAULT_TELEMETRY_ENABLED = true; export const DEFAULT_SHOW_RESOURCE_MONITOR = false; export const DEFAULT_OPEN_LINKS_IN_APP = false; diff --git a/apps/desktop/src/shared/hotkeys.ts b/apps/desktop/src/shared/hotkeys.ts index ae03e431014..3ff34bcdd11 100644 --- a/apps/desktop/src/shared/hotkeys.ts +++ b/apps/desktop/src/shared/hotkeys.ts @@ -519,8 +519,13 @@ export const HOTKEYS = { label: "New Terminal", category: "Terminal", }), - REOPEN_TAB: defineHotkey({ + NEW_CHAT: defineHotkey({ keys: "meta+shift+t", + label: "New Chat", + category: "Terminal", + }), + REOPEN_TAB: defineHotkey({ + keys: "meta+shift+r", label: "Reopen Closed Tab", category: "Terminal", }), diff --git a/packages/local-db/src/schema/zod.ts b/packages/local-db/src/schema/zod.ts index 8ae2e059f72..38bc0fbf776 100644 --- a/packages/local-db/src/schema/zod.ts +++ b/packages/local-db/src/schema/zod.ts @@ -62,6 +62,7 @@ export const terminalPresetSchema = z.object({ description: z.string().optional(), cwd: z.string(), commands: z.array(z.string()), + pinnedToBar: z.boolean().optional(), isDefault: z.boolean().optional(), applyOnWorkspaceCreated: z.boolean().optional(), applyOnNewTab: z.boolean().optional(),