refactor(tui): extract standalone package (#31193)
This commit is contained in:
parent
7a2c49e762
commit
106f8e94d6
84 changed files with 1147 additions and 1918 deletions
18
packages/tui/src/context/clipboard.tsx
Normal file
18
packages/tui/src/context/clipboard.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { createContext, type JSX, useContext } from "solid-js"
|
||||
import { read, write } from "../clipboard"
|
||||
|
||||
export type ClipboardContent = Readonly<{ data: string; mime: string }>
|
||||
export type ClipboardService = Readonly<{
|
||||
read?(): Promise<ClipboardContent | undefined>
|
||||
write?(text: string): Promise<void>
|
||||
}>
|
||||
const clipboard = { read, write }
|
||||
const ClipboardContext = createContext<ClipboardService>(clipboard)
|
||||
|
||||
export function ClipboardProvider(props: { value?: ClipboardService; children: JSX.Element }) {
|
||||
return <ClipboardContext.Provider value={props.value ?? clipboard}>{props.children}</ClipboardContext.Provider>
|
||||
}
|
||||
|
||||
export function useClipboard() {
|
||||
return useContext(ClipboardContext)
|
||||
}
|
||||
|
|
@ -1,15 +1,16 @@
|
|||
import { createMemo } from "solid-js"
|
||||
import { useProject } from "./project"
|
||||
import { useSync } from "./sync"
|
||||
import { abbreviateHome, useTuiEnvironment } from "../runtime"
|
||||
import { abbreviateHome } from "../runtime"
|
||||
import { useTuiPaths } from "./runtime"
|
||||
|
||||
export function useDirectory() {
|
||||
const project = useProject()
|
||||
const sync = useSync()
|
||||
const environment = useTuiEnvironment()
|
||||
const paths = useTuiPaths()
|
||||
return createMemo(() => {
|
||||
const directory = project.instance.path().directory || environment.cwd
|
||||
const result = abbreviateHome(directory, environment.paths.home)
|
||||
const directory = project.instance.path().directory || paths.cwd
|
||||
const result = abbreviateHome(directory, paths.home)
|
||||
if (sync.data.vcs?.branch) return result + ":" + sync.data.vcs.branch
|
||||
return result
|
||||
})
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ import { onCleanup, onMount } from "solid-js"
|
|||
import { createStore } from "solid-js/store"
|
||||
import { Option, Schema, SchemaGetter } from "effect"
|
||||
import { isRecord } from "../util/record"
|
||||
import { useTuiEnvironment } from "../runtime"
|
||||
import { useOptionalTuiPlatform } from "../platform"
|
||||
import { useTuiPaths } from "./runtime"
|
||||
import { createSimpleContext } from "./helper"
|
||||
import { editorIntegration } from "../editor"
|
||||
|
||||
const MCP_PROTOCOL_VERSION = "2025-11-25"
|
||||
|
||||
|
|
@ -104,11 +104,20 @@ type EditorConnection = {
|
|||
source: string
|
||||
}
|
||||
|
||||
export type EditorIntegration = Readonly<{
|
||||
connection?(directory: string): EditorConnection | undefined
|
||||
selection?(directory: string): Promise<unknown>
|
||||
}>
|
||||
|
||||
export const { use: useEditorContext, provider: EditorContextProvider } = createSimpleContext({
|
||||
name: "EditorContext",
|
||||
init: (props: { WebSocketImpl?: typeof WebSocket }) => {
|
||||
const environment = useTuiEnvironment()
|
||||
const platform = useOptionalTuiPlatform()
|
||||
init: (props: { integration?: EditorIntegration; WebSocketImpl?: typeof WebSocket }) => {
|
||||
const paths = useTuiPaths()
|
||||
const editor = props.integration ?? editorIntegration
|
||||
const value = process.env.CLAUDE_CODE_SSE_PORT || process.env.OPENCODE_EDITOR_SSE_PORT
|
||||
const parsedPort = value ? Number.parseInt(value, 10) : undefined
|
||||
const port = parsedPort && Number.isInteger(parsedPort) && parsedPort > 0 && parsedPort <= 65535 ? parsedPort : undefined
|
||||
const zedTerminal = process.env.ZED_TERM === "true" || process.env.TERM_PROGRAM?.toLowerCase() === "zed"
|
||||
const mentionListeners = new Set<(mention: EditorMention) => void>()
|
||||
const WebSocketImpl = props.WebSocketImpl ?? WebSocket
|
||||
const [store, setStore] = createStore<{
|
||||
|
|
@ -130,7 +139,7 @@ export const { use: useEditorContext, provider: EditorContextProvider } = create
|
|||
let requestID = 0
|
||||
let zedSelection: Promise<void> | undefined
|
||||
let lastZedSelectionKey: string | undefined
|
||||
let directory = environment.cwd
|
||||
let directory = paths.cwd
|
||||
let preserveSelectionOnReconnect = false
|
||||
const pending = new Map<number, string>()
|
||||
|
||||
|
|
@ -163,20 +172,20 @@ export const { use: useEditorContext, provider: EditorContextProvider } = create
|
|||
const connect = () => {
|
||||
if (closed) return
|
||||
|
||||
const connection = resolveEditorConnection(directory, environment.editor.port, platform?.editor?.connection)
|
||||
const connection = resolveEditorConnection(directory, port, editor.connection)
|
||||
if (!connection) {
|
||||
if (!environment.editor.zedTerminal) {
|
||||
if (!zedTerminal) {
|
||||
setStore("status", "disabled")
|
||||
scheduleReconnect()
|
||||
return
|
||||
}
|
||||
if (!editor.selection) {
|
||||
setStore("status", "disabled")
|
||||
scheduleReconnect()
|
||||
return
|
||||
}
|
||||
|
||||
if (!platform?.editor?.selection) {
|
||||
setStore("status", "disabled")
|
||||
scheduleReconnect()
|
||||
return
|
||||
}
|
||||
zedSelection ??= platform.editor
|
||||
zedSelection ??= editor
|
||||
.selection(directory)
|
||||
.then((result) => {
|
||||
if (closed || socket) return
|
||||
|
|
@ -278,7 +287,7 @@ export const { use: useEditorContext, provider: EditorContextProvider } = create
|
|||
}
|
||||
|
||||
const reconnectWithDirectory = (nextDirectory?: string) => {
|
||||
const resolved = nextDirectory || environment.cwd
|
||||
const resolved = nextDirectory || paths.cwd
|
||||
const sameDirectory = directory === resolved
|
||||
clearSelectionForReconnect({ resetZedSelectionKey: !sameDirectory })
|
||||
if (sameDirectory) return
|
||||
|
|
@ -311,8 +320,7 @@ export const { use: useEditorContext, provider: EditorContextProvider } = create
|
|||
return {
|
||||
enabled() {
|
||||
return Boolean(
|
||||
resolveEditorConnection(directory, environment.editor.port, platform?.editor?.connection) ||
|
||||
(environment.editor.zedTerminal && platform?.editor?.selection),
|
||||
resolveEditorConnection(directory, port, editor.connection) || (zedTerminal && editor.selection),
|
||||
)
|
||||
},
|
||||
connected() {
|
||||
|
|
|
|||
6
packages/tui/src/context/epilogue.tsx
Normal file
6
packages/tui/src/context/epilogue.tsx
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { createSimpleContext } from "./helper"
|
||||
|
||||
export const { use: useEpilogue, provider: EpilogueProvider } = createSimpleContext({
|
||||
name: "Epilogue",
|
||||
init: (props: { set(value?: string): void }) => props.set,
|
||||
})
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
import { createSimpleContext } from "./helper"
|
||||
|
||||
export type Exit = ((reason?: unknown) => Promise<void>) & {
|
||||
message: {
|
||||
set: (value?: string) => () => void
|
||||
clear: () => void
|
||||
get: () => string | undefined
|
||||
}
|
||||
}
|
||||
|
||||
export function createExit(run: (reason: unknown | undefined, message: () => string | undefined) => Promise<void>) {
|
||||
let message: string | undefined
|
||||
let task: Promise<void> | undefined
|
||||
const store = {
|
||||
set: (value?: string) => {
|
||||
const prev = message
|
||||
message = value
|
||||
return () => {
|
||||
message = prev
|
||||
}
|
||||
},
|
||||
clear: () => {
|
||||
message = undefined
|
||||
},
|
||||
get: () => message,
|
||||
}
|
||||
|
||||
return Object.assign(
|
||||
(reason?: unknown) => {
|
||||
task ??= run(reason, store.get)
|
||||
return task
|
||||
},
|
||||
{
|
||||
message: store,
|
||||
},
|
||||
) satisfies Exit
|
||||
}
|
||||
|
||||
export const { use: useExit, provider: ExitProvider } = createSimpleContext({
|
||||
name: "Exit",
|
||||
init: (input: { exit: Exit }) => input.exit,
|
||||
})
|
||||
|
|
@ -1,18 +1,25 @@
|
|||
import { createSignal, type Setter } from "solid-js"
|
||||
import { createStore, unwrap } from "solid-js/store"
|
||||
import { createSimpleContext } from "./helper"
|
||||
import { useOptionalTuiPlatform } from "../platform"
|
||||
import { Flock } from "@opencode-ai/core/util/flock"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { readJson, writeJsonAtomic } from "../util/persistence"
|
||||
import { useTuiPaths } from "./runtime"
|
||||
import path from "path"
|
||||
|
||||
export const { use: useKV, provider: KVProvider } = createSimpleContext({
|
||||
name: "KV",
|
||||
init: () => {
|
||||
const platform = useOptionalTuiPlatform()
|
||||
const paths = useTuiPaths()
|
||||
void Global.Path.state
|
||||
const file = path.join(paths.state, "kv.json")
|
||||
const lock = `tui-kv:${file}`
|
||||
const [ready, setReady] = createSignal(false)
|
||||
const [store, setStore] = createStore<Record<string, any>>()
|
||||
// Queue same-process writes so rapid updates persist in order.
|
||||
let write = Promise.resolve()
|
||||
|
||||
;(platform?.state?.read() ?? Promise.resolve({}))
|
||||
;Flock.withLock(lock, () => readJson<Record<string, unknown>>(file))
|
||||
.then((x) => {
|
||||
setStore(x)
|
||||
})
|
||||
|
|
@ -48,7 +55,9 @@ export const { use: useKV, provider: KVProvider } = createSimpleContext({
|
|||
setStore(key, value)
|
||||
const snapshot = structuredClone(unwrap(store))
|
||||
write = write
|
||||
.then(() => platform?.state?.write(snapshot))
|
||||
.then(() =>
|
||||
Flock.withLock(lock, () => writeJsonAtomic(file, snapshot)),
|
||||
)
|
||||
.catch((error) => {
|
||||
console.error("Failed to write KV state", { error })
|
||||
})
|
||||
|
|
|
|||
|
|
@ -4,11 +4,14 @@ import { batch, createEffect, createMemo } from "solid-js"
|
|||
import { useSync } from "./sync"
|
||||
import { useEvent } from "./event"
|
||||
import path from "path"
|
||||
import { useTuiEnvironment } from "../runtime"
|
||||
import { useTuiPaths } from "./runtime"
|
||||
import { useArgs } from "./args"
|
||||
import { useSDK } from "./sdk"
|
||||
import { RGBA } from "@opentui/core"
|
||||
import { readJson, writeJsonAtomic } from "../util/persistence"
|
||||
import { useTheme } from "./theme"
|
||||
import { useToast } from "../ui/toast"
|
||||
import { useRoute } from "./route"
|
||||
|
||||
export type LocalTheme = {
|
||||
secondary: RGBA
|
||||
|
|
@ -20,17 +23,6 @@ export type LocalTheme = {
|
|||
info: RGBA
|
||||
}
|
||||
|
||||
export type LocalDependencies = {
|
||||
theme: LocalTheme
|
||||
toast: {
|
||||
show(options: { variant: "info" | "warning" | "error"; message: string; duration?: number }): void
|
||||
}
|
||||
route: {
|
||||
readonly data: { type: string; sessionID?: string }
|
||||
navigate(route: { type: "session"; sessionID: string }): void
|
||||
}
|
||||
}
|
||||
|
||||
export function parseModel(model: string) {
|
||||
const [providerID, ...rest] = model.split("/")
|
||||
return {
|
||||
|
|
@ -57,11 +49,13 @@ export function recentModels(
|
|||
|
||||
export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
name: "Local",
|
||||
init: (props: LocalDependencies) => {
|
||||
init: () => {
|
||||
const sync = useSync()
|
||||
const sdk = useSDK()
|
||||
const toast = props.toast
|
||||
const environment = useTuiEnvironment()
|
||||
const toast = useToast()
|
||||
const theme = useTheme().theme
|
||||
const route = useRoute()
|
||||
const paths = useTuiPaths()
|
||||
|
||||
function isModelValid(model: { providerID: string; modelID: string }) {
|
||||
const provider = sync.data.provider.find((x) => x.id === model.providerID)
|
||||
|
|
@ -82,7 +76,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||
const [agentStore, setAgentStore] = createStore({
|
||||
current: undefined as string | undefined,
|
||||
})
|
||||
const theme = props.theme
|
||||
const colors = createMemo(() => [
|
||||
theme.secondary,
|
||||
theme.accent,
|
||||
|
|
@ -164,7 +157,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||
variant: {},
|
||||
})
|
||||
|
||||
const filePath = path.join(environment.paths.state, "model.json")
|
||||
const filePath = path.join(paths.state, "model.json")
|
||||
const state = {
|
||||
pending: false,
|
||||
}
|
||||
|
|
@ -421,7 +414,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||
pinned: [],
|
||||
})
|
||||
|
||||
const filePath = path.join(environment.paths.state, "session.json")
|
||||
const filePath = path.join(paths.state, "session.json")
|
||||
const state = {
|
||||
pending: false,
|
||||
}
|
||||
|
|
@ -453,7 +446,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||
if (state.pending) save()
|
||||
})
|
||||
|
||||
const route = props.route
|
||||
const event = useEvent()
|
||||
|
||||
const slots = createMemo(() => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import path from "path"
|
||||
import { createContext, useContext, type ParentProps } from "solid-js"
|
||||
import { abbreviateHome, useTuiEnvironment } from "../runtime"
|
||||
import { abbreviateHome } from "../runtime"
|
||||
import { useTuiPaths } from "./runtime"
|
||||
|
||||
const context = createContext<{
|
||||
path: () => string
|
||||
|
|
@ -8,12 +9,12 @@ const context = createContext<{
|
|||
}>()
|
||||
|
||||
export function PathFormatterProvider(props: ParentProps<{ path: string | undefined }>) {
|
||||
const environment = useTuiEnvironment()
|
||||
const paths = useTuiPaths()
|
||||
return (
|
||||
<context.Provider
|
||||
value={{
|
||||
path: () => props.path || environment.cwd,
|
||||
format: (input) => formatPath(input, props.path || environment.cwd, environment.paths.home),
|
||||
path: () => props.path || paths.cwd,
|
||||
format: (input) => formatPath(input, props.path || paths.cwd, paths.home),
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { createStore, reconcile } from "solid-js/store"
|
||||
import { createSimpleContext } from "./helper"
|
||||
import type { PromptInfo } from "../prompt/history"
|
||||
import { useTuiEnvironment } from "../runtime"
|
||||
import { useTuiStartup } from "./runtime"
|
||||
|
||||
export type HomeRoute = {
|
||||
type: "home"
|
||||
|
|
@ -25,9 +25,9 @@ export type Route = HomeRoute | SessionRoute | PluginRoute
|
|||
export const { use: useRoute, provider: RouteProvider } = createSimpleContext({
|
||||
name: "Route",
|
||||
init: (props: { initialRoute?: Route }) => {
|
||||
const environment = useTuiEnvironment()
|
||||
const startup = useTuiStartup()
|
||||
const [store, setStore] = createStore<Route>(
|
||||
props.initialRoute ?? initialRoute(environment.initialRoute) ?? { type: "home" },
|
||||
props.initialRoute ?? initialRoute(startup.initialRoute) ?? { type: "home" },
|
||||
)
|
||||
|
||||
return {
|
||||
|
|
|
|||
62
packages/tui/src/context/runtime.tsx
Normal file
62
packages/tui/src/context/runtime.tsx
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { createComponent, createContext, type JSX, useContext } from "solid-js"
|
||||
|
||||
export type TuiPaths = Readonly<{
|
||||
cwd: string
|
||||
home: string
|
||||
state: string
|
||||
worktree: string
|
||||
}>
|
||||
|
||||
export type TuiTerminalEnvironment = Readonly<{
|
||||
platform: string
|
||||
multiplexer?: "tmux" | "screen"
|
||||
displayServer?: "wayland" | "x11"
|
||||
}>
|
||||
|
||||
export type TuiStartup = Readonly<{
|
||||
initialRoute?: unknown
|
||||
skipInitialLoading: boolean
|
||||
}>
|
||||
|
||||
const PathsContext = createContext<TuiPaths>()
|
||||
const TerminalEnvironmentContext = createContext<TuiTerminalEnvironment>()
|
||||
const StartupContext = createContext<TuiStartup>()
|
||||
|
||||
function provider<T>(context: ReturnType<typeof createContext<T>>, value: T, children: () => JSX.Element) {
|
||||
return createComponent(context.Provider, {
|
||||
value: Object.freeze({ ...value }),
|
||||
get children() {
|
||||
return children()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function TuiPathsProvider(props: { value: TuiPaths; children: JSX.Element }) {
|
||||
return provider(PathsContext, props.value, () => props.children)
|
||||
}
|
||||
|
||||
export function TuiTerminalEnvironmentProvider(props: { value: TuiTerminalEnvironment; children: JSX.Element }) {
|
||||
return provider(TerminalEnvironmentContext, props.value, () => props.children)
|
||||
}
|
||||
|
||||
export function TuiStartupProvider(props: { value: TuiStartup; children: JSX.Element }) {
|
||||
return provider(StartupContext, props.value, () => props.children)
|
||||
}
|
||||
|
||||
function required<T>(context: ReturnType<typeof createContext<T>>, name: string) {
|
||||
const value = useContext(context)
|
||||
if (!value) throw new Error(`${name} is missing`)
|
||||
return value
|
||||
}
|
||||
|
||||
export function useTuiPaths() {
|
||||
return required(PathsContext, "TuiPathsProvider")
|
||||
}
|
||||
|
||||
export function useTuiTerminalEnvironment() {
|
||||
return required(TerminalEnvironmentContext, "TuiTerminalEnvironmentProvider")
|
||||
}
|
||||
|
||||
export function useTuiStartup() {
|
||||
return required(StartupContext, "TuiStartupProvider")
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
|
||||
import type { GlobalEvent } from "@opencode-ai/sdk/v2"
|
||||
import { useTuiEnvironment } from "../runtime"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { createSimpleContext } from "./helper"
|
||||
import { batch, onCleanup, onMount } from "solid-js"
|
||||
|
||||
|
|
@ -17,7 +17,6 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
|||
headers?: RequestInit["headers"]
|
||||
events?: EventSource
|
||||
}) => {
|
||||
const environment = useTuiEnvironment()
|
||||
const abort = new AbortController()
|
||||
let sse: AbortController | undefined
|
||||
|
||||
|
|
@ -94,7 +93,7 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
|||
sseMaxRetryAttempts: 0,
|
||||
})
|
||||
|
||||
if (environment.capabilities.workspaces) {
|
||||
if (Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) {
|
||||
// Start syncing workspaces, it's important to do this after
|
||||
// we've started listening to events
|
||||
await sdk.sync.start().catch(() => {})
|
||||
|
|
@ -122,7 +121,7 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
|||
const unsub = await props.events.subscribe(handleEvent)
|
||||
onCleanup(unsub)
|
||||
|
||||
if (environment.capabilities.workspaces) {
|
||||
if (Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) {
|
||||
// Start syncing workspaces, it's important to do this after
|
||||
// we've started listening to events
|
||||
await sdk.sync.start().catch(() => {})
|
||||
|
|
|
|||
|
|
@ -24,13 +24,15 @@ import { createStore, produce, reconcile } from "solid-js/store"
|
|||
import { useProject } from "./project"
|
||||
import { useEvent } from "./event"
|
||||
import { useSDK } from "./sdk"
|
||||
import { useTuiEnvironment } from "../runtime"
|
||||
import { useTuiStartup } from "./runtime"
|
||||
import { createSimpleContext } from "./helper"
|
||||
import { useExit } from "./exit"
|
||||
import { useRenderer } from "@opentui/solid"
|
||||
import { useArgs } from "./args"
|
||||
import { batch, onMount } from "solid-js"
|
||||
import path from "path"
|
||||
import { aggregateFailures } from "./aggregate-failures"
|
||||
import { useKV } from "./kv"
|
||||
import { destroyRenderer } from "../util/renderer"
|
||||
|
||||
const emptyConsoleState: ConsoleState = {
|
||||
consoleManagedProviders: [],
|
||||
|
|
@ -50,19 +52,11 @@ function search<T>(items: T[], target: string, key: (item: T) => string) {
|
|||
return { found: false, index: left }
|
||||
}
|
||||
|
||||
export type SyncDependencies = {
|
||||
kv: { get(key: string, defaultValue: boolean): boolean }
|
||||
logger: { error(message: string, extra?: Record<string, unknown>): void }
|
||||
}
|
||||
|
||||
export const {
|
||||
context: SyncContext,
|
||||
use: useSync,
|
||||
provider: SyncProvider,
|
||||
} = createSimpleContext({
|
||||
export const { context: SyncContext, use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
name: "Sync",
|
||||
init: (dependencies: SyncDependencies) => {
|
||||
const environment = useTuiEnvironment()
|
||||
init: () => {
|
||||
const startup = useTuiStartup()
|
||||
const kv = useKV()
|
||||
const [store, setStore] = createStore<{
|
||||
status: "loading" | "partial" | "complete"
|
||||
provider: Provider[]
|
||||
|
|
@ -136,7 +130,6 @@ export const {
|
|||
const event = useEvent()
|
||||
const project = useProject()
|
||||
const sdk = useSDK()
|
||||
const kv = dependencies.kv
|
||||
|
||||
const fullSyncedSessions = new Set<string>()
|
||||
const syncingSessions = new Map<string, Promise<void>>()
|
||||
|
|
@ -427,7 +420,7 @@ export const {
|
|||
}
|
||||
})
|
||||
|
||||
const exit = useExit()
|
||||
const renderer = useRenderer()
|
||||
const args = useArgs()
|
||||
|
||||
async function bootstrap(input: { fatal?: boolean } = {}) {
|
||||
|
|
@ -520,13 +513,13 @@ export const {
|
|||
})
|
||||
})
|
||||
.catch(async (e) => {
|
||||
dependencies.logger.error("tui bootstrap failed", {
|
||||
console.error("tui bootstrap failed", {
|
||||
error: e instanceof Error ? e.message : String(e),
|
||||
name: e instanceof Error ? e.name : undefined,
|
||||
stack: e instanceof Error ? e.stack : undefined,
|
||||
})
|
||||
if (fatal) {
|
||||
await exit(e)
|
||||
destroyRenderer(renderer)
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
|
|
@ -544,7 +537,7 @@ export const {
|
|||
return store.status
|
||||
},
|
||||
get ready() {
|
||||
if (environment.skipInitialLoading) return true
|
||||
if (startup.skipInitialLoading) return true
|
||||
return store.status !== "loading"
|
||||
},
|
||||
get path() {
|
||||
|
|
|
|||
|
|
@ -24,7 +24,41 @@ import { createStore, produce } from "solid-js/store"
|
|||
import { createSimpleContext } from "./helper"
|
||||
import { useKV } from "./kv"
|
||||
import { useTuiConfig } from "../config"
|
||||
import { useOptionalTuiPlatform } from "../platform"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { Glob } from "@opencode-ai/core/util/glob"
|
||||
import { readFile } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
|
||||
export type ThemeSource = Readonly<{
|
||||
discover(): Promise<Record<string, unknown>>
|
||||
subscribeRefresh?(refresh: () => void): () => void
|
||||
}>
|
||||
|
||||
const themeSource: ThemeSource = {
|
||||
async discover() {
|
||||
const directories = [Global.Path.config]
|
||||
for (let current = process.cwd(); ; current = path.dirname(current)) {
|
||||
directories.push(path.join(current, ".opencode"))
|
||||
if (path.dirname(current) === current) break
|
||||
}
|
||||
return discoverThemes(directories)
|
||||
},
|
||||
subscribeRefresh(refresh) {
|
||||
process.on("SIGUSR2", refresh)
|
||||
return () => process.off("SIGUSR2", refresh)
|
||||
},
|
||||
}
|
||||
|
||||
export async function discoverThemes(directories: string[]) {
|
||||
const result: Record<string, unknown> = {}
|
||||
for (const directory of directories) {
|
||||
const files = await Glob.scan("themes/*.json", { cwd: directory, absolute: true, dot: true, symlink: true })
|
||||
for (const file of files) {
|
||||
result[path.basename(file, ".json")] = JSON.parse(await readFile(file, "utf8")) as unknown
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export {
|
||||
DEFAULT_THEMES,
|
||||
|
|
@ -67,11 +101,11 @@ subscribeThemes((themes) => setStore("themes", themes))
|
|||
|
||||
export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
||||
name: "Theme",
|
||||
init: (props: { mode: "dark" | "light" }) => {
|
||||
init: (props: { mode: "dark" | "light"; source?: ThemeSource }) => {
|
||||
const renderer = useRenderer()
|
||||
const config = useTuiConfig()
|
||||
const kv = useKV()
|
||||
const platform = useOptionalTuiPlatform()
|
||||
const themes = props.source ?? themeSource
|
||||
const pick = (value: unknown) => {
|
||||
if (value === "dark" || value === "light") return value
|
||||
return
|
||||
|
|
@ -96,7 +130,8 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
|||
})
|
||||
|
||||
function syncCustomThemes() {
|
||||
return (platform?.themes?.discover() ?? Promise.resolve({}))
|
||||
return themes
|
||||
.discover()
|
||||
.then((themes) => {
|
||||
setCustomThemes(
|
||||
Object.entries(themes).reduce<Record<string, ThemeJson>>((result, [name, theme]) => {
|
||||
|
|
@ -207,7 +242,8 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
|||
}, delay),
|
||||
)
|
||||
}
|
||||
const unsubscribeRefresh = platform?.themes?.subscribeRefresh?.(refresh)
|
||||
let unsubscribeRefresh: (() => void) | undefined
|
||||
unsubscribeRefresh = themes.subscribeRefresh?.(refresh)
|
||||
|
||||
onCleanup(() => {
|
||||
renderer.off(CliRenderEvents.THEME_MODE, handle)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue