refactor(tui): extract standalone package (#31193)
This commit is contained in:
parent
7a2c49e762
commit
106f8e94d6
84 changed files with 1147 additions and 1918 deletions
|
|
@ -7,7 +7,8 @@ import { useDialog } from "../ui/dialog"
|
|||
import { useSDK } from "../context/sdk"
|
||||
import { useTheme } from "../context/theme"
|
||||
import { useSync } from "../context/sync"
|
||||
import { abbreviateHome, useTuiEnvironment } from "../runtime"
|
||||
import { abbreviateHome } from "../runtime"
|
||||
import { useTuiPaths } from "../context/runtime"
|
||||
import { Locale } from "../util/locale"
|
||||
import { errorMessage } from "../util/error"
|
||||
import { useToast } from "../ui/toast"
|
||||
|
|
@ -32,7 +33,7 @@ export function DialogMoveSession(props: {
|
|||
const sync = useSync()
|
||||
const projectContext = useProject()
|
||||
const toast = useToast()
|
||||
const environment = useTuiEnvironment()
|
||||
const paths = useTuiPaths()
|
||||
const [working, setWorking] = createSignal(Boolean(props.initialRemoving))
|
||||
const [toDelete, setToDelete] = createSignal<string>()
|
||||
const [removing, setRemoving] = createSignal(props.initialRemoving)
|
||||
|
|
@ -101,7 +102,7 @@ export function DialogMoveSession(props: {
|
|||
})
|
||||
const titleWidth = Math.max(1, Math.min(116, dimensions().width - 2) - 12)
|
||||
return list.map((item) => {
|
||||
const title = abbreviateHome(item.location, environment.paths.home)
|
||||
const title = abbreviateHome(item.location, paths.home)
|
||||
const suffix = item.location === item.root ? undefined : path.sep + path.relative(item.root, item.location)
|
||||
const visible = Locale.truncateLeft(title, titleWidth)
|
||||
const split = suffix ? Math.max(0, visible.length - suffix.length) : visible.length
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import { useToast } from "../ui/toast"
|
|||
import { isConsoleManagedProvider } from "../util/provider-origin"
|
||||
import { useConnected } from "./use-connected"
|
||||
import { useBindings } from "../keymap"
|
||||
import { useTuiPlatform } from "../platform"
|
||||
import { useClipboard } from "../context/clipboard"
|
||||
|
||||
const PROVIDER_PRIORITY: Record<string, number> = {
|
||||
opencode: 0,
|
||||
|
|
@ -242,7 +242,7 @@ function AutoMethod(props: AutoMethodProps) {
|
|||
const dialog = useDialog()
|
||||
const sync = useSync()
|
||||
const toast = useToast()
|
||||
const platform = useTuiPlatform()
|
||||
const clipboard = useClipboard()
|
||||
|
||||
useBindings(() => ({
|
||||
bindings: [
|
||||
|
|
@ -253,8 +253,8 @@ function AutoMethod(props: AutoMethodProps) {
|
|||
cmd: () => {
|
||||
const code =
|
||||
props.authorization.instructions.match(/[A-Z0-9]{4}-[A-Z0-9]{4,5}/)?.[0] ?? props.authorization.url
|
||||
platform.clipboard
|
||||
?.write?.(code)
|
||||
clipboard
|
||||
.write?.(code)
|
||||
.then(() => toast.show({ message: "Copied to clipboard", variant: "info" }))
|
||||
.catch(toast.error)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { useProject } from "../context/project"
|
|||
import { useTheme } from "../context/theme"
|
||||
import { useSDK } from "../context/sdk"
|
||||
import { useLocal } from "../context/local"
|
||||
import { useTuiEnvironment } from "../runtime"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { DialogSessionRename } from "./dialog-session-rename"
|
||||
import { createDebouncedSignal } from "../util/signal"
|
||||
import { useToast } from "../ui/toast"
|
||||
|
|
@ -27,7 +27,6 @@ export function DialogSessionList() {
|
|||
const { theme } = useTheme()
|
||||
const sdk = useSDK()
|
||||
const local = useLocal()
|
||||
const environment = useTuiEnvironment()
|
||||
const toast = useToast()
|
||||
const [toDelete, setToDelete] = createSignal<string>()
|
||||
const [search, setSearch] = createDebouncedSignal("", 150)
|
||||
|
|
@ -174,7 +173,7 @@ export function DialogSessionList() {
|
|||
const workspace = x.workspaceID ? project.workspace.get(x.workspaceID) : undefined
|
||||
|
||||
let footer: JSX.Element | string = ""
|
||||
if (environment.capabilities.workspaces) {
|
||||
if (Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) {
|
||||
if (x.workspaceID) {
|
||||
footer = workspace ? (
|
||||
<WorkspaceLabel
|
||||
|
|
|
|||
|
|
@ -1,22 +1,23 @@
|
|||
import { TextAttributes } from "@opentui/core"
|
||||
import { useKeyboard, useTerminalDimensions } from "@opentui/solid"
|
||||
import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid"
|
||||
import { createSignal } from "solid-js"
|
||||
import { getScrollAcceleration } from "../util/scroll"
|
||||
import { useTuiPlatform } from "../platform"
|
||||
import { useClipboard } from "../context/clipboard"
|
||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||
import { destroyRenderer } from "../util/renderer"
|
||||
|
||||
export function ErrorComponent(props: {
|
||||
error: Error
|
||||
reset: () => void
|
||||
exit: () => Promise<void>
|
||||
version: string
|
||||
mode?: "dark" | "light"
|
||||
}) {
|
||||
const term = useTerminalDimensions()
|
||||
const platform = useTuiPlatform()
|
||||
const renderer = useRenderer()
|
||||
const clipboard = useClipboard()
|
||||
|
||||
useKeyboard((evt) => {
|
||||
if (evt.ctrl && evt.name === "c") {
|
||||
void props.exit()
|
||||
destroyRenderer(renderer)
|
||||
}
|
||||
})
|
||||
const [copied, setCopied] = createSignal(false)
|
||||
|
|
@ -43,10 +44,10 @@ export function ErrorComponent(props: {
|
|||
)
|
||||
}
|
||||
|
||||
issueURL.searchParams.set("opencode-version", props.version)
|
||||
issueURL.searchParams.set("opencode-version", InstallationVersion)
|
||||
|
||||
const copyIssueURL = () => {
|
||||
void platform.clipboard?.write?.(issueURL.toString()).then(() => {
|
||||
void clipboard.write?.(issueURL.toString()).then(() => {
|
||||
setCopied(true)
|
||||
})
|
||||
}
|
||||
|
|
@ -69,7 +70,7 @@ export function ErrorComponent(props: {
|
|||
<box onMouseUp={props.reset} backgroundColor={colors.primary} padding={1}>
|
||||
<text fg={colors.bg}>Reset TUI</text>
|
||||
</box>
|
||||
<box onMouseUp={() => void props.exit()} backgroundColor={colors.primary} padding={1}>
|
||||
<box onMouseUp={() => destroyRenderer(renderer)} backgroundColor={colors.primary} padding={1}>
|
||||
<text fg={colors.bg}>Exit</text>
|
||||
</box>
|
||||
</box>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { useProject } from "../../context/project"
|
|||
import { useSDK } from "../../context/sdk"
|
||||
import { useSync } from "../../context/sync"
|
||||
import { getScrollAcceleration } from "../../util/scroll"
|
||||
import { useTuiEnvironment } from "../../runtime"
|
||||
import { useTuiPaths } from "../../context/runtime"
|
||||
import { useTuiConfig } from "../../config"
|
||||
import { useTheme, selectedForeground } from "../../context/theme"
|
||||
import { SplitBorder } from "../../ui/border"
|
||||
|
|
@ -92,7 +92,7 @@ export function Autocomplete(props: {
|
|||
const dimensions = useTerminalDimensions()
|
||||
const frecency = useFrecency()
|
||||
const tuiConfig = useTuiConfig()
|
||||
const environment = useTuiEnvironment()
|
||||
const paths = useTuiPaths()
|
||||
const [store, setStore] = createStore({
|
||||
index: 0,
|
||||
selected: 0,
|
||||
|
|
@ -236,7 +236,7 @@ export function Autocomplete(props: {
|
|||
}
|
||||
|
||||
function createFilePart(item: string, lineRange?: { startLine: number; endLine?: number }) {
|
||||
const baseDir = (sync.path.directory || environment.cwd).replace(/\/+$/, "")
|
||||
const baseDir = (sync.path.directory || paths.cwd).replace(/\/+$/, "")
|
||||
const fullPath = path.isAbsolute(item) ? item : path.join(baseDir, item)
|
||||
const urlObj = pathToFileURL(fullPath)
|
||||
const filename =
|
||||
|
|
@ -306,7 +306,7 @@ export function Autocomplete(props: {
|
|||
})
|
||||
|
||||
function normalizeMentionPath(filePath: string) {
|
||||
const baseDir = sync.path.directory || environment.cwd
|
||||
const baseDir = sync.path.directory || paths.cwd
|
||||
const absolute = path.resolve(filePath)
|
||||
const relative = path.relative(baseDir, absolute)
|
||||
|
||||
|
|
|
|||
|
|
@ -14,10 +14,11 @@ import "opentui-spinner/solid"
|
|||
import path from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
import { useLocal } from "../../context/local"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { tint, useTheme } from "../../context/theme"
|
||||
import { EmptyBorder, SplitBorder } from "../../ui/border"
|
||||
import { useTuiEnvironment } from "../../runtime"
|
||||
import { useTuiPlatform } from "../../platform"
|
||||
import { useTuiPaths, useTuiTerminalEnvironment } from "../../context/runtime"
|
||||
import { useClipboard } from "../../context/clipboard"
|
||||
import { Spinner } from "../spinner"
|
||||
import { useSDK } from "../../context/sdk"
|
||||
import { useRoute } from "../../context/route"
|
||||
|
|
@ -25,6 +26,8 @@ import { useProject } from "../../context/project"
|
|||
import { useSync } from "../../context/sync"
|
||||
import { useEvent } from "../../context/event"
|
||||
import { editorSelectionKey, useEditorContext, type EditorSelection } from "../../context/editor"
|
||||
import { openEditor } from "../../editor"
|
||||
import { destroyRenderer } from "../../util/renderer"
|
||||
import { promptOffsetWidth } from "../../prompt/display"
|
||||
import { createStore, produce, unwrap } from "solid-js/store"
|
||||
import { usePromptHistory, type PromptInfo } from "../../prompt/history"
|
||||
|
|
@ -34,7 +37,6 @@ import { usePromptStash } from "../../prompt/stash"
|
|||
import { DialogStash } from "../dialog-stash"
|
||||
import { type AutocompleteRef, Autocomplete } from "./autocomplete"
|
||||
import { useRenderer, useTerminalDimensions, type JSX } from "@opentui/solid"
|
||||
import { useExit } from "../../context/exit"
|
||||
import type { AssistantMessage, FilePart, UserMessage } from "@opencode-ai/sdk/v2"
|
||||
import { Locale } from "../../util/locale"
|
||||
import { errorMessage } from "../../util/error"
|
||||
|
|
@ -143,8 +145,9 @@ export function Prompt(props: PromptProps) {
|
|||
const leader = useLeaderActive()
|
||||
const local = useLocal()
|
||||
const args = useArgs()
|
||||
const environment = useTuiEnvironment()
|
||||
const platform = useTuiPlatform()
|
||||
const paths = useTuiPaths()
|
||||
const terminalEnvironment = useTuiTerminalEnvironment()
|
||||
const clipboard = useClipboard()
|
||||
const sdk = useSDK()
|
||||
const editor = useEditorContext()
|
||||
const route = useRoute()
|
||||
|
|
@ -366,7 +369,7 @@ export function Prompt(props: PromptProps) {
|
|||
run: async (ctx: CommandContext<Renderable, KeyEvent>) => {
|
||||
ctx.event.preventDefault()
|
||||
ctx.event.stopPropagation()
|
||||
const content = await platform.clipboard?.read?.()
|
||||
const content = await clipboard.read?.()
|
||||
if (content?.mime.startsWith("image/")) {
|
||||
await pasteAttachment({
|
||||
filename: "clipboard",
|
||||
|
|
@ -430,12 +433,13 @@ export function Prompt(props: PromptProps) {
|
|||
const nonTextParts = store.prompt.parts.filter((p) => p.type !== "text")
|
||||
|
||||
const value = text
|
||||
const content = await platform.editor?.open({
|
||||
const content = await openEditor({
|
||||
renderer,
|
||||
value,
|
||||
cwd:
|
||||
(project.instance.path().worktree === "/" ? undefined : project.instance.path().worktree) ||
|
||||
project.instance.directory() ||
|
||||
environment.cwd,
|
||||
paths.cwd,
|
||||
})
|
||||
if (!content) return
|
||||
|
||||
|
|
@ -526,7 +530,7 @@ export function Prompt(props: PromptProps) {
|
|||
desc: "Change the workspace for the session",
|
||||
name: "workspace.set",
|
||||
category: "Session",
|
||||
enabled: environment.capabilities.workspaces,
|
||||
enabled: Flag.OPENCODE_EXPERIMENTAL_WORKSPACES,
|
||||
slashName: "warp",
|
||||
run: () => {
|
||||
workspace.open()
|
||||
|
|
@ -950,7 +954,7 @@ export function Prompt(props: PromptProps) {
|
|||
if (!agent) return false
|
||||
const trimmed = store.prompt.input.trim()
|
||||
if (trimmed === "exit" || trimmed === "quit" || trimmed === ":q") {
|
||||
void exit()
|
||||
destroyRenderer(renderer)
|
||||
return true
|
||||
}
|
||||
const selectedModel = local.model.current()
|
||||
|
|
@ -1124,7 +1128,6 @@ export function Prompt(props: PromptProps) {
|
|||
if (finishMoveProgress) move.finishSubmit()
|
||||
return true
|
||||
}
|
||||
const exit = useExit()
|
||||
|
||||
function pasteText(text: string, virtualText: string) {
|
||||
const currentOffset = input.cursorOffset
|
||||
|
|
@ -1163,10 +1166,10 @@ export function Prompt(props: PromptProps) {
|
|||
async function pasteInputText(text: string) {
|
||||
const normalizedText = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n")
|
||||
const pastedContent = normalizedText.trim()
|
||||
const filepath = pastedFilepath(pastedContent, environment.platform)
|
||||
const filepath = pastedFilepath(pastedContent, terminalEnvironment.platform)
|
||||
const isUrl = /^(https?):\/\//.test(filepath)
|
||||
if (!isUrl) {
|
||||
const attachment = await readLocalAttachment(platform.files, filepath)
|
||||
const attachment = await readLocalAttachment(filepath)
|
||||
const filename = path.basename(filepath)
|
||||
if (attachment?.type === "text") {
|
||||
pasteText(attachment.content, `[SVG: ${filename ?? "image"}]`)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,39 @@
|
|||
import type { PlatformFiles } from "../../platform"
|
||||
import { readFile } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
|
||||
export type LocalFiles = Readonly<{
|
||||
readText(path: string): Promise<string>
|
||||
readBytes(path: string): Promise<Uint8Array>
|
||||
mime(path: string): Promise<string>
|
||||
}>
|
||||
|
||||
export type LocalAttachment =
|
||||
| Readonly<{ type: "text"; mime: "image/svg+xml"; content: string }>
|
||||
| Readonly<{ type: "binary"; mime: string; content: Uint8Array }>
|
||||
|
||||
export async function readLocalAttachment(files: PlatformFiles, path: string): Promise<LocalAttachment | undefined> {
|
||||
export function readLocalAttachment(file: string) {
|
||||
return readLocalAttachmentWith(
|
||||
{
|
||||
readText: (value) => readFile(value, "utf8"),
|
||||
readBytes: (value) => readFile(value),
|
||||
mime: async (value) => mimeTypes[path.extname(value).toLowerCase()] ?? "application/octet-stream",
|
||||
},
|
||||
file,
|
||||
)
|
||||
}
|
||||
|
||||
const mimeTypes: Record<string, string> = {
|
||||
".avif": "image/avif",
|
||||
".gif": "image/gif",
|
||||
".jpeg": "image/jpeg",
|
||||
".jpg": "image/jpeg",
|
||||
".pdf": "application/pdf",
|
||||
".png": "image/png",
|
||||
".svg": "image/svg+xml",
|
||||
".webp": "image/webp",
|
||||
}
|
||||
|
||||
export async function readLocalAttachmentWith(files: LocalFiles, path: string): Promise<LocalAttachment | undefined> {
|
||||
const mime = await files.mime(path).catch(() => undefined)
|
||||
if (!mime) return
|
||||
if (mime === "image/svg+xml") {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { createEffect, createMemo, createSignal, onCleanup } from "solid-js"
|
||||
import path from "path"
|
||||
import { useTuiEnvironment } from "../../runtime"
|
||||
import { useTuiPaths } from "../../context/runtime"
|
||||
import { errorMessage } from "../../util/error"
|
||||
import { useDialog } from "../../ui/dialog"
|
||||
import { useSDK } from "../../context/sdk"
|
||||
|
|
@ -20,7 +20,7 @@ export function usePromptMove(input: { projectID: () => string | undefined; sess
|
|||
const sync = useSync()
|
||||
const toast = useToast()
|
||||
const homeDestination = useHomeSessionDestination()
|
||||
const environment = useTuiEnvironment()
|
||||
const paths = useTuiPaths()
|
||||
const [creating, setCreating] = createSignal(false)
|
||||
const [creatingDots, setCreatingDots] = createSignal(3)
|
||||
const [progress, setProgress] = createSignal<string>()
|
||||
|
|
@ -35,7 +35,7 @@ export function usePromptMove(input: { projectID: () => string | undefined; sess
|
|||
{
|
||||
projectID,
|
||||
strategy: "git_worktree",
|
||||
directory: path.join(environment.paths.worktree, projectID.slice(0, 6)),
|
||||
directory: path.join(paths.worktree, projectID.slice(0, 6)),
|
||||
context,
|
||||
},
|
||||
{ throwOnError: true },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue