diff --git a/packages/tui/src/app.tsx b/packages/tui/src/app.tsx index 57f372ef709a..141a2a248f50 100644 --- a/packages/tui/src/app.tsx +++ b/packages/tui/src/app.tsx @@ -476,6 +476,16 @@ function App(props: { onSnapshot?: () => Promise; pluginHost: TuiPlugi }) const args = useArgs() + const seedInitialPrompt = () => { + const prompt = args.consumePrompt() + + return prompt + ? { + prompt: { input: prompt, parts: [] }, + autoSubmit: true, + } + : {} + } onMount(() => { batch(() => { if (args.agent) local.agent.set(args.agent) @@ -493,6 +503,7 @@ function App(props: { onSnapshot?: () => Promise; pluginHost: TuiPlugi route.navigate({ type: "session", sessionID: args.sessionID, + ...seedInitialPrompt(), }) } }) @@ -510,13 +521,13 @@ function App(props: { onSnapshot?: () => Promise; pluginHost: TuiPlugi 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() }) } } }) @@ -530,7 +541,7 @@ function App(props: { onSnapshot?: () => Promise; pluginHost: TuiPlugi 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/tui/src/context/args.tsx b/packages/tui/src/context/args.tsx index 9a528f2f0ece..2a665378ea28 100644 --- a/packages/tui/src/context/args.tsx +++ b/packages/tui/src/context/args.tsx @@ -1,3 +1,4 @@ +import { createSignal } from "solid-js" import { createSimpleContext } from "./helper" export interface Args { @@ -12,5 +13,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/tui/src/context/route.tsx b/packages/tui/src/context/route.tsx index 7355fe54d7dc..cc4c19985738 100644 --- a/packages/tui/src/context/route.tsx +++ b/packages/tui/src/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 "../prompt/history" import { useTuiStartup } from "./runtime" @@ -12,6 +12,7 @@ export type SessionRoute = { type: "session" sessionID: string prompt?: PromptInfo + autoSubmit?: boolean } export type PluginRoute = { @@ -26,16 +27,16 @@ export const { use: useRoute, provider: RouteProvider } = createSimpleContext({ name: "Route", init: (props: { initialRoute?: Route }) => { const startup = useTuiStartup() - const [store, setStore] = createStore( + const [data, setData] = createSignal( props.initialRoute ?? initialRoute(startup.initialRoute) ?? { type: "home" }, ) return { get data() { - return store + return data() }, navigate(route: Route) { - setStore(reconcile(route)) + setData(route) }, } }, diff --git a/packages/tui/src/routes/home.tsx b/packages/tui/src/routes/home.tsx index d4145e2d87be..4369561ac18f 100644 --- a/packages/tui/src/routes/home.tsx +++ b/packages/tui/src/routes/home.tsx @@ -13,7 +13,6 @@ import { useTerminalDimensions } from "@opentui/solid" import { useTuiConfig } from "../config" import { HomeSessionDestinationProvider } from "./home/session-destination" -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"], @@ -30,6 +29,8 @@ export function Home() { const editor = useEditorContext() const dimensions = useTerminalDimensions() const tuiConfig = useTuiConfig() + 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)) @@ -44,15 +45,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 } - if (!args.prompt) return - r.set({ input: args.prompt, parts: [] }) - once = true + if (args.continue || args.sessionID || args.fork) return + const input = args.consumePrompt() + if (!input) return + setArgPrompt(input) + setSeededPrompt(true) + r.set({ input, parts: [] }) } // Wait for sync and model store to be ready before auto-submitting --prompt @@ -61,8 +65,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 = argPrompt() + if (!input) return + if (r.current.input !== input) return sent = true r.submit() }) diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index 6d77b0ea58fd..8a386a2e0d21 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -186,6 +186,7 @@ export function Session() { const route = useRouteData("session") const { navigate } = useRoute() const sync = useSync() + const local = useLocal() const event = useEvent() const project = useProject() const paths = useTuiPaths() @@ -334,15 +335,34 @@ 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) } + + // 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 + 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() + }) const keymap = useOpencodeKeymap() const dialog = useDialog() const renderer = useRenderer() @@ -420,8 +440,6 @@ export function Session() { }, 50) } - const local = useLocal() - function enterChild(sessionID: string) { navigate({ type: "session",