refactor(tui): extract standalone package (#31193)

This commit is contained in:
Dax
2026-06-07 05:24:27 +00:00
committed by GitHub
parent 7a2c49e762
commit 106f8e94d6
84 changed files with 1148 additions and 1919 deletions
@@ -8,7 +8,7 @@ import {
type Setter,
} from "solid-js"
import { useSync } from "../../context/sync"
import { useTuiEnvironment } from "../../runtime"
import { useTuiPaths } from "../../context/runtime"
export type HomeSessionDestination = { type: "directory"; directory: string; subdirectory: boolean } | { type: "new" }
@@ -22,10 +22,10 @@ const HomeSessionDestinationContext = createContext<Context>()
export function HomeSessionDestinationProvider(props: ParentProps) {
const sync = useSync()
const environment = useTuiEnvironment()
const paths = useTuiPaths()
const [selected, setDestination] = createSignal<HomeSessionDestination>()
const destination = createMemo<HomeSessionDestination>(
() => selected() ?? { type: "directory", directory: sync.path.directory || environment.cwd, subdirectory: false },
() => selected() ?? { type: "directory", directory: sync.path.directory || paths.cwd, subdirectory: false },
)
return (
<HomeSessionDestinationContext.Provider
@@ -3,7 +3,7 @@ import { useSync } from "../../context/sync"
import { DialogSelect } from "../../ui/dialog-select"
import { useSDK } from "../../context/sdk"
import { useRoute } from "../../context/route"
import { useTuiPlatform } from "../../platform"
import { useClipboard } from "../../context/clipboard"
import type { PromptInfo } from "../../component/prompt/history"
import { stripPromptPartIDs as strip } from "../../prompt/part"
@@ -16,7 +16,7 @@ export function DialogMessage(props: {
const sdk = useSDK()
const message = createMemo(() => sync.data.message[props.sessionID]?.find((x) => x.id === props.messageID))
const route = useRoute()
const platform = useTuiPlatform()
const clipboard = useClipboard()
return (
<DialogSelect
@@ -69,7 +69,7 @@ export function DialogMessage(props: {
return agg
}, "")
await platform.clipboard?.write?.(text)
await clipboard.write?.(text)
dialog.clear()
},
},
+37 -27
View File
@@ -7,6 +7,7 @@ import {
For,
Match,
on,
onCleanup,
onMount,
Show,
Switch,
@@ -15,12 +16,13 @@ import {
} from "solid-js"
import { Dynamic } from "solid-js/web"
import path from "node:path"
import { mkdir, writeFile } from "node:fs/promises"
import { useRoute, useRouteData } from "../../context/route"
import { useProject } from "../../context/project"
import { useSync } from "../../context/sync"
import { useEvent } from "../../context/event"
import { SplitBorder } from "../../ui/border"
import { useTuiEnvironment } from "../../runtime"
import { useTuiPaths, useTuiTerminalEnvironment } from "../../context/runtime"
import { Spinner } from "../../component/spinner"
import { createSyntaxStyleMemo, generateSubtleSyntax, selectedForeground, useTheme } from "../../context/theme"
import { BoxRenderable, ScrollBoxRenderable, addDefaultParsers, TextAttributes, RGBA } from "@opentui/core"
@@ -41,6 +43,7 @@ import { webSearchProviderLabel } from "../../util/tool-display"
import { useRenderer, useTerminalDimensions, type JSX } from "@opentui/solid"
import { useSDK } from "../../context/sdk"
import { useEditorContext } from "../../context/editor"
import { openEditor } from "../../editor"
import { useDialog } from "../../ui/dialog"
import { DialogAlert } from "../../ui/dialog-alert"
import { TodoItem } from "../../component/todo-item"
@@ -59,17 +62,17 @@ import { Toast, useToast } from "../../ui/toast"
import { useKV } from "../../context/kv.tsx"
import stripAnsi from "strip-ansi"
import { usePromptRef } from "../../context/prompt"
import { useExit } from "../../context/exit"
import { useEpilogue } from "../../context/epilogue"
import { normalizePath } from "../../util/path"
import { PermissionPrompt } from "./permission"
import { QuestionPrompt } from "./question"
import { DialogExportOptions } from "../../ui/dialog-export-options"
import * as Model from "../../util/model"
import { formatTranscript } from "../../util/transcript"
import { sessionEpilogue } from "../../util/presentation"
import { setPreLayoutSiblingMargin } from "../../util/layout"
import { sessionExitSummary } from "../../util/presentation"
import { useTuiConfig } from "../../config"
import { useTuiPlatform } from "../../platform"
import { useClipboard } from "../../context/clipboard"
import { nextThinkingMode, reasoningSummary, useThinkingMode, type ThinkingMode } from "../../context/thinking"
import { getScrollAcceleration } from "../../util/scroll"
import { collapseToolOutput } from "../../util/collapse-tool-output"
@@ -171,19 +174,30 @@ function use() {
}
export function Session() {
const platform = useTuiPlatform()
const setEpilogue = useEpilogue()
const clipboard = useClipboard()
const writeExport = async (file: string, content: string) => {
await mkdir(path.dirname(file), { recursive: true })
await writeFile(file, content)
}
const pluginRuntime = usePluginRuntime()
const route = useRouteData("session")
const { navigate } = useRoute()
const sync = useSync()
const event = useEvent()
const project = useProject()
const environment = useTuiEnvironment()
const paths = useTuiPaths()
const tuiConfig = useTuiConfig()
const kv = useKV()
const { theme } = useTheme()
const promptRef = usePromptRef()
const session = createMemo(() => sync.session.get(route.sessionID))
createEffect(() => {
const title = Locale.truncate(session()?.title ?? "", 50)
setEpilogue(sessionEpilogue({ title, sessionID: session()?.id }))
})
onCleanup(() => setEpilogue())
const children = createMemo(() => {
const parentID = session()?.parentID ?? session()?.id
return sync.data.session
@@ -353,13 +367,6 @@ export function Session() {
})
})
const exit = useExit()
createEffect(() => {
const title = Locale.truncate(session()?.title ?? "", 50)
return exit.message.set(sessionExitSummary({ title, sessionID: session()?.id }))
})
// Helper: Find next visible message boundary in direction
const findNextVisibleMessage = (direction: "next" | "prev"): string | null => {
const children = scroll.getChildren()
@@ -460,8 +467,7 @@ export function Session() {
},
run: async () => {
const copy = (url: string) =>
platform.clipboard
?.write?.(url)
clipboard.write?.(url)
.then(() => toast.show({ message: "Share URL copied to clipboard!", variant: "success" }))
.catch(() => toast.show({ message: "Failed to copy URL to clipboard", variant: "error" }))
const url = session()?.share?.url
@@ -896,8 +902,7 @@ export function Session() {
return
}
platform.clipboard
?.write?.(text)
clipboard.write?.(text)
.then(() => toast.show({ message: "Message copied to clipboard!", variant: "success" }))
.catch(() => toast.show({ message: "Failed to copy to clipboard", variant: "error" }))
dialog.clear()
@@ -925,7 +930,7 @@ export function Session() {
providers: sync.data.provider,
},
)
await platform.clipboard?.write?.(transcript)
await clipboard.write?.(transcript)
toast.show({ message: "Session transcript copied to clipboard!", variant: "success" })
} catch {
toast.show({ message: "Failed to copy session transcript", variant: "error" })
@@ -972,30 +977,32 @@ export function Session() {
if (options.openWithoutSaving) {
// Just open in editor without saving
await platform.editor?.open({
await openEditor({
renderer,
value: transcript,
cwd:
(project.instance.path().worktree === "/" ? undefined : project.instance.path().worktree) ||
project.instance.directory() ||
environment.cwd,
paths.cwd,
})
} else {
const exportDir = environment.cwd
const exportDir = paths.cwd
const filename = options.filename.trim()
const filepath = path.join(exportDir, filename)
await platform.export?.write(filepath, transcript)
await writeExport(filepath, transcript)
// Open with EDITOR if available
const result = await platform.editor?.open({
const result = await openEditor({
renderer,
value: transcript,
cwd:
(project.instance.path().worktree === "/" ? undefined : project.instance.path().worktree) ||
project.instance.directory() ||
environment.cwd,
paths.cwd,
})
if (result !== undefined) {
await platform.export?.write(filepath, result)
await writeExport(filepath, result)
}
toast.show({ message: `Session exported to ${filename}`, variant: "success" })
@@ -2510,9 +2517,12 @@ function Skill(props: ToolProps) {
function Diagnostics(props: { diagnostics: unknown; filePath: string }) {
const { theme } = useTheme()
const environment = useTuiEnvironment()
const terminalEnvironment = useTuiTerminalEnvironment()
const errors = createMemo(() => {
const normalized = normalizePath(typeof props.filePath === "string" ? props.filePath : "", environment.platform)
const normalized = normalizePath(
typeof props.filePath === "string" ? props.filePath : "",
terminalEnvironment.platform,
)
return parseDiagnostics(props.diagnostics, normalized)
})
+3 -4
View File
@@ -3,7 +3,7 @@ import { useSync } from "../../context/sync"
import { createMemo, Show } from "solid-js"
import { useTheme } from "../../context/theme"
import { useTuiConfig } from "../../config"
import { useTuiBuildInfo } from "../../runtime"
import { InstallationChannel, InstallationVersion } from "@opencode-ai/core/installation/version"
import { usePluginRuntime } from "../../plugin/runtime"
import { getScrollAcceleration } from "../../util/scroll"
@@ -15,7 +15,6 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
const sync = useSync()
const { theme } = useTheme()
const tuiConfig = useTuiConfig()
const build = useTuiBuildInfo()
const session = createMemo(() => sync.session.get(props.sessionID))
const workspace = () => {
const workspaceID = session()?.workspaceID
@@ -58,7 +57,7 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
<text fg={theme.text}>
<b>{session()!.title}</b>
</text>
<Show when={build.channel !== "latest"}>
<Show when={InstallationChannel !== "latest"}>
<text fg={theme.textMuted}>{props.sessionID}</text>
</Show>
<Show when={session()!.workspaceID}>
@@ -94,7 +93,7 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
<span style={{ fg: theme.text }}>
<b>Code</b>
</span>{" "}
<span>{build.version}</span>
<span>{InstallationVersion}</span>
</text>
</pluginRuntime.Slot>
</box>