feat: unwrap cli-tui namespaces to flat exports + barrel (#22759)

This commit is contained in:
Kit Langton
2026-04-16 03:56:51 +00:00
committed by GitHub
parent 9f4b73b6a3
commit f6cc228684
14 changed files with 414 additions and 419 deletions
+3 -3
View File
@@ -1,7 +1,7 @@
import { render, TimeToFirstDraw, useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid" import { render, TimeToFirstDraw, useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid"
import { Clipboard } from "@tui/util/clipboard" import * as Clipboard from "@tui/util/clipboard"
import { Selection } from "@tui/util/selection" import * as Selection from "@tui/util/selection"
import { Terminal } from "@tui/util/terminal" import * as Terminal from "@tui/util/terminal"
import { createCliRenderer, MouseButton, type CliRendererConfig } from "@opentui/core" import { createCliRenderer, MouseButton, type CliRendererConfig } from "@opentui/core"
import { RouteProvider, useRoute } from "@tui/context/route" import { RouteProvider, useRoute } from "@tui/context/route"
import { import {
@@ -11,7 +11,7 @@ import { TextAttributes } from "@opentui/core"
import type { ProviderAuthAuthorization, ProviderAuthMethod } from "@opencode-ai/sdk/v2" import type { ProviderAuthAuthorization, ProviderAuthMethod } from "@opencode-ai/sdk/v2"
import { DialogModel } from "./dialog-model" import { DialogModel } from "./dialog-model"
import { useKeyboard } from "@opentui/solid" import { useKeyboard } from "@opentui/solid"
import { Clipboard } from "@tui/util/clipboard" import * as Clipboard from "@tui/util/clipboard"
import { useToast } from "../ui/toast" import { useToast } from "../ui/toast"
import { isConsoleManagedProvider } from "@tui/util/provider-origin" import { isConsoleManagedProvider } from "@tui/util/provider-origin"
@@ -1,6 +1,6 @@
import { TextAttributes } from "@opentui/core" import { TextAttributes } from "@opentui/core"
import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid" import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid"
import { Clipboard } from "@tui/util/clipboard" import * as Clipboard from "@tui/util/clipboard"
import { createSignal } from "solid-js" import { createSignal } from "solid-js"
import { Installation } from "@/installation" import { Installation } from "@/installation"
import { win32FlushInputBuffer } from "../win32" import { win32FlushInputBuffer } from "../win32"
@@ -1,7 +1,7 @@
import { BoxRenderable, MouseButton, MouseEvent, RGBA, TextAttributes } from "@opentui/core" import { BoxRenderable, MouseButton, MouseEvent, RGBA, TextAttributes } from "@opentui/core"
import { For, createMemo, createSignal, onCleanup, type JSX } from "solid-js" import { For, createMemo, createSignal, onCleanup, type JSX } from "solid-js"
import { useTheme, tint } from "@tui/context/theme" import { useTheme, tint } from "@tui/context/theme"
import { Sound } from "@tui/util/sound" import * as Sound from "@tui/util/sound"
import { logo } from "@/cli/logo" import { logo } from "@/cli/logo"
// Shadow markers (rendered chars in parens): // Shadow markers (rendered chars in parens):
@@ -21,9 +21,9 @@ import { DialogStash } from "../dialog-stash"
import { type AutocompleteRef, Autocomplete } from "./autocomplete" import { type AutocompleteRef, Autocomplete } from "./autocomplete"
import { useCommandDialog } from "../dialog-command" import { useCommandDialog } from "../dialog-command"
import { useRenderer, type JSX } from "@opentui/solid" import { useRenderer, type JSX } from "@opentui/solid"
import { Editor } from "@tui/util/editor" import * as Editor from "@tui/util/editor"
import { useExit } from "../../context/exit" import { useExit } from "../../context/exit"
import { Clipboard } from "../../util/clipboard" import * as Clipboard from "../../util/clipboard"
import type { AssistantMessage, FilePart, UserMessage } from "@opencode-ai/sdk/v2" import type { AssistantMessage, FilePart, UserMessage } from "@opencode-ai/sdk/v2"
import { TuiEvent } from "../../event" import { TuiEvent } from "../../event"
import { iife } from "@/util/iife" import { iife } from "@/util/iife"
@@ -3,7 +3,7 @@ import { useSync } from "@tui/context/sync"
import { DialogSelect } from "@tui/ui/dialog-select" import { DialogSelect } from "@tui/ui/dialog-select"
import { useSDK } from "@tui/context/sdk" import { useSDK } from "@tui/context/sdk"
import { useRoute } from "@tui/context/route" import { useRoute } from "@tui/context/route"
import { Clipboard } from "@tui/util/clipboard" import * as Clipboard from "@tui/util/clipboard"
import type { PromptInfo } from "@tui/component/prompt/history" import type { PromptInfo } from "@tui/component/prompt/history"
import { strip } from "@tui/component/prompt/part" import { strip } from "@tui/component/prompt/part"
@@ -66,10 +66,10 @@ import { SubagentFooter } from "./subagent-footer.tsx"
import { Flag } from "@/flag/flag" import { Flag } from "@/flag/flag"
import { LANGUAGE_EXTENSIONS } from "@/lsp/language" import { LANGUAGE_EXTENSIONS } from "@/lsp/language"
import parsers from "../../../../../../parsers-config.ts" import parsers from "../../../../../../parsers-config.ts"
import { Clipboard } from "../../util/clipboard" import * as Clipboard from "../../util/clipboard"
import { Toast, useToast } from "../../ui/toast" import { Toast, useToast } from "../../ui/toast"
import { useKV } from "../../context/kv.tsx" import { useKV } from "../../context/kv.tsx"
import { Editor } from "../../util/editor" import * as Editor from "../../util/editor"
import stripAnsi from "strip-ansi" import stripAnsi from "strip-ansi"
import { usePromptRef } from "../../context/prompt" import { usePromptRef } from "../../context/prompt"
import { useExit } from "../../context/exit" import { useExit } from "../../context/exit"
@@ -5,7 +5,7 @@ import { MouseButton, Renderable, RGBA } from "@opentui/core"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { useToast } from "./toast" import { useToast } from "./toast"
import { Flag } from "@/flag/flag" import { Flag } from "@/flag/flag"
import { Selection } from "@tui/util/selection" import * as Selection from "@tui/util/selection"
export function Dialog( export function Dialog(
props: ParentProps<{ props: ParentProps<{
@@ -22,21 +22,20 @@ function writeOsc52(text: string): void {
process.stdout.write(sequence) process.stdout.write(sequence)
} }
export namespace Clipboard { export interface Content {
export interface Content {
data: string data: string
mime: string mime: string
} }
// Checks clipboard for images first, then falls back to text. // Checks clipboard for images first, then falls back to text.
// //
// On Windows prompt/ can call this from multiple paste signals because // On Windows prompt/ can call this from multiple paste signals because
// terminals surface image paste differently: // terminals surface image paste differently:
// 1. A forwarded Ctrl+V keypress // 1. A forwarded Ctrl+V keypress
// 2. An empty bracketed-paste hint for image-only clipboard in Windows // 2. An empty bracketed-paste hint for image-only clipboard in Windows
// Terminal <1.25 // Terminal <1.25
// 3. A kitty Ctrl+V key-release fallback for Windows Terminal 1.25+ // 3. A kitty Ctrl+V key-release fallback for Windows Terminal 1.25+
export async function read(): Promise<Content | undefined> { export async function read(): Promise<Content | undefined> {
const os = platform() const os = platform()
if (os === "darwin") { if (os === "darwin") {
@@ -99,9 +98,9 @@ export namespace Clipboard {
if (text) { if (text) {
return { data: text, mime: "text/plain" } return { data: text, mime: "text/plain" }
} }
} }
const getCopyMethod = lazy(() => { const getCopyMethod = lazy(() => {
const os = platform() const os = platform()
if (os === "darwin" && which("osascript")) { if (os === "darwin" && which("osascript")) {
@@ -183,10 +182,9 @@ export namespace Clipboard {
return async (text: string) => { return async (text: string) => {
await clipboardy.write(text).catch(() => {}) await clipboardy.write(text).catch(() => {})
} }
}) })
export async function copy(text: string): Promise<void> { export async function copy(text: string): Promise<void> {
writeOsc52(text) writeOsc52(text)
await getCopyMethod()(text) await getCopyMethod()(text)
}
} }
@@ -6,8 +6,7 @@ import { CliRenderer } from "@opentui/core"
import { Filesystem } from "@/util" import { Filesystem } from "@/util"
import { Process } from "@/util" import { Process } from "@/util"
export namespace Editor { export async function open(opts: { value: string; renderer: CliRenderer }): Promise<string | undefined> {
export async function open(opts: { value: string; renderer: CliRenderer }): Promise<string | undefined> {
const editor = process.env["VISUAL"] || process.env["EDITOR"] const editor = process.env["VISUAL"] || process.env["EDITOR"]
if (!editor) return if (!editor) return
@@ -33,5 +32,4 @@ export namespace Editor {
opts.renderer.resume() opts.renderer.resume()
opts.renderer.requestRender() opts.renderer.requestRender()
} }
}
} }
@@ -0,0 +1,5 @@
export * as Editor from "./editor"
export * as Selection from "./selection"
export * as Sound from "./sound"
export * as Terminal from "./terminal"
export * as Clipboard from "./clipboard"
@@ -1,4 +1,4 @@
import { Clipboard } from "./clipboard" import * as Clipboard from "./clipboard"
type Toast = { type Toast = {
show: (input: { message: string; variant: "info" | "success" | "warning" | "error" }) => void show: (input: { message: string; variant: "info" | "success" | "warning" | "error" }) => void
@@ -10,8 +10,7 @@ type Renderer = {
clearSelection: () => void clearSelection: () => void
} }
export namespace Selection { export function copy(renderer: Renderer, toast: Toast): boolean {
export function copy(renderer: Renderer, toast: Toast): boolean {
const text = renderer.getSelection()?.getSelectedText() const text = renderer.getSelection()?.getSelectedText()
if (!text) return false if (!text) return false
@@ -21,5 +20,4 @@ export namespace Selection {
renderer.clearSelection() renderer.clearSelection()
return true return true
}
} }
+28 -30
View File
@@ -43,16 +43,15 @@ function args(kind: Kind, file: string, volume: number) {
return [kind, "-c", `(New-Object Media.SoundPlayer '${file.replace(/'/g, "''")}').PlaySync()`] return [kind, "-c", `(New-Object Media.SoundPlayer '${file.replace(/'/g, "''")}').PlaySync()`]
} }
export namespace Sound { let item: Player | null | undefined
let item: Player | null | undefined let kind: Kind | null | undefined
let kind: Kind | null | undefined let proc: Process.Child | undefined
let proc: Process.Child | undefined let tail: ReturnType<typeof setTimeout> | undefined
let tail: ReturnType<typeof setTimeout> | undefined let cache: Promise<{ hum: string; pulse: string[] }> | undefined
let cache: Promise<{ hum: string; pulse: string[] }> | undefined let seq = 0
let seq = 0 let shot = 0
let shot = 0
function load() { function load() {
if (item !== undefined) return item if (item !== undefined) return item
try { try {
item = new Player({ volume: 0.35 }) item = new Player({ volume: 0.35 })
@@ -60,29 +59,29 @@ export namespace Sound {
item = null item = null
} }
return item return item
} }
async function file(path: string) { async function file(path: string) {
mkdirSync(DIR, { recursive: true }) mkdirSync(DIR, { recursive: true })
const next = join(DIR, basename(path)) const next = join(DIR, basename(path))
const out = Bun.file(next) const out = Bun.file(next)
if (await out.exists()) return next if (await out.exists()) return next
await Bun.write(out, Bun.file(path)) await Bun.write(out, Bun.file(path))
return next return next
} }
function asset() { function asset() {
cache ??= Promise.all([file(HUM), Promise.all(FILE.map(file))]).then(([hum, pulse]) => ({ hum, pulse })) cache ??= Promise.all([file(HUM), Promise.all(FILE.map(file))]).then(([hum, pulse]) => ({ hum, pulse }))
return cache return cache
} }
function pick() { function pick() {
if (kind !== undefined) return kind if (kind !== undefined) return kind
kind = LIST.find((item) => which(item)) ?? null kind = LIST.find((item) => which(item)) ?? null
return kind return kind
} }
function run(file: string, volume: number) { function run(file: string, volume: number) {
const kind = pick() const kind = pick()
if (!kind) return if (!kind) return
return Process.spawn(args(kind, file, volume), { return Process.spawn(args(kind, file, volume), {
@@ -90,21 +89,21 @@ export namespace Sound {
stdout: "ignore", stdout: "ignore",
stderr: "ignore", stderr: "ignore",
}) })
} }
function clear() { function clear() {
if (!tail) return if (!tail) return
clearTimeout(tail) clearTimeout(tail)
tail = undefined tail = undefined
} }
function play(file: string, volume: number) { function play(file: string, volume: number) {
const item = load() const item = load()
if (!item) return run(file, volume)?.exited if (!item) return run(file, volume)?.exited
return item.play(file, { volume }).catch(() => run(file, volume)?.exited) return item.play(file, { volume }).catch(() => run(file, volume)?.exited)
} }
export function start() { export function start() {
stop() stop()
const id = ++seq const id = ++seq
void asset().then(({ hum }) => { void asset().then(({ hum }) => {
@@ -123,9 +122,9 @@ export namespace Sound {
}, },
) )
}) })
} }
export function stop(delay = 0) { export function stop(delay = 0) {
seq++ seq++
clear() clear()
if (!proc) return if (!proc) return
@@ -140,17 +139,16 @@ export namespace Sound {
if (proc === next) proc = undefined if (proc === next) proc = undefined
void Process.stop(next).catch(() => undefined) void Process.stop(next).catch(() => undefined)
}, delay) }, delay)
} }
export function pulse(scale = 1) { export function pulse(scale = 1) {
stop(140) stop(140)
const index = shot++ % FILE.length const index = shot++ % FILE.length
void asset() void asset()
.then(({ pulse }) => play(pulse[index], 0.26 + 0.14 * scale)) .then(({ pulse }) => play(pulse[index], 0.26 + 0.14 * scale))
.catch(() => undefined) .catch(() => undefined)
} }
export function dispose() { export function dispose() {
stop() stop()
}
} }
@@ -1,9 +1,8 @@
import { RGBA } from "@opentui/core" import { RGBA } from "@opentui/core"
export namespace Terminal { export type Colors = Awaited<ReturnType<typeof colors>>
export type Colors = Awaited<ReturnType<typeof colors>>
function parse(color: string): RGBA | null { function parse(color: string): RGBA | null {
if (color.startsWith("rgb:")) { if (color.startsWith("rgb:")) {
const parts = color.substring(4).split("/") const parts = color.substring(4).split("/")
return RGBA.fromInts(parseInt(parts[0], 16) >> 8, parseInt(parts[1], 16) >> 8, parseInt(parts[2], 16) >> 8, 255) return RGBA.fromInts(parseInt(parts[0], 16) >> 8, parseInt(parts[1], 16) >> 8, parseInt(parts[2], 16) >> 8, 255)
@@ -16,15 +15,15 @@ export namespace Terminal {
return RGBA.fromInts(parseInt(parts[0]), parseInt(parts[1]), parseInt(parts[2]), 255) return RGBA.fromInts(parseInt(parts[0]), parseInt(parts[1]), parseInt(parts[2]), 255)
} }
return null return null
} }
function mode(bg: RGBA | null): "dark" | "light" { function mode(bg: RGBA | null): "dark" | "light" {
if (!bg) return "dark" if (!bg) return "dark"
const luminance = (0.299 * bg.r + 0.587 * bg.g + 0.114 * bg.b) / 255 const luminance = (0.299 * bg.r + 0.587 * bg.g + 0.114 * bg.b) / 255
return luminance > 0.5 ? "light" : "dark" return luminance > 0.5 ? "light" : "dark"
} }
/** /**
* Query terminal colors including background, foreground, and palette (0-15). * Query terminal colors including background, foreground, and palette (0-15).
* Uses OSC escape sequences to retrieve actual terminal color values. * Uses OSC escape sequences to retrieve actual terminal color values.
* *
@@ -34,11 +33,11 @@ export namespace Terminal {
* Returns an object with background, foreground, and colors array. * Returns an object with background, foreground, and colors array.
* Any query that fails will be null/empty. * Any query that fails will be null/empty.
*/ */
export async function colors(): Promise<{ export async function colors(): Promise<{
background: RGBA | null background: RGBA | null
foreground: RGBA | null foreground: RGBA | null
colors: RGBA[] colors: RGBA[]
}> { }> {
if (!process.stdin.isTTY) return { background: null, foreground: null, colors: [] } if (!process.stdin.isTTY) return { background: null, foreground: null, colors: [] }
return new Promise((resolve) => { return new Promise((resolve) => {
@@ -100,12 +99,12 @@ export namespace Terminal {
resolve({ background, foreground, colors: paletteColors }) resolve({ background, foreground, colors: paletteColors })
}, 1000) }, 1000)
}) })
} }
// Keep startup mode detection separate from `colors()`: the TUI boot path only // Keep startup mode detection separate from `colors()`: the TUI boot path only
// needs OSC 11 and should resolve on the first background response instead of // needs OSC 11 and should resolve on the first background response instead of
// waiting on the full palette query used by system theme generation. // waiting on the full palette query used by system theme generation.
export async function getTerminalBackgroundColor(): Promise<"dark" | "light"> { export async function getTerminalBackgroundColor(): Promise<"dark" | "light"> {
if (!process.stdin.isTTY) return "dark" if (!process.stdin.isTTY) return "dark"
return new Promise((resolve) => { return new Promise((resolve) => {
@@ -133,5 +132,4 @@ export namespace Terminal {
resolve("dark") resolve("dark")
}, 1000) }, 1000)
}) })
}
} }