Skip to content
4 changes: 3 additions & 1 deletion src/main/codex/codex-structured-thread-open.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ function isCodexNoRolloutError(error: unknown, threadId: string): boolean {
* every Orca key the app-server holds. Codex adds `set` keys after that filter and merges these
* per key, so the user's own policy stays as written.
*/
function shellEnvironmentSetConfig(env: Record<string, string> | undefined): Record<string, string> {
function shellEnvironmentSetConfig(
env: Record<string, string> | undefined
): Record<string, string> {
return Object.fromEntries(
Object.entries(env ?? {}).map(([key, value]) => [`shell_environment_policy.set.${key}`, value])
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { act, cleanup, fireEvent, render } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { UNFOCUSED_SPLIT_GROUP_OPACITY } from '../tab-group/split-group-dim'
import type { Tab, TabGroup } from '../../../../shared/tab-types'

type MockAppState = {
Expand Down Expand Up @@ -216,6 +217,17 @@ describe('StructuredAgentSessionPaneOverlayLayer', () => {
expect(chatSurface(view.container, SECOND_TAB_ID).dataset.chatVisible).toBe('true')
expect(chatSurface(view.container, FIRST_TAB_ID).dataset.chatFocusedGroup).toBe('false')
expect(chatSurface(view.container, SECOND_TAB_ID).dataset.chatFocusedGroup).toBe('true')
const host = (tabId: string): HTMLElement | null =>
chatSurface(view.container, tabId).closest<HTMLElement>('[data-retained-pane-host]')
const first = host(FIRST_TAB_ID)
const second = host(SECOND_TAB_ID)
const dim = String(UNFOCUSED_SPLIT_GROUP_OPACITY)
// The unfocused chat takes the same dim as every other unfocused split group, never more.
expect(first?.style.opacity).toBe(dim)
expect(second?.style.opacity).toBe('1')
act(() => mocks.store?.setState({ activeGroupIdByWorktree: { [WORKTREE_ID]: GROUP_ID } }))
expect(first?.style.opacity).toBe('1')
expect(second?.style.opacity).toBe(dim)
})

it('marks no column as focused when the focused group id names nothing', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@ const StructuredAgentSessionOverlaySlot = memo(function StructuredAgentSessionOv
groupId,
isActive,
isFocusedGroup,
hasSplitGroups,
onFocusOwningGroup
}: {
tab: StructuredAgentSessionTab
groupId: string | undefined
isActive: boolean
isFocusedGroup: boolean
hasSplitGroups: boolean
onFocusOwningGroup: ((groupId: string) => void) | undefined
}): React.JSX.Element | null {
// Each chat is read from the host recorded on its tab, never from its workspace id, which two
Expand All @@ -42,6 +44,7 @@ const StructuredAgentSessionOverlaySlot = memo(function StructuredAgentSessionOv
<RetainedPaneHost
groupId={groupId}
isVisible={isActive}
dimmed={hasSplitGroups && !isFocusedGroup}
data-structured-agent-session-overlay-tab-id={tab.id}
onFocusOwningGroup={onFocusOwningGroup}
>
Expand Down Expand Up @@ -101,6 +104,7 @@ const StructuredAgentSessionPaneOverlayLayer = memo(
groupActiveTabById.get(tab.groupId) === tab.id &&
tab.groupId === activeGroupId
)}
hasSplitGroups={groups.length > 1}
onFocusOwningGroup={focusOwningGroup}
/>
))}
Expand Down
11 changes: 8 additions & 3 deletions src/renderer/src/components/tab-group/RetainedPaneHost.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'
import { SYNC_FIT_PANES_EVENT } from '@/constants/terminal'
import { tabGroupBodyAnchorName } from './tab-group-body-anchor'
import { subscribeTabGroupBodyRect, type TabGroupBodyRect } from './tab-group-body-geometry'
import { UNFOCUSED_SPLIT_GROUP_OPACITY } from './split-group-dim'

const HAS_CSS_ANCHOR_POSITIONING =
typeof CSS !== 'undefined' &&
Expand All @@ -23,6 +24,8 @@ type RetainedPaneHostProps = {
isVisible: boolean
measureWhileHidden?: boolean
fitTerminal?: boolean
/** Unfocused side of a split: overlays sit outside the group's DOM, so they take its dim here. */
dimmed?: boolean
onFocusOwningGroup?: (groupId: string) => void
children: React.ReactNode
'data-terminal-overlay-tab-id'?: string
Expand All @@ -34,6 +37,7 @@ export function RetainedPaneHost({
isVisible,
measureWhileHidden = false,
fitTerminal = false,
dimmed = false,
onFocusOwningGroup,
children,
...identity
Expand Down Expand Up @@ -94,6 +98,7 @@ export function RetainedPaneHost({
}
}, [anchorName, fitTerminal, isVisible, measuredFallbackRect])

const visibleOpacity = dimmed ? UNFOCUSED_SPLIT_GROUP_OPACITY : 1
const style: React.CSSProperties = useMemo(
() =>
anchorName && shouldUseCssAnchorPositioning()
Expand All @@ -105,7 +110,7 @@ export function RetainedPaneHost({
width: `anchor-size(${anchorName} width)`,
height: `anchor-size(${anchorName} height)`,
display: isVisible || measureWhileHidden ? 'flex' : 'none',
opacity: isVisible ? 1 : 0,
opacity: isVisible ? visibleOpacity : 0,
pointerEvents: isVisible ? 'auto' : 'none'
}
: anchorName
Expand All @@ -119,7 +124,7 @@ export function RetainedPaneHost({
width: measuredFallbackRect?.width ?? '100%',
height: measuredFallbackRect?.height ?? 'calc(100% - 32px)',
display: isVisible || measureWhileHidden ? 'flex' : 'none',
opacity: isVisible ? 1 : 0,
opacity: isVisible ? visibleOpacity : 0,
pointerEvents: isVisible ? 'auto' : 'none'
}
: {
Expand All @@ -131,7 +136,7 @@ export function RetainedPaneHost({
display: 'none',
pointerEvents: 'none'
},
[anchorName, isVisible, measuredFallbackRect, measureWhileHidden]
[anchorName, isVisible, measuredFallbackRect, measureWhileHidden, visibleOpacity]
)
const focusGroup = useCallback(() => {
if (groupId !== undefined && onFocusOwningGroup) {
Expand Down
4 changes: 3 additions & 1 deletion src/renderer/src/components/tab-group/TabGroupPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { useClientHostedBrowserRows } from '@/lib/pane-manager/client-hosted-bro
import { resolveClientHostedBrowserRowStripGroupId } from '../tab-bar/client-hosted-browser-row-strip-placement'
import { useEditorGroupFileDropOwner } from '../editor/use-editor-group-file-drop-owner'
import { cn } from '@/lib/utils'
import { UNFOCUSED_SPLIT_GROUP_OPACITY } from './split-group-dim'

const EditorPanel = lazy(() => import('../editor/EditorPanel'))
const EMPTY_GROUPS: readonly TabGroup[] = []
Expand Down Expand Up @@ -259,9 +260,10 @@ export default function TabGroupPanel({
touchesBottomEdge || suppressBottomBorder ? '' : 'border-b'
} border-border ${
isFocused && !touchesBottomEdge && !suppressBottomBorder ? 'border-b-accent' : ''
} ${isFocused ? '' : 'opacity-95'}`
}`
: ''
}`}
style={hasSplitGroups && !isFocused ? { opacity: UNFOCUSED_SPLIT_GROUP_OPACITY } : undefined}
onPointerDown={commands.focusGroup}
// Why: keyboard/AT focus can enter a split group without a pointer event, so sync group focus to DOM focus for global shortcuts.
onFocusCapture={commands.focusGroup}
Expand Down
2 changes: 2 additions & 0 deletions src/renderer/src/components/tab-group/split-group-dim.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/** The one dim every unfocused split group's content gets, terminals, editors and chats alike. */
export const UNFOCUSED_SPLIT_GROUP_OPACITY = 0.95
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ type TerminalOverlaySlotProps = {
isWorktreeActive: boolean
isVisible: boolean
isActive: boolean
/** Visible on the unfocused side of a split. */
dimmed?: boolean
activityTerminalPortal: ActivityTerminalPortalTarget | null
onFocusOwningGroup: ((groupId: string) => void) | undefined
consumeSuppressedPtyExit: (ptyId: string) => boolean
Expand All @@ -35,6 +37,7 @@ export const TerminalOverlaySlot = memo(function TerminalOverlaySlot({
isWorktreeActive,
isVisible,
isActive,
dimmed = false,
activityTerminalPortal,
onFocusOwningGroup,
consumeSuppressedPtyExit,
Expand Down Expand Up @@ -106,6 +109,7 @@ export const TerminalOverlaySlot = memo(function TerminalOverlaySlot({
isVisible={isVisible}
measureWhileHidden={shouldMeasureHiddenStartup}
fitTerminal
dimmed={dimmed}
data-terminal-overlay-tab-id={terminalTabId}
onFocusOwningGroup={onFocusOwningGroup}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ const TerminalPaneOverlayLayer = memo(function TerminalPaneOverlayLayer({
isWorktreeActive={isWorktreeActive}
isVisible={isVisible}
isActive={isActive}
dimmed={groups.length > 1 && isVisible && !isActive}
activityTerminalPortal={activityTerminalPortal}
onFocusOwningGroup={focusOwningGroup}
consumeSuppressedPtyExit={consumeSuppressedPtyExit}
Expand Down
Loading