From 07210ae24ca78f88c2b9fa51af20f4e312465092 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Wed, 13 May 2026 16:18:22 -0700 Subject: [PATCH] Fix floating terminal default migration (#1767) * Fix floating terminal default migration * Add floating terminal icon context menu --- src/main/persistence.test.ts | 37 +++++++ src/main/persistence.ts | 10 ++ .../FloatingTerminalIconContextMenu.tsx | 97 +++++++++++++++++++ .../FloatingTerminalPanel.tsx | 53 +++++----- .../src/components/status-bar/StatusBar.tsx | 62 ++++++------ src/shared/constants.ts | 1 + src/shared/types.ts | 10 +- 7 files changed, 214 insertions(+), 56 deletions(-) create mode 100644 src/renderer/src/components/floating-terminal/FloatingTerminalIconContextMenu.tsx diff --git a/src/main/persistence.test.ts b/src/main/persistence.test.ts index c5d63e31f3c..694618151eb 100644 --- a/src/main/persistence.test.ts +++ b/src/main/persistence.test.ts @@ -91,6 +91,8 @@ describe('Store', () => { expect(settings.rightSidebarOpenByDefault).toBe(true) expect(settings.showTasksButton).toBe(true) expect(settings.experimentalActivity).toBe(true) + expect(settings.floatingTerminalEnabled).toBe(true) + expect(settings.floatingTerminalDefaultedForAllUsers).toBe(true) expect(settings.notifications.customSoundPath).toBeNull() }) @@ -166,6 +168,41 @@ describe('Store', () => { expect(store.getRepos()).toHaveLength(1) }) + it('migrates the legacy floating terminal disabled default to enabled', async () => { + writeDataFile({ + schemaVersion: 1, + repos: [], + worktreeMeta: {}, + settings: { floatingTerminalEnabled: false }, + ui: {}, + githubCache: { pr: {}, issue: {} }, + workspaceSession: {} + }) + + const store = await createStore() + expect(store.getSettings().floatingTerminalEnabled).toBe(true) + expect(store.getSettings().floatingTerminalDefaultedForAllUsers).toBe(true) + }) + + it('preserves a post-migration floating terminal opt-out', async () => { + writeDataFile({ + schemaVersion: 1, + repos: [], + worktreeMeta: {}, + settings: { + floatingTerminalEnabled: false, + floatingTerminalDefaultedForAllUsers: true + }, + ui: {}, + githubCache: { pr: {}, issue: {} }, + workspaceSession: {} + }) + + const store = await createStore() + expect(store.getSettings().floatingTerminalEnabled).toBe(false) + expect(store.getSettings().floatingTerminalDefaultedForAllUsers).toBe(true) + }) + it('preserves custom notification sound paths from persisted settings', async () => { writeDataFile({ schemaVersion: 1, diff --git a/src/main/persistence.ts b/src/main/persistence.ts index a458838faac..c1fd20d941e 100644 --- a/src/main/persistence.ts +++ b/src/main/persistence.ts @@ -277,6 +277,14 @@ export class Store { : rawOptionAsAlt === undefined || rawOptionAsAlt === 'true' ? 'auto' : rawOptionAsAlt + const floatingTerminalDefaultedForAllUsers = + parsed.settings?.floatingTerminalDefaultedForAllUsers === true + // Why: early floating-terminal builds persisted the old off-by-default + // value into user profiles. Flip only unmigrated profiles so a later + // deliberate opt-out still survives reload. + const migratedFloatingTerminalEnabled = floatingTerminalDefaultedForAllUsers + ? (parsed.settings?.floatingTerminalEnabled ?? true) + : true result = { ...defaults, ...parsed, @@ -293,6 +301,8 @@ export class Store { experimentalActivity: true, terminalMacOptionAsAlt: migratedOptionAsAlt, terminalMacOptionAsAltMigrated: true, + floatingTerminalEnabled: migratedFloatingTerminalEnabled, + floatingTerminalDefaultedForAllUsers: true, notifications: { ...getDefaultNotificationSettings(), ...parsed.settings?.notifications diff --git a/src/renderer/src/components/floating-terminal/FloatingTerminalIconContextMenu.tsx b/src/renderer/src/components/floating-terminal/FloatingTerminalIconContextMenu.tsx new file mode 100644 index 00000000000..891164f51f7 --- /dev/null +++ b/src/renderer/src/components/floating-terminal/FloatingTerminalIconContextMenu.tsx @@ -0,0 +1,97 @@ +import { useMemo, useState } from 'react' +import { EyeOff, PanelBottom, PanelTop } from 'lucide-react' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger +} from '@/components/ui/dropdown-menu' +import { useAppStore } from '@/store' +import type { FloatingTerminalTriggerLocation } from '../../../../shared/types' + +type FloatingTerminalIconContextMenuProps = { + children: React.ReactNode + currentLocation: FloatingTerminalTriggerLocation + className?: string +} + +export function FloatingTerminalIconContextMenu({ + children, + currentLocation, + className +}: FloatingTerminalIconContextMenuProps): React.JSX.Element { + const updateSettings = useAppStore((s) => s.updateSettings) + const [open, setOpen] = useState(false) + const [menuPoint, setMenuPoint] = useState({ x: 0, y: 0 }) + + const moveAction = useMemo(() => { + if (currentLocation === 'floating-button') { + return { + icon: , + label: 'Move to Status Bar', + location: 'status-bar' as const + } + } + return { + icon: , + label: 'Move to Floating Button', + location: 'floating-button' as const + } + }, [currentLocation]) + + return ( + <> + { + // Why: workspace cards use DropdownMenu anchored at the cursor for + // right-click menus; match that style instead of Radix ContextMenu. + event.preventDefault() + event.stopPropagation() + setMenuPoint({ x: event.clientX, y: event.clientY }) + setOpen(false) + window.requestAnimationFrame(() => setOpen(true)) + }} + onContextMenu={(event) => { + event.preventDefault() + event.stopPropagation() + }} + > + {children} + + + + - - {`${open ? 'Hide' : 'Show'} floating terminal (${shortcutLabel})`} - + + + + + + {`${open ? 'Minimize' : 'Show'} floating terminal (${shortcutLabel})`} + + ) } @@ -312,14 +317,14 @@ export function FloatingTerminalPanel({ type="button" variant="ghost" size="icon-xs" - aria-label="Hide floating terminal" + aria-label="Minimize floating terminal" onClick={() => onOpenChange(false)} > - + - Hide floating terminal + Minimize diff --git a/src/renderer/src/components/status-bar/StatusBar.tsx b/src/renderer/src/components/status-bar/StatusBar.tsx index 0ab5a0ae844..d33152a00ec 100644 --- a/src/renderer/src/components/status-bar/StatusBar.tsx +++ b/src/renderer/src/components/status-bar/StatusBar.tsx @@ -37,6 +37,7 @@ import { ResourceUsageStatusSegment } from './ResourceUsageStatusSegment' import { isStatusBarItemAvailable } from './status-bar-agent-gating' import { PetStatusSegment } from './PetStatusSegment' import { TOGGLE_FLOATING_TERMINAL_EVENT } from '@/lib/floating-terminal' +import { FloatingTerminalIconContextMenu } from '@/components/floating-terminal/FloatingTerminalIconContextMenu' type StatusBarProps = { floatingTerminalOpen: boolean @@ -827,13 +828,19 @@ function StatusBarInner({ floatingTerminalOpen }: StatusBarProps): React.JSX.Ele const compact = containerWidth < 900 const iconOnly = containerWidth < 500 - const floatingTerminalActionLabel = floatingTerminalOpen ? 'Hide Terminal' : 'Show Terminal' + const floatingTerminalActionLabel = floatingTerminalOpen ? 'Minimize Terminal' : 'Show Terminal' return (
{ + if ( + event.target instanceof Element && + event.target.closest('[data-floating-terminal-toggle]') + ) { + return + } // Why: mirror the right-click pattern used across the app // (WorktreeContextMenu, TerminalContextMenu, tab bar) — dispatch the // global close event so peer menus dismiss, then place a hidden @@ -893,36 +900,33 @@ function StatusBarInner({ floatingTerminalOpen }: StatusBarProps): React.JSX.Ele
{petEnabled && } - {showFloatingTerminalToggle && ( - - - - - - {floatingTerminalActionLabel} ( - {typeof navigator !== 'undefined' && navigator.userAgent.includes('Mac') - ? '⌘⌥T' - : 'Ctrl+Alt+T'} - ) - - - )} {showResourceUsage && } {showSsh && } + {showFloatingTerminalToggle && ( + + + + + + + {floatingTerminalActionLabel} ( + {typeof navigator !== 'undefined' && navigator.userAgent.includes('Mac') + ? '⌘⌥T' + : 'Ctrl+Alt+T'} + ) + + + + )}
diff --git a/src/shared/constants.ts b/src/shared/constants.ts index c1a1d85bf0c..f0a0b4eb2e5 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -203,6 +203,7 @@ export function getDefaultSettings(homedir: string): GlobalSettings { showTitlebarAppName: true, showTasksButton: true, floatingTerminalEnabled: true, + floatingTerminalDefaultedForAllUsers: true, floatingTerminalCwd: '~', floatingTerminalTriggerLocation: 'floating-button', notifications: getDefaultNotificationSettings(), diff --git a/src/shared/types.ts b/src/shared/types.ts index b382ce7df95..b1e4860c2f2 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -1187,14 +1187,18 @@ export type GlobalSettings = { * left sidebar free of its button entirely. Hiding the button here also * removes it from keyboard navigation. */ showTasksButton: boolean - /** Why: Floating Terminal is a global terminal surface. Keep it opt-in until - * users explicitly want a global shell outside repo/worktree context. */ + /** Why: Floating Terminal is the default global shell surface so users can + * reach a terminal outside repo/worktree context immediately. */ floatingTerminalEnabled: boolean + /** One-shot migration flag for the default-on rollout. Before this field + * landed, the floating terminal defaulted off and many profiles persisted + * that inherited false. Once migrated, an explicit off choice sticks. */ + floatingTerminalDefaultedForAllUsers?: boolean /** Where new Floating Terminal tabs start. Defaults to '~' so the visible * setting matches the shell-oriented directory users expect. */ floatingTerminalCwd: string /** Where the Floating Terminal toggle is shown. Defaults to the floating - * button for discoverability after the user opts into the feature. */ + * button for discoverability. */ floatingTerminalTriggerLocation: FloatingTerminalTriggerLocation diffDefaultView: 'inline' | 'side-by-side' notifications: NotificationSettings