diff --git a/packages/tui/src/component/dialog-error-details.tsx b/packages/tui/src/component/dialog-error-details.tsx index 22d9baacd71..eeb5239cb40 100644 --- a/packages/tui/src/component/dialog-error-details.tsx +++ b/packages/tui/src/component/dialog-error-details.tsx @@ -1,6 +1,6 @@ -import { CliRenderEvents, TextAttributes, type ScrollBoxRenderable } from "@opentui/core" -import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid" -import { createEffect, createMemo, createSignal, onCleanup, Show } from "solid-js" +import { TextAttributes, type ScrollBoxRenderable } from "@opentui/core" +import { useKeyboard, useTerminalDimensions } from "@opentui/solid" +import { createSignal, Show } from "solid-js" import { useConfig } from "../config" import { useClipboard } from "../context/clipboard" import { Keymap } from "../context/keymap" @@ -28,34 +28,10 @@ export function DialogErrorDetails(props: { const route = useRoute() const toast = useToast() const theme = useTheme("elevated") - const renderer = useRenderer() const dimensions = useTerminalDimensions() const config = useConfig().data const [copied, setCopied] = createSignal(false) - const [scrollable, setScrollable] = createSignal(false) - const [height, setHeight] = createSignal(1) - const maxHeight = createMemo(() => Math.max(3, Math.floor(dimensions().height / 2) - 5)) let scroll: ScrollBoxRenderable | undefined - let measure: (() => void) | undefined - - createEffect(() => { - dimensions() - props.error - if (measure) renderer.off(CliRenderEvents.FRAME, measure) - measure = () => { - measure = undefined - if (!scroll) return - const next = Math.max(1, Math.min(maxHeight(), scroll.scrollHeight)) - setHeight(next) - setScrollable(scroll.scrollHeight > next) - } - renderer.once(CliRenderEvents.FRAME, measure) - renderer.requestRender() - }) - - onCleanup(() => { - if (measure) renderer.off(CliRenderEvents.FRAME, measure) - }) const copy = () => { void clipboard @@ -86,11 +62,10 @@ export function DialogErrorDetails(props: { })) useKeyboard((event) => { - if (!scrollable()) return if (event.name === "up") return scroll?.scrollBy(-1) if (event.name === "down") return scroll?.scrollBy(1) - if (event.name === "pageup") return scroll?.scrollBy(-maxHeight()) - if (event.name === "pagedown") return scroll?.scrollBy(maxHeight()) + if (event.name === "pageup") return scroll?.scrollBy(-20) + if (event.name === "pagedown") return scroll?.scrollBy(20) if (event.name === "home") return scroll?.scrollTo(0) if (event.name === "end" && scroll) return scroll.scrollTo(scroll.scrollHeight) }) @@ -126,7 +101,8 @@ export function DialogErrorDetails(props: { (scroll = element)} - height={height()} + maxHeight={20} + contentOptions={{ minHeight: 0 }} scrollbarOptions={{ visible: false }} scrollAcceleration={getScrollAcceleration(config)} > @@ -151,9 +127,7 @@ export function DialogErrorDetails(props: { {copied() ? "" : " copy details"} - - ↑/↓ scroll - + ↑/↓ scroll ) diff --git a/packages/tui/src/routes/session/form.tsx b/packages/tui/src/routes/session/form.tsx index d059e404eb0..c3b38da128f 100644 --- a/packages/tui/src/routes/session/form.tsx +++ b/packages/tui/src/routes/session/form.tsx @@ -2,10 +2,10 @@ import { createStore, unwrap } from "solid-js/store" import { createEffect, createMemo, createSignal, For, onCleanup, onMount, Show } from "solid-js" import { usePaste, useRenderer, useTerminalDimensions } from "@opentui/solid" import { - CliRenderEvents, decodePasteBytes, stripAnsiSequences, TextAttributes, + type BoxRenderable, type ScrollBoxRenderable, type TextareaRenderable, } from "@opentui/core" @@ -75,8 +75,7 @@ export function FormPrompt(props: { form: FormWithLocation }) { drafts.delete(props.form.id) const [tabHover, setTabHover] = createSignal(null) - const [reviewHeight, setReviewHeight] = createSignal(1) - const [reviewScrollable, setReviewScrollable] = createSignal(false) + const [reviewContentHeight, setReviewContentHeight] = createSignal(0) const [store, setStore] = createStore( draft ?? { tab: 0, @@ -92,7 +91,6 @@ export function FormPrompt(props: { form: FormWithLocation }) { let textarea: TextareaRenderable | undefined const [inputTarget, setInputTarget] = createSignal() let review: ScrollBoxRenderable | undefined - let measureReview: (() => void) | undefined const message = createMemo(() => { const value = props.form.metadata?.["message"] @@ -141,6 +139,7 @@ export function FormPrompt(props: { form: FormWithLocation }) { return current?.type === "external" ? current : undefined }) const confirm = createMemo(() => !single() && store.tab >= fields().length) + const reviewMaxHeight = createMemo(() => Math.max(3, dimensions().height - 14)) const configuredRows = createMemo(() => { const current = answerField() return current ? formRows(current) : [] @@ -211,32 +210,7 @@ export function FormPrompt(props: { form: FormWithLocation }) { return "confirm" }) - createEffect(() => { - if (measureReview) renderer.off(CliRenderEvents.FRAME, measureReview) - if (!confirm()) { - measureReview = undefined - review = undefined - setReviewScrollable(false) - return - } - const limit = Math.max(3, dimensions().height - 14) - const initial = Math.min(Math.max(1, fields().length), limit) - Object.values(store.answers) - setReviewHeight(initial) - setReviewScrollable(false) - measureReview = () => { - measureReview = undefined - const content = review?.scrollHeight ?? initial - const height = Math.min(Math.max(1, content), limit) - setReviewHeight(height) - setReviewScrollable(content > height) - } - renderer.once(CliRenderEvents.FRAME, measureReview) - renderer.requestRender() - }) - onCleanup(() => { - if (measureReview) renderer.off(CliRenderEvents.FRAME, measureReview) // A reply or cancel removes the form from data before this unmount runs, so a // form still listed here is only hidden by navigation and worth restoring. const pending = data.session.form @@ -1102,56 +1076,63 @@ export function FormPrompt(props: { form: FormWithLocation }) { (review = r)} > - - {(item) => { - if (item.type === "external") { - const acknowledged = () => store.answers[item.key] === true + + + {(item) => { + if (item.type === "external") { + const acknowledged = () => store.answers[item.key] === true + return ( + + + {truncate(formLabel(item), 40)}:{" "} + + {acknowledged() ? "Acknowledged" : "(acknowledgement required)"} + + + + ) + } + const value = () => formDisplayValue(item, store.answers[item.key], "(none)") + const answered = () => store.answers[item.key] !== undefined + const missing = () => !answered() && item.required === true + const invalid = () => formValidateValue(item, store.answers[item.key]) return ( {truncate(formLabel(item), 40)}:{" "} - {acknowledged() ? "Acknowledged" : "(acknowledgement required)"} + {invalid() ?? (answered() ? value() : missing() ? "(required)" : "(not answered)")} ) - } - const value = () => formDisplayValue(item, store.answers[item.key], "(none)") - const answered = () => store.answers[item.key] !== undefined - const missing = () => !answered() && item.required === true - const invalid = () => formValidateValue(item, store.answers[item.key]) - return ( - - - {truncate(formLabel(item), 40)}:{" "} - - {invalid() ?? (answered() ? value() : missing() ? "(required)" : "(not answered)")} - - - - ) - }} - + }} + + @@ -1175,7 +1156,7 @@ export function FormPrompt(props: { form: FormWithLocation }) { {"↑↓"} select - + reviewMaxHeight()}> {"↑↓"} scroll