Fix CLI-launched agent terminal viewport (#2561)
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
630de67fcb
commit
a75a822339
|
|
@ -216,6 +216,8 @@ orca terminal read --json
|
|||
|
||||
Why: `--terminal` is optional for most commands. When omitted, Orca auto-resolves to the active terminal in the current worktree (same as browser commands target the active tab). Use explicit `--terminal <handle>` when operating on a specific pane.
|
||||
|
||||
Why: `terminal create` creates a background session unless `--focus` is explicit. Interactive local agent commands such as bare `codex` or bare `claude` use Orca's renderer-backed terminal path so they can start at the app's measured terminal geometry without stealing focus from the user.
|
||||
|
||||
Why: long terminal transcripts should be read with cursors. After a limited tail preview without an input cursor, page retained transcript from `oldestCursor`; in that case `nextCursor` already equals `latestCursor` and would skip omitted output. After a cursor read, if `limited` remains true and `nextCursor !== latestCursor`, continue with the returned `nextCursor`. Cursor reads default to the retained transcript size; `--limit` can request a smaller page. If `truncated` is true, older output has already fallen out of the retained buffer; use `oldestCursor` as the earliest available cursor.
|
||||
|
||||
Why: terminal handles are runtime-scoped and may go stale after reloads. If Orca returns `terminal_handle_stale`, reacquire a fresh handle with `terminal list`.
|
||||
|
|
|
|||
|
|
@ -1,39 +1,69 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { shouldForceVisibleCodexTerminal } from './codex-command-classification'
|
||||
import {
|
||||
shouldUseRendererBackedCodexTerminal,
|
||||
shouldUseRendererBackedInteractiveTerminal
|
||||
} from './codex-command-classification'
|
||||
|
||||
describe('shouldForceVisibleCodexTerminal', () => {
|
||||
it('forces visible terminal creation for interactive Codex sessions', () => {
|
||||
expect(shouldForceVisibleCodexTerminal('codex')).toBe(true)
|
||||
expect(shouldForceVisibleCodexTerminal('codex -m gpt-5 "fix the flaky test"')).toBe(true)
|
||||
expect(shouldForceVisibleCodexTerminal('codex resume --last')).toBe(true)
|
||||
expect(shouldForceVisibleCodexTerminal('codex fork')).toBe(true)
|
||||
expect(shouldForceVisibleCodexTerminal('codex login')).toBe(true)
|
||||
expect(shouldForceVisibleCodexTerminal('codex cloud')).toBe(true)
|
||||
expect(shouldForceVisibleCodexTerminal('codex -c active=cloud cloud')).toBe(true)
|
||||
expect(shouldForceVisibleCodexTerminal('codex.cmd resume --last')).toBe(true)
|
||||
expect(shouldForceVisibleCodexTerminal('env OPENAI_API_KEY=stub codex')).toBe(true)
|
||||
describe('shouldUseRendererBackedCodexTerminal', () => {
|
||||
it('uses renderer-backed terminal creation for interactive Codex sessions', () => {
|
||||
expect(shouldUseRendererBackedCodexTerminal('codex')).toBe(true)
|
||||
expect(shouldUseRendererBackedCodexTerminal('codex -m gpt-5 "fix the flaky test"')).toBe(true)
|
||||
expect(shouldUseRendererBackedCodexTerminal('codex resume --last')).toBe(true)
|
||||
expect(shouldUseRendererBackedCodexTerminal('codex fork')).toBe(true)
|
||||
expect(shouldUseRendererBackedCodexTerminal('codex login')).toBe(true)
|
||||
expect(shouldUseRendererBackedCodexTerminal('codex cloud')).toBe(true)
|
||||
expect(shouldUseRendererBackedCodexTerminal('codex -c active=cloud cloud')).toBe(true)
|
||||
expect(shouldUseRendererBackedCodexTerminal('codex.cmd resume --last')).toBe(true)
|
||||
expect(shouldUseRendererBackedCodexTerminal('env OPENAI_API_KEY=stub codex')).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps one-shot Codex commands on the background path', () => {
|
||||
expect(shouldForceVisibleCodexTerminal('codex exec summarize')).toBe(false)
|
||||
expect(shouldForceVisibleCodexTerminal('codex -m gpt-5 review')).toBe(false)
|
||||
expect(shouldForceVisibleCodexTerminal('codex login status')).toBe(false)
|
||||
expect(shouldForceVisibleCodexTerminal('codex login --with-api-key')).toBe(false)
|
||||
expect(shouldForceVisibleCodexTerminal('codex cloud list --json')).toBe(false)
|
||||
expect(shouldForceVisibleCodexTerminal('codex -c active=cloud cloud list --json')).toBe(false)
|
||||
expect(shouldForceVisibleCodexTerminal('codex cloud --enable foo list --json')).toBe(false)
|
||||
expect(shouldUseRendererBackedCodexTerminal('codex exec summarize')).toBe(false)
|
||||
expect(shouldUseRendererBackedCodexTerminal('codex -m gpt-5 review')).toBe(false)
|
||||
expect(shouldUseRendererBackedCodexTerminal('codex login status')).toBe(false)
|
||||
expect(shouldUseRendererBackedCodexTerminal('codex login --with-api-key')).toBe(false)
|
||||
expect(shouldUseRendererBackedCodexTerminal('codex cloud list --json')).toBe(false)
|
||||
expect(shouldUseRendererBackedCodexTerminal('codex -c active=cloud cloud list --json')).toBe(
|
||||
false
|
||||
)
|
||||
expect(shouldUseRendererBackedCodexTerminal('codex cloud --enable foo list --json')).toBe(false)
|
||||
expect(
|
||||
shouldForceVisibleCodexTerminal('env -u DEBUG CODEX_HOME=/tmp/codex codex exec summarize')
|
||||
shouldUseRendererBackedCodexTerminal(
|
||||
'env -u DEBUG CODEX_HOME=/tmp/codex codex exec summarize'
|
||||
)
|
||||
).toBe(false)
|
||||
expect(shouldForceVisibleCodexTerminal('codex cloud exec "fix it"')).toBe(false)
|
||||
expect(shouldForceVisibleCodexTerminal('codex cloud --version')).toBe(false)
|
||||
expect(shouldForceVisibleCodexTerminal('codex --help')).toBe(false)
|
||||
expect(shouldUseRendererBackedCodexTerminal('codex cloud exec "fix it"')).toBe(false)
|
||||
expect(shouldUseRendererBackedCodexTerminal('codex cloud --version')).toBe(false)
|
||||
expect(shouldUseRendererBackedCodexTerminal('codex --help')).toBe(false)
|
||||
})
|
||||
|
||||
it('ignores non-Codex commands', () => {
|
||||
expect(shouldForceVisibleCodexTerminal(undefined)).toBe(false)
|
||||
expect(shouldForceVisibleCodexTerminal('claude')).toBe(false)
|
||||
expect(shouldForceVisibleCodexTerminal('npm exec codex')).toBe(false)
|
||||
expect(shouldUseRendererBackedCodexTerminal(undefined)).toBe(false)
|
||||
expect(shouldUseRendererBackedCodexTerminal('claude')).toBe(false)
|
||||
expect(shouldUseRendererBackedCodexTerminal('npm exec codex')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('shouldUseRendererBackedInteractiveTerminal', () => {
|
||||
it('uses renderer-backed terminal creation for interactive Claude sessions', () => {
|
||||
expect(shouldUseRendererBackedInteractiveTerminal('claude')).toBe(true)
|
||||
expect(shouldUseRendererBackedInteractiveTerminal('claude --prefill "review this"')).toBe(true)
|
||||
expect(shouldUseRendererBackedInteractiveTerminal('/opt/anthropic/bin/claude')).toBe(true)
|
||||
expect(shouldUseRendererBackedInteractiveTerminal('env ANTHROPIC_BASE_URL=test claude')).toBe(
|
||||
true
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps one-shot Claude commands on the background path', () => {
|
||||
expect(shouldUseRendererBackedInteractiveTerminal('claude -p "summarize"')).toBe(false)
|
||||
expect(shouldUseRendererBackedInteractiveTerminal('claude --print "summarize"')).toBe(false)
|
||||
expect(shouldUseRendererBackedInteractiveTerminal('claude --help')).toBe(false)
|
||||
expect(shouldUseRendererBackedInteractiveTerminal('claude --version')).toBe(false)
|
||||
})
|
||||
|
||||
it('preserves Codex command classification', () => {
|
||||
expect(shouldUseRendererBackedInteractiveTerminal('codex')).toBe(true)
|
||||
expect(shouldUseRendererBackedInteractiveTerminal('codex exec summarize')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -130,6 +130,10 @@ function isCodexExecutable(command: string): boolean {
|
|||
return command === 'codex' || command === 'codex.exe' || command === 'codex.cmd'
|
||||
}
|
||||
|
||||
function isClaudeExecutable(command: string): boolean {
|
||||
return command === 'claude' || command === 'claude.exe' || command === 'claude.cmd'
|
||||
}
|
||||
|
||||
function isShellAssignment(token: string): boolean {
|
||||
return /^[A-Za-z_][A-Za-z0-9_]*=/.test(token)
|
||||
}
|
||||
|
|
@ -178,6 +182,11 @@ function isVersionFlag(token: string): boolean {
|
|||
return token === '--version' || token === '-V'
|
||||
}
|
||||
|
||||
function isClaudePrintFlag(token: string): boolean {
|
||||
const optionName = codexGlobalOptionName(token)
|
||||
return optionName === '-p' || optionName === '--print'
|
||||
}
|
||||
|
||||
function findCodexSubcommand(
|
||||
tokens: string[],
|
||||
startIndex: number,
|
||||
|
|
@ -259,7 +268,7 @@ function isNonInteractiveCodexSubcommand(tokens: string[]): boolean {
|
|||
return CODEX_NON_INTERACTIVE_SUBCOMMANDS.has(normalizedSubcommand)
|
||||
}
|
||||
|
||||
export function shouldForceVisibleCodexTerminal(command: string | undefined): boolean {
|
||||
export function shouldUseRendererBackedCodexTerminal(command: string | undefined): boolean {
|
||||
if (!command) {
|
||||
return false
|
||||
}
|
||||
|
|
@ -275,3 +284,24 @@ export function shouldForceVisibleCodexTerminal(command: string | undefined): bo
|
|||
|
||||
return !isNonInteractiveCodexSubcommand(tokens)
|
||||
}
|
||||
|
||||
export function shouldUseRendererBackedInteractiveTerminal(command: string | undefined): boolean {
|
||||
if (!command) {
|
||||
return false
|
||||
}
|
||||
|
||||
const tokens = stripShellLaunchPrefix(
|
||||
tokenizeLeadingShellWords(command.trim(), 32).filter((token) => token.length > 0)
|
||||
)
|
||||
|
||||
const executable = tokens[0] ? commandBasename(tokens[0]) : ''
|
||||
if (isCodexExecutable(executable)) {
|
||||
return !isNonInteractiveCodexSubcommand(tokens)
|
||||
}
|
||||
if (isClaudeExecutable(executable)) {
|
||||
return !tokens
|
||||
.slice(1)
|
||||
.some((token) => isHelpFlag(token) || isVersionFlag(token) || isClaudePrintFlag(token))
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import type {
|
|||
RuntimeTerminalWait
|
||||
} from '../../shared/runtime-types'
|
||||
import type { CommandHandler } from '../dispatch'
|
||||
import { shouldForceVisibleCodexTerminal } from '../codex-command-classification'
|
||||
import { shouldUseRendererBackedInteractiveTerminal } from '../codex-command-classification'
|
||||
import {
|
||||
formatTerminalClose,
|
||||
formatTerminalCreate,
|
||||
|
|
@ -130,17 +130,18 @@ export const TERMINAL_HANDLERS: Record<string, CommandHandler> = {
|
|||
)
|
||||
}
|
||||
const command = getOptionalStringFlag(flags, 'command')
|
||||
const useRendererBackedInteractiveTerminal =
|
||||
!client.isRemote && shouldUseRendererBackedInteractiveTerminal(command)
|
||||
const focus = flags.get('focus') === true
|
||||
const rendererBacked = !client.isRemote && shouldForceVisibleCodexTerminal(command)
|
||||
const result = await client.call<{ terminal: RuntimeTerminalCreate }>('terminal.create', {
|
||||
worktree: await getBrowserWorktreeSelector(flags, cwd, client),
|
||||
command,
|
||||
title: getOptionalStringFlag(flags, 'title'),
|
||||
// Why: Codex's interactive TUI must be born in a renderer-backed
|
||||
// terminal. The runtime's default create path spawns first in a
|
||||
// headless/background PTY and only adopts into the UI afterward.
|
||||
// Why: interactive local agent TUIs need the renderer-backed terminal
|
||||
// path for browser-side features, but CLI creates must stay backgrounded
|
||||
// unless the caller explicitly asks for focus.
|
||||
focus,
|
||||
...(rendererBacked ? { rendererBacked: true, activate: focus } : {})
|
||||
...(useRendererBackedInteractiveTerminal ? { rendererBacked: true, activate: focus } : {})
|
||||
})
|
||||
printResult(result, json, formatTerminalCreate)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -923,7 +923,7 @@ describe('orca cli worktree awareness', () => {
|
|||
})
|
||||
})
|
||||
|
||||
it('forces the visible terminal path for interactive Codex startup commands', async () => {
|
||||
it('keeps interactive Codex startup commands backgrounded unless focus is explicit', async () => {
|
||||
queueFixtures(
|
||||
callMock,
|
||||
okFixture('req_terminal_create', {
|
||||
|
|
@ -1144,7 +1144,7 @@ describe('orca cli worktree awareness', () => {
|
|||
})
|
||||
})
|
||||
|
||||
it('forces the visible terminal path for Codex prompts after global options', async () => {
|
||||
it('keeps Codex prompts after global options backgrounded unless focus is explicit', async () => {
|
||||
queueFixtures(
|
||||
callMock,
|
||||
okFixture('req_terminal_create', {
|
||||
|
|
@ -1182,6 +1182,80 @@ describe('orca cli worktree awareness', () => {
|
|||
})
|
||||
})
|
||||
|
||||
it('keeps interactive Claude startup commands backgrounded unless focus is explicit', async () => {
|
||||
queueFixtures(
|
||||
callMock,
|
||||
okFixture('req_terminal_create', {
|
||||
terminal: {
|
||||
handle: 'term_1',
|
||||
worktreeId: 'repo-1::/tmp/repo/feature',
|
||||
title: 'Claude'
|
||||
}
|
||||
})
|
||||
)
|
||||
vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||
|
||||
await main(
|
||||
[
|
||||
'terminal',
|
||||
'create',
|
||||
'--worktree',
|
||||
'path:/tmp/repo/feature',
|
||||
'--title',
|
||||
'Claude',
|
||||
'--command',
|
||||
'claude',
|
||||
'--json'
|
||||
],
|
||||
'/tmp/repo'
|
||||
)
|
||||
|
||||
expect(callMock).toHaveBeenCalledWith('terminal.create', {
|
||||
worktree: 'path:/tmp/repo/feature',
|
||||
command: 'claude',
|
||||
title: 'Claude',
|
||||
focus: false,
|
||||
rendererBacked: true,
|
||||
activate: false
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps Claude print commands on the background terminal path', async () => {
|
||||
queueFixtures(
|
||||
callMock,
|
||||
okFixture('req_terminal_create', {
|
||||
terminal: {
|
||||
handle: 'term_1',
|
||||
worktreeId: 'repo-1::/tmp/repo/feature',
|
||||
title: 'Claude print'
|
||||
}
|
||||
})
|
||||
)
|
||||
vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||
|
||||
await main(
|
||||
[
|
||||
'terminal',
|
||||
'create',
|
||||
'--worktree',
|
||||
'path:/tmp/repo/feature',
|
||||
'--title',
|
||||
'Claude print',
|
||||
'--command',
|
||||
'claude -p "summarize"',
|
||||
'--json'
|
||||
],
|
||||
'/tmp/repo'
|
||||
)
|
||||
|
||||
expect(callMock).toHaveBeenCalledWith('terminal.create', {
|
||||
worktree: 'path:/tmp/repo/feature',
|
||||
command: 'claude -p "summarize"',
|
||||
title: 'Claude print',
|
||||
focus: false
|
||||
})
|
||||
})
|
||||
|
||||
it('uses the resolved enclosing worktree for other worktree consumers', async () => {
|
||||
queueFixtures(
|
||||
callMock,
|
||||
|
|
|
|||
|
|
@ -519,25 +519,50 @@ function Terminal(): React.JSX.Element | null {
|
|||
// Only mount TerminalPanes for visited worktrees to prevent mass PTY
|
||||
// spawning when restoring a session with many saved worktree tabs.
|
||||
const mountedWorktreeIdsRef = useRef(new Set<string>())
|
||||
const measurableBackgroundWorktreeIdsRef = useRef(new Set<string>())
|
||||
const measurableBackgroundWorktreeTimersRef = useRef(new Map<string, number>())
|
||||
const [, setBackgroundMountRevision] = useState(0)
|
||||
useEffect(() => {
|
||||
const timers = measurableBackgroundWorktreeTimersRef.current
|
||||
const onBackgroundMountTerminalWorktree = (event: Event): void => {
|
||||
const customEvent = event as CustomEvent<BackgroundMountTerminalWorktreeDetail>
|
||||
addBackgroundMountedTerminalWorktree(
|
||||
mountedWorktreeIdsRef.current,
|
||||
customEvent.detail?.worktreeId,
|
||||
() => setBackgroundMountRevision((revision) => revision + 1)
|
||||
const worktreeId = customEvent.detail?.worktreeId
|
||||
addBackgroundMountedTerminalWorktree(mountedWorktreeIdsRef.current, worktreeId, () =>
|
||||
setBackgroundMountRevision((revision) => revision + 1)
|
||||
)
|
||||
if (!worktreeId) {
|
||||
return
|
||||
}
|
||||
measurableBackgroundWorktreeIdsRef.current.add(worktreeId)
|
||||
const existingTimer = timers.get(worktreeId)
|
||||
if (existingTimer !== undefined) {
|
||||
window.clearTimeout(existingTimer)
|
||||
}
|
||||
// Why: background renderer-backed terminal creation must be measurable
|
||||
// for the first xterm fit, but it must not keep hidden worktrees laid
|
||||
// out indefinitely after the PTY has started.
|
||||
const timer = window.setTimeout(() => {
|
||||
measurableBackgroundWorktreeIdsRef.current.delete(worktreeId)
|
||||
timers.delete(worktreeId)
|
||||
setBackgroundMountRevision((revision) => revision + 1)
|
||||
}, 3000)
|
||||
timers.set(worktreeId, timer)
|
||||
setBackgroundMountRevision((revision) => revision + 1)
|
||||
}
|
||||
window.addEventListener(
|
||||
BACKGROUND_MOUNT_TERMINAL_WORKTREE_EVENT,
|
||||
onBackgroundMountTerminalWorktree as EventListener
|
||||
)
|
||||
return () =>
|
||||
return () => {
|
||||
window.removeEventListener(
|
||||
BACKGROUND_MOUNT_TERMINAL_WORKTREE_EVENT,
|
||||
onBackgroundMountTerminalWorktree as EventListener
|
||||
)
|
||||
for (const timer of timers.values()) {
|
||||
window.clearTimeout(timer)
|
||||
}
|
||||
timers.clear()
|
||||
}
|
||||
}, [])
|
||||
// Why: gated on workspaceSessionReady to prevent TerminalPane from mounting
|
||||
// before reconnectPersistedTerminals() has finished eagerly spawning PTYs.
|
||||
|
|
@ -1384,6 +1409,8 @@ function Terminal(): React.JSX.Element | null {
|
|||
// Why: use strict equality with 'terminal' instead of !== 'settings'
|
||||
// so the terminal/browser surface hides on the tasks page too.
|
||||
const isVisible = activeView === 'terminal' && worktree.id === activeWorktreeId
|
||||
const shouldMeasureHiddenWorktree =
|
||||
!isVisible && measurableBackgroundWorktreeIdsRef.current.has(worktree.id)
|
||||
return (
|
||||
<WorktreeSplitSurface
|
||||
key={`tab-groups-${worktree.id}`}
|
||||
|
|
@ -1392,6 +1419,7 @@ function Terminal(): React.JSX.Element | null {
|
|||
layout={layout}
|
||||
focusedGroupId={activeGroupIdByWorktree[worktree.id]}
|
||||
isVisible={isVisible}
|
||||
shouldMeasureHiddenWorktree={shouldMeasureHiddenWorktree}
|
||||
activityTerminalPortals={activityTerminalPortals}
|
||||
/>
|
||||
)
|
||||
|
|
@ -1439,10 +1467,18 @@ function Terminal(): React.JSX.Element | null {
|
|||
// Why: use strict equality with 'terminal' instead of !== 'settings'
|
||||
// so the terminal/browser surface hides on the tasks page too.
|
||||
const isVisible = activeView === 'terminal' && worktree.id === activeWorktreeId
|
||||
const shouldMeasureHiddenWorktree =
|
||||
!isVisible && measurableBackgroundWorktreeIdsRef.current.has(worktree.id)
|
||||
return (
|
||||
<div
|
||||
key={worktree.id}
|
||||
className={isVisible ? 'absolute inset-0' : 'absolute inset-0 hidden'}
|
||||
className={
|
||||
isVisible
|
||||
? 'absolute inset-0'
|
||||
: shouldMeasureHiddenWorktree
|
||||
? 'absolute inset-0 opacity-0 pointer-events-none'
|
||||
: 'absolute inset-0 hidden'
|
||||
}
|
||||
aria-hidden={!isVisible}
|
||||
>
|
||||
<CodexRestartChip worktreeId={worktree.id} />
|
||||
|
|
@ -1639,6 +1675,7 @@ const WorktreeSplitSurface = React.memo(function WorktreeSplitSurface({
|
|||
layout,
|
||||
focusedGroupId,
|
||||
isVisible,
|
||||
shouldMeasureHiddenWorktree,
|
||||
activityTerminalPortals
|
||||
}: {
|
||||
worktreeId: string
|
||||
|
|
@ -1646,11 +1683,18 @@ const WorktreeSplitSurface = React.memo(function WorktreeSplitSurface({
|
|||
layout: TabGroupLayoutNode
|
||||
focusedGroupId?: string
|
||||
isVisible: boolean
|
||||
shouldMeasureHiddenWorktree: boolean
|
||||
activityTerminalPortals: ActivityTerminalPortalTarget[]
|
||||
}): React.JSX.Element {
|
||||
return (
|
||||
<div
|
||||
className={isVisible ? 'absolute inset-0 flex' : 'absolute inset-0 hidden'}
|
||||
className={
|
||||
isVisible
|
||||
? 'absolute inset-0 flex'
|
||||
: shouldMeasureHiddenWorktree
|
||||
? 'absolute inset-0 flex opacity-0 pointer-events-none'
|
||||
: 'absolute inset-0 hidden'
|
||||
}
|
||||
aria-hidden={!isVisible}
|
||||
>
|
||||
<CodexRestartChip worktreeId={worktreeId} />
|
||||
|
|
|
|||
|
|
@ -321,6 +321,7 @@ export default function TerminalPane({
|
|||
// without changing the other platforms' interaction model.
|
||||
const rightClickToPaste = isWindowsUserAgent() && (settings?.terminalRightClickToPaste ?? true)
|
||||
const [startup] = useState(() => useAppStore.getState().pendingStartupByTabId[tabId])
|
||||
const shouldMeasureHiddenStartup = startup !== undefined && !isVisible
|
||||
const consumeTabStartupCommand = useAppStore((store) => store.consumeTabStartupCommand)
|
||||
const [setupSplit] = useState(() => useAppStore.getState().pendingSetupSplitByTabId[tabId])
|
||||
const consumeTabSetupSplit = useAppStore((store) => store.consumeTabSetupSplit)
|
||||
|
|
@ -1486,7 +1487,8 @@ export default function TerminalPane({
|
|||
// Why: split groups can keep one terminal visible in an unfocused group so
|
||||
// users still see its output while typing elsewhere. Hiding on `isActive`
|
||||
// blanked the previously focused pane and exposed the white group body.
|
||||
display: isVisible ? 'flex' : 'none',
|
||||
display: isVisible || shouldMeasureHiddenStartup ? 'flex' : 'none',
|
||||
...(shouldMeasureHiddenStartup ? { opacity: 0, pointerEvents: 'none' } : {}),
|
||||
['--orca-terminal-divider-color' as string]:
|
||||
effectiveAppearance?.dividerColor ?? DEFAULT_TERMINAL_DIVIDER_DARK,
|
||||
['--orca-terminal-divider-color-strong' as string]: normalizeColor(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { memo, useCallback, useMemo } from 'react'
|
||||
import { memo, useCallback, useMemo, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import type { Tab, TabGroup, TerminalTab } from '../../../../shared/types'
|
||||
|
|
@ -50,6 +50,9 @@ const TerminalOverlaySlot = memo(function TerminalOverlaySlot({
|
|||
leaveWorktreeIfEmpty
|
||||
}: TerminalOverlaySlotProps): React.JSX.Element {
|
||||
const anchorName = groupId !== undefined ? tabGroupBodyAnchorName(groupId) : undefined
|
||||
const [shouldMeasureHiddenStartup] = useState(
|
||||
() => useAppStore.getState().pendingStartupByTabId[terminalTabId] !== undefined
|
||||
)
|
||||
const style: React.CSSProperties = useMemo(
|
||||
() =>
|
||||
anchorName
|
||||
|
|
@ -60,7 +63,8 @@ const TerminalOverlaySlot = memo(function TerminalOverlaySlot({
|
|||
left: `anchor(${anchorName} left)`,
|
||||
width: `anchor-size(${anchorName} width)`,
|
||||
height: `anchor-size(${anchorName} height)`,
|
||||
display: isVisible ? 'flex' : 'none',
|
||||
display: isVisible || shouldMeasureHiddenStartup ? 'flex' : 'none',
|
||||
opacity: isVisible ? 1 : 0,
|
||||
pointerEvents: isVisible ? 'auto' : 'none'
|
||||
}
|
||||
: {
|
||||
|
|
@ -72,7 +76,7 @@ const TerminalOverlaySlot = memo(function TerminalOverlaySlot({
|
|||
display: 'none',
|
||||
pointerEvents: 'none'
|
||||
},
|
||||
[anchorName, isVisible]
|
||||
[anchorName, isVisible, shouldMeasureHiddenStartup]
|
||||
)
|
||||
const focusGroup = useCallback(() => {
|
||||
if (groupId !== undefined && onFocusOwningGroup) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue