mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 16:02:32 +00:00
Fix floating terminal default migration (#1767)
* Fix floating terminal default migration * Add floating terminal icon context menu
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: <PanelBottom className="size-3.5" />,
|
||||
label: 'Move to Status Bar',
|
||||
location: 'status-bar' as const
|
||||
}
|
||||
}
|
||||
return {
|
||||
icon: <PanelTop className="size-3.5" />,
|
||||
label: 'Move to Floating Button',
|
||||
location: 'floating-button' as const
|
||||
}
|
||||
}, [currentLocation])
|
||||
|
||||
return (
|
||||
<>
|
||||
<span
|
||||
className={className}
|
||||
data-floating-terminal-toggle
|
||||
onContextMenuCapture={(event) => {
|
||||
// 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}
|
||||
</span>
|
||||
<DropdownMenu open={open} onOpenChange={setOpen} modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
aria-hidden
|
||||
tabIndex={-1}
|
||||
className="pointer-events-none fixed size-px opacity-0"
|
||||
style={{ left: menuPoint.x, top: menuPoint.y }}
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-52" sideOffset={0} align="start">
|
||||
<DropdownMenuItem
|
||||
className="whitespace-nowrap"
|
||||
onSelect={() => {
|
||||
void updateSettings({ floatingTerminalTriggerLocation: moveAction.location })
|
||||
}}
|
||||
>
|
||||
{moveAction.icon}
|
||||
{moveAction.label}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="whitespace-nowrap"
|
||||
onSelect={() => {
|
||||
void updateSettings({ floatingTerminalEnabled: false })
|
||||
}}
|
||||
>
|
||||
<EyeOff className="size-3.5" />
|
||||
Hide
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
className="fixed bottom-8 right-3 z-40 bg-card/95 shadow-xs"
|
||||
data-floating-terminal-toggle
|
||||
aria-label={open ? 'Hide floating terminal' : 'Show floating terminal'}
|
||||
aria-pressed={open}
|
||||
onClick={onToggle}
|
||||
>
|
||||
<TerminalSquare className="size-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="left"
|
||||
sideOffset={6}
|
||||
>{`${open ? 'Hide' : 'Show'} floating terminal (${shortcutLabel})`}</TooltipContent>
|
||||
</Tooltip>
|
||||
<FloatingTerminalIconContextMenu
|
||||
currentLocation="floating-button"
|
||||
className="fixed bottom-8 right-3 z-40"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
className="bg-card/95 shadow-xs"
|
||||
aria-label={open ? 'Minimize floating terminal' : 'Show floating terminal'}
|
||||
aria-pressed={open}
|
||||
onClick={onToggle}
|
||||
>
|
||||
<TerminalSquare className="size-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="left"
|
||||
sideOffset={6}
|
||||
>{`${open ? 'Minimize' : 'Show'} floating terminal (${shortcutLabel})`}</TooltipContent>
|
||||
</Tooltip>
|
||||
</FloatingTerminalIconContextMenu>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)}
|
||||
>
|
||||
<X className="size-3.5" />
|
||||
<Minus className="size-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" sideOffset={6}>
|
||||
Hide floating terminal
|
||||
Minimize
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
ref={containerRefCallback}
|
||||
className="flex items-center h-6 min-h-[24px] px-3 gap-4 border-t border-border bg-[var(--bg-titlebar,var(--card))] text-xs select-none shrink-0 relative"
|
||||
onContextMenuCapture={(event) => {
|
||||
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
|
||||
<div className="flex items-center gap-3">
|
||||
<UpdateStatusSegment compact={compact} iconOnly={iconOnly} />
|
||||
{petEnabled && <PetStatusSegment />}
|
||||
{showFloatingTerminalToggle && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
||||
aria-label={floatingTerminalActionLabel}
|
||||
onClick={() => {
|
||||
window.dispatchEvent(new CustomEvent(TOGGLE_FLOATING_TERMINAL_EVENT))
|
||||
}}
|
||||
>
|
||||
<TerminalSquare className="size-3.5" />
|
||||
{!iconOnly && (
|
||||
<span className="inline-block w-[86px] whitespace-nowrap text-left">
|
||||
{floatingTerminalActionLabel}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" sideOffset={6}>
|
||||
{floatingTerminalActionLabel} (
|
||||
{typeof navigator !== 'undefined' && navigator.userAgent.includes('Mac')
|
||||
? '⌘⌥T'
|
||||
: 'Ctrl+Alt+T'}
|
||||
)
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{showResourceUsage && <ResourceUsageStatusSegment compact={compact} iconOnly={iconOnly} />}
|
||||
{showSsh && <SshStatusSegment compact={compact} iconOnly={iconOnly} />}
|
||||
{showFloatingTerminalToggle && (
|
||||
<FloatingTerminalIconContextMenu currentLocation="status-bar" className="relative">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex size-5 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
||||
aria-label={floatingTerminalActionLabel}
|
||||
onClick={() => {
|
||||
window.dispatchEvent(new CustomEvent(TOGGLE_FLOATING_TERMINAL_EVENT))
|
||||
}}
|
||||
>
|
||||
<TerminalSquare className="size-3.5" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" sideOffset={6}>
|
||||
{floatingTerminalActionLabel} (
|
||||
{typeof navigator !== 'undefined' && navigator.userAgent.includes('Mac')
|
||||
? '⌘⌥T'
|
||||
: 'Ctrl+Alt+T'}
|
||||
)
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</FloatingTerminalIconContextMenu>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DropdownMenu open={menuOpen} onOpenChange={setMenuOpen} modal={false}>
|
||||
|
||||
@@ -203,6 +203,7 @@ export function getDefaultSettings(homedir: string): GlobalSettings {
|
||||
showTitlebarAppName: true,
|
||||
showTasksButton: true,
|
||||
floatingTerminalEnabled: true,
|
||||
floatingTerminalDefaultedForAllUsers: true,
|
||||
floatingTerminalCwd: '~',
|
||||
floatingTerminalTriggerLocation: 'floating-button',
|
||||
notifications: getDefaultNotificationSettings(),
|
||||
|
||||
+7
-3
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user