Merge branch 'dev' into feat/canceled-prompts-in-history

This commit is contained in:
Ariane Emory
2026-02-10 16:29:05 -05:00
862 changed files with 199017 additions and 6218 deletions
@@ -2,7 +2,7 @@ import { createMemo, createSignal } from "solid-js"
import { useLocal } from "@tui/context/local"
import { useSync } from "@tui/context/sync"
import { map, pipe, flatMap, entries, filter, sortBy, take } from "remeda"
import { DialogSelect, type DialogSelectRef } from "@tui/ui/dialog-select"
import { DialogSelect } from "@tui/ui/dialog-select"
import { useDialog } from "@tui/ui/dialog"
import { createDialogProviderOptions, DialogProvider } from "./dialog-provider"
import { useKeybind } from "../context/keybind"
@@ -20,96 +20,51 @@ export function DialogModel(props: { providerID?: string }) {
const sync = useSync()
const dialog = useDialog()
const keybind = useKeybind()
const [ref, setRef] = createSignal<DialogSelectRef<unknown>>()
const [query, setQuery] = createSignal("")
const connected = useConnected()
const providers = createDialogProviderOptions()
const showExtra = createMemo(() => {
if (!connected()) return false
if (props.providerID) return false
return true
})
const showExtra = createMemo(() => connected() && !props.providerID)
const options = createMemo(() => {
const q = query()
const needle = q.trim()
const needle = query().trim()
const showSections = showExtra() && needle.length === 0
const favorites = connected() ? local.model.favorite() : []
const recents = local.model.recent()
const recentList = showSections
? recents.filter(
(item) => !favorites.some((fav) => fav.providerID === item.providerID && fav.modelID === item.modelID),
)
: []
const favoriteOptions = showSections
? favorites.flatMap((item) => {
const provider = sync.data.provider.find((x) => x.id === item.providerID)
if (!provider) return []
const model = provider.models[item.modelID]
if (!model) return []
return [
{
key: item,
value: {
providerID: provider.id,
modelID: model.id,
},
title: model.name ?? item.modelID,
description: provider.name,
category: "Favorites",
disabled: provider.id === "opencode" && model.id.includes("-nano"),
footer: model.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
onSelect: () => {
dialog.clear()
local.model.set(
{
providerID: provider.id,
modelID: model.id,
},
{ recent: true },
)
},
function toOptions(items: typeof favorites, category: string) {
if (!showSections) return []
return items.flatMap((item) => {
const provider = sync.data.provider.find((x) => x.id === item.providerID)
if (!provider) return []
const model = provider.models[item.modelID]
if (!model) return []
return [
{
key: item,
value: { providerID: provider.id, modelID: model.id },
title: model.name ?? item.modelID,
description: provider.name,
category,
disabled: provider.id === "opencode" && model.id.includes("-nano"),
footer: model.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
onSelect: () => {
dialog.clear()
local.model.set({ providerID: provider.id, modelID: model.id }, { recent: true })
},
]
})
: []
},
]
})
}
const recentOptions = showSections
? recentList.flatMap((item) => {
const provider = sync.data.provider.find((x) => x.id === item.providerID)
if (!provider) return []
const model = provider.models[item.modelID]
if (!model) return []
return [
{
key: item,
value: {
providerID: provider.id,
modelID: model.id,
},
title: model.name ?? item.modelID,
description: provider.name,
category: "Recent",
disabled: provider.id === "opencode" && model.id.includes("-nano"),
footer: model.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
onSelect: () => {
dialog.clear()
local.model.set(
{
providerID: provider.id,
modelID: model.id,
},
{ recent: true },
)
},
},
]
})
: []
const favoriteOptions = toOptions(favorites, "Favorites")
const recentOptions = toOptions(
recents.filter(
(item) => !favorites.some((fav) => fav.providerID === item.providerID && fav.modelID === item.modelID),
),
"Recent",
)
const providerOptions = pipe(
sync.data.provider,
@@ -123,45 +78,26 @@ export function DialogModel(props: { providerID?: string }) {
entries(),
filter(([_, info]) => info.status !== "deprecated"),
filter(([_, info]) => (props.providerID ? info.providerID === props.providerID : true)),
map(([model, info]) => {
const value = {
providerID: provider.id,
modelID: model,
}
return {
value,
title: info.name ?? model,
description: favorites.some(
(item) => item.providerID === value.providerID && item.modelID === value.modelID,
)
? "(Favorite)"
: undefined,
category: connected() ? provider.name : undefined,
disabled: provider.id === "opencode" && model.includes("-nano"),
footer: info.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
onSelect() {
dialog.clear()
local.model.set(
{
providerID: provider.id,
modelID: model,
},
{ recent: true },
)
},
}
}),
map(([model, info]) => ({
value: { providerID: provider.id, modelID: model },
title: info.name ?? model,
description: favorites.some((item) => item.providerID === provider.id && item.modelID === model)
? "(Favorite)"
: undefined,
category: connected() ? provider.name : undefined,
disabled: provider.id === "opencode" && model.includes("-nano"),
footer: info.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
onSelect() {
dialog.clear()
local.model.set({ providerID: provider.id, modelID: model }, { recent: true })
},
})),
filter((x) => {
if (!showSections) return true
const value = x.value
const inFavorites = favorites.some(
(item) => item.providerID === value.providerID && item.modelID === value.modelID,
)
if (inFavorites) return false
const inRecents = recents.some(
(item) => item.providerID === value.providerID && item.modelID === value.modelID,
)
if (inRecents) return false
if (favorites.some((item) => item.providerID === x.value.providerID && item.modelID === x.value.modelID))
return false
if (recents.some((item) => item.providerID === x.value.providerID && item.modelID === x.value.modelID))
return false
return true
}),
sortBy(
@@ -175,21 +111,19 @@ export function DialogModel(props: { providerID?: string }) {
const popularProviders = !connected()
? pipe(
providers(),
map((option) => {
return {
...option,
category: "Popular providers",
}
}),
map((option) => ({
...option,
category: "Popular providers",
})),
take(6),
)
: []
// Search shows a single merged list (favorites inline)
if (needle) {
const filteredProviders = fuzzysort.go(needle, providerOptions, { keys: ["title", "category"] }).map((x) => x.obj)
const filteredPopular = fuzzysort.go(needle, popularProviders, { keys: ["title"] }).map((x) => x.obj)
return [...filteredProviders, ...filteredPopular]
return [
...fuzzysort.go(needle, providerOptions, { keys: ["title", "category"] }).map((x) => x.obj),
...fuzzysort.go(needle, popularProviders, { keys: ["title"] }).map((x) => x.obj),
]
}
return [...favoriteOptions, ...recentOptions, ...providerOptions, ...popularProviders]
@@ -199,13 +133,11 @@ export function DialogModel(props: { providerID?: string }) {
props.providerID ? sync.data.provider.find((x) => x.id === props.providerID) : null,
)
const title = createMemo(() => {
if (provider()) return provider()!.name
return "Select model"
})
const title = createMemo(() => provider()?.name ?? "Select model")
return (
<DialogSelect
<DialogSelect<ReturnType<typeof options>[number]["value"]>
options={options()}
keybind={[
{
keybind: keybind.all.model_provider_list?.[0],
@@ -223,12 +155,11 @@ export function DialogModel(props: { providerID?: string }) {
},
},
]}
ref={setRef}
onFilter={setQuery}
flat={true}
skipFilter={true}
title={title()}
current={local.model.current()}
options={options()}
/>
)
}
@@ -26,82 +26,67 @@ export function createDialogProviderOptions() {
const sync = useSync()
const dialog = useDialog()
const sdk = useSDK()
const connected = createMemo(() => new Set(sync.data.provider_next.connected))
const options = createMemo(() => {
return pipe(
sync.data.provider_next.all,
sortBy((x) => PROVIDER_PRIORITY[x.id] ?? 99),
map((provider) => {
const isConnected = connected().has(provider.id)
return {
title: provider.name,
value: provider.id,
description: {
opencode: "(Recommended)",
anthropic: "(Claude Max or API key)",
openai: "(ChatGPT Plus/Pro or API key)",
}[provider.id],
category: provider.id in PROVIDER_PRIORITY ? "Popular" : "Other",
footer: isConnected ? "Connected" : undefined,
async onSelect() {
const methods = sync.data.provider_auth[provider.id] ?? [
{
type: "api",
label: "API key",
},
]
let index: number | null = 0
if (methods.length > 1) {
index = await new Promise<number | null>((resolve) => {
dialog.replace(
() => (
<DialogSelect
title="Select auth method"
options={methods.map((x, index) => ({
title: x.label,
value: index,
}))}
onSelect={(option) => resolve(option.value)}
/>
),
() => resolve(null),
)
})
}
if (index == null) return
const method = methods[index]
if (method.type === "oauth") {
const result = await sdk.client.provider.oauth.authorize({
providerID: provider.id,
method: index,
})
if (result.data?.method === "code") {
dialog.replace(() => (
<CodeMethod
providerID={provider.id}
title={method.label}
index={index}
authorization={result.data!}
map((provider) => ({
title: provider.name,
value: provider.id,
description: {
opencode: "(Recommended)",
anthropic: "(Claude Max or API key)",
openai: "(ChatGPT Plus/Pro or API key)",
}[provider.id],
category: provider.id in PROVIDER_PRIORITY ? "Popular" : "Other",
async onSelect() {
const methods = sync.data.provider_auth[provider.id] ?? [
{
type: "api",
label: "API key",
},
]
let index: number | null = 0
if (methods.length > 1) {
index = await new Promise<number | null>((resolve) => {
dialog.replace(
() => (
<DialogSelect
title="Select auth method"
options={methods.map((x, index) => ({
title: x.label,
value: index,
}))}
onSelect={(option) => resolve(option.value)}
/>
))
}
if (result.data?.method === "auto") {
dialog.replace(() => (
<AutoMethod
providerID={provider.id}
title={method.label}
index={index}
authorization={result.data!}
/>
))
}
),
() => resolve(null),
)
})
}
if (index == null) return
const method = methods[index]
if (method.type === "oauth") {
const result = await sdk.client.provider.oauth.authorize({
providerID: provider.id,
method: index,
})
if (result.data?.method === "code") {
dialog.replace(() => (
<CodeMethod providerID={provider.id} title={method.label} index={index} authorization={result.data!} />
))
}
if (method.type === "api") {
return dialog.replace(() => <ApiMethod providerID={provider.id} title={method.label} />)
if (result.data?.method === "auto") {
dialog.replace(() => (
<AutoMethod providerID={provider.id} title={method.label} index={index} authorization={result.data!} />
))
}
},
}
}),
}
if (method.type === "api") {
return dialog.replace(() => <ApiMethod providerID={provider.id} title={method.label} />)
}
},
})),
)
})
return options
@@ -124,7 +109,6 @@ function AutoMethod(props: AutoMethodProps) {
const dialog = useDialog()
const sync = useSync()
const toast = useToast()
const [hover, setHover] = createSignal(false)
useKeyboard((evt) => {
if (evt.name === "c" && !evt.ctrl && !evt.meta) {
@@ -155,16 +139,9 @@ function AutoMethod(props: AutoMethodProps) {
<text attributes={TextAttributes.BOLD} fg={theme.text}>
{props.title}
</text>
<box
paddingLeft={1}
paddingRight={1}
backgroundColor={hover() ? theme.primary : undefined}
onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)}
onMouseUp={() => dialog.clear()}
>
<text fg={hover() ? theme.selectedListItemText : theme.textMuted}>esc</text>
</box>
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
<box gap={1}>
<Link href={props.authorization.url} fg={theme.primary} />
@@ -3,8 +3,7 @@ import { fileURLToPath } from "bun"
import { useTheme } from "../context/theme"
import { useDialog } from "@tui/ui/dialog"
import { useSync } from "@tui/context/sync"
import { For, Match, Switch, Show, createMemo, createSignal } from "solid-js"
import { Installation } from "@/installation"
import { For, Match, Switch, Show, createMemo } from "solid-js"
export type DialogStatusProps = {}
@@ -12,7 +11,6 @@ export function DialogStatus() {
const sync = useSync()
const { theme } = useTheme()
const dialog = useDialog()
const [hover, setHover] = createSignal(false)
const enabledFormatters = createMemo(() => sync.data.formatter.filter((f) => f.enabled))
@@ -47,18 +45,10 @@ export function DialogStatus() {
<text fg={theme.text} attributes={TextAttributes.BOLD}>
Status
</text>
<box
paddingLeft={1}
paddingRight={1}
backgroundColor={hover() ? theme.primary : undefined}
onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)}
onMouseUp={() => dialog.clear()}
>
<text fg={hover() ? theme.selectedListItemText : theme.textMuted}>esc</text>
</box>
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
<text fg={theme.textMuted}>OpenCode v{Installation.VERSION}</text>
<Show when={Object.keys(sync.data.mcp).length > 0} fallback={<text fg={theme.text}>No MCP Servers</text>}>
<box>
<text fg={theme.text}>{Object.keys(sync.data.mcp).length} MCP Servers</text>
@@ -7,7 +7,6 @@ import { SplitBorder } from "@tui/component/border"
import type { AssistantMessage, Session } from "@opencode-ai/sdk/v2"
import { useCommandDialog } from "@tui/component/dialog-command"
import { useKeybind } from "../../context/keybind"
import { Installation } from "@/installation"
import { useTerminalDimensions } from "@opentui/solid"
const Title = (props: { session: Accessor<Session> }) => {
@@ -87,10 +86,7 @@ export function Header() {
<text fg={theme.text}>
<b>Subagent session</b>
</text>
<box flexDirection="row" gap={1} flexShrink={0}>
<ContextInfo context={context} cost={cost} />
<text fg={theme.textMuted}>v{Installation.VERSION}</text>
</box>
<ContextInfo context={context} cost={cost} />
</box>
<box flexDirection="row" gap={2}>
<box
@@ -129,10 +125,7 @@ export function Header() {
<Match when={true}>
<box flexDirection={narrow() ? "column" : "row"} justifyContent="space-between" gap={1}>
<Title session={session} />
<box flexDirection="row" gap={1} flexShrink={0}>
<ContextInfo context={context} cost={cost} />
<text fg={theme.textMuted}>v{Installation.VERSION}</text>
</box>
<ContextInfo context={context} cost={cost} />
</box>
</Match>
</Switch>
@@ -17,7 +17,7 @@ import { useRoute, useRouteData } from "@tui/context/route"
import { useSync } from "@tui/context/sync"
import { SplitBorder } from "@tui/component/border"
import { Spinner } from "@tui/component/spinner"
import { useTheme } from "@tui/context/theme"
import { selectedForeground, useTheme } from "@tui/context/theme"
import {
BoxRenderable,
ScrollBoxRenderable,
@@ -1099,9 +1099,6 @@ export function Session() {
sessionID={route.sessionID}
/>
</box>
<Show when={!sidebarVisible() || !wide()}>
<Footer />
</Show>
</Show>
<Toast />
</box>
@@ -1155,7 +1152,8 @@ function UserMessage(props: {
const { theme } = useTheme()
const [hover, setHover] = createSignal(false)
const queued = createMemo(() => props.pending && props.message.id > props.pending)
const color = createMemo(() => (queued() ? theme.accent : local.agent.color(props.message.agent)))
const color = createMemo(() => local.agent.color(props.message.agent))
const queuedFg = createMemo(() => selectedForeground(theme, color()))
const metadataVisible = createMemo(() => queued() || ctx.showTimestamps())
const compaction = createMemo(() => props.parts.find((x) => x.type === "compaction"))
@@ -1217,7 +1215,7 @@ function UserMessage(props: {
}
>
<text fg={theme.textMuted}>
<span style={{ bg: theme.accent, fg: theme.backgroundPanel, bold: true }}> QUEUED </span>
<span style={{ bg: color(), fg: queuedFg(), bold: true }}> QUEUED </span>
</text>
</Show>
</box>
@@ -2,7 +2,6 @@ import { TextAttributes } from "@opentui/core"
import { useTheme } from "../context/theme"
import { useDialog, type DialogContext } from "./dialog"
import { useKeyboard } from "@opentui/solid"
import { createSignal } from "solid-js"
export type DialogAlertProps = {
title: string
@@ -13,7 +12,6 @@ export type DialogAlertProps = {
export function DialogAlert(props: DialogAlertProps) {
const dialog = useDialog()
const { theme } = useTheme()
const [hover, setHover] = createSignal(false)
useKeyboard((evt) => {
if (evt.name === "return") {
@@ -27,16 +25,9 @@ export function DialogAlert(props: DialogAlertProps) {
<text attributes={TextAttributes.BOLD} fg={theme.text}>
{props.title}
</text>
<box
paddingLeft={1}
paddingRight={1}
backgroundColor={hover() ? theme.primary : undefined}
onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)}
onMouseUp={() => dialog.clear()}
>
<text fg={hover() ? theme.selectedListItemText : theme.textMuted}>esc</text>
</box>
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
<box paddingBottom={1}>
<text fg={theme.textMuted}>{props.message}</text>
@@ -2,7 +2,7 @@ import { TextAttributes } from "@opentui/core"
import { useTheme } from "../context/theme"
import { useDialog, type DialogContext } from "./dialog"
import { createStore } from "solid-js/store"
import { createSignal, For } from "solid-js"
import { For } from "solid-js"
import { useKeyboard } from "@opentui/solid"
import { Locale } from "@/util/locale"
@@ -16,7 +16,6 @@ export type DialogConfirmProps = {
export function DialogConfirm(props: DialogConfirmProps) {
const dialog = useDialog()
const { theme } = useTheme()
const [hover, setHover] = createSignal(false)
const [store, setStore] = createStore({
active: "confirm" as "confirm" | "cancel",
})
@@ -38,16 +37,9 @@ export function DialogConfirm(props: DialogConfirmProps) {
<text attributes={TextAttributes.BOLD} fg={theme.text}>
{props.title}
</text>
<box
paddingLeft={1}
paddingRight={1}
backgroundColor={hover() ? theme.primary : undefined}
onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)}
onMouseUp={() => dialog.clear()}
>
<text fg={hover() ? theme.selectedListItemText : theme.textMuted}>esc</text>
</box>
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
<box paddingBottom={1}>
<text fg={theme.textMuted}>{props.message}</text>
@@ -2,7 +2,7 @@ import { TextareaRenderable, TextAttributes } from "@opentui/core"
import { useTheme } from "../context/theme"
import { useDialog, type DialogContext } from "./dialog"
import { createStore } from "solid-js/store"
import { createSignal, onMount, Show, type JSX } from "solid-js"
import { onMount, Show, type JSX } from "solid-js"
import { useKeyboard } from "@opentui/solid"
export type DialogExportOptionsProps = {
@@ -25,7 +25,6 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
const dialog = useDialog()
const { theme } = useTheme()
let textarea: TextareaRenderable
const [hover, setHover] = createSignal(false)
const [store, setStore] = createStore({
thinking: props.defaultThinking,
toolDetails: props.defaultToolDetails,
@@ -81,16 +80,9 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
<text attributes={TextAttributes.BOLD} fg={theme.text}>
Export Options
</text>
<box
paddingLeft={1}
paddingRight={1}
backgroundColor={hover() ? theme.primary : undefined}
onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)}
onMouseUp={() => dialog.clear()}
>
<text fg={hover() ? theme.selectedListItemText : theme.textMuted}>esc</text>
</box>
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
<box gap={1}>
<box>
@@ -3,13 +3,11 @@ import { useTheme } from "@tui/context/theme"
import { useDialog } from "./dialog"
import { useKeyboard } from "@opentui/solid"
import { useKeybind } from "@tui/context/keybind"
import { createSignal } from "solid-js"
export function DialogHelp() {
const dialog = useDialog()
const { theme } = useTheme()
const keybind = useKeybind()
const [hover, setHover] = createSignal(false)
useKeyboard((evt) => {
if (evt.name === "return" || evt.name === "escape") {
@@ -23,16 +21,9 @@ export function DialogHelp() {
<text attributes={TextAttributes.BOLD} fg={theme.text}>
Help
</text>
<box
paddingLeft={1}
paddingRight={1}
backgroundColor={hover() ? theme.primary : undefined}
onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)}
onMouseUp={() => dialog.clear()}
>
<text fg={hover() ? theme.selectedListItemText : theme.textMuted}>esc/enter</text>
</box>
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
esc/enter
</text>
</box>
<box paddingBottom={1}>
<text fg={theme.textMuted}>
@@ -1,7 +1,7 @@
import { TextareaRenderable, TextAttributes } from "@opentui/core"
import { useTheme } from "../context/theme"
import { useDialog, type DialogContext } from "./dialog"
import { createSignal, onMount, type JSX } from "solid-js"
import { onMount, type JSX } from "solid-js"
import { useKeyboard } from "@opentui/solid"
export type DialogPromptProps = {
@@ -17,7 +17,6 @@ export function DialogPrompt(props: DialogPromptProps) {
const dialog = useDialog()
const { theme } = useTheme()
let textarea: TextareaRenderable
const [hover, setHover] = createSignal(false)
useKeyboard((evt) => {
if (evt.name === "return") {
@@ -40,16 +39,9 @@ export function DialogPrompt(props: DialogPromptProps) {
<text attributes={TextAttributes.BOLD} fg={theme.text}>
{props.title}
</text>
<box
paddingLeft={1}
paddingRight={1}
backgroundColor={hover() ? theme.primary : undefined}
onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)}
onMouseUp={() => dialog.clear()}
>
<text fg={hover() ? theme.selectedListItemText : theme.textMuted}>esc</text>
</box>
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
<box gap={1}>
{props.description}
@@ -1,7 +1,7 @@
import { InputRenderable, RGBA, ScrollBoxRenderable, TextAttributes } from "@opentui/core"
import { useTheme, selectedForeground } from "@tui/context/theme"
import { entries, filter, flatMap, groupBy, pipe, take } from "remeda"
import { batch, createEffect, createMemo, createSignal, For, Show, type JSX, on } from "solid-js"
import { batch, createEffect, createMemo, For, Show, type JSX, on } from "solid-js"
import { createStore } from "solid-js/store"
import { useKeyboard, useTerminalDimensions } from "@opentui/solid"
import * as fuzzysort from "fuzzysort"
@@ -15,6 +15,7 @@ export interface DialogSelectProps<T> {
title: string
placeholder?: string
options: DialogSelectOption<T>[]
flat?: boolean
ref?: (ref: DialogSelectRef<T>) => void
onMove?: (option: DialogSelectOption<T>) => void
onFilter?: (query: string) => void
@@ -49,7 +50,6 @@ export type DialogSelectRef<T> = {
export function DialogSelect<T>(props: DialogSelectProps<T>) {
const dialog = useDialog()
const { theme } = useTheme()
const [hover, setHover] = createSignal(false)
const [store, setStore] = createStore({
selected: 0,
filter: "",
@@ -101,7 +101,10 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
setStore("input", "keyboard")
})
const grouped = createMemo(() => {
const flatten = createMemo(() => props.flat && store.filter.length > 0)
const grouped = createMemo<[string, DialogSelectOption<T>[]][]>(() => {
if (flatten()) return [["", filtered()]]
const result = pipe(
filtered(),
groupBy((x) => x.category ?? ""),
@@ -118,10 +121,16 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
)
})
const rows = createMemo(() => {
const headers = grouped().reduce((acc, [category], i) => {
if (!category) return acc
return acc + (i > 0 ? 2 : 1)
}, 0)
return flat().length + headers
})
const dimensions = useTerminalDimensions()
const height = createMemo(() =>
Math.min(flat().length + grouped().length * 2 - 1, Math.floor(dimensions().height / 2) - 6),
)
const height = createMemo(() => Math.min(rows(), Math.floor(dimensions().height / 2) - 6))
const selected = createMemo(() => flat()[store.selected])
@@ -227,16 +236,9 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
<text fg={theme.text} attributes={TextAttributes.BOLD}>
{props.title}
</text>
<box
paddingLeft={1}
paddingRight={1}
backgroundColor={hover() ? theme.primary : undefined}
onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)}
onMouseUp={() => dialog.clear()}
>
<text fg={hover() ? theme.selectedListItemText : theme.textMuted}>esc</text>
</box>
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
<box paddingTop={1}>
<input
@@ -319,7 +321,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
>
<Option
title={option.title}
footer={option.footer}
footer={flatten() ? (option.category ?? option.footer) : option.footer}
description={option.description !== category ? option.description : undefined}
active={active()}
current={current()}