mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-05 00:49:53 +00:00
fix(tui): focus palette settings after layout (#39585)
This commit is contained in:
parent
cc1289048e
commit
8e3e94aa26
2 changed files with 36 additions and 32 deletions
|
|
@ -1,10 +1,10 @@
|
|||
import { InputRenderable, RGBA, ScrollBoxRenderable, TextAttributes } from "@opentui/core"
|
||||
import { CliRenderEvents, InputRenderable, RGBA, ScrollBoxRenderable, TextAttributes } from "@opentui/core"
|
||||
import { Keymap, type KeymapCommand } from "../context/keymap"
|
||||
import { useTheme, useThemes } from "../context/theme"
|
||||
import { entries, filter, flatMap, groupBy, pipe } from "remeda"
|
||||
import { batch, createEffect, createMemo, createSignal, For, Show, type JSX, on, onCleanup } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { useTerminalDimensions } from "@opentui/solid"
|
||||
import { useRenderer, useTerminalDimensions } from "@opentui/solid"
|
||||
import * as fuzzysort from "fuzzysort"
|
||||
import { isDeepEqual } from "remeda"
|
||||
import { useDialog, type DialogContext } from "./dialog"
|
||||
|
|
@ -100,6 +100,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
|||
const mode = themes.mode
|
||||
const config = useConfig().data
|
||||
const scrollAcceleration = createMemo(() => getScrollAcceleration(config))
|
||||
const renderer = useRenderer()
|
||||
|
||||
const [store, setStore] = createStore({
|
||||
selected: 0,
|
||||
|
|
@ -110,7 +111,18 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
|||
const actionFocused = createMemo(() => focusedAction() !== undefined)
|
||||
let selection: { value: T; category?: string } | undefined
|
||||
let resetSelection = false
|
||||
let visibilityGeneration = 0
|
||||
let pendingScroll: (() => void) | undefined
|
||||
|
||||
function scrollAfterLayout(center: boolean, value: T) {
|
||||
if (pendingScroll) renderer.off(CliRenderEvents.FRAME, pendingScroll)
|
||||
pendingScroll = () => {
|
||||
pendingScroll = undefined
|
||||
if (!isDeepEqual(selected()?.value, value)) return
|
||||
scrollToSelection(center)
|
||||
}
|
||||
renderer.once(CliRenderEvents.FRAME, pendingScroll)
|
||||
renderer.requestRender()
|
||||
}
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
|
|
@ -264,16 +276,8 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
|||
setStore("selected", index)
|
||||
selection = option
|
||||
if (!moved) return
|
||||
const value = option.value
|
||||
const generation = ++visibilityGeneration
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
if (generation !== visibilityGeneration) return
|
||||
if (!props.preserveSelection || store.filter.length > 0) return
|
||||
if (!isDeepEqual(selected()?.value, value)) return
|
||||
scrollToSelection(false)
|
||||
})
|
||||
})
|
||||
if (!props.preserveSelection || store.filter.length > 0) return
|
||||
scrollAfterLayout(false, option.value)
|
||||
return
|
||||
}
|
||||
const next = reconcileSelection(store.selected, flat().length)
|
||||
|
|
@ -284,22 +288,26 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
|||
),
|
||||
)
|
||||
onCleanup(() => {
|
||||
visibilityGeneration++
|
||||
if (!pendingScroll) return
|
||||
renderer.off(CliRenderEvents.FRAME, pendingScroll)
|
||||
pendingScroll = undefined
|
||||
})
|
||||
|
||||
createEffect(
|
||||
on([() => store.filter, () => props.current], ([filter, current]) => {
|
||||
if (filter.length > 0) resetSelection = true
|
||||
setTimeout(() => {
|
||||
if (filter.length > 0) {
|
||||
moveTo(0, true, false)
|
||||
} else if (current && props.focusCurrent !== false) {
|
||||
const currentIndex = flat().findIndex((opt) => isDeepEqual(opt.value, current))
|
||||
if (currentIndex >= 0) {
|
||||
moveTo(currentIndex, true)
|
||||
}
|
||||
}
|
||||
}, 0)
|
||||
if (filter.length > 0) {
|
||||
const option = flat()[0]
|
||||
if (!option) return
|
||||
moveTo(0, true, false)
|
||||
scrollAfterLayout(true, option.value)
|
||||
return
|
||||
}
|
||||
if (!current || props.focusCurrent === false) return
|
||||
const currentIndex = flat().findIndex((opt) => isDeepEqual(opt.value, current))
|
||||
if (currentIndex < 0) return
|
||||
moveTo(currentIndex, true)
|
||||
scrollAfterLayout(true, current)
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -73,16 +73,12 @@ test("searches settings globally and opens the matching setting", async () => {
|
|||
expect(app.captureCharFrame()).not.toContain("Animations")
|
||||
await app.waitFor(() => app.renderer.currentFocusedEditor instanceof InputRenderable)
|
||||
|
||||
for (const key of "side") app.mockInput.pressKey(key)
|
||||
await app.waitForFrame((frame) => frame.includes("Sidebar"))
|
||||
expect(app.captureCharFrame()).not.toContain("New session")
|
||||
expect(app.captureCharFrame()).not.toContain("Switch model")
|
||||
expect(app.captureCharFrame()).not.toContain("Markdown")
|
||||
|
||||
app.mockInput.pressArrow("down")
|
||||
for (const key of "sounds") app.mockInput.pressKey(key)
|
||||
app.mockInput.pressEnter()
|
||||
await app.waitForFrame((frame) => frame.includes("Settings") && frame.includes("Color mode"))
|
||||
await app.waitForFrame((frame) => frame.includes("Settings") && frame.includes("Sounds"))
|
||||
app.mockInput.pressEnter()
|
||||
await app.waitFor(() => current.session?.sidebar === "hide")
|
||||
await app.waitFor(() => current.attention?.sound === false)
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue