Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions packages/opencode/src/cli/cmd/tui/component/border.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,19 @@ export const SplitBorder = {
vertical: "┃",
},
}

export const SplitBorderAscii = {
border: ["left" as const, "right" as const],
customBorderChars: {
...EmptyBorder,
vertical: "|",
},
}

export function getSplitBorder(accessible: boolean) {
return accessible ? SplitBorderAscii : SplitBorder
}

export function getSplitBorderChars(accessible: boolean) {
return accessible ? SplitBorderAscii.customBorderChars : SplitBorder.customBorderChars
}
12 changes: 9 additions & 3 deletions packages/opencode/src/cli/cmd/tui/component/dialog-mcp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,22 @@ import { useTheme } from "../context/theme"
import { Keybind } from "@/util/keybind"
import { TextAttributes } from "@opentui/core"
import { useSDK } from "@tui/context/sdk"
import { useAccessibility } from "@tui/util/accessibility"

function Status(props: { enabled: boolean; loading: boolean }) {
const { theme } = useTheme()
const accessibility = useAccessibility()
if (props.loading) {
return <span style={{ fg: theme.textMuted }}>⋯ Loading</span>
return <span style={{ fg: theme.textMuted }}>{accessibility() ? "Loading" : "⋯ Loading"}</span>
}
if (props.enabled) {
return <span style={{ fg: theme.success, attributes: TextAttributes.BOLD }}>✓ Enabled</span>
return (
<span style={{ fg: theme.success, attributes: TextAttributes.BOLD }}>
{accessibility() ? "Enabled" : "✓ Enabled"}
</span>
)
}
return <span style={{ fg: theme.textMuted }}>○ Disabled</span>
return <span style={{ fg: theme.textMuted }}>{accessibility() ? "Disabled" : "○ Disabled"}</span>
}

export function DialogMcp() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { DialogSessionRename } from "./dialog-session-rename"
import { useKV } from "../context/kv"
import { createDebouncedSignal } from "../util/signal"
import "opentui-spinner/solid"
import { useAccessibility } from "@tui/util/accessibility"

export function DialogSessionList() {
const dialog = useDialog()
Expand All @@ -19,6 +20,7 @@ export function DialogSessionList() {
const route = useRoute()
const sdk = useSDK()
const kv = useKV()
const accessibility = useAccessibility()

const [toDelete, setToDelete] = createSignal<string>()
const [search, setSearch] = createDebouncedSignal("", 150)
Expand All @@ -34,6 +36,8 @@ export function DialogSessionList() {
const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined))

const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
const busyFallback = () => (accessibility() ? "[busy]" : "[⋯]")
const showSpinner = () => kv.get("animations_enabled", true) && !accessibility()

const sessions = createMemo(() => searchResults() ?? sync.data.session)

Expand All @@ -58,7 +62,7 @@ export function DialogSessionList() {
category,
footer: Locale.time(x.time.updated),
gutter: isWorking ? (
<Show when={kv.get("animations_enabled", true)} fallback={<text fg={theme.textMuted}>[⋯]</text>}>
<Show when={showSpinner()} fallback={<text fg={theme.textMuted}>{busyFallback()}</text>}>
<spinner frames={spinnerFrames} interval={80} color={theme.primary} />
</Show>
) : undefined,
Expand Down
11 changes: 7 additions & 4 deletions packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ import { TextAttributes } from "@opentui/core"
import { useTheme } from "../context/theme"
import { useSync } from "@tui/context/sync"
import { For, Match, Switch, Show, createMemo } from "solid-js"
import { useAccessibility } from "@tui/util/accessibility"

export type DialogStatusProps = {}

export function DialogStatus() {
const sync = useSync()
const { theme } = useTheme()
const accessibility = useAccessibility()
const bullet = createMemo(() => (accessibility() ? "-" : "•"))

const enabledFormatters = createMemo(() => sync.data.formatter.filter((f) => f.enabled))

Expand Down Expand Up @@ -64,7 +67,7 @@ export function DialogStatus() {
)[item.status],
}}
>
{bullet()}
</text>
<text fg={theme.text} wrapMode="word">
<b>{key}</b>{" "}
Expand Down Expand Up @@ -102,7 +105,7 @@ export function DialogStatus() {
}[item.status],
}}
>
{bullet()}
</text>
<text fg={theme.text} wrapMode="word">
<b>{item.id}</b> <span style={{ fg: theme.textMuted }}>{item.root}</span>
Expand All @@ -124,7 +127,7 @@ export function DialogStatus() {
fg: theme.success,
}}
>
{bullet()}
</text>
<text wrapMode="word" fg={theme.text}>
<b>{item.name}</b>
Expand All @@ -146,7 +149,7 @@ export function DialogStatus() {
fg: theme.success,
}}
>
{bullet()}
</text>
<text wrapMode="word" fg={theme.text}>
<b>{item.name}</b>
Expand Down
37 changes: 24 additions & 13 deletions packages/opencode/src/cli/cmd/tui/component/logo.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,38 @@
import { TextAttributes } from "@opentui/core"
import { For } from "solid-js"
import { For, Show } from "solid-js"
import { useTheme } from "@tui/context/theme"
import { useAccessibility } from "@tui/util/accessibility"

const LOGO_LEFT = [` `, `█▀▀█ █▀▀█ █▀▀█ █▀▀▄`, `█░░█ █░░█ █▀▀▀ █░░█`, `▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀`]

const LOGO_RIGHT = [` ▄ `, `█▀▀▀ █▀▀█ █▀▀█ █▀▀█`, `█░░░ █░░█ █░░█ █▀▀▀`, `▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀`]

export function Logo() {
const { theme } = useTheme()
const accessibility = useAccessibility()
return (
<box>
<For each={LOGO_LEFT}>
{(line, index) => (
<box flexDirection="row" gap={1}>
<text fg={theme.textMuted} selectable={false}>
{line}
</text>
<text fg={theme.text} attributes={TextAttributes.BOLD} selectable={false}>
{LOGO_RIGHT[index()]}
</text>
</box>
)}
</For>
<Show
when={!accessibility()}
fallback={
<text fg={theme.text} attributes={TextAttributes.BOLD} selectable={false}>
OpenCode
</text>
}
>
<For each={LOGO_LEFT}>
{(line, index) => (
<box flexDirection="row" gap={1}>
<text fg={theme.textMuted} selectable={false}>
{line}
</text>
<text fg={theme.text} attributes={TextAttributes.BOLD} selectable={false}>
{LOGO_RIGHT[index()]}
</text>
</box>
)}
</For>
</Show>
</box>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ import { createStore } from "solid-js/store"
import { useSDK } from "@tui/context/sdk"
import { useSync } from "@tui/context/sync"
import { useTheme, selectedForeground } from "@tui/context/theme"
import { SplitBorder } from "@tui/component/border"
import { getSplitBorder } from "@tui/component/border"
import { useCommandDialog } from "@tui/component/dialog-command"
import { useTerminalDimensions } from "@opentui/solid"
import { Locale } from "@/util/locale"
import type { PromptInfo } from "./history"
import { useFrecency } from "./frecency"
import { useAccessibility } from "@tui/util/accessibility"

function removeLineRange(input: string) {
const hashIndex = input.lastIndexOf("#")
Expand Down Expand Up @@ -78,6 +79,7 @@ export function Autocomplete(props: {
const sync = useSync()
const command = useCommandDialog()
const { theme } = useTheme()
const accessibility = useAccessibility()
const dimensions = useTerminalDimensions()
const frecency = useFrecency()

Expand Down Expand Up @@ -720,7 +722,7 @@ export function Autocomplete(props: {
left={position().x}
width={position().width}
zIndex={100}
{...SplitBorder}
{...getSplitBorder(accessibility())}
borderColor={theme.border}
>
<scrollbox
Expand Down
45 changes: 23 additions & 22 deletions packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { DialogAlert } from "../../ui/dialog-alert"
import { useToast } from "../../ui/toast"
import { useKV } from "../../context/kv"
import { useTextareaKeybindings } from "../textarea-keybindings"
import { useAccessibility } from "@tui/util/accessibility"

export type PromptProps = {
sessionID?: string
Expand Down Expand Up @@ -72,6 +73,7 @@ export function Prompt(props: PromptProps) {
const renderer = useRenderer()
const { theme, syntax } = useTheme()
const kv = useKV()
const accessibility = useAccessibility()

function promptModelWarning() {
toast.show({
Expand Down Expand Up @@ -718,6 +720,22 @@ export function Prompt(props: PromptProps) {
}),
}
})
const showSpinner = createMemo(() => kv.get("animations_enabled", true) && !accessibility())
const busyFallback = createMemo(() => (accessibility() ? "[busy]" : "[⋯]"))
const separator = createMemo(() => (accessibility() ? "-" : "·"))
const promptBorderChars = createMemo(() => ({
...EmptyBorder,
vertical: accessibility() ? "|" : "┃",
bottomLeft: accessibility() ? "|" : "╹",
}))
const dividerBorderChars = createMemo(() => ({
...EmptyBorder,
vertical: theme.backgroundElement.a !== 0 ? (accessibility() ? "|" : "╹") : " ",
}))
const dividerHorizontalChars = createMemo(() => ({
...EmptyBorder,
horizontal: theme.backgroundElement.a !== 0 ? (accessibility() ? "-" : "▀") : " ",
}))

return (
<>
Expand Down Expand Up @@ -745,11 +763,7 @@ export function Prompt(props: PromptProps) {
<box
border={["left"]}
borderColor={highlight()}
customBorderChars={{
...EmptyBorder,
vertical: "┃",
bottomLeft: "╹",
}}
customBorderChars={promptBorderChars()}
>
<box
paddingLeft={2}
Expand Down Expand Up @@ -943,7 +957,7 @@ export function Prompt(props: PromptProps) {
</text>
<text fg={theme.textMuted}>{local.model.parsed().provider}</text>
<Show when={showVariant()}>
<text fg={theme.textMuted}>·</text>
<text fg={theme.textMuted}>{separator()}</text>
<text>
<span style={{ fg: theme.warning, bold: true }}>{local.model.variant.current()}</span>
</text>
Expand All @@ -957,26 +971,13 @@ export function Prompt(props: PromptProps) {
height={1}
border={["left"]}
borderColor={highlight()}
customBorderChars={{
...EmptyBorder,
vertical: theme.backgroundElement.a !== 0 ? "╹" : " ",
}}
customBorderChars={dividerBorderChars()}
>
<box
height={1}
border={["bottom"]}
borderColor={theme.backgroundElement}
customBorderChars={
theme.backgroundElement.a !== 0
? {
...EmptyBorder,
horizontal: "▀",
}
: {
...EmptyBorder,
horizontal: " ",
}
}
customBorderChars={dividerHorizontalChars()}
/>
</box>
<box flexDirection="row" justifyContent="space-between">
Expand All @@ -989,7 +990,7 @@ export function Prompt(props: PromptProps) {
>
<box flexShrink={0} flexDirection="row" gap={1}>
<box marginLeft={1}>
<Show when={kv.get("animations_enabled", true)} fallback={<text fg={theme.textMuted}>[⋯]</text>}>
<Show when={showSpinner()} fallback={<text fg={theme.textMuted}>{busyFallback()}</text>}>
<spinner color={spinnerDef().color} frames={spinnerDef().frames} interval={40} />
</Show>
</box>
Expand Down
4 changes: 3 additions & 1 deletion packages/opencode/src/cli/cmd/tui/component/tips.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createMemo, createSignal, For } from "solid-js"
import { useTheme } from "@tui/context/theme"
import { useAccessibility } from "@tui/util/accessibility"

type TipPart = { text: string; highlight: boolean }

Expand Down Expand Up @@ -29,12 +30,13 @@ function parse(tip: string): TipPart[] {

export function Tips() {
const theme = useTheme().theme
const accessibility = useAccessibility()
const parts = parse(TIPS[Math.floor(Math.random() * TIPS.length)])

return (
<box flexDirection="row" maxWidth="100%">
<text flexShrink={0} style={{ fg: theme.warning }}>
● Tip{" "}
{accessibility() ? "Tip:" : "● Tip"}{" "}
</text>
<text flexShrink={1}>
<For each={parts}>
Expand Down
12 changes: 11 additions & 1 deletion packages/opencode/src/cli/cmd/tui/component/todo-item.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useTheme } from "../context/theme"
import { useAccessibility } from "@tui/util/accessibility"

export interface TodoItemProps {
status: string
Expand All @@ -7,6 +8,15 @@ export interface TodoItemProps {

export function TodoItem(props: TodoItemProps) {
const { theme } = useTheme()
const accessibility = useAccessibility()
const marker = () => {
if (!accessibility()) {
return props.status === "completed" ? "✓" : props.status === "in_progress" ? "•" : " "
}
if (props.status === "completed") return "x"
if (props.status === "in_progress") return "~"
return " "
}

return (
<box flexDirection="row" gap={0}>
Expand All @@ -16,7 +26,7 @@ export function TodoItem(props: TodoItemProps) {
fg: props.status === "in_progress" ? theme.warning : theme.textMuted,
}}
>
[{props.status === "completed" ? "✓" : props.status === "in_progress" ? "•" : " "}]{" "}
[{marker()}]{" "}
</text>
<text
flexGrow={1}
Expand Down
Loading