Make floating terminal trigger more obvious (#1943)

This commit is contained in:
Jinjing 2026-05-15 11:29:33 -07:00 committed by GitHub
parent da95082fd7
commit 7c384a36e3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 153 additions and 46 deletions

View File

@ -1,10 +1,8 @@
/* eslint-disable max-lines -- Why: the floating surface owns both terminal chrome and local notes tabs until the shared floating shell is extracted. */
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Maximize2, Minimize2, Minus } from 'lucide-react'
import TabBar from '@/components/tab-bar/TabBar'
import TerminalPane from '@/components/terminal-pane/TerminalPane'
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { focusTerminalTabSurface } from '@/lib/focus-terminal-tab-surface'
import {
ORCHESTRATION_SETUP_DISMISSED_STORAGE_KEY,
@ -23,6 +21,7 @@ import type { TerminalTab } from '../../../../shared/types'
import { FloatingTerminalOrchestrationDialog } from './FloatingTerminalOrchestrationDialog'
import ProjectNotesTabContent from '@/components/notes/ProjectNotesTabContent'
import { FloatingTerminalResizeHandles } from './FloatingTerminalResizeHandles'
import { FloatingTerminalWindowControls } from './FloatingTerminalWindowControls'
export { FloatingTerminalToggleButton } from './FloatingTerminalToggleButton'
import {
clampFloatingTerminalBounds,
@ -361,7 +360,7 @@ export function FloatingTerminalPanel({
>
<div className="flex min-h-0 flex-1 flex-col">
<div
className="flex h-9 shrink-0 items-center border-b border-border bg-[var(--bg-titlebar,var(--card))]"
className="flex h-9 shrink-0 cursor-grab items-center border-b border-border bg-[var(--bg-titlebar,var(--card))] active:cursor-grabbing"
onPointerDown={handleDragStart}
onPointerMove={handleDragMove}
onPointerUp={handleDragEnd}
@ -401,47 +400,11 @@ export function FloatingTerminalPanel({
tabBarOrder={tabBarOrder}
/>
</div>
<div className="flex items-center gap-1 px-2" data-floating-terminal-no-drag>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon-xs"
aria-label={
maximized ? 'Restore floating terminal' : 'Maximize floating terminal'
}
aria-pressed={maximized}
onClick={toggleMaximized}
>
{maximized ? (
<Minimize2 className="size-3.5" />
) : (
<Maximize2 className="size-3.5" />
)}
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={6}>
{maximized ? 'Restore' : 'Maximize'}
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon-xs"
aria-label="Minimize floating terminal"
onClick={() => onOpenChange(false)}
>
<Minus className="size-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={6}>
Minimize
</TooltipContent>
</Tooltip>
</div>
<FloatingTerminalWindowControls
maximized={maximized}
onToggleMaximized={toggleMaximized}
onMinimize={() => onOpenChange(false)}
/>
</div>
<div className="relative min-h-0 flex-1 overflow-hidden bg-background">

View File

@ -23,7 +23,7 @@ export function FloatingTerminalToggleButton({
type="button"
variant="outline"
size="icon-sm"
className="bg-card/95 shadow-xs"
className="border-border bg-secondary text-secondary-foreground shadow-xs hover:bg-accent hover:text-accent-foreground"
data-floating-terminal-toggle
aria-label={open ? 'Minimize floating terminal' : 'Show floating terminal'}
aria-pressed={open}

View File

@ -0,0 +1,144 @@
import { useCallback, useMemo } from 'react'
import { Maximize2, Minimize2, Minus } from 'lucide-react'
import { toast } from 'sonner'
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { AGENT_CATALOG, AgentIcon } from '@/lib/agent-catalog'
import { focusTerminalTabSurface } from '@/lib/focus-terminal-tab-surface'
import { CLIENT_PLATFORM } from '@/lib/new-workspace'
import { buildAgentStartupPlan } from '@/lib/tui-agent-startup'
import { tuiAgentToAgentKind } from '@/lib/telemetry'
import { useAppStore } from '@/store'
import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../shared/constants'
type FloatingTerminalWindowControlsProps = {
maximized: boolean
onToggleMaximized: () => void
onMinimize: () => void
}
const controlButtonClassName =
'border-border bg-secondary text-secondary-foreground shadow-xs hover:bg-accent hover:text-accent-foreground'
export function FloatingTerminalWindowControls({
maximized,
onToggleMaximized,
onMinimize
}: FloatingTerminalWindowControlsProps): React.JSX.Element {
const defaultTuiAgent = useAppStore((s) => s.settings?.defaultTuiAgent ?? null)
const createTab = useAppStore((s) => s.createTab)
const setActiveTabForWorktree = useAppStore((s) => s.setActiveTabForWorktree)
const defaultAgent = defaultTuiAgent && defaultTuiAgent !== 'blank' ? defaultTuiAgent : null
const defaultAgentLabel = useMemo(
() =>
defaultAgent
? (AGENT_CATALOG.find((agent) => agent.id === defaultAgent)?.label ?? defaultAgent)
: null,
[defaultAgent]
)
const launchDefaultAgent = useCallback(() => {
if (!defaultAgent) {
return
}
const state = useAppStore.getState()
const startupPlan = buildAgentStartupPlan({
agent: defaultAgent,
prompt: '',
cmdOverrides: state.settings?.agentCmdOverrides ?? {},
platform: CLIENT_PLATFORM,
allowEmptyPromptLaunch: true
})
if (!startupPlan) {
toast.error(`Could not build launch command for ${defaultAgentLabel ?? defaultAgent}.`)
return
}
const tab = createTab(FLOATING_TERMINAL_WORKTREE_ID, undefined, undefined, { activate: false })
state.queueTabStartupCommand(tab.id, {
command: startupPlan.launchCommand,
...(startupPlan.env ? { env: startupPlan.env } : {}),
telemetry: {
agent_kind: tuiAgentToAgentKind(defaultAgent),
launch_source: 'shortcut',
request_kind: 'new'
}
})
setActiveTabForWorktree(FLOATING_TERMINAL_WORKTREE_ID, tab.id)
const fresh = useAppStore.getState()
const currentTabs = fresh.tabsByWorktree[FLOATING_TERMINAL_WORKTREE_ID] ?? []
const stored = fresh.tabBarOrderByWorktree[FLOATING_TERMINAL_WORKTREE_ID] ?? []
const validIds = new Set(currentTabs.map((entry) => entry.id))
const order = stored.filter((id) => validIds.has(id) && id !== tab.id)
for (const entry of currentTabs) {
if (entry.id !== tab.id && !order.includes(entry.id)) {
order.push(entry.id)
}
}
order.push(tab.id)
fresh.setTabBarOrder(FLOATING_TERMINAL_WORKTREE_ID, order)
focusTerminalTabSurface(tab.id)
}, [createTab, defaultAgent, defaultAgentLabel, setActiveTabForWorktree])
return (
<div className="flex items-center gap-1 px-2" data-floating-terminal-no-drag>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="outline"
size="icon-xs"
className={controlButtonClassName}
aria-label={
defaultAgentLabel
? `Open ${defaultAgentLabel} in floating terminal`
: 'No default agent configured'
}
disabled={!defaultAgent}
onClick={launchDefaultAgent}
>
<AgentIcon agent={defaultAgent} size={14} />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={6}>
{defaultAgentLabel ? `Open ${defaultAgentLabel}` : 'Choose a default agent first'}
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="outline"
size="icon-xs"
className={controlButtonClassName}
aria-label={maximized ? 'Restore floating terminal' : 'Maximize floating terminal'}
aria-pressed={maximized}
onClick={onToggleMaximized}
>
{maximized ? <Minimize2 className="size-3.5" /> : <Maximize2 className="size-3.5" />}
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={6}>
{maximized ? 'Restore' : 'Maximize'}
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="outline"
size="icon-xs"
className={controlButtonClassName}
aria-label="Minimize floating terminal"
onClick={onMinimize}
>
<Minus className="size-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={6}>
Minimize
</TooltipContent>
</Tooltip>
</div>
)
}

View File

@ -908,7 +908,7 @@ function StatusBarInner({ floatingTerminalOpen }: StatusBarProps): React.JSX.Ele
<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"
className="inline-flex size-5 cursor-pointer items-center justify-center rounded border border-border bg-secondary text-secondary-foreground shadow-xs transition-colors hover:bg-accent hover:text-accent-foreground"
aria-label={floatingTerminalActionLabel}
onClick={() => {
window.dispatchEvent(new CustomEvent(TOGGLE_FLOATING_TERMINAL_EVENT))