From a7b6df4b0ab3a69a3cc24acdd1710a7cd071542f Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Tue, 17 Mar 2026 13:54:51 -0700 Subject: [PATCH] tab menu, badges --- src/main/index.ts | 1 + src/renderer/src/components/Settings.tsx | 4 +- src/renderer/src/components/TabBar.tsx | 188 ++++++++++++++---- src/renderer/src/components/Terminal.tsx | 45 ++++- src/renderer/src/components/TerminalPane.tsx | 8 +- .../src/components/repo/RepoDotLabel.tsx | 23 +++ .../components/sidebar/AddWorktreeDialog.tsx | 14 +- .../src/components/sidebar/SearchBar.tsx | 16 +- .../src/components/sidebar/WorktreeCard.tsx | 10 +- .../components/sidebar/WorktreeMetaDialog.tsx | 137 +++++++++++++ src/renderer/src/components/sidebar/index.tsx | 2 + src/renderer/src/components/ui/badge.tsx | 1 + src/renderer/src/store/slices/terminals.ts | 24 +++ src/shared/types.ts | 2 + 14 files changed, 425 insertions(+), 50 deletions(-) create mode 100644 src/renderer/src/components/repo/RepoDotLabel.tsx create mode 100644 src/renderer/src/components/sidebar/WorktreeMetaDialog.tsx diff --git a/src/main/index.ts b/src/main/index.ts index 4bc26b0a3..dbc6bff3f 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -40,6 +40,7 @@ function createWindow(): BrowserWindow { }) mainWindow.on('ready-to-show', () => { + mainWindow.maximize() mainWindow.show() }) diff --git a/src/renderer/src/components/Settings.tsx b/src/renderer/src/components/Settings.tsx index f6872a646..62ff805cb 100644 --- a/src/renderer/src/components/Settings.tsx +++ b/src/renderer/src/components/Settings.tsx @@ -91,7 +91,7 @@ function Settings(): React.JSX.Element { } return ( -
+
{/* Header */}
{/* Content */} - +
{/* ── Workspace ────────────────────────────────────── */}
diff --git a/src/renderer/src/components/TabBar.tsx b/src/renderer/src/components/TabBar.tsx index 48d054d16..763a0994f 100644 --- a/src/renderer/src/components/TabBar.tsx +++ b/src/renderer/src/components/TabBar.tsx @@ -14,17 +14,57 @@ import { arrayMove } from '@dnd-kit/sortable' import { CSS } from '@dnd-kit/utilities' -import { X, Plus } from 'lucide-react' +import { X, Plus, Terminal as TerminalIcon } from 'lucide-react' +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuRadioGroup, + ContextMenuRadioItem, + ContextMenuSeparator, + ContextMenuSub, + ContextMenuSubContent, + ContextMenuSubTrigger, + ContextMenuTrigger +} from '@/components/ui/context-menu' import type { TerminalTab } from '../../../shared/types' interface SortableTabProps { tab: TerminalTab + tabCount: number + hasTabsToRight: boolean isActive: boolean onActivate: (tabId: string) => void onClose: (tabId: string) => void + onCloseOthers: (tabId: string) => void + onCloseToRight: (tabId: string) => void + onSetCustomTitle: (tabId: string, title: string | null) => void + onSetTabColor: (tabId: string, color: string | null) => void } -function SortableTab({ tab, isActive, onActivate, onClose }: SortableTabProps): React.JSX.Element { +const TAB_COLORS = [ + { label: 'None', value: 'none' }, + { label: 'Red', value: '#ef4444' }, + { label: 'Orange', value: '#f97316' }, + { label: 'Yellow', value: '#eab308' }, + { label: 'Green', value: '#22c55e' }, + { label: 'Blue', value: '#3b82f6' }, + { label: 'Purple', value: '#a855f7' }, + { label: 'Pink', value: '#ec4899' } +] + +function SortableTab({ + tab, + tabCount, + hasTabsToRight, + isActive, + onActivate, + onClose, + onCloseOthers, + onCloseToRight, + onSetCustomTitle, + onSetTabColor +}: SortableTabProps): React.JSX.Element { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: tab.id }) @@ -37,39 +77,99 @@ function SortableTab({ tab, isActive, onActivate, onClose }: SortableTabProps): } return ( -
{ - // Allow dnd-kit to handle drag, but also activate tab - onActivate(tab.id) - // Forward to dnd-kit listeners - listeners?.onPointerDown?.(e) - }} - > - {tab.title} - -
+ + +
{ + onActivate(tab.id) + listeners?.onPointerDown?.(e) + }} + onMouseDown={(e) => { + if (e.button === 1) { + e.preventDefault() + e.stopPropagation() + onClose(tab.id) + } + }} + > + + {tab.customTitle ?? tab.title} + + {tab.color && ( + + )} +
+
+ + + onClose(tab.id)}>Close + onCloseOthers(tab.id)} disabled={tabCount <= 1}> + Close Others + + onCloseToRight(tab.id)} disabled={!hasTabsToRight}> + Close Tabs To The Right + + + { + const next = window.prompt('Change tab title', tab.customTitle ?? tab.title) + if (next === null) return + const trimmed = next.trim() + onSetCustomTitle(tab.id, trimmed.length > 0 ? trimmed : null) + }} + > + Change Title + + + Assign Tab Color + + onSetTabColor(tab.id, value === 'none' ? null : value)} + > + {TAB_COLORS.map((color) => ( + + {color.value !== 'none' ? ( + + ) : ( + + )} + {color.label} + + ))} + + + + +
) } @@ -79,8 +179,12 @@ interface TabBarProps { worktreeId: string onActivate: (tabId: string) => void onClose: (tabId: string) => void + onCloseOthers: (tabId: string) => void + onCloseToRight: (tabId: string) => void onReorder: (worktreeId: string, tabIds: string[]) => void onNewTab: () => void + onSetCustomTitle: (tabId: string, title: string | null) => void + onSetTabColor: (tabId: string, color: string | null) => void } export default function TabBar({ @@ -89,8 +193,12 @@ export default function TabBar({ worktreeId, onActivate, onClose, + onCloseOthers, + onCloseToRight, onReorder, - onNewTab + onNewTab, + onSetCustomTitle, + onSetTabColor }: TabBarProps): React.JSX.Element { const sensors = useSensors( useSensor(PointerSensor, { @@ -120,13 +228,19 @@ export default function TabBar({
- {tabs.map((tab) => ( + {tabs.map((tab, index) => ( ))}
diff --git a/src/renderer/src/components/Terminal.tsx b/src/renderer/src/components/Terminal.tsx index 5dbda7c0c..44d8c211f 100644 --- a/src/renderer/src/components/Terminal.tsx +++ b/src/renderer/src/components/Terminal.tsx @@ -13,10 +13,10 @@ export default function Terminal(): React.JSX.Element | null { const setActiveTab = useAppStore((s) => s.setActiveTab) const reorderTabs = useAppStore((s) => s.reorderTabs) const setActiveWorktree = useAppStore((s) => s.setActiveWorktree) + const setTabCustomTitle = useAppStore((s) => s.setTabCustomTitle) + const setTabColor = useAppStore((s) => s.setTabColor) - if (!activeWorktreeId) return null - - const tabs = tabsByWorktree[activeWorktreeId] ?? [] + const tabs = activeWorktreeId ? (tabsByWorktree[activeWorktreeId] ?? []) : [] const prevTabCountRef = useRef(tabs.length) const tabBarRef = useRef(null) @@ -31,6 +31,7 @@ export default function Terminal(): React.JSX.Element | null { // Auto-create first tab when worktree activates useEffect(() => { + if (!activeWorktreeId) return if (activeWorktreeId && tabs.length === 0) { createTab(activeWorktreeId) } @@ -58,11 +59,13 @@ export default function Terminal(): React.JSX.Element | null { }, [tabs.length]) const handleNewTab = useCallback(() => { + if (!activeWorktreeId) return createTab(activeWorktreeId) }, [activeWorktreeId, createTab]) const handleCloseTab = useCallback( (tabId: string) => { + if (!activeWorktreeId) return const currentTabs = useAppStore.getState().tabsByWorktree[activeWorktreeId] ?? [] if (currentTabs.length <= 1) { // Last tab - deactivate worktree @@ -89,8 +92,38 @@ export default function Terminal(): React.JSX.Element | null { [handleCloseTab] ) + const handleCloseOthers = useCallback( + (tabId: string) => { + if (!activeWorktreeId) return + const currentTabs = useAppStore.getState().tabsByWorktree[activeWorktreeId] ?? [] + setActiveTab(tabId) + for (const tab of currentTabs) { + if (tab.id !== tabId) { + closeTab(tab.id) + } + } + }, + [activeWorktreeId, closeTab, setActiveTab] + ) + + const handleCloseTabsToRight = useCallback( + (tabId: string) => { + if (!activeWorktreeId) return + const currentTabs = useAppStore.getState().tabsByWorktree[activeWorktreeId] ?? [] + const index = currentTabs.findIndex((t) => t.id === tabId) + if (index === -1) return + const rightTabs = currentTabs.slice(index + 1) + for (const tab of rightTabs) { + closeTab(tab.id) + } + }, + [activeWorktreeId, closeTab] + ) + // Keyboard shortcuts useEffect(() => { + if (!activeWorktreeId) return + const onKeyDown = (e: KeyboardEvent): void => { // Cmd+T - new tab if (e.metaKey && e.key === 't' && !e.shiftKey && !e.repeat) { @@ -127,6 +160,8 @@ export default function Terminal(): React.JSX.Element | null { return () => window.removeEventListener('keydown', onKeyDown, { capture: true }) }, [activeWorktreeId, handleNewTab, handleCloseTab, setActiveTab]) + if (!activeWorktreeId) return null + return (
{/* Animated tab bar container using CSS grid for smooth height animation */} @@ -142,8 +177,12 @@ export default function Terminal(): React.JSX.Element | null { worktreeId={activeWorktreeId} onActivate={setActiveTab} onClose={handleCloseTab} + onCloseOthers={handleCloseOthers} + onCloseToRight={handleCloseTabsToRight} onReorder={reorderTabs} onNewTab={handleNewTab} + onSetCustomTitle={setTabCustomTitle} + onSetTabColor={setTabColor} />
diff --git a/src/renderer/src/components/TerminalPane.tsx b/src/renderer/src/components/TerminalPane.tsx index 886417659..92e714670 100644 --- a/src/renderer/src/components/TerminalPane.tsx +++ b/src/renderer/src/components/TerminalPane.tsx @@ -276,12 +276,14 @@ export default function TerminalPane({ // ResizeObserver to keep terminal sized to container useEffect(() => { + if (!isActive) return + const container = containerRef.current if (!container) return const ro = new ResizeObserver(() => { const restty = resttyRef.current - if (!restty || !isActive) return + if (!restty) return const panes = restty.getPanes() for (const p of panes) { p.app.updateSize(true) @@ -294,8 +296,10 @@ export default function TerminalPane({ // Terminal pane shortcuts handled at window capture phase so they remain // reliable even when focus is inside the canvas/IME internals. useEffect(() => { + if (!isActive) return + const onKeyDown = (e: KeyboardEvent): void => { - if (!isActive || e.repeat) return + if (e.repeat) return if (!e.metaKey || e.altKey || e.ctrlKey) return const restty = resttyRef.current diff --git a/src/renderer/src/components/repo/RepoDotLabel.tsx b/src/renderer/src/components/repo/RepoDotLabel.tsx new file mode 100644 index 000000000..1dec3aba8 --- /dev/null +++ b/src/renderer/src/components/repo/RepoDotLabel.tsx @@ -0,0 +1,23 @@ +import type { CSSProperties } from 'react' +import { cn } from '@/lib/utils' + +interface RepoDotLabelProps { + name: string + color: string + className?: string + dotClassName?: string +} + +function RepoDotLabel({ name, color, className, dotClassName }: RepoDotLabelProps) { + return ( + + + {name} + + ) +} + +export default RepoDotLabel diff --git a/src/renderer/src/components/sidebar/AddWorktreeDialog.tsx b/src/renderer/src/components/sidebar/AddWorktreeDialog.tsx index fde677909..9f2bd58c0 100644 --- a/src/renderer/src/components/sidebar/AddWorktreeDialog.tsx +++ b/src/renderer/src/components/sidebar/AddWorktreeDialog.tsx @@ -17,6 +17,7 @@ import { SelectContent, SelectItem } from '@/components/ui/select' +import RepoDotLabel from '@/components/repo/RepoDotLabel' const AddWorktreeDialog = React.memo(function AddWorktreeDialog() { const activeModal = useAppStore((s) => s.activeModal) @@ -32,6 +33,7 @@ const AddWorktreeDialog = React.memo(function AddWorktreeDialog() { const [creating, setCreating] = useState(false) const isOpen = activeModal === 'create-worktree' + const selectedRepo = repos.find((r) => r.id === repoId) const handleOpenChange = useCallback( (open: boolean) => { @@ -93,12 +95,20 @@ const AddWorktreeDialog = React.memo(function AddWorktreeDialog() { setIssueInput(e.target.value)} + placeholder="e.g. 42" + className="h-8 text-xs" + autoFocus + /> +
+ ) : ( +
+ +