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}
+
+
+
+
+
+
+ {
+ void updateSettings({ floatingTerminalTriggerLocation: moveAction.location })
+ }}
+ >
+ {moveAction.icon}
+ {moveAction.label}
+
+
+ {
+ void updateSettings({ floatingTerminalEnabled: false })
+ }}
+ >
+
+ Hide
+
+
+
+ >
+ )
+}
diff --git a/src/renderer/src/components/floating-terminal/FloatingTerminalPanel.tsx b/src/renderer/src/components/floating-terminal/FloatingTerminalPanel.tsx
index a376fb43cdb..cf8d5ec7c0b 100644
--- a/src/renderer/src/components/floating-terminal/FloatingTerminalPanel.tsx
+++ b/src/renderer/src/components/floating-terminal/FloatingTerminalPanel.tsx
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
-import { Maximize2, Minimize2, TerminalSquare, X } from 'lucide-react'
+import { Maximize2, Minimize2, Minus, TerminalSquare } from 'lucide-react'
import TabBar from '@/components/tab-bar/TabBar'
import TerminalPane from '@/components/terminal-pane/TerminalPane'
import { Button } from '@/components/ui/button'
@@ -15,6 +15,7 @@ import {
getMaximizedFloatingTerminalBounds,
type FloatingTerminalPanelBounds
} from './floating-terminal-panel-bounds'
+import { FloatingTerminalIconContextMenu } from './FloatingTerminalIconContextMenu'
const EMPTY_TERMINAL_TABS: TerminalTab[] = []
type FloatingTerminalPanelProps = {
@@ -32,26 +33,30 @@ export function FloatingTerminalToggleButton({
const shortcutLabel =
typeof navigator !== 'undefined' && navigator.userAgent.includes('Mac') ? '⌘⌥T' : 'Ctrl+Alt+T'
return (
-
-
-
-
-
-
- {`${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 && (
-
-
- {
- window.dispatchEvent(new CustomEvent(TOGGLE_FLOATING_TERMINAL_EVENT))
- }}
- >
-
- {!iconOnly && (
-
- {floatingTerminalActionLabel}
-
- )}
-
-
-
- {floatingTerminalActionLabel} (
- {typeof navigator !== 'undefined' && navigator.userAgent.includes('Mac')
- ? '⌘⌥T'
- : 'Ctrl+Alt+T'}
- )
-
-
- )}
{showResourceUsage &&
}
{showSsh &&
}
+ {showFloatingTerminalToggle && (
+
+
+
+ {
+ window.dispatchEvent(new CustomEvent(TOGGLE_FLOATING_TERMINAL_EVENT))
+ }}
+ >
+
+
+
+
+ {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