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