From 6fed0b4befd89fdc6c2eba340a6283455a5a3e75 Mon Sep 17 00:00:00 2001 From: OpeOginni Date: Fri, 20 Feb 2026 14:32:18 +0100 Subject: [PATCH 1/7] feat(tui): implement onMount logic to handle prompts passed in cli to continue a session --- packages/opencode/src/cli/cmd/tui/app.tsx | 14 +++++++++++--- .../opencode/src/cli/cmd/tui/context/route.tsx | 1 + .../src/cli/cmd/tui/routes/session/index.tsx | 4 ++++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index a26b8cfdfe88..affb1a59c4be 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -332,6 +332,13 @@ function App(props: { onSnapshot?: () => Promise }) { }) const args = useArgs() + const seedInitialPrompt = () => + args.prompt + ? { + prompt: { input: args.prompt, parts: [] }, + autoSubmit: true, + } + : {} onMount(() => { batch(() => { if (args.agent) local.agent.set(args.agent) @@ -349,6 +356,7 @@ function App(props: { onSnapshot?: () => Promise }) { route.navigate({ type: "session", sessionID: args.sessionID, + ...seedInitialPrompt(), }) } }) @@ -366,13 +374,13 @@ function App(props: { onSnapshot?: () => Promise }) { if (args.fork) { void sdk.client.session.fork({ sessionID: match }).then((result) => { if (result.data?.id) { - route.navigate({ type: "session", sessionID: result.data.id }) + route.navigate({ type: "session", sessionID: result.data.id, ...seedInitialPrompt() }) } else { toast.show({ message: "Failed to fork session", variant: "error" }) } }) } else { - route.navigate({ type: "session", sessionID: match }) + route.navigate({ type: "session", sessionID: match, ...seedInitialPrompt() }) } } }) @@ -386,7 +394,7 @@ function App(props: { onSnapshot?: () => Promise }) { forked = true void sdk.client.session.fork({ sessionID: args.sessionID }).then((result) => { if (result.data?.id) { - route.navigate({ type: "session", sessionID: result.data.id }) + route.navigate({ type: "session", sessionID: result.data.id, ...seedInitialPrompt() }) } else { toast.show({ message: "Failed to fork session", variant: "error" }) } diff --git a/packages/opencode/src/cli/cmd/tui/context/route.tsx b/packages/opencode/src/cli/cmd/tui/context/route.tsx index 35be17801b1f..3abc5bb7c53c 100644 --- a/packages/opencode/src/cli/cmd/tui/context/route.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/route.tsx @@ -11,6 +11,7 @@ export type SessionRoute = { type: "session" sessionID: string prompt?: PromptInfo + autoSubmit?: boolean } export type PluginRoute = { diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 8855338d1d4b..4553463d85a4 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -116,6 +116,9 @@ function use() { return ctx } +// TODO: what is the best way to do this? +let once = false + export function Session() { const route = useRouteData("session") const { navigate } = useRoute() @@ -248,6 +251,7 @@ export function Session() { if (seeded || !route.prompt || !r) return seeded = true r.set(route.prompt) + if (route.autoSubmit) r.submit() } const keybind = useKeybind() const dialog = useDialog() From 79d44b26153909697c022b5276548ac6934bedfc Mon Sep 17 00:00:00 2001 From: OpeOginni Date: Fri, 10 Apr 2026 09:37:14 +0100 Subject: [PATCH 2/7] refactor(session): added guards for the initial Prompt submission during session navigation --- packages/opencode/src/cli/cmd/tui/routes/session/index.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 4553463d85a4..65e5b119e914 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -116,9 +116,6 @@ function use() { return ctx } -// TODO: what is the best way to do this? -let once = false - export function Session() { const route = useRouteData("session") const { navigate } = useRoute() @@ -251,7 +248,7 @@ export function Session() { if (seeded || !route.prompt || !r) return seeded = true r.set(route.prompt) - if (route.autoSubmit) r.submit() + if (route.autoSubmit && route.prompt.input) r.submit() } const keybind = useKeybind() const dialog = useDialog() From 8713871e9da33b1892ec45cc3850ca0fdfaad369 Mon Sep 17 00:00:00 2001 From: OpeOginni Date: Thu, 4 Jun 2026 21:19:49 +0200 Subject: [PATCH 3/7] fix(tui): prevent potential timing issue with auto prompt --- .../src/cli/cmd/tui/routes/session/index.tsx | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 022bb07f9ea0..12d56af674e4 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -184,6 +184,7 @@ export function Session() { const route = useRouteData("session") const { navigate } = useRoute() const sync = useSync() + const local = useLocal() const event = useEvent() const project = useProject() const tuiConfig = useTuiConfig() @@ -308,16 +309,29 @@ export function Session() { }) let seeded = false + let submitted = false let scroll: ScrollBoxRenderable let prompt: PromptRef | undefined + const [promptSignal, setPromptSignal] = createSignal() const bind = (r: PromptRef | undefined) => { prompt = r + setPromptSignal(r) promptRef.set(r) if (seeded || !route.prompt || !r) return seeded = true r.set(route.prompt) - if (route.autoSubmit && route.prompt.input) r.submit() } + + createEffect(() => { + const r = promptSignal() + if (submitted) return + if (!r) return + if (!sync.ready || !local.model.ready) return + if (!route.autoSubmit || !route.prompt?.input) return + if (r.current.input !== route.prompt.input) return + submitted = true + r.submit() + }) const keymap = useOpencodeKeymap() const dialog = useDialog() const renderer = useRenderer() @@ -416,8 +430,6 @@ export function Session() { }, 50) } - const local = useLocal() - function enterChild(sessionID: string) { navigate({ type: "session", From a5a94df50bb8ab16a8338f3869867adab36cfe90 Mon Sep 17 00:00:00 2001 From: OpeOginni Date: Thu, 4 Jun 2026 21:19:57 +0200 Subject: [PATCH 4/7] fix(tui): prevent replaying startup prompts on home --- packages/opencode/src/cli/cmd/tui/routes/home.tsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/routes/home.tsx b/packages/opencode/src/cli/cmd/tui/routes/home.tsx index 16797573c007..b14ef13ffda9 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/home.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/home.tsx @@ -28,6 +28,10 @@ export function Home() { const editor = useEditorContext() const dimensions = useTerminalDimensions() const tuiConfig = useTuiConfig() + const homePrompt = () => { + if (args.continue || args.sessionID || args.fork) return + return args.prompt + } const promptMaxWidth = createMemo(() => { const configured = tuiConfig.prompt?.max_width if (configured === "auto") return Math.max(75, Math.floor(dimensions().width * 0.7)) @@ -48,8 +52,9 @@ export function Home() { once = true return } - if (!args.prompt) return - r.set({ input: args.prompt, parts: [] }) + const input = homePrompt() + if (!input) return + r.set({ input, parts: [] }) once = true } @@ -59,8 +64,9 @@ export function Home() { if (sent) return if (!r) return if (!sync.ready || !local.model.ready) return - if (!args.prompt) return - if (r.current.input !== args.prompt) return + const input = homePrompt() + if (!input) return + if (r.current.input !== input) return sent = true r.submit() }) From 9e04f403d6d699d9f9900459bae9777cc1fee4c2 Mon Sep 17 00:00:00 2001 From: OpeOginni Date: Thu, 4 Jun 2026 21:29:24 +0200 Subject: [PATCH 5/7] fix(tui): guard autosubmit until session metadata loads --- packages/opencode/src/bus/global.ts | 12 ++++++------ .../src/cli/cmd/tui/routes/session/index.tsx | 2 ++ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/opencode/src/bus/global.ts b/packages/opencode/src/bus/global.ts index 3cfd453624c1..8cb9c84e4e2f 100644 --- a/packages/opencode/src/bus/global.ts +++ b/packages/opencode/src/bus/global.ts @@ -11,12 +11,12 @@ export type GlobalEvent = { class GlobalBusEmitter extends EventEmitter<{ event: [GlobalEvent] }> { - override emit(eventName: "event", event: GlobalEvent): boolean { - if (event.payload && typeof event.payload === "object" && !("id" in event.payload)) { - event.payload.id = event.payload.syncEvent?.id ?? Identifier.create("evt", "ascending") - } - return super.emit(eventName, event) - } + // override emit(eventName: "event", event: GlobalEvent): boolean { + // if (event.payload && typeof event.payload === "object" && !("id" in event.payload)) { + // event.payload.id = event.payload.syncEvent?.id ?? Identifier.create("evt", "ascending") + // } + // return super.emit(eventName, event) + // } } export const GlobalBus = new GlobalBusEmitter() diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 12d56af674e4..6fa720d9a43b 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -328,6 +328,8 @@ export function Session() { if (!r) return if (!sync.ready || !local.model.ready) return if (!route.autoSubmit || !route.prompt?.input) return + const target = session() + if (!target || target.parentID) return if (r.current.input !== route.prompt.input) return submitted = true r.submit() From 04a5bd65928e6fc1875fbd19c86b4f1454d3899c Mon Sep 17 00:00:00 2001 From: OpeOginni Date: Thu, 4 Jun 2026 21:30:49 +0200 Subject: [PATCH 6/7] revert globals.ts push --- packages/opencode/src/bus/global.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/opencode/src/bus/global.ts b/packages/opencode/src/bus/global.ts index 8cb9c84e4e2f..3cfd453624c1 100644 --- a/packages/opencode/src/bus/global.ts +++ b/packages/opencode/src/bus/global.ts @@ -11,12 +11,12 @@ export type GlobalEvent = { class GlobalBusEmitter extends EventEmitter<{ event: [GlobalEvent] }> { - // override emit(eventName: "event", event: GlobalEvent): boolean { - // if (event.payload && typeof event.payload === "object" && !("id" in event.payload)) { - // event.payload.id = event.payload.syncEvent?.id ?? Identifier.create("evt", "ascending") - // } - // return super.emit(eventName, event) - // } + override emit(eventName: "event", event: GlobalEvent): boolean { + if (event.payload && typeof event.payload === "object" && !("id" in event.payload)) { + event.payload.id = event.payload.syncEvent?.id ?? Identifier.create("evt", "ascending") + } + return super.emit(eventName, event) + } } export const GlobalBus = new GlobalBusEmitter() From b3ddd643134ccb5d7d65b7f136fb818d7bdc90da Mon Sep 17 00:00:00 2001 From: OpeOginni Date: Fri, 5 Jun 2026 15:32:43 +0200 Subject: [PATCH 7/7] refactor(tui): make use of signals for cli prompt args --- packages/opencode/src/cli/cmd/tui/app.tsx | 9 ++++-- .../opencode/src/cli/cmd/tui/context/args.tsx | 32 ++++++++++++++++++- .../src/cli/cmd/tui/context/route.tsx | 8 ++--- .../opencode/src/cli/cmd/tui/routes/home.tsx | 19 ++++++----- .../src/cli/cmd/tui/routes/session/index.tsx | 3 ++ 5 files changed, 53 insertions(+), 18 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 50134e980139..579db701b344 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -478,13 +478,16 @@ function App(props: { onSnapshot?: () => Promise }) { }) const args = useArgs() - const seedInitialPrompt = () => - args.prompt + const seedInitialPrompt = () => { + const prompt = args.consumePrompt() + + return prompt ? { - prompt: { input: args.prompt, parts: [] }, + prompt: { input: prompt, parts: [] }, autoSubmit: true, } : {} + } onMount(() => { batch(() => { if (args.agent) local.agent.set(args.agent) diff --git a/packages/opencode/src/cli/cmd/tui/context/args.tsx b/packages/opencode/src/cli/cmd/tui/context/args.tsx index 8a229ffaba69..5f1b791a16eb 100644 --- a/packages/opencode/src/cli/cmd/tui/context/args.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/args.tsx @@ -1,3 +1,4 @@ +import { createSignal } from "solid-js" import { createSimpleContext } from "./helper" export interface Args { @@ -11,5 +12,34 @@ 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 { + get model() { + return props.model + }, + get agent() { + return props.agent + }, + get prompt() { + return prompt() + }, + get continue() { + return props.continue + }, + get sessionID() { + return props.sessionID + }, + get fork() { + return props.fork + }, + consumePrompt() { + const value = prompt() + if (!value) return + setPrompt(undefined) + return value + }, + } + }, }) diff --git a/packages/opencode/src/cli/cmd/tui/context/route.tsx b/packages/opencode/src/cli/cmd/tui/context/route.tsx index 3abc5bb7c53c..196effdd282d 100644 --- a/packages/opencode/src/cli/cmd/tui/context/route.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/route.tsx @@ -1,4 +1,4 @@ -import { createStore, reconcile } from "solid-js/store" +import { createSignal } from "solid-js" import { createSimpleContext } from "./helper" import type { PromptInfo } from "../component/prompt/history" @@ -25,7 +25,7 @@ export type Route = HomeRoute | SessionRoute | PluginRoute export const { use: useRoute, provider: RouteProvider } = createSimpleContext({ name: "Route", init: (props: { initialRoute?: Route }) => { - const [store, setStore] = createStore( + const [data, setData] = createSignal( props.initialRoute ?? (process.env["OPENCODE_ROUTE"] ? JSON.parse(process.env["OPENCODE_ROUTE"]) @@ -36,10 +36,10 @@ export const { use: useRoute, provider: RouteProvider } = createSimpleContext({ return { get data() { - return store + return data() }, navigate(route: Route) { - setStore(reconcile(route)) + setData(route) }, } }, diff --git a/packages/opencode/src/cli/cmd/tui/routes/home.tsx b/packages/opencode/src/cli/cmd/tui/routes/home.tsx index b14ef13ffda9..54d610ea8355 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/home.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/home.tsx @@ -12,7 +12,6 @@ import { useEditorContext } from "@tui/context/editor" import { useTerminalDimensions } from "@opentui/solid" import { useTuiConfig } from "../context/tui-config" -let once = false const placeholder = { normal: ["Fix a TODO in the codebase", "What is the tech stack of this project?", "Fix broken tests"], shell: ["ls -la", "git status", "pwd"], @@ -28,10 +27,8 @@ export function Home() { const editor = useEditorContext() const dimensions = useTerminalDimensions() const tuiConfig = useTuiConfig() - const homePrompt = () => { - if (args.continue || args.sessionID || args.fork) return - return args.prompt - } + const [argPrompt, setArgPrompt] = createSignal() + const [seededPrompt, setSeededPrompt] = createSignal(false) const promptMaxWidth = createMemo(() => { const configured = tuiConfig.prompt?.max_width if (configured === "auto") return Math.max(75, Math.floor(dimensions().width * 0.7)) @@ -46,16 +43,18 @@ export function Home() { const bind = (r: PromptRef | undefined) => { setRef(r) promptRef.set(r) - if (once || !r) return + if (seededPrompt() || !r) return if (route.prompt) { r.set(route.prompt) - once = true + setSeededPrompt(true) return } - const input = homePrompt() + if (args.continue || args.sessionID || args.fork) return + const input = args.consumePrompt() if (!input) return + setArgPrompt(input) + setSeededPrompt(true) r.set({ input, parts: [] }) - once = true } // Wait for sync and model store to be ready before auto-submitting --prompt @@ -64,7 +63,7 @@ export function Home() { if (sent) return if (!r) return if (!sync.ready || !local.model.ready) return - const input = homePrompt() + const input = argPrompt() if (!input) return if (r.current.input !== input) return sent = true diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 6fa720d9a43b..83d256facb08 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -322,6 +322,9 @@ export function Session() { r.set(route.prompt) } + // CLI prompts can be routed into an existing session before sync/model hydration finishes. + // Wait for those stores so the submission uses the right defaults, and only auto-submit if + // the prompt still matches the seeded value so user edits are not submitted unexpectedly. createEffect(() => { const r = promptSignal() if (submitted) return