diff --git a/packages/tui/src/component/dialog-status.tsx b/packages/tui/src/component/dialog-status.tsx index b848176958..4f82dd680d 100644 --- a/packages/tui/src/component/dialog-status.tsx +++ b/packages/tui/src/component/dialog-status.tsx @@ -4,8 +4,6 @@ import { useDialog } from "../ui/dialog" import { useData } from "../context/data" import { For, Match, Switch, Show, createMemo } from "solid-js" -export type DialogStatusProps = {} - export function DialogStatus() { const data = useData() const theme = useTheme("elevated") diff --git a/packages/tui/src/component/prompt/autocomplete.tsx b/packages/tui/src/component/prompt/autocomplete.tsx index 8912a052b3..922cc165c0 100644 --- a/packages/tui/src/component/prompt/autocomplete.tsx +++ b/packages/tui/src/component/prompt/autocomplete.tsx @@ -23,6 +23,7 @@ import { displayCharAt, mentionTriggerIndex } from "../../prompt/display" import type { FileSystemEntry } from "@opencode-ai/client" import { stringWidth } from "../../util/string-width" import { parseFileLineRange, stripFileLineRange } from "../../prompt/parse" +import { moveSelection, revealSelectionOffset } from "../../ui/select-controller" export type AutocompleteRef = { onInput: (value: string) => void @@ -501,22 +502,19 @@ export function Autocomplete(props: { function move(direction: -1 | 1) { if (!store.visible) return if (!options().length) return - let next = store.selected + direction - if (next < 0) next = options().length - 1 - if (next >= options().length) next = 0 - moveTo(next) + moveTo(moveSelection(store.selected, { count: options().length, delta: direction, policy: "wrap" })) } function moveTo(next: number) { setStore("selected", next) if (!scroll) return - const viewportHeight = Math.min(height(), options().length) - const scrollBottom = scroll.scrollTop + viewportHeight - if (next < scroll.scrollTop) { - scroll.scrollBy(next - scroll.scrollTop) - } else if (next + 1 > scrollBottom) { - scroll.scrollBy(next + 1 - scrollBottom) - } + const offset = revealSelectionOffset(scroll.scrollTop, { + count: options().length, + limit: Math.min(height(), options().length), + selected: next, + }) + if (offset === scroll.scrollTop) return + scroll.scrollBy(offset - scroll.scrollTop) } function select() { diff --git a/packages/tui/src/component/prompt/index.tsx b/packages/tui/src/component/prompt/index.tsx index fdbff6af76..34df09fcc0 100644 --- a/packages/tui/src/component/prompt/index.tsx +++ b/packages/tui/src/component/prompt/index.tsx @@ -8,7 +8,6 @@ import { type KeyEvent, } from "@opentui/core" import { createEffect, createMemo, onMount, createSignal, onCleanup, on, Show, Switch, Match } from "solid-js" -import { registerOpencodeSpinner } from "../register-spinner" import path from "path" import { fileURLToPath } from "url" import { useLocal } from "../../context/local" @@ -55,8 +54,6 @@ import { Keymap, type KeymapCommand } from "../../context/keymap" import { abbreviateHome } from "../../runtime" import { PluginSlot } from "../../plugin/render" -registerOpencodeSpinner() - export type PromptProps = { sessionID?: string visible?: boolean diff --git a/packages/tui/src/component/session-tabs.tsx b/packages/tui/src/component/session-tabs.tsx index 186466e3dd..c3fef81b09 100644 --- a/packages/tui/src/component/session-tabs.tsx +++ b/packages/tui/src/component/session-tabs.tsx @@ -65,6 +65,8 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat const accent = () => theme.hue.accent[hueStep()] const activeNumber = () => theme.hue.interactive[hueStep()] const idleNumber = () => tint(theme.text.subdued, theme.background.default, 0.35) + const separatorUpperPulseColor = createMemo(() => tint(theme.background.default, theme.text.default, 0.04)) + const separatorLowerPulseColor = createMemo(() => tint(theme.background.default, theme.text.default, 0.05)) const [hovered, setHovered] = createSignal() const [dragging, setDragging] = createSignal() const [preview, setPreview] = createSignal<{ sessionID: string; index: number }>() @@ -199,12 +201,6 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat } const separatorUpperColor = createMemo(() => tint(theme.background.default, previousGlowHue(), 0.1)) const separatorLowerColor = createMemo(() => tint(theme.background.default, glowHue(), 0.12)) - const separatorUpperPulseColor = createMemo(() => - tint(theme.background.default, theme.text.default, 0.04), - ) - const separatorLowerPulseColor = createMemo(() => - tint(theme.background.default, theme.text.default, 0.05), - ) const titleColor = (index: number) => { const color = glows() ? glowTextColor(foreground(), glowColor(), 1 + numberWidth() + index, width()) @@ -297,7 +293,7 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat - export function useRouteData(type: T) { const route = useRoute() return route.data as Extract diff --git a/packages/tui/src/feature-plugins/prompt/footer.tsx b/packages/tui/src/feature-plugins/prompt/footer.tsx index 1169da7510..f0aeb5175b 100644 --- a/packages/tui/src/feature-plugins/prompt/footer.tsx +++ b/packages/tui/src/feature-plugins/prompt/footer.tsx @@ -10,24 +10,18 @@ const money = new Intl.NumberFormat("en-US", { export function PromptFooter(props: { context: Plugin.Context; sessionID?: string; mode: "normal" | "shell" }) { const dimensions = useTerminalDimensions() - const activeSubagents = createMemo(() => { + const subagents = createMemo(() => { if (!props.sessionID) return 0 - return props.context.data.session + const count = props.context.data.session .family(props.sessionID) .filter((id) => id !== props.sessionID && props.context.data.session.status(id) === "running").length - }) - const runningShells = createMemo(() => { - if (!props.sessionID) return 0 - return props.context.data.shell - .list(props.context.location) - .filter((shell) => shell.metadata.sessionID === props.sessionID).length - }) - const subagents = createMemo(() => { - const count = activeSubagents() return count ? `${count} subagent${count === 1 ? "" : "s"}` : undefined }) const shells = createMemo(() => { - const count = runningShells() + if (!props.sessionID) return 0 + const count = props.context.data.shell + .list(props.context.location) + .filter((shell) => shell.metadata.sessionID === props.sessionID).length return count ? `${count} shell${count === 1 ? "" : "s"}` : undefined }) const status = createMemo(() => { @@ -40,8 +34,10 @@ export function PromptFooter(props: { context: Plugin.Context; sessionID?: strin session.revert?.messageID, ) const cost = props.context.data.session.cost(props.sessionID) - return [usage ? formatContextUsage(usage.tokens, usage.percent) : undefined, cost > 0 ? money.format(cost) : undefined] - .filter((item): item is string => Boolean(item)) + return [ + usage ? formatContextUsage(usage.tokens, usage.percent) : undefined, + cost > 0 ? money.format(cost) : undefined, + ].filter((item): item is string => Boolean(item)) }) const live = createMemo(() => Boolean(subagents() || shells())) const shortcut = (id: string) => props.context.keymap.shortcuts(id)[0] diff --git a/packages/tui/src/mini/runtime.queue.ts b/packages/tui/src/mini/runtime.queue.ts index 1a66980bae..75894237e3 100644 --- a/packages/tui/src/mini/runtime.queue.ts +++ b/packages/tui/src/mini/runtime.queue.ts @@ -90,10 +90,7 @@ export async function runPromptQueue(input: QueueInput): Promise { draining = (async () => { try { while (!state.closed && state.queue.length > 0) { - const prompt = state.queue.shift() - if (!prompt) { - continue - } + const prompt = state.queue.shift()! if (prompt.mode !== "shell" && isNewCommand(prompt.text)) { if (!input.onNewSession) { diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index 2f667ca0f2..67e0f47e9b 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -200,19 +200,21 @@ export function Session() { const diffWrapMode = createMemo(() => config.diffs?.wrap ?? "word") const groupExploration = createMemo(() => config.session?.grouping !== "none") - const tabRailWidth = createMemo(() => - config.tabs?.enabled && config.tabs.vertical && sessionTabsFitVertically(dimensions().width) - ? SESSION_SIDEBAR_WIDTH - : 0, + const availableWidth = createMemo( + () => + dimensions().width - + (config.tabs?.enabled && config.tabs.vertical && sessionTabsFitVertically(dimensions().width) + ? SESSION_SIDEBAR_WIDTH + : 0), ) - const wide = createMemo(() => dimensions().width - tabRailWidth() > 120) + const wide = createMemo(() => availableWidth() > 120) const sidebarVisible = createMemo(() => { if (session()?.parentID) return false if (sidebarOpen()) return true if (sidebar() === "auto" && wide()) return true return false }) - const contentWidth = createMemo(() => dimensions().width - tabRailWidth() - (sidebarVisible() ? 42 : 0) - 4) + const contentWidth = createMemo(() => availableWidth() - (sidebarVisible() ? 42 : 0) - 4) const models = createMemo(() => data.location.model.list(location()) ?? []) const scrollAcceleration = createMemo(() => getScrollAcceleration(config)) diff --git a/packages/tui/src/routes/session/sidebar.tsx b/packages/tui/src/routes/session/sidebar.tsx index e12b524c94..91f7e8bfa8 100644 --- a/packages/tui/src/routes/session/sidebar.tsx +++ b/packages/tui/src/routes/session/sidebar.tsx @@ -28,7 +28,11 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) { position={props.overlay ? "absolute" : "relative"} > queueMicrotask(() => scroll.verticalScrollBar.resetVisibilityControl())} + ref={(scroll) => + queueMicrotask(() => { + if (!scroll.isDestroyed) scroll.verticalScrollBar.resetVisibilityControl() + }) + } flexGrow={1} scrollAcceleration={scrollAcceleration()} verticalScrollbarOptions={{ diff --git a/packages/tui/src/ui/dialog-prompt.tsx b/packages/tui/src/ui/dialog-prompt.tsx index 9eb0ec0f4f..db779b2aba 100644 --- a/packages/tui/src/ui/dialog-prompt.tsx +++ b/packages/tui/src/ui/dialog-prompt.tsx @@ -1,7 +1,7 @@ import { TextareaRenderable, TextAttributes } from "@opentui/core" import { Keymap } from "../context/keymap" import { useTheme } from "../context/theme" -import { useDialog, type DialogContext } from "./dialog" +import { useDialog } from "./dialog" import { Show, createEffect, createSignal, onMount, type JSX } from "solid-js" import { Spinner } from "../component/spinner" @@ -112,14 +112,3 @@ export function DialogPrompt(props: DialogPromptProps) { ) } - -DialogPrompt.show = (dialog: DialogContext, title: string, options?: Omit) => { - return new Promise((resolve) => { - dialog.replace( - () => ( - resolve(value)} onCancel={() => resolve(null)} /> - ), - () => resolve(null), - ) - }) -}