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 && (
-
-
-
+ )}
+ {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(),