tui: remove legacy V1 CLI TUI integration to simplify startup
This commit is contained in:
@@ -159,7 +159,6 @@ for (const item of targets) {
|
|||||||
const localPath = path.resolve(dir, "node_modules/@opentui/core/parser.worker.js")
|
const localPath = path.resolve(dir, "node_modules/@opentui/core/parser.worker.js")
|
||||||
const rootPath = path.resolve(dir, "../../node_modules/@opentui/core/parser.worker.js")
|
const rootPath = path.resolve(dir, "../../node_modules/@opentui/core/parser.worker.js")
|
||||||
const parserWorker = fs.realpathSync(fs.existsSync(localPath) ? localPath : rootPath)
|
const parserWorker = fs.realpathSync(fs.existsSync(localPath) ? localPath : rootPath)
|
||||||
const workerPath = "./src/cli/tui/worker.ts"
|
|
||||||
|
|
||||||
// Use platform-specific bunfs root path based on target OS
|
// Use platform-specific bunfs root path based on target OS
|
||||||
const bunfsRoot = item.os === "win32" ? "B:/~BUN/root/" : "/$bunfs/root/"
|
const bunfsRoot = item.os === "win32" ? "B:/~BUN/root/" : "/$bunfs/root/"
|
||||||
@@ -185,13 +184,12 @@ for (const item of targets) {
|
|||||||
windows: {},
|
windows: {},
|
||||||
},
|
},
|
||||||
files: embeddedFileMap ? { "opencode-web-ui.gen.ts": embeddedFileMap } : {},
|
files: embeddedFileMap ? { "opencode-web-ui.gen.ts": embeddedFileMap } : {},
|
||||||
entrypoints: ["./src/index.ts", parserWorker, workerPath, ...(embeddedFileMap ? ["opencode-web-ui.gen.ts"] : [])],
|
entrypoints: ["./src/index.ts", parserWorker, ...(embeddedFileMap ? ["opencode-web-ui.gen.ts"] : [])],
|
||||||
define: {
|
define: {
|
||||||
FFF_LIBC: JSON.stringify(item.abi === "musl" ? "musl" : "gnu"),
|
FFF_LIBC: JSON.stringify(item.abi === "musl" ? "musl" : "gnu"),
|
||||||
OPENCODE_VERSION: `'${Script.version}'`,
|
OPENCODE_VERSION: `'${Script.version}'`,
|
||||||
OPENCODE_MODELS_DEV: generated.modelsData,
|
OPENCODE_MODELS_DEV: generated.modelsData,
|
||||||
OTUI_TREE_SITTER_WORKER_PATH: bunfsRoot + workerRelativePath,
|
OTUI_TREE_SITTER_WORKER_PATH: bunfsRoot + workerRelativePath,
|
||||||
OPENCODE_WORKER_PATH: workerPath,
|
|
||||||
OPENCODE_CHANNEL: `'${Script.channel}'`,
|
OPENCODE_CHANNEL: `'${Script.channel}'`,
|
||||||
OPENCODE_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "",
|
OPENCODE_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "",
|
||||||
...(item.os === "linux" ? { "process.env.OPENTUI_LIBC": JSON.stringify(item.abi ?? "glibc") } : {}),
|
...(item.os === "linux" ? { "process.env.OPENTUI_LIBC": JSON.stringify(item.abi ?? "glibc") } : {}),
|
||||||
|
|||||||
@@ -1,100 +0,0 @@
|
|||||||
import { cmd } from "./cmd"
|
|
||||||
import { UI } from "@/cli/ui"
|
|
||||||
import { errorMessage } from "@opencode-ai/tui/util/error"
|
|
||||||
import { validateSession } from "../tui/validate-session"
|
|
||||||
import { ServerAuth } from "@/server/auth"
|
|
||||||
import { OpenCode } from "@opencode-ai/client/promise"
|
|
||||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
|
|
||||||
|
|
||||||
export const AttachCommand = cmd({
|
|
||||||
command: "attach <url>",
|
|
||||||
describe: "attach to a running opencode server",
|
|
||||||
builder: (yargs) =>
|
|
||||||
yargs
|
|
||||||
.positional("url", {
|
|
||||||
type: "string",
|
|
||||||
describe: "http://localhost:4096",
|
|
||||||
demandOption: true,
|
|
||||||
})
|
|
||||||
.option("dir", {
|
|
||||||
type: "string",
|
|
||||||
description: "directory to run in",
|
|
||||||
})
|
|
||||||
.option("continue", {
|
|
||||||
alias: ["c"],
|
|
||||||
describe: "continue the last session",
|
|
||||||
type: "boolean",
|
|
||||||
})
|
|
||||||
.option("session", {
|
|
||||||
alias: ["s"],
|
|
||||||
type: "string",
|
|
||||||
describe: "session id to continue",
|
|
||||||
})
|
|
||||||
.option("fork", {
|
|
||||||
type: "boolean",
|
|
||||||
describe: "fork the session when continuing (use with --continue or --session)",
|
|
||||||
})
|
|
||||||
.option("password", {
|
|
||||||
alias: ["p"],
|
|
||||||
type: "string",
|
|
||||||
describe: "basic auth password (defaults to OPENCODE_SERVER_PASSWORD)",
|
|
||||||
})
|
|
||||||
.option("username", {
|
|
||||||
alias: ["u"],
|
|
||||||
type: "string",
|
|
||||||
describe: "basic auth username (defaults to OPENCODE_SERVER_USERNAME or 'opencode')",
|
|
||||||
}),
|
|
||||||
handler: async (args) => {
|
|
||||||
const directory = (() => {
|
|
||||||
if (!args.dir) return undefined
|
|
||||||
try {
|
|
||||||
process.chdir(args.dir)
|
|
||||||
return process.cwd()
|
|
||||||
} catch {
|
|
||||||
// If the directory doesn't exist locally (remote attach), pass it through.
|
|
||||||
return args.dir
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
|
|
||||||
const { TuiConfig } = await import("@/config/tui")
|
|
||||||
if (args.fork && !args.continue && !args.session) {
|
|
||||||
UI.error("--fork requires --continue or --session")
|
|
||||||
process.exitCode = 1
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const headers = ServerAuth.headers({ password: args.password, username: args.username })
|
|
||||||
const config = await TuiConfig.get()
|
|
||||||
|
|
||||||
try {
|
|
||||||
await validateSession({
|
|
||||||
url: args.url,
|
|
||||||
sessionID: args.session,
|
|
||||||
directory,
|
|
||||||
headers,
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
UI.error(errorMessage(error))
|
|
||||||
process.exitCode = 1
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const { Effect } = await import("effect")
|
|
||||||
const { run } = await import("../tui/layer")
|
|
||||||
const { createLegacyTuiPluginHost } = await import("@/plugin/tui/runtime")
|
|
||||||
await Effect.runPromise(
|
|
||||||
run({
|
|
||||||
// @ts-expect-error V1 does not consume the V2-only server input.
|
|
||||||
client: createOpencodeClient({ baseUrl: args.url, headers, directory }),
|
|
||||||
api: OpenCode.make({ baseUrl: args.url, headers }),
|
|
||||||
config,
|
|
||||||
pluginHost: createLegacyTuiPluginHost(),
|
|
||||||
args: {
|
|
||||||
continue: args.continue,
|
|
||||||
sessionID: args.session,
|
|
||||||
fork: args.fork,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -1,198 +0,0 @@
|
|||||||
import { cmd } from "@/cli/cmd/cmd"
|
|
||||||
import { Rpc } from "@/util/rpc"
|
|
||||||
import { type rpc } from "../tui/worker"
|
|
||||||
import path from "path"
|
|
||||||
import { fileURLToPath } from "url"
|
|
||||||
import { UI } from "@/cli/ui"
|
|
||||||
import { errorMessage } from "@opencode-ai/tui/util/error"
|
|
||||||
import { withTimeout } from "@/util/timeout"
|
|
||||||
import { withNetworkOptions, resolveNetworkOptionsNoConfig, hasArg } from "@/cli/network"
|
|
||||||
import { Filesystem } from "@/util/filesystem"
|
|
||||||
import { OpenCode } from "@opencode-ai/client/promise"
|
|
||||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
|
|
||||||
import { writeHeapSnapshot } from "v8"
|
|
||||||
import { ServerAuth } from "@/server/auth"
|
|
||||||
import { validateSession } from "../tui/validate-session"
|
|
||||||
import { win32InstallCtrlCGuard } from "@opencode-ai/tui/terminal-win32"
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
const OPENCODE_WORKER_PATH: string
|
|
||||||
}
|
|
||||||
|
|
||||||
async function target() {
|
|
||||||
if (typeof OPENCODE_WORKER_PATH !== "undefined") return OPENCODE_WORKER_PATH
|
|
||||||
const dist = new URL("./cli/tui/worker.js", import.meta.url)
|
|
||||||
if (await Filesystem.exists(fileURLToPath(dist))) return dist
|
|
||||||
return new URL("../tui/worker.ts", import.meta.url)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function input(value?: string) {
|
|
||||||
const piped = process.stdin.isTTY ? undefined : await Bun.stdin.text()
|
|
||||||
if (!value) return piped
|
|
||||||
if (!piped) return value
|
|
||||||
return piped + "\n" + value
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveThreadDirectory(project?: string, envPWD = process.env.PWD, cwd = process.cwd()) {
|
|
||||||
const root = Filesystem.resolve(envPWD ?? cwd)
|
|
||||||
if (project) return Filesystem.resolve(path.isAbsolute(project) ? project : path.join(root, project))
|
|
||||||
return Filesystem.resolve(cwd)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TuiThreadCommand = cmd({
|
|
||||||
command: "$0 [project]",
|
|
||||||
describe: "start opencode tui",
|
|
||||||
builder: (yargs) =>
|
|
||||||
withNetworkOptions(yargs)
|
|
||||||
.positional("project", {
|
|
||||||
type: "string",
|
|
||||||
describe: "path to start opencode in",
|
|
||||||
})
|
|
||||||
.option("model", {
|
|
||||||
type: "string",
|
|
||||||
alias: ["m"],
|
|
||||||
describe: "model to use in the format of provider/model",
|
|
||||||
})
|
|
||||||
.option("continue", {
|
|
||||||
alias: ["c"],
|
|
||||||
describe: "continue the last session",
|
|
||||||
type: "boolean",
|
|
||||||
})
|
|
||||||
.option("session", {
|
|
||||||
alias: ["s"],
|
|
||||||
type: "string",
|
|
||||||
describe: "session id to continue",
|
|
||||||
})
|
|
||||||
.option("fork", {
|
|
||||||
type: "boolean",
|
|
||||||
describe: "fork the session when continuing (use with --continue or --session)",
|
|
||||||
})
|
|
||||||
.option("prompt", {
|
|
||||||
type: "string",
|
|
||||||
describe: "prompt to use",
|
|
||||||
})
|
|
||||||
.option("agent", {
|
|
||||||
type: "string",
|
|
||||||
describe: "agent to use",
|
|
||||||
})
|
|
||||||
.option("auto", {
|
|
||||||
type: "boolean",
|
|
||||||
describe: "auto-approve permissions that are not explicitly denied (dangerous!)",
|
|
||||||
default: false,
|
|
||||||
})
|
|
||||||
.option("yolo", {
|
|
||||||
type: "boolean",
|
|
||||||
hidden: true,
|
|
||||||
default: false,
|
|
||||||
})
|
|
||||||
.option("dangerously-skip-permissions", {
|
|
||||||
type: "boolean",
|
|
||||||
hidden: true,
|
|
||||||
default: false,
|
|
||||||
}),
|
|
||||||
handler: async (args) => {
|
|
||||||
const unguard = win32InstallCtrlCGuard()
|
|
||||||
try {
|
|
||||||
const { TuiConfig } = await import("@/config/tui")
|
|
||||||
if (args.fork && !args.continue && !args.session) {
|
|
||||||
UI.error("--fork requires --continue or --session")
|
|
||||||
process.exitCode = 1
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve relative --project paths from PWD, then use the real cwd after
|
|
||||||
// chdir so the thread and worker share the same directory key.
|
|
||||||
const next = resolveThreadDirectory(args.project)
|
|
||||||
const file = await target()
|
|
||||||
try {
|
|
||||||
process.chdir(next)
|
|
||||||
} catch {
|
|
||||||
UI.error("Failed to change directory to " + next)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const cwd = Filesystem.resolve(process.cwd())
|
|
||||||
|
|
||||||
const worker = new Worker(file, {
|
|
||||||
env: Object.fromEntries(
|
|
||||||
Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] !== undefined),
|
|
||||||
),
|
|
||||||
})
|
|
||||||
const client = Rpc.client<typeof rpc>(worker)
|
|
||||||
const reload = () => {
|
|
||||||
client.call("reload", undefined).catch(() => {})
|
|
||||||
}
|
|
||||||
process.on("SIGUSR2", reload)
|
|
||||||
|
|
||||||
let stopped = false
|
|
||||||
const stop = async () => {
|
|
||||||
if (stopped) return
|
|
||||||
stopped = true
|
|
||||||
process.off("SIGUSR2", reload)
|
|
||||||
await withTimeout(client.call("shutdown", undefined), 5000).catch(() => {})
|
|
||||||
worker.terminate()
|
|
||||||
}
|
|
||||||
|
|
||||||
const prompt = await input(args.prompt)
|
|
||||||
const config = await TuiConfig.get()
|
|
||||||
|
|
||||||
const network = resolveNetworkOptionsNoConfig(args)
|
|
||||||
const external = hasArg("--port") || hasArg("--hostname") || network.mdns === true
|
|
||||||
const headers = external ? ServerAuth.headers() : undefined
|
|
||||||
const url = (await client.call("server", network)).url
|
|
||||||
|
|
||||||
try {
|
|
||||||
await validateSession({
|
|
||||||
url,
|
|
||||||
sessionID: args.session,
|
|
||||||
directory: cwd,
|
|
||||||
headers,
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
UI.error(errorMessage(error))
|
|
||||||
process.exitCode = 1
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
client.call("checkUpgrade", { directory: cwd }).catch(() => {})
|
|
||||||
}, 1000).unref?.()
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { Effect } = await import("effect")
|
|
||||||
const { run } = await import("../tui/layer")
|
|
||||||
const { createLegacyTuiPluginHost } = await import("@/plugin/tui/runtime")
|
|
||||||
await Effect.runPromise(
|
|
||||||
run({
|
|
||||||
// @ts-expect-error V1 does not consume the V2-only server input.
|
|
||||||
client: createOpencodeClient({ baseUrl: url, headers, directory: cwd }),
|
|
||||||
api: OpenCode.make({ baseUrl: url, headers }),
|
|
||||||
async onSnapshot() {
|
|
||||||
const tui = writeHeapSnapshot("tui.heapsnapshot")
|
|
||||||
const server = await client.call("snapshot", undefined)
|
|
||||||
return [tui, server]
|
|
||||||
},
|
|
||||||
config,
|
|
||||||
pluginHost: createLegacyTuiPluginHost(),
|
|
||||||
args: {
|
|
||||||
continue: args.continue,
|
|
||||||
sessionID: args.session,
|
|
||||||
agent: args.agent,
|
|
||||||
model: args.model,
|
|
||||||
prompt,
|
|
||||||
fork: args.fork,
|
|
||||||
auto: args.auto || args.yolo || args["dangerously-skip-permissions"],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
} finally {
|
|
||||||
await stop()
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
try {
|
|
||||||
unguard?.()
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
process.exit(0)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
// scratch
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import { run as runTui, type TuiInput } from "@opencode-ai/tui"
|
|
||||||
import { Global } from "@opencode-ai/core/global"
|
|
||||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
|
||||||
import { Effect } from "effect"
|
|
||||||
|
|
||||||
export function run(input: TuiInput) {
|
|
||||||
return runTui(input).pipe(Effect.provide(AppNodeBuilder.build(Global.node)))
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import { Server } from "@/server/server"
|
|
||||||
import { InstanceRuntime } from "@/project/instance-runtime"
|
|
||||||
import { Rpc } from "@/util/rpc"
|
|
||||||
import { upgrade } from "@/cli/upgrade"
|
|
||||||
import { Config } from "@/config/config"
|
|
||||||
import { writeHeapSnapshot } from "node:v8"
|
|
||||||
import { Heap } from "@/cli/heap"
|
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
|
||||||
import { Effect } from "effect"
|
|
||||||
import { disposeAllInstancesAndEmitGlobalDisposed } from "@/server/global-lifecycle"
|
|
||||||
|
|
||||||
Heap.start()
|
|
||||||
|
|
||||||
const onUnhandledRejection = (_error: unknown) => {}
|
|
||||||
|
|
||||||
const onUncaughtException = (_error: Error) => {}
|
|
||||||
|
|
||||||
process.on("unhandledRejection", onUnhandledRejection)
|
|
||||||
process.on("uncaughtException", onUncaughtException)
|
|
||||||
|
|
||||||
let server: Awaited<ReturnType<typeof Server.listen>> | undefined
|
|
||||||
|
|
||||||
export const rpc = {
|
|
||||||
snapshot() {
|
|
||||||
const result = writeHeapSnapshot("server.heapsnapshot")
|
|
||||||
return result
|
|
||||||
},
|
|
||||||
async server(input: { port: number; hostname: string; mdns?: boolean; cors?: string[] }) {
|
|
||||||
if (server) await server.stop(true)
|
|
||||||
server = await Server.listen(input)
|
|
||||||
return { url: server.url.toString() }
|
|
||||||
},
|
|
||||||
async checkUpgrade(input: { directory: string }) {
|
|
||||||
await InstanceRuntime.load({ directory: input.directory })
|
|
||||||
await upgrade().catch(() => {})
|
|
||||||
},
|
|
||||||
async reload() {
|
|
||||||
await AppRuntime.runPromise(
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const cfg = yield* Config.Service
|
|
||||||
yield* cfg.invalidate()
|
|
||||||
yield* disposeAllInstancesAndEmitGlobalDisposed({ swallowErrors: true })
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
async shutdown() {
|
|
||||||
await InstanceRuntime.disposeAllInstances()
|
|
||||||
if (server) await server.stop(true)
|
|
||||||
process.off("unhandledRejection", onUnhandledRejection)
|
|
||||||
process.off("uncaughtException", onUncaughtException)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
Rpc.listen(rpc)
|
|
||||||
@@ -18,9 +18,7 @@ import { McpCommand } from "./cli/cmd/mcp"
|
|||||||
import { GithubCommand } from "./cli/cmd/github"
|
import { GithubCommand } from "./cli/cmd/github"
|
||||||
import { ExportCommand } from "./cli/cmd/export"
|
import { ExportCommand } from "./cli/cmd/export"
|
||||||
import { ImportCommand } from "./cli/cmd/import"
|
import { ImportCommand } from "./cli/cmd/import"
|
||||||
import { AttachCommand } from "./cli/cmd/attach"
|
|
||||||
import { V2ServeCommand } from "./cli/cmd/v2-serve"
|
import { V2ServeCommand } from "./cli/cmd/v2-serve"
|
||||||
import { TuiThreadCommand } from "./cli/cmd/tui"
|
|
||||||
import { AcpCommand } from "./cli/cmd/acp"
|
import { AcpCommand } from "./cli/cmd/acp"
|
||||||
import { EOL } from "os"
|
import { EOL } from "os"
|
||||||
import { WebCommand } from "./cli/cmd/web"
|
import { WebCommand } from "./cli/cmd/web"
|
||||||
@@ -82,8 +80,6 @@ const cli = yargs(args)
|
|||||||
.command(AcpCommand)
|
.command(AcpCommand)
|
||||||
.command(McpCommand)
|
.command(McpCommand)
|
||||||
.command(V2ServeCommand)
|
.command(V2ServeCommand)
|
||||||
.command(TuiThreadCommand)
|
|
||||||
.command(AttachCommand)
|
|
||||||
.command(RunCommand)
|
.command(RunCommand)
|
||||||
.command(GenerateCommand)
|
.command(GenerateCommand)
|
||||||
.command(DebugCommand)
|
.command(DebugCommand)
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ import { ConfigPluginV1 } from "@opencode-ai/core/v1/config/plugin"
|
|||||||
import { createCommandShim } from "@opencode-ai/tui/plugin/command-shim"
|
import { createCommandShim } from "@opencode-ai/tui/plugin/command-shim"
|
||||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { createPluginRuntime, type PluginRuntime, type TuiPluginHost } from "@opencode-ai/tui/plugin/runtime"
|
import { createPluginRuntime, type PluginRuntime } from "@opencode-ai/tui/plugin/runtime"
|
||||||
|
|
||||||
ensureRuntimePluginSupport({ additional: keymapRuntimeModules })
|
ensureRuntimePluginSupport({ additional: keymapRuntimeModules })
|
||||||
|
|
||||||
@@ -1121,11 +1121,4 @@ async function load(input: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createLegacyTuiPluginHost(): TuiPluginHost {
|
|
||||||
return {
|
|
||||||
start: init,
|
|
||||||
dispose,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export * as TuiPluginRuntime from "./runtime"
|
export * as TuiPluginRuntime from "./runtime"
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import yargs from "yargs"
|
import yargs from "yargs"
|
||||||
import { TuiThreadCommand } from "./cli/cmd/tui"
|
|
||||||
import { V2ServeCommand } from "./cli/cmd/v2-serve"
|
import { V2ServeCommand } from "./cli/cmd/v2-serve"
|
||||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||||
import { hideBin } from "yargs/helpers"
|
import { hideBin } from "yargs/helpers"
|
||||||
@@ -29,5 +28,4 @@ const cli = yargs(hideBin(process.argv))
|
|||||||
if (opts.logLevel) process.env.OPENCODE_LOG_LEVEL = opts.logLevel
|
if (opts.logLevel) process.env.OPENCODE_LOG_LEVEL = opts.logLevel
|
||||||
})
|
})
|
||||||
.command(V2ServeCommand)
|
.command(V2ServeCommand)
|
||||||
.command(TuiThreadCommand)
|
|
||||||
.parse()
|
.parse()
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
|
||||||
|
|
||||||
describe("tui attach", () => {
|
|
||||||
test("loads the TUI integration lazily", async () => {
|
|
||||||
const source = await Bun.file(new URL("../../../src/cli/cmd/attach.ts", import.meta.url)).text()
|
|
||||||
|
|
||||||
expect(source).toContain('await import("../tui/layer")')
|
|
||||||
expect(source).toMatch(/await import\(["']@\/plugin\/tui\/runtime["']\)/)
|
|
||||||
expect(source).not.toContain('import("./app")')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
|
||||||
import { Effect } from "effect"
|
|
||||||
import fs from "fs/promises"
|
|
||||||
import path from "path"
|
|
||||||
import yargs from "yargs"
|
|
||||||
import { tmpdir } from "../../fixture/fixture"
|
|
||||||
import { TuiThreadCommand, resolveThreadDirectory } from "../../../src/cli/cmd/tui"
|
|
||||||
import { cliIt } from "../../lib/cli-process"
|
|
||||||
|
|
||||||
describe("tui thread", () => {
|
|
||||||
test("loads the TUI integration lazily", async () => {
|
|
||||||
const source = await Bun.file(new URL("../../../src/cli/cmd/tui.ts", import.meta.url)).text()
|
|
||||||
|
|
||||||
expect(source).toContain('await import("../tui/layer")')
|
|
||||||
expect(source).toMatch(/await import\(["']@\/plugin\/tui\/runtime["']\)/)
|
|
||||||
expect(source).not.toContain('import("./app")')
|
|
||||||
})
|
|
||||||
|
|
||||||
test("forwards the CLI environment to the TUI worker", async () => {
|
|
||||||
const source = await Bun.file(new URL("../../../src/cli/cmd/tui.ts", import.meta.url)).text()
|
|
||||||
|
|
||||||
expect(source).toMatch(/new Worker\(file, \{\s*env: Object\.fromEntries\(\s*Object\.entries\(process\.env\)/)
|
|
||||||
})
|
|
||||||
|
|
||||||
async function check(project?: string) {
|
|
||||||
await using tmp = await tmpdir({ git: true })
|
|
||||||
const link = path.join(path.dirname(tmp.path), path.basename(tmp.path) + "-link")
|
|
||||||
const type = process.platform === "win32" ? "junction" : "dir"
|
|
||||||
|
|
||||||
try {
|
|
||||||
await fs.symlink(tmp.path, link, type)
|
|
||||||
expect(resolveThreadDirectory(project, link, tmp.path)).toBe(tmp.path)
|
|
||||||
} finally {
|
|
||||||
await fs.rm(link, { recursive: true, force: true }).catch(() => undefined)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
test("uses the real cwd when PWD points at a symlink", async () => {
|
|
||||||
await check()
|
|
||||||
})
|
|
||||||
|
|
||||||
test("uses the real cwd after resolving a relative project from PWD", async () => {
|
|
||||||
await check(".")
|
|
||||||
})
|
|
||||||
|
|
||||||
test("resolves a relative project from PWD when cwd differs", async () => {
|
|
||||||
await using pwd = await tmpdir({ git: true })
|
|
||||||
await using cwd = await tmpdir({ git: true })
|
|
||||||
|
|
||||||
expect(resolveThreadDirectory(".", pwd.path, cwd.path)).toBe(pwd.path)
|
|
||||||
expect(resolveThreadDirectory(undefined, pwd.path, cwd.path)).toBe(cwd.path)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("preserves boolean negation for existing options", async () => {
|
|
||||||
const args = await yargs([])
|
|
||||||
.command({ ...TuiThreadCommand, handler: () => {} })
|
|
||||||
.exitProcess(false)
|
|
||||||
.parse(["--mdns", "--no-mdns"])
|
|
||||||
|
|
||||||
expect(args.mdns).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
cliIt.live("rejects removed top-level mini alias", ({ opencode }) =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const result = yield* opencode.spawn(["--mini"])
|
|
||||||
|
|
||||||
opencode.expectExit(result, 1)
|
|
||||||
expect(result.stderr).not.toContain("opencode mini requires a TTY stdout")
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
cliIt.live("rejects removed run mini flag", ({ opencode }) =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const result = yield* opencode.spawn(["run", "--mini"])
|
|
||||||
|
|
||||||
opencode.expectExit(result, 1)
|
|
||||||
expect(result.stderr).not.toContain("opencode mini requires a TTY stdout")
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
cliIt.live("rejects removed attach mini alias", ({ opencode }) =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const result = yield* opencode.spawn(["attach", "http://127.0.0.1:1", "--mini"])
|
|
||||||
|
|
||||||
opencode.expectExit(result, 1)
|
|
||||||
expect(result.stderr).not.toContain("opencode mini requires a TTY stdout")
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
@@ -77,7 +77,6 @@ import { ArgsProvider, useArgs, type Args } from "./context/args"
|
|||||||
import open from "open"
|
import open from "open"
|
||||||
import { PromptRefProvider, usePromptRef } from "./context/prompt"
|
import { PromptRefProvider, usePromptRef } from "./context/prompt"
|
||||||
import { Config, ConfigProvider, useConfig } from "./config"
|
import { Config, ConfigProvider, useConfig } from "./config"
|
||||||
import { TuiConfigV1 } from "./config/v1"
|
|
||||||
import { createTuiApiAdapters } from "./plugin/adapters"
|
import { createTuiApiAdapters } from "./plugin/adapters"
|
||||||
import { createTuiApi } from "./plugin/api"
|
import { createTuiApi } from "./plugin/api"
|
||||||
import { createPluginRuntime, PluginRuntimeProvider, usePluginRuntime, type TuiPluginHost } from "./plugin/runtime"
|
import { createPluginRuntime, PluginRuntimeProvider, usePluginRuntime, type TuiPluginHost } from "./plugin/runtime"
|
||||||
@@ -139,7 +138,6 @@ const appBindingCommands = [
|
|||||||
"diff.open",
|
"diff.open",
|
||||||
"app.debug",
|
"app.debug",
|
||||||
"app.console",
|
"app.console",
|
||||||
"app.heap_snapshot",
|
|
||||||
"terminal.suspend",
|
"terminal.suspend",
|
||||||
"terminal.title.toggle",
|
"terminal.title.toggle",
|
||||||
"app.toggle.animations",
|
"app.toggle.animations",
|
||||||
@@ -155,8 +153,7 @@ export type TuiInput = {
|
|||||||
reload?: () => Promise<void>
|
reload?: () => Promise<void>
|
||||||
}
|
}
|
||||||
args: Args
|
args: Args
|
||||||
config: Config.Interface | TuiConfigV1.Resolved
|
config: Config.Interface
|
||||||
onSnapshot?: () => Promise<string[]>
|
|
||||||
pluginHost: TuiPluginHost
|
pluginHost: TuiPluginHost
|
||||||
terminalHandoff?: () => Promise<
|
terminalHandoff?: () => Promise<
|
||||||
| {
|
| {
|
||||||
@@ -201,45 +198,12 @@ function isVersionGreater(left: string, right: string) {
|
|||||||
return a.prerelease.localeCompare(b.prerelease, undefined, { numeric: true }) > 0
|
return a.prerelease.localeCompare(b.prerelease, undefined, { numeric: true }) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function fromV1(config: TuiConfigV1.Resolved): Config.Info {
|
|
||||||
return {
|
|
||||||
theme: config.theme ? { name: config.theme } : undefined,
|
|
||||||
plugins: config.plugin?.map((plugin) =>
|
|
||||||
typeof plugin === "string" ? plugin : { package: plugin[0], options: plugin[1] },
|
|
||||||
),
|
|
||||||
leader: { timeout: config.leader_timeout },
|
|
||||||
scroll:
|
|
||||||
config.scroll_speed === undefined && config.scroll_acceleration === undefined
|
|
||||||
? undefined
|
|
||||||
: { speed: config.scroll_speed, acceleration: config.scroll_acceleration?.enabled },
|
|
||||||
attention: config.attention,
|
|
||||||
diffs: config.diff_style === undefined ? undefined : { view: config.diff_style === "stacked" ? "unified" : "auto" },
|
|
||||||
mouse: config.mouse,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
|
export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
|
||||||
const log = input.log ?? (() => {})
|
const log = input.log ?? (() => {})
|
||||||
const global = yield* Global.Service
|
const global = yield* Global.Service
|
||||||
const configInput = input.config
|
const config = Config.resolve(yield* Effect.tryPromise(() => input.config.get()), {
|
||||||
const loaded = yield* Effect.gen(function* () {
|
terminalSuspend: process.platform !== "win32",
|
||||||
if (isConfigInterface(configInput)) {
|
|
||||||
return {
|
|
||||||
service: configInput,
|
|
||||||
info: yield* Effect.tryPromise(() => configInput.get()),
|
|
||||||
legacy: undefined,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { service: undefined, info: fromV1(configInput), legacy: configInput }
|
|
||||||
})
|
})
|
||||||
const config = Config.resolve(loaded.info, { terminalSuspend: process.platform !== "win32" })
|
|
||||||
if (loaded.legacy) config.keybinds = loaded.legacy.keybinds
|
|
||||||
const options = { baseUrl: input.server.endpoint.url, headers: Service.headers(input.server.endpoint) }
|
const options = { baseUrl: input.server.endpoint.url, headers: Service.headers(input.server.endpoint) }
|
||||||
const api = OpenCode.make(options)
|
const api = OpenCode.make(options)
|
||||||
const directory = yield* Effect.tryPromise(() => api.file.list({ location: { directory: process.cwd() } })).pipe(
|
const directory = yield* Effect.tryPromise(() => api.file.list({ location: { directory: process.cwd() } })).pipe(
|
||||||
@@ -376,7 +340,7 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
|
|||||||
<ArgsProvider {...input.args}>
|
<ArgsProvider {...input.args}>
|
||||||
<ConfigProvider
|
<ConfigProvider
|
||||||
config={config}
|
config={config}
|
||||||
service={loaded.service}
|
service={input.config}
|
||||||
options={{ terminalSuspend: process.platform !== "win32" }}
|
options={{ terminalSuspend: process.platform !== "win32" }}
|
||||||
>
|
>
|
||||||
<KVProvider>
|
<KVProvider>
|
||||||
@@ -412,9 +376,7 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
|
|||||||
<EditorContextProvider>
|
<EditorContextProvider>
|
||||||
<LocationProvider>
|
<LocationProvider>
|
||||||
<App
|
<App
|
||||||
onSnapshot={input.onSnapshot}
|
|
||||||
pluginHost={input.pluginHost}
|
pluginHost={input.pluginHost}
|
||||||
pluginConfig={loaded.legacy ?? config}
|
|
||||||
pair={
|
pair={
|
||||||
input.server.endpoint.auth
|
input.server.endpoint.auth
|
||||||
? input.server.endpoint.auth
|
? input.server.endpoint.auth
|
||||||
@@ -473,15 +435,12 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
function App(props: {
|
function App(props: {
|
||||||
onSnapshot?: () => Promise<string[]>
|
|
||||||
pluginHost: TuiPluginHost
|
pluginHost: TuiPluginHost
|
||||||
pluginConfig: any
|
|
||||||
pair?: DialogPairCredentials
|
pair?: DialogPairCredentials
|
||||||
}) {
|
}) {
|
||||||
const log = useLog({ component: "app" })
|
const log = useLog({ component: "app" })
|
||||||
const startup = useTuiStartup()
|
const startup = useTuiStartup()
|
||||||
const configContext = useConfig()
|
const config = useConfig().data
|
||||||
const config = configContext.data
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const dimensions = useTerminalDimensions()
|
const dimensions = useTerminalDimensions()
|
||||||
const renderer = useRenderer()
|
const renderer = useRenderer()
|
||||||
@@ -555,7 +514,6 @@ function App(props: {
|
|||||||
props.pluginHost
|
props.pluginHost
|
||||||
.start({
|
.start({
|
||||||
api,
|
api,
|
||||||
config: props.pluginConfig,
|
|
||||||
runtime: pluginRuntime,
|
runtime: pluginRuntime,
|
||||||
dispose: () => attention.dispose(),
|
dispose: () => attention.dispose(),
|
||||||
})
|
})
|
||||||
@@ -859,7 +817,6 @@ function App(props: {
|
|||||||
name: "opencode.settings",
|
name: "opencode.settings",
|
||||||
title: "Open settings",
|
title: "Open settings",
|
||||||
slashName: "settings",
|
slashName: "settings",
|
||||||
enabled: configContext.writable,
|
|
||||||
run: () => {
|
run: () => {
|
||||||
dialog.replace(() => <DialogConfig />)
|
dialog.replace(() => <DialogConfig />)
|
||||||
},
|
},
|
||||||
@@ -983,20 +940,6 @@ function App(props: {
|
|||||||
dialog.clear()
|
dialog.clear()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "app.heap_snapshot",
|
|
||||||
title: "Write heap snapshot",
|
|
||||||
category: "System",
|
|
||||||
run: async () => {
|
|
||||||
const files = await props.onSnapshot?.()
|
|
||||||
toast.show({
|
|
||||||
variant: "info",
|
|
||||||
message: `Heap snapshot written to ${files?.join(", ")}`,
|
|
||||||
duration: 5000,
|
|
||||||
})
|
|
||||||
dialog.clear()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "terminal.suspend",
|
name: "terminal.suspend",
|
||||||
title: "Suspend terminal",
|
title: "Suspend terminal",
|
||||||
|
|||||||
@@ -179,7 +179,6 @@ export function resolve(input: Info, options: { terminalSuspend: boolean }): Res
|
|||||||
const ConfigContext = createContext<{
|
const ConfigContext = createContext<{
|
||||||
data: Resolved
|
data: Resolved
|
||||||
update: Interface["update"]
|
update: Interface["update"]
|
||||||
writable: boolean
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
export function ConfigProvider(props: {
|
export function ConfigProvider(props: {
|
||||||
@@ -197,7 +196,7 @@ export function ConfigProvider(props: {
|
|||||||
return info
|
return info
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<ConfigContext.Provider value={{ data: config, update, writable: !!host }}>{props.children}</ConfigContext.Provider>
|
<ConfigContext.Provider value={{ data: config, update }}>{props.children}</ConfigContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,6 @@ export type PluginRuntime = ReturnType<typeof createPluginRuntime>
|
|||||||
export type TuiPluginHost = {
|
export type TuiPluginHost = {
|
||||||
start(input: {
|
start(input: {
|
||||||
api: TuiPluginApi
|
api: TuiPluginApi
|
||||||
config: any
|
|
||||||
runtime: PluginRuntime
|
runtime: PluginRuntime
|
||||||
dispose?: () => void
|
dispose?: () => void
|
||||||
}): Promise<void>
|
}): Promise<void>
|
||||||
|
|||||||
Reference in New Issue
Block a user