mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-11 20:19:53 -04:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c5ac6ba4f3 | |||
| 20af1f5e79 | |||
| 8daf912fbf |
@@ -173,7 +173,6 @@ export const layer = Layer.effect(
|
||||
const file = path.join(outputDir, `${id}.out`)
|
||||
const env = {
|
||||
...process.env,
|
||||
...ShellSelect.env(),
|
||||
TERM: "xterm-256color",
|
||||
OPENCODE_TERMINAL: "1",
|
||||
} as Record<string, string>
|
||||
|
||||
@@ -163,37 +163,12 @@ function info(file: string): Item {
|
||||
}
|
||||
}
|
||||
|
||||
export function env(platform = process.platform) {
|
||||
if (platform === "win32") return { PYTHONIOENCODING: "utf-8" }
|
||||
return {}
|
||||
}
|
||||
|
||||
export function args(file: string, command: string) {
|
||||
const n = name(file)
|
||||
if (n === "nu" || n === "fish") return ["-c", command]
|
||||
if (n === "zsh" || n === "bash") return ["-c", command]
|
||||
if (n === "cmd") return ["/d", "/c", `chcp 65001 >nul 2>nul & ${command}`]
|
||||
if (ps(file)) {
|
||||
const payload = Buffer.from(command, "utf8").toString("base64")
|
||||
const script = [
|
||||
"$utf8 = [System.Text.UTF8Encoding]::new($false)",
|
||||
"[Console]::InputEncoding = $utf8",
|
||||
"[Console]::OutputEncoding = $utf8",
|
||||
"$OutputEncoding = $utf8",
|
||||
`& ([scriptblock]::Create([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('${payload}'))))`,
|
||||
"$success = $?",
|
||||
"$code = $LASTEXITCODE",
|
||||
"if ($null -ne $code) { exit $code }",
|
||||
"if (-not $success) { exit 1 }",
|
||||
].join("; ")
|
||||
return [
|
||||
"-NoLogo",
|
||||
"-NoProfile",
|
||||
"-NonInteractive",
|
||||
"-EncodedCommand",
|
||||
Buffer.from(script, "utf16le").toString("base64"),
|
||||
]
|
||||
}
|
||||
if (n === "cmd") return ["/c", command]
|
||||
if (ps(file)) return ["-NoProfile", "-Command", command]
|
||||
return ["-c", command]
|
||||
}
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ const modelOutput = (output: Output): string | undefined => {
|
||||
// TODO: Port tree-sitter bash / PowerShell parser-based approval reduction.
|
||||
// TODO: Port BashArity reusable command-prefix approvals.
|
||||
// TODO: Replace token-based command-argument external-directory advisories with parser-based detection.
|
||||
// TODO: Restore Windows shell-specific path handling.
|
||||
// TODO: Restore PowerShell and cmd-specific invocation/path handling on Windows.
|
||||
// TODO: Add plugin shell.env environment augmentation once V2 plugin hooks exist.
|
||||
// TODO: Add durable/live progress metadata streaming for long-running commands once V2 tool invocation progress context is wired.
|
||||
// TODO: Persist job status and define restart recovery before exposing remote observation.
|
||||
|
||||
@@ -61,23 +61,6 @@ describe("shell", () => {
|
||||
expect(ShellSelect.args("/bin/bash", "echo hi")).toEqual(["-c", "echo hi"])
|
||||
})
|
||||
|
||||
test("switches cmd output to UTF-8", () => {
|
||||
expect(ShellSelect.args("cmd", 'echo "你好"')).toEqual(["/d", "/c", 'chcp 65001 >nul 2>nul & echo "你好"'])
|
||||
})
|
||||
|
||||
test("transports PowerShell commands independently of the active code page", () => {
|
||||
const args = ShellSelect.args("pwsh", 'Write-Output "你好"')
|
||||
expect(args.slice(0, 4)).toEqual(["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand"])
|
||||
expect(Buffer.from(args[4], "base64").toString("utf16le")).toBe(
|
||||
"$utf8 = [System.Text.UTF8Encoding]::new($false); [Console]::InputEncoding = $utf8; [Console]::OutputEncoding = $utf8; $OutputEncoding = $utf8; & ([scriptblock]::Create([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('V3JpdGUtT3V0cHV0ICLkvaDlpb0i')))); $success = $?; $code = $LASTEXITCODE; if ($null -ne $code) { exit $code }; if (-not $success) { exit 1 }",
|
||||
)
|
||||
})
|
||||
|
||||
test("forces piped Python output to UTF-8 on Windows", () => {
|
||||
expect(ShellSelect.env("win32")).toEqual({ PYTHONIOENCODING: "utf-8" })
|
||||
expect(ShellSelect.env("linux")).toEqual({})
|
||||
})
|
||||
|
||||
if (process.platform === "win32") {
|
||||
test("rejects blacklisted shells case-insensitively", async () => {
|
||||
await withShell("NU.EXE", async () => {
|
||||
|
||||
@@ -570,7 +570,7 @@ test("keeps locked deferred parity TODOs visible", async () => {
|
||||
"Port tree-sitter bash / PowerShell parser-based approval reduction.",
|
||||
"Port BashArity reusable command-prefix approvals.",
|
||||
"Replace token-based command-argument external-directory advisories with parser-based detection.",
|
||||
"Restore Windows shell-specific path handling.",
|
||||
"Restore PowerShell and cmd-specific invocation/path handling on Windows.",
|
||||
"Add plugin shell.env environment augmentation once V2 plugin hooks exist.",
|
||||
"Add durable/live progress metadata streaming for long-running commands once V2 tool invocation progress context is wired.",
|
||||
"Persist job status and define restart recovery before exposing remote observation.",
|
||||
|
||||
@@ -53,6 +53,7 @@ import { DialogModel } from "./component/dialog-model"
|
||||
import { useConnected } from "./component/use-connected"
|
||||
import { DialogMcp } from "./component/dialog-mcp"
|
||||
import { DialogStatus } from "./component/dialog-status"
|
||||
import { DialogConfig } from "./component/dialog-config"
|
||||
import { DialogDebug } from "./component/dialog-debug"
|
||||
import { DialogPair, type DialogPairCredentials } from "./component/dialog-pair"
|
||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
||||
@@ -218,7 +219,9 @@ function fromV1(config: TuiConfigV1.Resolved): Config.Info {
|
||||
}
|
||||
|
||||
function isConfigInterface(config: Config.Interface | TuiConfigV1.Resolved): config is Config.Interface {
|
||||
return "get" in config && typeof config.get === "function" && "update" in config && typeof config.update === "function"
|
||||
return (
|
||||
"get" in config && typeof config.get === "function" && "update" in config && typeof config.update === "function"
|
||||
)
|
||||
}
|
||||
|
||||
export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
|
||||
@@ -477,7 +480,8 @@ function App(props: {
|
||||
}) {
|
||||
const log = useLog({ component: "app" })
|
||||
const startup = useTuiStartup()
|
||||
const config = useConfig().data
|
||||
const configContext = useConfig()
|
||||
const config = configContext.data
|
||||
const route = useRoute()
|
||||
const dimensions = useTerminalDimensions()
|
||||
const renderer = useRenderer()
|
||||
@@ -587,10 +591,12 @@ function App(props: {
|
||||
|
||||
renderer.clearSelection()
|
||||
}
|
||||
const [terminalTitleEnabled, setTerminalTitleEnabled] = createSignal(kv.get("terminal_title_enabled", true))
|
||||
const [pasteSummaryEnabled, setPasteSummaryEnabled] = createSignal(
|
||||
kv.get("paste_summary_enabled", true),
|
||||
)
|
||||
const [terminalTitleEnabled, setTerminalTitleEnabled] = kv.signal("terminal_title_enabled", true)
|
||||
const [pasteSummaryEnabled, setPasteSummaryEnabled] = kv.signal("paste_summary_enabled", true)
|
||||
|
||||
createEffect(() => {
|
||||
renderer.useMouse = !Flag.OPENCODE_DISABLE_MOUSE && config.mouse
|
||||
})
|
||||
|
||||
// Update terminal window title based on current route and session
|
||||
createEffect(() => {
|
||||
@@ -849,6 +855,16 @@ function App(props: {
|
||||
},
|
||||
category: "Integration",
|
||||
},
|
||||
{
|
||||
name: "opencode.settings",
|
||||
title: "Open settings",
|
||||
slashName: "settings",
|
||||
enabled: configContext.writable,
|
||||
run: () => {
|
||||
dialog.replace(() => <DialogConfig />)
|
||||
},
|
||||
category: "System",
|
||||
},
|
||||
{
|
||||
name: "opencode.status",
|
||||
title: "View status",
|
||||
|
||||
@@ -0,0 +1,354 @@
|
||||
import { TextAttributes } from "@opentui/core"
|
||||
import { useTerminalDimensions } from "@opentui/solid"
|
||||
import { createMemo, createSignal, onMount, Show } from "solid-js"
|
||||
import { useConfig } from "../config"
|
||||
import { useTheme } from "../context/theme"
|
||||
import { useDialog } from "../ui/dialog"
|
||||
import { DialogSelect } from "../ui/dialog-select"
|
||||
import { useToast } from "../ui/toast"
|
||||
|
||||
type Setting = {
|
||||
title: string
|
||||
category: string
|
||||
description: string
|
||||
detail?: string
|
||||
path: string[]
|
||||
default: unknown
|
||||
values?: readonly unknown[]
|
||||
labels?: readonly string[]
|
||||
step?: number
|
||||
min?: number
|
||||
max?: number
|
||||
format?: (value: unknown) => string
|
||||
}
|
||||
|
||||
const settings: Setting[] = [
|
||||
{
|
||||
title: "Theme",
|
||||
category: "Appearance",
|
||||
description: "Interface color theme",
|
||||
detail:
|
||||
"Choose the color theme used throughout OpenCode. Custom themes discovered from your config directory appear here alongside the built-in themes.",
|
||||
path: ["theme", "name"],
|
||||
default: "opencode",
|
||||
},
|
||||
{
|
||||
title: "Color mode",
|
||||
category: "Appearance",
|
||||
description: "Terminal color preference",
|
||||
detail:
|
||||
"Choose how OpenCode selects its colors. System follows your terminal preference, while dark and light keep the interface in a fixed mode.",
|
||||
path: ["theme", "mode"],
|
||||
default: "system",
|
||||
values: ["system", "dark", "light"],
|
||||
},
|
||||
{
|
||||
title: "Animations",
|
||||
category: "Appearance",
|
||||
description: "Interface motion",
|
||||
path: ["animations"],
|
||||
default: true,
|
||||
values: [false, true],
|
||||
labels: ["off", "on"],
|
||||
},
|
||||
{
|
||||
title: "Tips",
|
||||
category: "Appearance",
|
||||
description: "Home screen hints",
|
||||
path: ["hints", "tips"],
|
||||
default: true,
|
||||
values: [false, true],
|
||||
labels: ["off", "on"],
|
||||
},
|
||||
{
|
||||
title: "Onboarding",
|
||||
category: "Appearance",
|
||||
description: "Getting-started guidance",
|
||||
path: ["hints", "onboarding"],
|
||||
default: true,
|
||||
values: [false, true],
|
||||
labels: ["off", "on"],
|
||||
},
|
||||
{
|
||||
title: "Sidebar",
|
||||
category: "Session",
|
||||
description: "Session sidebar visibility",
|
||||
path: ["session", "sidebar"],
|
||||
default: "auto",
|
||||
values: ["hide", "auto"],
|
||||
},
|
||||
{
|
||||
title: "Scrollbar",
|
||||
category: "Session",
|
||||
description: "Transcript scrollbar",
|
||||
path: ["session", "scrollbar"],
|
||||
default: false,
|
||||
values: [false, true],
|
||||
labels: ["off", "on"],
|
||||
},
|
||||
{
|
||||
title: "Thinking",
|
||||
category: "Session",
|
||||
description: "Model reasoning by default",
|
||||
path: ["session", "thinking"],
|
||||
default: "hide",
|
||||
values: ["hide", "show"],
|
||||
},
|
||||
{
|
||||
title: "Grouping",
|
||||
category: "Session",
|
||||
description: "Related transcript items",
|
||||
path: ["session", "grouping"],
|
||||
default: "auto",
|
||||
values: ["none", "auto"],
|
||||
},
|
||||
{
|
||||
title: "Layout",
|
||||
category: "Diffs",
|
||||
description: "Diff presentation",
|
||||
path: ["diffs", "view"],
|
||||
default: "auto",
|
||||
values: ["auto", "split", "unified"],
|
||||
},
|
||||
{
|
||||
title: "Wrapping",
|
||||
category: "Diffs",
|
||||
description: "Long diff lines",
|
||||
path: ["diffs", "wrap"],
|
||||
default: "word",
|
||||
values: ["none", "word"],
|
||||
},
|
||||
{
|
||||
title: "File tree",
|
||||
category: "Diffs",
|
||||
description: "Diff file navigation",
|
||||
path: ["diffs", "tree"],
|
||||
default: true,
|
||||
values: [false, true],
|
||||
labels: ["off", "on"],
|
||||
},
|
||||
{
|
||||
title: "Single patch",
|
||||
category: "Diffs",
|
||||
description: "Only the selected patch",
|
||||
path: ["diffs", "single"],
|
||||
default: false,
|
||||
values: [false, true],
|
||||
labels: ["off", "on"],
|
||||
},
|
||||
{
|
||||
title: "Scroll speed",
|
||||
category: "Input",
|
||||
description: "Distance per input tick",
|
||||
path: ["scroll", "speed"],
|
||||
default: 3,
|
||||
step: 0.25,
|
||||
min: 0.25,
|
||||
max: 10,
|
||||
format: (value) => Number(value).toFixed(2),
|
||||
},
|
||||
{
|
||||
title: "Acceleration",
|
||||
category: "Input",
|
||||
description: "Repeated scrolling",
|
||||
path: ["scroll", "acceleration"],
|
||||
default: false,
|
||||
values: [false, true],
|
||||
labels: ["off", "on"],
|
||||
},
|
||||
{
|
||||
title: "Mouse",
|
||||
category: "Input",
|
||||
description: "Terminal mouse capture",
|
||||
path: ["mouse"],
|
||||
default: true,
|
||||
values: [false, true],
|
||||
labels: ["off", "on"],
|
||||
},
|
||||
{
|
||||
title: "Editor context",
|
||||
category: "Input",
|
||||
description: "Active selection in prompts",
|
||||
path: ["prompt", "editor"],
|
||||
default: true,
|
||||
values: [false, true],
|
||||
labels: ["off", "on"],
|
||||
},
|
||||
{
|
||||
title: "Large pastes",
|
||||
category: "Input",
|
||||
description: "Paste display style",
|
||||
path: ["prompt", "paste"],
|
||||
default: "compact",
|
||||
values: ["compact", "full"],
|
||||
},
|
||||
{
|
||||
title: "Leader timeout",
|
||||
category: "Input",
|
||||
description: "Wait after leader key",
|
||||
path: ["leader", "timeout"],
|
||||
default: 2000,
|
||||
step: 250,
|
||||
min: 250,
|
||||
max: 10000,
|
||||
format: (value) => `${value} ms`,
|
||||
},
|
||||
{
|
||||
title: "Attention",
|
||||
category: "Alerts",
|
||||
description: "Alerts when input is needed",
|
||||
path: ["attention", "enabled"],
|
||||
default: false,
|
||||
values: [false, true],
|
||||
labels: ["off", "on"],
|
||||
},
|
||||
{
|
||||
title: "Notifications",
|
||||
category: "Alerts",
|
||||
description: "System notifications",
|
||||
path: ["attention", "notifications"],
|
||||
default: true,
|
||||
values: [false, true],
|
||||
labels: ["off", "on"],
|
||||
},
|
||||
{
|
||||
title: "Sounds",
|
||||
category: "Alerts",
|
||||
description: "Attention sounds",
|
||||
path: ["attention", "sound"],
|
||||
default: true,
|
||||
values: [false, true],
|
||||
labels: ["off", "on"],
|
||||
},
|
||||
{
|
||||
title: "Volume",
|
||||
category: "Alerts",
|
||||
description: "Attention sound level",
|
||||
path: ["attention", "volume"],
|
||||
default: 0.4,
|
||||
step: 0.1,
|
||||
min: 0,
|
||||
max: 1,
|
||||
format: (value) => `${Math.round(Number(value) * 100)}%`,
|
||||
},
|
||||
{
|
||||
title: "Window title",
|
||||
category: "Terminal",
|
||||
description: "Update terminal title",
|
||||
path: ["terminal", "title"],
|
||||
default: true,
|
||||
values: [false, true],
|
||||
labels: ["off", "on"],
|
||||
},
|
||||
]
|
||||
|
||||
export function DialogConfig() {
|
||||
const config = useConfig()
|
||||
const dialog = useDialog()
|
||||
const toast = useToast()
|
||||
const themeState = useTheme()
|
||||
const { theme } = themeState
|
||||
const dimensions = useTerminalDimensions()
|
||||
const [selected, setSelected] = createSignal(settings[0])
|
||||
const [saving, setSaving] = createSignal(false)
|
||||
onMount(() => {
|
||||
dialog.setSize("xlarge")
|
||||
dialog.setCentered(true)
|
||||
})
|
||||
|
||||
const value = (setting: Setting) => {
|
||||
const current = setting.path.reduce<unknown>((result, key) => {
|
||||
if (!result || typeof result !== "object") return undefined
|
||||
return (result as Record<string, unknown>)[key]
|
||||
}, config.data)
|
||||
if (setting.path.join(".") === "theme.name") return current ?? themeState.selected
|
||||
return current ?? setting.default
|
||||
}
|
||||
const values = (setting: Setting) =>
|
||||
setting.path.join(".") === "theme.name"
|
||||
? Object.keys(themeState.all()).sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }))
|
||||
: setting.values
|
||||
const display = (setting: Setting) => {
|
||||
const current = value(setting)
|
||||
if (setting.format) return setting.format(current)
|
||||
const index = setting.values?.indexOf(current)
|
||||
return index === undefined || index < 0 ? String(current) : (setting.labels?.[index] ?? String(current))
|
||||
}
|
||||
const options = createMemo(() =>
|
||||
settings.map((setting) => ({
|
||||
title: setting.title,
|
||||
category: setting.category,
|
||||
value: setting,
|
||||
footer: selected() === setting ? `‹ ${display(setting)} ›` : ` ${display(setting)} `,
|
||||
})),
|
||||
)
|
||||
const split = createMemo(() => dimensions().width >= 110)
|
||||
const height = createMemo(() => Math.max(8, Math.min(36, dimensions().height - 12)))
|
||||
|
||||
async function change(setting: Setting, direction: number) {
|
||||
if (saving()) return
|
||||
const current = value(setting)
|
||||
const choices = values(setting)
|
||||
const next = choices
|
||||
? choices[(choices.indexOf(current) + direction + choices.length) % choices.length]
|
||||
: Math.min(setting.max!, Math.max(setting.min!, Number(current) + direction * setting.step!))
|
||||
if (next === current) return
|
||||
setSaving(true)
|
||||
await config
|
||||
.update((draft) => {
|
||||
const parent = setting.path.slice(0, -1).reduce<Record<string, unknown>>((result, key) => {
|
||||
if (!result[key] || typeof result[key] !== "object") result[key] = {}
|
||||
return result[key] as Record<string, unknown>
|
||||
}, draft)
|
||||
parent[setting.path.at(-1)!] = next
|
||||
})
|
||||
.catch(toast.error)
|
||||
.finally(() => setSaving(false))
|
||||
}
|
||||
|
||||
return (
|
||||
<box flexDirection="row" height={height() + 1}>
|
||||
<box width={split() ? "54%" : "100%"}>
|
||||
<DialogSelect
|
||||
title="Settings"
|
||||
options={options()}
|
||||
renderFilter={false}
|
||||
hideClose={split()}
|
||||
maxHeight={height() - 2}
|
||||
onMove={(option) => setSelected(option.value)}
|
||||
onSelect={(option) => void change(option.value, 1)}
|
||||
bindings={[
|
||||
{ key: "left", desc: "Previous value", group: "Settings", cmd: () => void change(selected(), -1) },
|
||||
{ key: "right", desc: "Next value", group: "Settings", cmd: () => void change(selected(), 1) },
|
||||
]}
|
||||
/>
|
||||
</box>
|
||||
<Show when={split()}>
|
||||
<box
|
||||
position="relative"
|
||||
top={-1}
|
||||
width="46%"
|
||||
height={height() + 2}
|
||||
paddingTop={1}
|
||||
paddingLeft={2}
|
||||
paddingRight={2}
|
||||
backgroundColor={theme.backgroundElement}
|
||||
>
|
||||
<box flexDirection="row" justifyContent="space-between">
|
||||
<text fg={theme.primary} attributes={TextAttributes.BOLD}>
|
||||
{selected().title}
|
||||
</text>
|
||||
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
|
||||
esc
|
||||
</text>
|
||||
</box>
|
||||
<box paddingTop={1}>
|
||||
<text fg={theme.text} wrapMode="word">
|
||||
{selected().detail ?? selected().description}
|
||||
</text>
|
||||
</box>
|
||||
</box>
|
||||
</Show>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
@@ -179,6 +179,7 @@ export function resolve(input: Info, options: { terminalSuspend: boolean }): Res
|
||||
const ConfigContext = createContext<{
|
||||
data: Resolved
|
||||
update: Interface["update"]
|
||||
writable: boolean
|
||||
}>()
|
||||
|
||||
export function ConfigProvider(props: {
|
||||
@@ -195,7 +196,9 @@ export function ConfigProvider(props: {
|
||||
setConfig(reconcile(resolve(info, props.options ?? { terminalSuspend: true })))
|
||||
return info
|
||||
}
|
||||
return <ConfigContext.Provider value={{ data: config, update }}>{props.children}</ConfigContext.Provider>
|
||||
return (
|
||||
<ConfigContext.Provider value={{ data: config, update, writable: !!host }}>{props.children}</ConfigContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useConfig() {
|
||||
|
||||
@@ -129,6 +129,15 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
||||
if (theme) setStore("active", theme)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const mode = config.theme?.mode
|
||||
if (mode === "dark" || mode === "light") {
|
||||
pin(mode)
|
||||
return
|
||||
}
|
||||
if (mode === "system" && store.lock !== undefined) free()
|
||||
})
|
||||
|
||||
function syncCustomThemes() {
|
||||
return themes
|
||||
.discover()
|
||||
|
||||
@@ -51,6 +51,8 @@ export interface DialogSelectProps<T> {
|
||||
}[]
|
||||
bindings?: readonly Binding<Renderable, KeyEvent>[]
|
||||
current?: T
|
||||
hideClose?: boolean
|
||||
maxHeight?: number
|
||||
}
|
||||
|
||||
export interface DialogSelectOption<T = any> {
|
||||
@@ -212,7 +214,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
||||
})
|
||||
|
||||
const dimensions = useTerminalDimensions()
|
||||
const height = createMemo(() => Math.min(rows(), Math.floor(dimensions().height / 2) - 6))
|
||||
const height = createMemo(() => Math.min(rows(), props.maxHeight ?? Math.floor(dimensions().height / 2) - 6))
|
||||
|
||||
const selected = createMemo(() => flat()[store.selected])
|
||||
|
||||
@@ -565,9 +567,11 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
||||
{props.title}
|
||||
</text>
|
||||
)}
|
||||
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
|
||||
esc
|
||||
</text>
|
||||
<Show when={!props.hideClose}>
|
||||
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
|
||||
esc
|
||||
</text>
|
||||
</Show>
|
||||
</box>
|
||||
<Show when={props.renderFilter !== false}>
|
||||
<box paddingTop={1}>
|
||||
@@ -789,7 +793,11 @@ function Option(props: {
|
||||
</text>
|
||||
<Show when={props.footer}>
|
||||
<box flexShrink={0}>
|
||||
<text fg={props.active && !props.muted ? fg : theme.textMuted}>{props.footer}</text>
|
||||
{typeof props.footer === "string" ? (
|
||||
<text fg={props.active && !props.muted ? fg : theme.textMuted}>{props.footer}</text>
|
||||
) : (
|
||||
props.footer
|
||||
)}
|
||||
</box>
|
||||
</Show>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user