Skip to content
Closed
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
8 changes: 5 additions & 3 deletions packages/tui/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,7 @@ function App(props: { onSnapshot?: () => Promise<string[]>; pluginHost: TuiPlugi
})

const args = useArgs()
const startupPrompt = args.prompt ? { input: args.prompt, parts: [] } : undefined
onMount(() => {
batch(() => {
if (args.agent) local.agent.set(args.agent)
Expand All @@ -493,6 +494,7 @@ function App(props: { onSnapshot?: () => Promise<string[]>; pluginHost: TuiPlugi
route.navigate({
type: "session",
sessionID: args.sessionID,
prompt: startupPrompt,
})
}
})
Expand All @@ -510,13 +512,13 @@ function App(props: { onSnapshot?: () => Promise<string[]>; 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, prompt: startupPrompt })
} else {
toast.show({ message: "Failed to fork session", variant: "error" })
}
})
} else {
route.navigate({ type: "session", sessionID: match })
route.navigate({ type: "session", sessionID: match, prompt: startupPrompt })
}
}
})
Expand All @@ -530,7 +532,7 @@ function App(props: { onSnapshot?: () => Promise<string[]>; 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, prompt: startupPrompt })
} else {
toast.show({ message: "Failed to fork session", variant: "error" })
}
Expand Down
23 changes: 23 additions & 0 deletions packages/tui/src/component/prompt/startup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { PromptInfo } from "./history"
import type { PromptRef } from "."

export function createStartupPrompt(value: PromptInfo | undefined) {
let ref: PromptRef | undefined
let seeded = false
let submitted = false

return {
bind(next: PromptRef | undefined) {
ref = next
if (!value || !next || seeded) return
seeded = true
next.set(value)
},
submitWhenReady(ready: boolean) {
if (!value || !ref || !ready || submitted) return
if (ref.current.input !== value.input) return
submitted = true
ref.submit()
},
}
}
13 changes: 4 additions & 9 deletions packages/tui/src/routes/home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { useEditorContext } from "../context/editor"
import { useTerminalDimensions } from "@opentui/solid"
import { useTuiConfig } from "../config"
import { HomeSessionDestinationProvider } from "./home/session-destination"
import { createStartupPrompt } from "../component/prompt/startup"

let once = false
const placeholder = {
Expand All @@ -35,7 +36,7 @@ export function Home() {
if (configured === "auto") return Math.max(75, Math.floor(dimensions().width * 0.7))
return configured ?? 75
})
let sent = false
const startupPrompt = createStartupPrompt(args.prompt ? { input: args.prompt, parts: [] } : undefined)

onMount(() => {
editor.clearSelection()
Expand All @@ -51,20 +52,14 @@ export function Home() {
return
}
if (!args.prompt) return
r.set({ input: args.prompt, parts: [] })
startupPrompt.bind(r)
once = true
}

// Wait for sync and model store to be ready before auto-submitting --prompt
createEffect(() => {
const r = ref()
if (sent) return
if (!r) return
if (!sync.ready || !local.model.ready) return
if (!args.prompt) return
if (r.current.input !== args.prompt) return
sent = true
r.submit()
startupPrompt.submitWhenReady(!!r && sync.ready && local.model.ready)
})

return (
Expand Down
16 changes: 12 additions & 4 deletions packages/tui/src/routes/session/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { Spinner } from "../../component/spinner"
import { createSyntaxStyleMemo, generateSubtleSyntax, selectedForeground, useTheme } from "../../context/theme"
import { BoxRenderable, ScrollBoxRenderable, addDefaultParsers, TextAttributes, RGBA } from "@opentui/core"
import { Prompt, type PromptRef } from "../../component/prompt"
import { createStartupPrompt } from "../../component/prompt/startup"
import type {
AssistantMessage,
Part,
Expand Down Expand Up @@ -275,6 +276,7 @@ export function Session() {
const toast = useToast()
const sdk = useSDK()
const editor = useEditorContext()
const [sessionReady, setSessionReady] = createSignal(false)

createEffect(() => {
const sessionID = route.sessionID
Expand Down Expand Up @@ -304,6 +306,7 @@ export function Session() {
}
editor.reconnect(result.data.directory)
await sync.session.sync(sessionID)
if (route.sessionID === sessionID) setSessionReady(true)
if (route.sessionID === sessionID && scroll) scroll.scrollBy(100_000)
})().catch((error) => {
if (route.sessionID !== sessionID) return
Expand Down Expand Up @@ -333,16 +336,21 @@ export function Session() {
}
})

let seeded = false
let scroll: ScrollBoxRenderable
let prompt: PromptRef | undefined
const [startupRef, setStartupRef] = createSignal<PromptRef>()
const startupPrompt = createStartupPrompt(route.prompt)
const bind = (r: PromptRef | undefined) => {
prompt = r
promptRef.set(r)
if (seeded || !route.prompt || !r) return
seeded = true
r.set(route.prompt)
setStartupRef(() => r)
startupPrompt.bind(r)
}

createEffect(() => {
startupRef()
startupPrompt.submitWhenReady(sessionReady() && sync.ready && local.model.ready)
})
const keymap = useOpencodeKeymap()
const dialog = useDialog()
const renderer = useRenderer()
Expand Down
72 changes: 72 additions & 0 deletions packages/tui/test/cli/tui/startup-prompt.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { expect, test } from "bun:test"
import type { PromptRef } from "../../../src/component/prompt"
import type { PromptInfo } from "../../../src/component/prompt/history"
import { createStartupPrompt } from "../../../src/component/prompt/startup"

function promptRef(input: {
current(): PromptInfo
set(value: PromptInfo): void
submit(): void
}): PromptRef {
return {
get current() {
return input.current()
},
get focused() {
return false
},
set: input.set,
submit: input.submit,
reset() {},
blur() {},
focus() {},
}
}

test("seeds and submits a startup prompt exactly once after readiness", () => {
const calls = {
set: 0,
submit: 0,
}
let current: PromptInfo = { input: "", parts: [] }
const ref = promptRef({
current: () => current,
set(value) {
calls.set++
current = value
},
submit() {
calls.submit++
},
})
const startup = createStartupPrompt({ input: "continue the work", parts: [] })

startup.bind(ref)
startup.bind(ref)
startup.submitWhenReady(false)
startup.submitWhenReady(true)
startup.submitWhenReady(true)

expect(calls).toEqual({ set: 1, submit: 1 })
})

test("does not submit after the seeded prompt has been edited", () => {
let current: PromptInfo = { input: "", parts: [] }
let submits = 0
const ref = promptRef({
current: () => current,
set(value) {
current = value
},
submit() {
submits++
},
})
const startup = createStartupPrompt({ input: "continue the work", parts: [] })

startup.bind(ref)
current = { input: "user edit", parts: [] }
startup.submitWhenReady(true)

expect(submits).toBe(0)
})
Loading