Compare commits

...

2 Commits

Author SHA1 Message Date
Aiden Cline 1c88de1224 Merge remote-tracking branch 'origin/v2' into optimistic-prompt
# Conflicts:
#	packages/tui/src/routes/session/index.tsx
2026-07-01 12:03:11 -05:00
Aiden Cline 4c80946b4b fix(tui): render submitted prompts optimistically 2026-07-01 11:56:53 -05:00
2 changed files with 44 additions and 12 deletions
+19 -10
View File
@@ -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
@@ -1095,6 +1095,7 @@ export function Prompt(props: PromptProps) {
]
: []
let submittedUserMessage: SessionMessageUser | undefined
if (store.mode === "shell") {
move.startSubmit()
void sdk.client.session.shell({
@@ -1170,7 +1171,7 @@ export function Prompt(props: PromptProps) {
return false
}
}
const error = await sdk.api.session
const admitted = await sdk.api.session
.prompt({
sessionID,
prompt: {
@@ -1207,14 +1208,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,
@@ -1226,7 +1235,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) {
+25 -2
View File
@@ -241,6 +241,13 @@ export function Session() {
const sdk = useSDK()
const editor = useEditorContext()
const rows = createSessionRows(() => route.sessionID)
const [optimisticPrompts, setOptimisticPrompts] = createSignal<SessionMessageUser[]>([])
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) => {
@@ -923,6 +930,13 @@ export function Session() {
/>
)}
</For>
<For each={visibleOptimisticPrompts()}>
{(message) => (
<box marginTop={1} flexShrink={0}>
<UserMessage message={message} optimistic={true} />
</box>
)}
</For>
<BackgroundToolHint messages={messages()} />
<Show when={session()?.revert?.messageID}>
<RevertMessage
@@ -964,7 +978,12 @@ export function Session() {
visible={true}
ref={bind}
disabled={false}
onSubmit={() => {
onSubmit={(message) => {
if (message)
setOptimisticPrompts((messages) => [
...messages.filter((item) => item.id !== message.id),
message,
])
toBottom()
}}
sessionID={route.sessionID}
@@ -1353,7 +1372,7 @@ function RevertMessage(props: {
)
}
function UserMessage(props: { message: SessionMessageUser }) {
function UserMessage(props: { message: SessionMessageUser; optimistic?: boolean }) {
const ctx = use()
const data = useData()
const local = useLocal()
@@ -1383,6 +1402,7 @@ function UserMessage(props: { message: SessionMessageUser }) {
setHover(false)
}}
onMouseUp={() => {
if (props.optimistic) return
if (renderer.getSelection()?.getSelectedText()) return
dialog.replace(() => <DialogMessage messageID={props.message.id} sessionID={ctx.sessionID} />)
}}
@@ -1393,6 +1413,9 @@ function UserMessage(props: { message: SessionMessageUser }) {
flexShrink={0}
>
<text fg={theme.text}>{props.message.text}</text>
<Show when={props.optimistic}>
<text fg={theme.textMuted}>optimistic prompt</text>
</Show>
<Show when={files().length}>
<box
flexDirection="row"