8c94e9005f
Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Co-authored-by: Kit Langton <kit.langton@gmail.com> Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com> Co-authored-by: Affan Ali <93028901+affanali2k3@users.noreply.github.com> Co-authored-by: affanali2k3 <affanalikhanxx@gmail.com> Co-authored-by: Frank <frank@anoma.ly> Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> Co-authored-by: 𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴 <little-frank@opencord.local> Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: Jay V <air@live.ca> Co-authored-by: Dax Raad <d@ironbay.co> Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com> Co-authored-by: OpeOginni <107570612+OpeOginni@users.noreply.github.com> Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com> Co-authored-by: Ben Guthrie <benjee.012@gmail.com> Co-authored-by: Dax <mail@thdxr.com> Co-authored-by: Filip <34747899+neriousy@users.noreply.github.com> Co-authored-by: Max Anderson <max.a.anderson95@gmail.com> Co-authored-by: Brendan Allan <git@brendonovich.dev> Co-authored-by: Jack <jack@anoma.ly> Co-authored-by: Shoubhit Dash <shoubhit2005@gmail.com> Co-authored-by: Dustin Deus <deusdustin@gmail.com> Co-authored-by: starptech <starptech@starptechs-MBP.fritz.box> Co-authored-by: Aiden Cline <aidenpcline@gmail.com> Co-authored-by: usrnk1 <7547651+usrnk1@users.noreply.github.com> Co-authored-by: Jay <53023+jayair@users.noreply.github.com> Co-authored-by: runvip <164729189+runvip@users.noreply.github.com> Co-authored-by: opencode <opencode@sst.dev> Co-authored-by: Julian Coy <julian@ex-machina.co> Co-authored-by: Vladimir Glafirov <vglafirov@gitlab.com>
256 lines
7.7 KiB
TypeScript
256 lines
7.7 KiB
TypeScript
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"
|
|
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,
|
|
})
|
|
.option("mini", {
|
|
type: "boolean",
|
|
describe: "start the minimal interactive interface",
|
|
default: false,
|
|
})
|
|
.option("replay", {
|
|
type: "boolean",
|
|
hidden: true,
|
|
})
|
|
.option("no-replay", {
|
|
type: "boolean",
|
|
describe: "disable mini session history replay on resume and after resize",
|
|
})
|
|
.option("replay-limit", {
|
|
type: "number",
|
|
describe: "cap visible mini replay to the newest N messages",
|
|
})
|
|
.option("demo", {
|
|
type: "boolean",
|
|
hidden: true,
|
|
}),
|
|
handler: async (args) => {
|
|
if (args.replay === true) {
|
|
UI.error("--replay is not supported; replay is enabled by default")
|
|
process.exitCode = 1
|
|
return
|
|
}
|
|
const noReplay = args.replay === false || args.noReplay === true
|
|
|
|
if (args.mini) {
|
|
const network = ["--port", "--hostname", "--mdns", "--no-mdns", "--mdns-domain", "--cors"].find((option) =>
|
|
process.argv.some((arg) => arg === option || arg.startsWith(option + "=")),
|
|
)
|
|
if (network) {
|
|
UI.error(`${network} cannot be used with --mini`)
|
|
process.exitCode = 1
|
|
return
|
|
}
|
|
|
|
const { runMini } = await import("./run")
|
|
await runMini({
|
|
directory: resolveThreadDirectory(args.project),
|
|
continue: args.continue,
|
|
session: args.session,
|
|
fork: args.fork,
|
|
model: args.model,
|
|
agent: args.agent,
|
|
prompt: args.prompt,
|
|
replay: noReplay ? false : undefined,
|
|
replayLimit: args.replayLimit,
|
|
demo: args.demo,
|
|
})
|
|
return
|
|
}
|
|
|
|
const unsupported = [
|
|
["--no-replay", noReplay],
|
|
["--replay-limit", args.replayLimit !== undefined],
|
|
["--demo", args.demo !== undefined],
|
|
].find((entry) => entry[1])?.[0]
|
|
if (unsupported) {
|
|
UI.error(`${unsupported} requires --mini`)
|
|
process.exitCode = 1
|
|
return
|
|
}
|
|
|
|
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)
|
|
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 url = (await client.call("server", network)).url
|
|
|
|
try {
|
|
await validateSession({
|
|
url,
|
|
sessionID: args.session,
|
|
directory: cwd,
|
|
})
|
|
} 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({
|
|
client: createOpencodeClient({ baseUrl: url, directory: cwd }),
|
|
api: OpenCode.make({ baseUrl: url }),
|
|
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
|