File size: 5,096 Bytes
064bfd6
6db367d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
064bfd6
6db367d
 
 
 
 
 
064bfd6
6db367d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
064bfd6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
// biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered
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)
  // True while Config's own Esc handler is active (search mode with content
  // focused). Settings must cede Esc so search can clear/exit first.
  const [configOwnsEsc, setConfigOwnsEsc] = useState(false)
  const [gatesOwnsEsc, setGatesOwnsEsc] = useState(false)
  // Fixed content height so switching tabs doesn't shift the pane height.
  // Outside modals cap at min(80% viewport, 30). Inside a Modal the modal's
  // innerSize.rows IS the ScrollBox viewport — the 0.8 multiplier over-
  // shrinks, leaving empty rows while Config shows "↓ N more below".
  //
  // Inside-modal math: Config's paneCap-10 chrome estimate was tuned for
  // marginY={1} (2 rows) which is stripped inside modals → +2 to recover.
  // Then -2 for Tabs' header row + its marginTop=1. Plus +1 observed gap
  // from the paneCap-10 estimate being slightly generous. Net: rows + 1.
  const insideModal = useIsInsideModal()
  const { rows } = useModalOrTerminalSize(useTerminalSize())
  const contentHeight = insideModal
    ? rows + 1
    : Math.max(15, Math.min(Math.floor(rows * 0.8), 30))
  // Kick off diagnostics once when the pane opens. Status use()s this so
  // it resolves once per /config invocation — no re-fetch flash when
  // tabbing back to Status (Tab unmounts children when not selected).
  const [diagnosticsPromise] = useState(() =>
    buildDiagnostics().catch(() => []),
  )

  useExitOnCtrlCDWithKeybindings()

  // Handle escape via keybinding - only when not in submenu
  const handleEscape = () => {
    // Don't handle escape when a submenu is showing (tabsHidden means submenu is open)
    // Let the submenu handle escape to return to the main menu
    if (tabsHidden) {
      return
    }
    // TODO: Update to "Settings" dialog once we define '/settings'.
    onClose('Status dialog dismissed', { display: 'system' })
  }

  // Disable when submenu is open so the submenu's Dialog can handle ESC,
  // and when Config's search mode is active so its useInput handler
  // (clear query → exit search) processes Escape first.
  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>
  )
}