Skip to content

Commit 8f98e5b

Browse files
committed
feat(tui): hydrate pending session work
1 parent 6c8df5b commit 8f98e5b

3 files changed

Lines changed: 77 additions & 14 deletions

File tree

packages/tui/src/context/data.tsx

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,31 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
199199
const item = messages.findLast((item) => item.type === "compaction" && item.status === "running")
200200
return item?.type === "compaction" ? item : undefined
201201
},
202+
fromPending(item: SessionPendingInfo): SessionMessageInfo {
203+
if (item.type === "user")
204+
return {
205+
id: item.id,
206+
type: "user",
207+
...item.data,
208+
time: { created: item.timeCreated },
209+
}
210+
if (item.type === "synthetic")
211+
return {
212+
id: item.id,
213+
type: "synthetic",
214+
...item.data,
215+
time: { created: item.timeCreated },
216+
}
217+
return {
218+
id: item.id,
219+
type: "compaction",
220+
status: "running",
221+
reason: "manual",
222+
summary: "",
223+
recent: "",
224+
time: { created: item.timeCreated },
225+
}
226+
},
202227
latestTool(assistant: SessionMessageAssistant | undefined, callID?: string) {
203228
return assistant?.content.findLast(
204229
(item): item is SessionMessageAssistantTool =>
@@ -960,16 +985,29 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
960985
},
961986
sync(sessionID: string) {
962987
return sync.run(`session.message:${sessionID}`, async () => {
963-
const pending = new Set(store.session.input[sessionID] ?? [])
964-
const messages = (
965-
await client.api.message.list({ sessionID, limit: 200, order: "desc" })
966-
).data.toReversed()
967-
const index = new Map(messages.map((message, index) => [message.id, index]))
968-
store.session.message[sessionID]
969-
?.filter((message) => pending.has(message.id))
970-
.forEach((item) => message.append(messages, index, item))
971-
messageIndex.set(sessionID, index)
972-
setStore("session", "message", sessionID, reconcile(messages))
988+
const inputsBeforeSync = new Set(store.session.input[sessionID] ?? [])
989+
const [projected, pending] = await Promise.all([
990+
client.api.message.list({ sessionID, limit: 200, order: "desc" }),
991+
client.api.session.pending.list({ sessionID }),
992+
])
993+
const messages = projected.data.toReversed()
994+
const projectedIDs = new Set(messages.map((message) => message.id))
995+
const pendingMessages = pending.filter((item) => !projectedIDs.has(item.id)).map(message.fromPending)
996+
const next = [...messages, ...pendingMessages]
997+
messageIndex.set(sessionID, new Map(next.map((message, index) => [message.id, index])))
998+
const inputsAfterSync = new Set(store.session.input[sessionID] ?? [])
999+
setStore(
1000+
"session",
1001+
"input",
1002+
sessionID,
1003+
pendingMessages.flatMap((message) =>
1004+
(message.type === "user" || message.type === "synthetic") &&
1005+
(!inputsBeforeSync.has(message.id) || inputsAfterSync.has(message.id))
1006+
? [message.id]
1007+
: [],
1008+
),
1009+
)
1010+
setStore("session", "message", sessionID, reconcile(next))
9731011
})
9741012
},
9751013
invalidate(sessionID: string) {

packages/tui/test/cli/tui/data.test.tsx

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2381,9 +2381,33 @@ test("preserves admitted prompts when hydration races with promotion", async ()
23812381
const events = createEventStream()
23822382
const sessionID = "session-1"
23832383
const messageID = "msg_user_1"
2384+
const queuedID = "msg_user_2"
23842385
const requested = Promise.withResolvers<void>()
23852386
const response = Promise.withResolvers<Response>()
23862387
const calls = createFetch((url) => {
2388+
if (url.pathname === `/api/session/${sessionID}/pending`)
2389+
return json({
2390+
data: [
2391+
{
2392+
admittedSeq: 0,
2393+
id: messageID,
2394+
sessionID,
2395+
timeCreated: 0,
2396+
type: "user",
2397+
data: { text: "hello" },
2398+
delivery: "steer",
2399+
},
2400+
{
2401+
admittedSeq: 1,
2402+
id: queuedID,
2403+
sessionID,
2404+
timeCreated: 1,
2405+
type: "user",
2406+
data: { text: "queued" },
2407+
delivery: "queue",
2408+
},
2409+
],
2410+
})
23872411
if (url.pathname !== `/api/session/${sessionID}/message`) return
23882412
requested.resolve()
23892413
return response.promise
@@ -2463,14 +2487,14 @@ test("preserves admitted prompts when hydration races with promotion", async ()
24632487
response.resolve(json({ data: [], cursor: {} }))
24642488
await refresh
24652489

2466-
const message = sync.session.message.list(sessionID)?.[0]
2490+
const message = sync.session.message.get(sessionID, messageID)
24672491
expect(message?.type).toBe("user")
24682492
if (message?.type !== "user") return
24692493
expect(message).toMatchObject({ id: messageID, text: "hello" })
24702494
expect(message.metadata).toBeUndefined()
2471-
expect(sync.session.pending.list(sessionID)).toEqual([])
2472-
expect(sync.session.input.list(sessionID)).toEqual([])
2473-
expect(sync.session.message.list(sessionID).map((message) => message.id)).toEqual([messageID])
2495+
expect(sync.session.input.list(sessionID)).toEqual([queuedID])
2496+
expect(sync.session.message.list(sessionID).map((message) => message.id)).toEqual([messageID, queuedID])
2497+
expect(sync.session.message.get(sessionID, queuedID)).toMatchObject({ id: queuedID, text: "queued" })
24742498
expect(sync.session.message.list("missing")).toEqual([])
24752499
expect(sync.session.message.get(sessionID, messageID)).toBe(message)
24762500
expect(sync.session.message.get(sessionID, "missing")).toBeUndefined()

packages/tui/test/fixture/tui-client.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ export function createFetch(override?: FetchHandler, events?: ReturnType<typeof
109109
})
110110
if (url.pathname === "/api/session") return json({ data: [], cursor: {} })
111111
if (url.pathname === "/api/session/active") return json({ data: {} })
112+
if (/^\/api\/session\/[^/]+\/pending$/.test(url.pathname)) return json({ data: [] })
112113
if (url.pathname === "/api/permission/request")
113114
return json({ location: { directory, project: { id: "proj_test", directory: worktree } }, data: [] })
114115
if (url.pathname === "/api/form/request")

0 commit comments

Comments
 (0)