tui: optimistically render submitted prompts

This commit is contained in:
Kit Langton 2026-05-08 13:53:17 -04:00
parent 0ba1081cf1
commit f5c770d65e
2 changed files with 101 additions and 10 deletions

View file

@ -1178,6 +1178,23 @@ export function Prompt(props: PromptProps) {
})),
})
} else {
const parts = [
...editorParts,
{
id: PartID.ascending(),
type: "text" as const,
text: inputText,
},
...nonTextParts.map(assign),
]
sync.session.addOptimisticPrompt({
sessionID,
messageID,
agent: agent.name,
model: selectedModel,
variant,
parts,
})
sdk.client.session
.prompt({
sessionID,
@ -1186,17 +1203,9 @@ export function Prompt(props: PromptProps) {
agent: agent.name,
model: selectedModel,
variant,
parts: [
...editorParts,
{
id: PartID.ascending(),
type: "text",
text: inputText,
},
...nonTextParts.map(assign),
],
parts,
})
.catch(() => {})
.catch(() => sync.session.removeOptimisticPrompt(sessionID, messageID))
if (editorParts.length > 0) editor.markSelectionSent()
}
history.append({

View file

@ -4,6 +4,10 @@ import type {
Provider,
Session,
Part,
TextPartInput,
FilePartInput,
AgentPartInput,
SubtaskPartInput,
Config,
Todo,
Command,
@ -34,6 +38,8 @@ import path from "path"
import { useKV } from "./kv"
import { aggregateFailures } from "./aggregate-failures"
type OptimisticPromptPart = (TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput) & { id: string }
export const { use: useSync, provider: SyncProvider } = createSimpleContext({
name: "Sync",
init: () => {
@ -518,6 +524,82 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
if (last.role === "user") return "working"
return last.time.completed ? "idle" : "working"
},
addOptimisticPrompt(input: {
sessionID: string
messageID: string
agent: string
model: { providerID: string; modelID: string }
variant?: string
parts: OptimisticPromptPart[]
}) {
const messages = store.message[input.sessionID]
const match = messages ? Binary.search(messages, input.messageID, (m) => m.id) : undefined
const info: Message = {
id: input.messageID,
sessionID: input.sessionID,
role: "user",
time: { created: Date.now() },
agent: input.agent,
model: {
providerID: input.model.providerID,
modelID: input.model.modelID,
...(input.variant ? { variant: input.variant } : {}),
},
}
const parts = input.parts.map((part): Part => {
const withIDs = {
...part,
sessionID: input.sessionID,
messageID: input.messageID,
}
if (withIDs.type !== "text") return withIDs
return {
...withIDs,
metadata: {
...withIDs.metadata,
optimistic: true,
},
}
})
batch(() => {
if (!messages) {
setStore("message", input.sessionID, [info])
} else if (!match?.found) {
setStore(
"message",
input.sessionID,
produce((draft) => {
draft.splice(match?.index ?? draft.length, 0, info)
}),
)
}
setStore("part", input.messageID, reconcile(parts))
})
},
removeOptimisticPrompt(sessionID: string, messageID: string) {
if (!store.part[messageID]?.some((part) => part.type === "text" && part.metadata?.optimistic === true)) return
const messages = store.message[sessionID]
if (!messages) return
const match = Binary.search(messages, messageID, (m) => m.id)
batch(() => {
if (match.found) {
setStore(
"message",
sessionID,
produce((draft) => {
draft.splice(match.index, 1)
}),
)
}
setStore(
"part",
produce((draft) => {
delete draft[messageID]
}),
)
})
},
async sync(sessionID: string) {
if (fullSyncedSessions.has(sessionID)) return
const [session, messages, todo, diff] = await Promise.all([