Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/cli/src/commands/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
Flag.withDescription("Session ID to continue"),
Flag.optional,
),
fork: Flag.boolean("fork").pipe(
Flag.withDescription("Fork the session before continuing"),
Flag.withDefault(false),
),
prompt: Flag.string("prompt").pipe(Flag.withDescription("Prompt to use"), Flag.optional),
},
commands: [
Expand Down
6 changes: 5 additions & 1 deletion packages/cli/src/commands/handlers/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ export default Runtime.handler(Commands, (input) =>
Effect.gen(function* () {
const requestedDirectory = Option.getOrUndefined(input.directory)
const requestedServer = Option.getOrUndefined(input.server)
const requestedSession = Option.getOrUndefined(input.session)
if (input.fork && !input.continue && !requestedSession)
yield* Effect.fail(new Error("--fork requires --continue or --session"))
if (requestedDirectory !== undefined) process.chdir(requestedDirectory)
const preflight = UpdatePreflight.make()
yield* Effect.addFinalizer(() => Effect.promise(() => preflight.close()))
Expand Down Expand Up @@ -74,7 +77,8 @@ export default Runtime.handler(Commands, (input) =>
},
args: {
continue: input.continue,
sessionID: Option.getOrUndefined(input.session),
sessionID: requestedSession,
fork: input.fork,
prompt: Option.getOrUndefined(input.prompt),
auto: input.auto || input.yolo || input.dangerouslySkipPermissions,
},
Expand Down
8 changes: 8 additions & 0 deletions packages/cli/test/mini.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,18 @@ describe("mini command", () => {
const result = await cli(["--help"])

expect(result.exitCode).toBe(0)
expect(result.stdout).toContain("--fork")
expect(result.stdout).toContain("mini Start the minimal interactive interface")
expect(result.stdout).toContain("run Run OpenCode with a message")
})

test("requires a session target when forking from the full TUI", async () => {
const result = await cli(["--fork"])

expect(result.exitCode).toBe(1)
expect(result.stdout).toContain("--fork requires --continue or --session")
})

test("exposes run without legacy interactive, attach, or command modes", async () => {
const result = await cli(["run", "--help"])

Expand Down
4 changes: 2 additions & 2 deletions packages/tui/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
<ToastProvider>
<RouteProvider
initialRoute={
input.args.continue
input.args.continue && !input.args.sessionID
? {
type: "session",
sessionID: "dummy",
Expand Down Expand Up @@ -630,7 +630,7 @@ function App(props: { pair?: DialogPairCredentials }) {

let continued = false
createEffect(() => {
if (continued || !args.continue) return
if (continued || !args.continue || args.sessionID) return
continued = true
const location = data.location.default()
void client.api.session
Expand Down
21 changes: 20 additions & 1 deletion packages/tui/src/context/args.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createSimpleContext } from "./helper"
import { createSignal } from "solid-js"

export interface Args {
model?: string
Expand All @@ -12,5 +13,23 @@ export interface Args {

export const { use: useArgs, provider: ArgsProvider } = createSimpleContext({
name: "Args",
init: (props: Args) => props,
init: (props: Args) => {
const [prompt, setPrompt] = createSignal(props.prompt)
return {
model: props.model,
agent: props.agent,
continue: props.continue,
sessionID: props.sessionID,
fork: props.fork,
auto: props.auto,
get prompt() {
return prompt()
},
consumePrompt() {
const value = prompt()
setPrompt(undefined)
return value
},
}
},
})
1 change: 1 addition & 0 deletions packages/tui/src/routes/home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export function Home() {
if (!args.prompt) return
if (r.current.text !== args.prompt) return
sent = true
args.consumePrompt()
r.submit()
})

Expand Down
1 change: 1 addition & 0 deletions packages/tui/src/routes/session/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,7 @@ export function Session(props: {
if (!local.agent.current() || !local.model.current()) return
if (!args.prompt || route.prompt?.text !== args.prompt || current.current.text !== args.prompt) return
sent = true
args.consumePrompt()
current.submit()
})
const dialog = useDialog()
Expand Down
26 changes: 26 additions & 0 deletions packages/tui/test/context/args.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/** @jsxImportSource @opentui/solid */
import { expect, test } from "bun:test"
import { testRender } from "@opentui/solid"
import { ArgsProvider, useArgs } from "../../src/context/args"

test("consumes the startup prompt once", async () => {
let args: ReturnType<typeof useArgs>
function Probe() {
args = useArgs()
return <box />
}
const app = await testRender(() => (
<ArgsProvider prompt="FOLLOW_UP">
<Probe />
</ArgsProvider>
))

try {
expect(args!.prompt).toBe("FOLLOW_UP")
expect(args!.consumePrompt()).toBe("FOLLOW_UP")
expect(args!.prompt).toBeUndefined()
expect(args!.consumePrompt()).toBeUndefined()
} finally {
app.renderer.destroy()
}
})
Loading