From 4c80946b4bcb346427fd3c19b9a9642c20f5d63e Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Wed, 1 Jul 2026 11:56:53 -0500 Subject: [PATCH] fix(tui): render submitted prompts optimistically --- packages/tui/src/component/prompt/index.tsx | 29 ++++++++++++++------- packages/tui/src/routes/session/index.tsx | 27 +++++++++++++++++-- 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/packages/tui/src/component/prompt/index.tsx b/packages/tui/src/component/prompt/index.tsx index 53c6a520cb..0b45428038 100644 --- a/packages/tui/src/component/prompt/index.tsx +++ b/packages/tui/src/component/prompt/index.tsx @@ -37,7 +37,7 @@ import { usePromptStash } from "../../prompt/stash" import { DialogStash } from "../dialog-stash" import { type AutocompleteRef, Autocomplete } from "./autocomplete" import { useRenderer, useTerminalDimensions, type JSX } from "@opentui/solid" -import type { AssistantMessage, FilePart, SessionV2Info, UserMessage } from "@opencode-ai/sdk/v2" +import type { AssistantMessage, FilePart, SessionMessageUser, SessionV2Info, UserMessage } from "@opencode-ai/sdk/v2" import { Locale } from "../../util/locale" import { errorMessage } from "../../util/error" import { createColors, createFrames } from "../../ui/spinner" @@ -62,7 +62,7 @@ export type PromptProps = { sessionID?: string visible?: boolean disabled?: boolean - onSubmit?: () => void + onSubmit?: (message?: SessionMessageUser) => void ref?: (ref: PromptRef | undefined) => void hint?: JSX.Element right?: JSX.Element @@ -1077,6 +1077,7 @@ export function Prompt(props: PromptProps) { ] : [] + let submittedUserMessage: SessionMessageUser | undefined if (store.mode === "shell") { move.startSubmit() void sdk.client.session.shell({ @@ -1152,7 +1153,7 @@ export function Prompt(props: PromptProps) { return false } } - const error = await sdk.api.session + const admitted = await sdk.api.session .prompt({ sessionID, prompt: { @@ -1189,14 +1190,22 @@ export function Prompt(props: PromptProps) { }, }) .then( - () => undefined, - (error) => error, + (result) => result, + (error) => { + toast.show({ title: "Failed to send prompt", message: errorMessage(error), variant: "error" }) + return undefined + }, ) - if (error) { - toast.show({ title: "Failed to send prompt", message: errorMessage(error), variant: "error" }) - return false - } + if (!admitted) return false if (editorParts.length > 0) editor.markSelectionSent() + submittedUserMessage = { + id: admitted.id, + type: "user", + text: admitted.prompt.text, + files: admitted.prompt.files ? [...admitted.prompt.files] : undefined, + agents: admitted.prompt.agents ? [...admitted.prompt.agents] : undefined, + time: { created: admitted.timeCreated }, + } } history.append({ ...store.prompt, @@ -1208,7 +1217,7 @@ export function Prompt(props: PromptProps) { parts: [], }) setStore("extmarkToPartIndex", new Map()) - props.onSubmit?.() + props.onSubmit?.(submittedUserMessage) // temporary hack to make sure the message is sent if (!props.sessionID) { diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index f1f3e18283..c92ed18b84 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -241,6 +241,13 @@ export function Session() { const sdk = useSDK() const editor = useEditorContext() const rows = createSessionRows(() => route.sessionID) + const [optimisticPrompts, setOptimisticPrompts] = createSignal([]) + const visibleOptimisticPrompts = createMemo(() => { + const ids = new Set(messageIDs()) + return optimisticPrompts().filter((message) => !ids.has(message.id)) + }) + + createEffect(on(() => route.sessionID, () => setOptimisticPrompts([]))) createEffect( on(descendantSessionIDs, (sessionIDs) => { @@ -920,6 +927,13 @@ export function Session() { /> )} + + {(message) => ( + + + + )} + { + onSubmit={(message) => { + if (message) + setOptimisticPrompts((messages) => [ + ...messages.filter((item) => item.id !== message.id), + message, + ]) toBottom() }} sessionID={route.sessionID} @@ -1317,7 +1336,7 @@ function RevertMessage(props: { ) } -function UserMessage(props: { message: SessionMessageUser }) { +function UserMessage(props: { message: SessionMessageUser; optimistic?: boolean }) { const ctx = use() const local = useLocal() const files = createMemo(() => props.message.files ?? []) @@ -1343,6 +1362,7 @@ function UserMessage(props: { message: SessionMessageUser }) { setHover(false) }} onMouseUp={() => { + if (props.optimistic) return if (renderer.getSelection()?.getSelectedText()) return dialog.replace(() => ) }} @@ -1353,6 +1373,9 @@ function UserMessage(props: { message: SessionMessageUser }) { flexShrink={0} > {props.message.text} + + optimistic prompt +