Compare commits

...

2 Commits

Author SHA1 Message Date
Kit Langton 9a03d2e008 fix(tui): scroll after completed frame 2026-07-29 21:41:35 -04:00
Kit Langton fa773ed126 fix(tui): focus palette settings after layout 2026-07-29 19:33:19 -04:00
2 changed files with 31 additions and 27 deletions
+28 -19
View File
@@ -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,17 @@ 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 pendingVisibility: (() => void) | undefined
function scheduleVisibility(callback: () => void) {
if (pendingVisibility) renderer.off(CliRenderEvents.FRAME, pendingVisibility)
pendingVisibility = () => {
pendingVisibility = undefined
callback()
}
renderer.once(CliRenderEvents.FRAME, pendingVisibility)
renderer.requestRender()
}
createEffect(
on(
@@ -265,14 +276,10 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
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)
})
scheduleVisibility(() => {
if (!props.preserveSelection || store.filter.length > 0) return
if (!isDeepEqual(selected()?.value, value)) return
scrollToSelection(false)
})
return
}
@@ -284,22 +291,24 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
),
)
onCleanup(() => {
visibilityGeneration++
if (!pendingVisibility) return
renderer.off(CliRenderEvents.FRAME, pendingVisibility)
pendingVisibility = undefined
})
createEffect(
on([() => store.filter, () => props.current], ([filter, current]) => {
if (filter.length > 0) resetSelection = true
setTimeout(() => {
scheduleVisibility(() => {
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)
}
return
}
}, 0)
if (current && props.focusCurrent !== false) {
const currentIndex = flat().findIndex((opt) => isDeepEqual(opt.value, current))
if (currentIndex >= 0) moveTo(currentIndex, true)
}
})
}),
)
@@ -73,16 +73,11 @@ 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")
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()
}