| |
| import * as React from 'react' |
| import { Suspense, useState } from 'react' |
| import { useKeybinding } from '../../keybindings/useKeybinding.js' |
| import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js' |
| import { useTerminalSize } from '../../hooks/useTerminalSize.js' |
| import { |
| useIsInsideModal, |
| useModalOrTerminalSize, |
| } from '../../context/modalContext.js' |
| import { Pane } from '../design-system/Pane.js' |
| import { Tabs, Tab } from '../design-system/Tabs.js' |
| import { Status, buildDiagnostics } from './Status.js' |
| import { Config } from './Config.js' |
| import { Usage } from './Usage.js' |
| import type { |
| LocalJSXCommandContext, |
| CommandResultDisplay, |
| } from '../../commands.js' |
|
|
| type Props = { |
| onClose: ( |
| result?: string, |
| options?: { display?: CommandResultDisplay }, |
| ) => void |
| context: LocalJSXCommandContext |
| defaultTab: 'Status' | 'Config' | 'Usage' | 'Gates' |
| } |
|
|
| export function Settings({ |
| onClose, |
| context, |
| defaultTab, |
| }: Props): React.ReactNode { |
| const [selectedTab, setSelectedTab] = useState<string>(defaultTab) |
| const [tabsHidden, setTabsHidden] = useState(false) |
| |
| |
| const [configOwnsEsc, setConfigOwnsEsc] = useState(false) |
| const [gatesOwnsEsc, setGatesOwnsEsc] = useState(false) |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const insideModal = useIsInsideModal() |
| const { rows } = useModalOrTerminalSize(useTerminalSize()) |
| const contentHeight = insideModal |
| ? rows + 1 |
| : Math.max(15, Math.min(Math.floor(rows * 0.8), 30)) |
| |
| |
| |
| const [diagnosticsPromise] = useState(() => |
| buildDiagnostics().catch(() => []), |
| ) |
|
|
| useExitOnCtrlCDWithKeybindings() |
|
|
| |
| const handleEscape = () => { |
| |
| |
| if (tabsHidden) { |
| return |
| } |
| |
| onClose('Status dialog dismissed', { display: 'system' }) |
| } |
|
|
| |
| |
| |
| useKeybinding('confirm:no', handleEscape, { |
| context: 'Settings', |
| isActive: |
| !tabsHidden && |
| !(selectedTab === 'Config' && configOwnsEsc) && |
| !(selectedTab === 'Gates' && gatesOwnsEsc), |
| }) |
|
|
| const tabs = [ |
| <Tab key="status" title="Status"> |
| <Status context={context} diagnosticsPromise={diagnosticsPromise} /> |
| </Tab>, |
| <Tab key="config" title="Config"> |
| <Suspense fallback={null}> |
| <Config |
| context={context} |
| onClose={onClose} |
| setTabsHidden={setTabsHidden} |
| onIsSearchModeChange={setConfigOwnsEsc} |
| contentHeight={contentHeight} |
| /> |
| </Suspense> |
| </Tab>, |
| <Tab key="usage" title="Usage"> |
| <Usage /> |
| </Tab>, |
| ...("external" === 'ant' |
| ? [ |
| <Tab key="gates" title="Gates"> |
| <Gates |
| onOwnsEscChange={setGatesOwnsEsc} |
| contentHeight={contentHeight} |
| /> |
| </Tab>, |
| ] |
| : []), |
| ] |
|
|
| return ( |
| <Pane color="permission"> |
| <Tabs |
| color="permission" |
| selectedTab={selectedTab} |
| onTabChange={setSelectedTab} |
| hidden={tabsHidden} |
| // Config has interactive content — start with header unfocused so |
| // left/right/tab cycle option values instead of switching tabs. |
| initialHeaderFocused={defaultTab !== 'Config' && defaultTab !== 'Gates'} |
| // Inside a Modal, skip the Tabs-level cap so tall tabs (Status's |
| // MCP list) flow to their natural height for the Modal's ScrollBox |
| // to scroll. Config/Gates still get contentHeight above — they |
| // paginate internally so this only affects Status/Usage. |
| contentHeight={tabsHidden || insideModal ? undefined : contentHeight} |
| > |
| {tabs} |
| </Tabs> |
| </Pane> |
| ) |
| } |
|
|