fix(tui): render submitted prompts optimistically

This commit is contained in:
Kit Langton 2026-07-11 13:25:10 -04:00
commit 1803aaf857
5 changed files with 126 additions and 21 deletions

View file

@ -1064,9 +1064,7 @@ test("tracks session status from active sessions and execution events", async ()
const message = data.session.message.get("session-manual", "message-compaction")
return message?.type === "compaction" && message.status === "running" && message.summary === "Streamed summary"
})
const compactionRow = manualRows.find(
(row) => row.type === "message" && row.messageID === "message-compaction",
)
const compactionRow = manualRows.find((row) => row.type === "message" && row.messageID === "message-compaction")
emitEvent(events, {
id: "evt_manual_compaction_ended",
created: 3,
@ -1108,9 +1106,7 @@ test("tracks session status from active sessions and execution events", async ()
const message = data.session.message.get("session-live", "msg_compaction_started")
return message?.type === "compaction" && message.status === "running" && message.summary === "Live summary"
})
const autoCompactionRow = rows.find(
(row) => row.type === "message" && row.messageID === "msg_compaction_started",
)
const autoCompactionRow = rows.find((row) => row.type === "message" && row.messageID === "msg_compaction_started")
emitEvent(events, {
id: "evt_compaction_ended",
@ -2059,6 +2055,14 @@ test("renders admitted prompts immediately and tracks them until promoted", asyn
await mounted
const received: string[] = []
const unsubscribe = sync.listen((event) => received.push(event.name))
sync.session.input.optimistic(sessionID, {
id: messageID,
type: "user",
text: "optimistic",
time: { created: -1 },
})
expect(sync.session.message.get(sessionID, messageID)).toMatchObject({ text: "optimistic", time: { created: -1 } })
expect(sync.session.input.list(sessionID)).toEqual([messageID])
emitEvent(events, {
id: "evt_admitted_1",
created: 0,
@ -2070,11 +2074,13 @@ test("renders admitted prompts immediately and tracks them until promoted", asyn
input: { type: "user", data: { text: "hello" }, delivery: "steer" },
},
})
await wait(() => sync.session.message.list(sessionID)?.length === 1)
await wait(() => sync.session.message.get(sessionID, messageID)?.time.created === 0)
const admitted = sync.session.message.list(sessionID)?.[0]
expect(admitted).toMatchObject({ id: messageID, type: "user", text: "hello" })
expect(admitted?.metadata).toBeUndefined()
expect(sync.session.input.list(sessionID)).toEqual([messageID])
sync.session.input.rollback(sessionID, messageID)
expect(sync.session.message.ids(sessionID)).toEqual([messageID])
await sync.session.message.refresh(sessionID)
expect(sync.session.message.list(sessionID)?.[0]?.metadata).toBeUndefined()
@ -2109,6 +2115,66 @@ test("renders admitted prompts immediately and tracks them until promoted", asyn
}
})
test("rolls back unconfirmed optimistic prompts and accepts later admission", async () => {
const events = createEventStream()
const sessionID = "session-1"
const messageID = "msg_user_rollback"
const calls = createFetch(undefined, events)
let sync!: ReturnType<typeof useData>
let ready!: () => void
const mounted = new Promise<void>((resolve) => {
ready = resolve
})
function Probe() {
sync = useData()
onMount(ready)
return <box />
}
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</TestTuiContexts>
))
try {
await mounted
sync.session.input.optimistic(sessionID, {
id: messageID,
type: "user",
text: "optimistic",
time: { created: -1 },
})
sync.session.input.rollback(sessionID, messageID)
expect(sync.session.message.ids(sessionID)).toEqual([])
expect(sync.session.input.list(sessionID)).toEqual([])
emitEvent(events, {
id: "evt_admitted_after_rollback",
created: 1,
type: "session.input.admitted",
durable: durable(sessionID),
data: {
sessionID,
inputID: messageID,
input: { type: "user", data: { text: "accepted" }, delivery: "steer" },
},
})
await wait(() => sync.session.message.ids(sessionID).length === 1)
expect(sync.session.message.get(sessionID, messageID)).toMatchObject({ text: "accepted", time: { created: 1 } })
expect(sync.session.input.list(sessionID)).toEqual([messageID])
} finally {
app.renderer.destroy()
}
})
test("projects live instruction updates with their message ID", async () => {
const events = createEventStream()
const calls = createFetch(undefined, events)
@ -2180,8 +2246,7 @@ function sessionInfo(id: string, parentID: string | undefined, cost = 0) {
async function mountData(parents: Record<string, string>, costs: Record<string, number> = {}) {
const calls = createFetch((url) => {
const match = url.pathname.match(/^\/api\/session\/([^/]+)$/)
if (match && match[1] !== "active")
return json({ data: sessionInfo(match[1], parents[match[1]], costs[match[1]]) })
if (match && match[1] !== "active") return json({ data: sessionInfo(match[1], parents[match[1]], costs[match[1]]) })
})
let data!: ReturnType<typeof useData>
let ready!: () => void
@ -2250,10 +2315,7 @@ test("indexes arbitrarily deep nesting under a single root", async () => {
})
test("totals family cost for roots and keeps subagent cost scoped", async () => {
const { data, app } = await mountData(
{ grandchild: "child", child: "root" },
{ root: 1, child: 2, grandchild: 3 },
)
const { data, app } = await mountData({ grandchild: "child", child: "root" }, { root: 1, child: 2, grandchild: 3 })
try {
await data.session.refresh("grandchild")
await data.session.refresh("child")