diff --git a/apps/desktop/src/renderer/src/features/composer/ModelSelector.tsx b/apps/desktop/src/renderer/src/features/composer/ModelSelector.tsx index ebb4f773..4f7a32a4 100644 --- a/apps/desktop/src/renderer/src/features/composer/ModelSelector.tsx +++ b/apps/desktop/src/renderer/src/features/composer/ModelSelector.tsx @@ -101,6 +101,7 @@ export function ModelSelector({ const [loaded, setLoaded] = useState(false) const listRef = useRef(null) const searchRef = useRef(null) + const rootRef = useRef(null) const listboxId = useId() const activeId = `${listboxId}-opt-${activeIndex}` @@ -278,11 +279,26 @@ export function ModelSelector({ ?.scrollIntoView?.({ block: 'nearest' }) }, [activeIndex]) - const close = (): void => { + const close = useCallback((): void => { setOpen(false) setQuery('') setActiveKind(null) - } + }, []) + + /** + * Outside pointerdown (not a fullscreen backdrop) dismisses the picker, so + * a click on a sibling composer chip reaches that chip: the open picker + * closes and the new one opens in the same gesture instead of the click + * being swallowed and the composer collapsing underneath. + */ + useEffect(() => { + if (!open) return + const onPointerDown = (e: PointerEvent): void => { + if (rootRef.current?.contains(e.target as Node) !== true) close() + } + document.addEventListener('pointerdown', onPointerDown) + return () => document.removeEventListener('pointerdown', onPointerDown) + }, [open, close]) const pickModel = (opt: SelectorOption): void => { // Endpoint options carry `ep:`; they ride the ari-core driver, @@ -300,7 +316,10 @@ export function ModelSelector({ const switchProvider = (step: 1 | -1): void => { if (lockedTo !== null || providers.length === 0) return - const at = Math.max(0, providers.findIndex((p) => p.kind === activeKind)) + const at = Math.max( + 0, + providers.findIndex((p) => p.kind === activeKind), + ) const next = providers[(at + step + providers.length) % providers.length] if (next !== undefined) { setActiveKind(next.kind) @@ -363,7 +382,8 @@ export function ModelSelector({ // by prefix so their label still resolves. const exact = endpointModels.find((e) => e.id === modelId) const legacy = - exact ?? (modelId != null ? endpointModels.find((e) => e.id.startsWith(`${modelId}:`)) : undefined) + exact ?? + (modelId != null ? endpointModels.find((e) => e.id.startsWith(`${modelId}:`)) : undefined) return legacy?.label ?? modelId ?? 'Ari Core' } const list = optionsFor(driverKind) @@ -423,7 +443,7 @@ export function ModelSelector({ ) return ( -
+
{open ? ( - <> -
-
-
- - setQuery(e.target.value)} - placeholder="Search models…" - aria-label="Search models" - aria-controls={listboxId} - aria-activedescendant={visibleCount > 0 ? activeId : undefined} - role="combobox" - aria-autocomplete="list" - aria-expanded - autoComplete="off" - spellCheck={false} - className="h-8 w-full bg-transparent pe-2 ps-7 text-xs text-fg placeholder:text-fg-subtle focus:outline-none" - /> -
+
+
+ + setQuery(e.target.value)} + placeholder="Search models…" + aria-label="Search models" + aria-controls={listboxId} + aria-activedescendant={visibleCount > 0 ? activeId : undefined} + role="combobox" + aria-autocomplete="list" + aria-expanded + autoComplete="off" + spellCheck={false} + className="h-8 w-full bg-transparent pe-2 ps-7 text-xs text-fg placeholder:text-fg-subtle focus:outline-none" + /> +
- {searching ? ( + {searching ? ( +
+ {visibleCount === 0 + ? emptyState + : results.map((group) => ( +
+

+ {group.label} · {group.options.length} +

+ {group.options.map((opt, i) => optionRow(opt, group.start + i, group.kind))} +
+ ))} +
+ ) : ( +
+ {lockedTo === null && providers.length > 0 ? ( +
+ {providers.map((provider) => ( + + ))} +
+ ) : null}
{visibleCount === 0 ? emptyState - : results.map((group) => ( -
-

- {group.label} · {group.options.length} -

- {group.options.map((opt, i) => optionRow(opt, group.start + i, group.kind))} -
- ))} + : paneModels.map((opt, index) => optionRow(opt, index, null))}
- ) : ( -
- {lockedTo === null && providers.length > 0 ? ( -
- {providers.map((provider) => ( - - ))} -
- ) : null} -
- {visibleCount === 0 - ? emptyState - : paneModels.map((opt, index) => optionRow(opt, index, null))} -
-
- )} - - {!searching && legacyCount > 0 ? ( +
+ )} + + {!searching && legacyCount > 0 ? ( + + ) : null} + + {!searching && fallbackLabel !== null && activeKind !== null ? ( +
+ + {driverLabel(activeKind)} has not reported its own models — this is{' '} + {fallbackLabel}. It may not accept every entry. + - ) : null} - - {!searching && fallbackLabel !== null && activeKind !== null ? ( -
- - {driverLabel(activeKind)} has not reported its own models — this is{' '} - {fallbackLabel}. It may not accept every entry. - - -
- ) : null} - - {lockedTo !== null ? ( -

- This session runs on {driverLabel(lockedTo)}. Start a new session to use another agent. -

- ) : null} - - {lockedTo === null && withheld.length > 0 ? ( -
-

Not shown

- {withheldNote} -
- ) : null} -
- +
+ ) : null} + + {lockedTo !== null ? ( +

+ This session runs on {driverLabel(lockedTo)}. Start a new session to use another + agent. +

+ ) : null} + + {lockedTo === null && withheld.length > 0 ? ( +
+

Not shown

+ {withheldNote} +
+ ) : null} +
) : null}
) diff --git a/apps/desktop/src/renderer/src/features/session/SessionView.tsx b/apps/desktop/src/renderer/src/features/session/SessionView.tsx index 55ac2d4f..bc834ecc 100644 --- a/apps/desktop/src/renderer/src/features/session/SessionView.tsx +++ b/apps/desktop/src/renderer/src/features/session/SessionView.tsx @@ -1034,6 +1034,16 @@ export function EffortChip({ const [open, setOpen] = useState(false) const [loaded, setLoaded] = useState(false) const [options, setOptions] = useState([]) + const rootRef = useRef(null) + + useEffect(() => { + if (!open) return + const onPointerDown = (e: PointerEvent): void => { + if (rootRef.current?.contains(e.target as Node) !== true) setOpen(false) + } + document.addEventListener('pointerdown', onPointerDown) + return () => document.removeEventListener('pointerdown', onPointerDown) + }, [open]) useEffect(() => { let cancelled = false @@ -1080,7 +1090,7 @@ export function EffortChip({ if (current === undefined) return null return ( -
+
{open ? ( - <> -
setOpen(false)} /> -
- {options.map((option) => ( - - ))} -
- +
+ {options.map((option) => ( + + ))} +
) : null}
) @@ -1199,6 +1206,16 @@ export function PermissionModeChip({ const [open, setOpen] = useState(false) const [discovered, setDiscovered] = useState([]) const menuRef = useRef(null) + const rootRef = useRef(null) + + useEffect(() => { + if (!open) return + const onPointerDown = (e: PointerEvent): void => { + if (rootRef.current?.contains(e.target as Node) !== true) setOpen(false) + } + document.addEventListener('pointerdown', onPointerDown) + return () => document.removeEventListener('pointerdown', onPointerDown) + }, [open]) useEffect(() => { let cancelled = false @@ -1257,7 +1274,7 @@ export function PermissionModeChip({ } return ( -
+
{open ? ( - <> -
setOpen(false)} /> -
- {entries.map((m) => { - const selected = m === current - return ( - - ) - })} -
- +
+ {entries.map((m) => { + const selected = m === current + return ( + + ) + })} +
) : null}
)