feat(tui): hydrate pending session work
This commit is contained in:
parent
1004fbb2f5
commit
e689829315
3 changed files with 175 additions and 4 deletions
|
|
@ -22,6 +22,7 @@ import type {
|
|||
SessionMessageAssistantText,
|
||||
SessionMessageAssistantTool,
|
||||
SessionInfo,
|
||||
SessionPendingInfo,
|
||||
Shell,
|
||||
SkillInfo,
|
||||
} from "@opencode-ai/sdk/v2"
|
||||
|
|
@ -145,6 +146,31 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
const item = messages.findLast((item) => item.type === "compaction" && item.status === "running")
|
||||
return item?.type === "compaction" ? item : undefined
|
||||
},
|
||||
fromPending(item: SessionPendingInfo): SessionMessageInfo {
|
||||
if (item.type === "user")
|
||||
return {
|
||||
id: item.id,
|
||||
type: "user",
|
||||
...item.data,
|
||||
time: { created: item.timeCreated },
|
||||
}
|
||||
if (item.type === "synthetic")
|
||||
return {
|
||||
id: item.id,
|
||||
type: "synthetic",
|
||||
...item.data,
|
||||
time: { created: item.timeCreated },
|
||||
}
|
||||
return {
|
||||
id: item.id,
|
||||
type: "compaction",
|
||||
status: "running",
|
||||
reason: "manual",
|
||||
summary: "",
|
||||
recent: "",
|
||||
time: { created: item.timeCreated },
|
||||
}
|
||||
},
|
||||
latestTool(assistant: SessionMessageAssistant | undefined, callID?: string) {
|
||||
return assistant?.content.findLast(
|
||||
(item): item is SessionMessageAssistantTool =>
|
||||
|
|
@ -612,11 +638,38 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
setSessionStatus(event.data.sessionID, "running")
|
||||
break
|
||||
case "session.compaction.admitted":
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
message.append(draft, index, {
|
||||
id: event.data.inputID,
|
||||
type: "compaction",
|
||||
status: "running",
|
||||
reason: "manual",
|
||||
summary: "",
|
||||
recent: "",
|
||||
time: { created: event.created },
|
||||
})
|
||||
})
|
||||
break
|
||||
case "session.compaction.started":
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
const id = event.data.inputID ?? messageIDFromEvent(event.id)
|
||||
const position = index.get(id)
|
||||
const existing = position === undefined ? undefined : draft[position]
|
||||
if (position !== undefined && existing?.type === "compaction") {
|
||||
draft[position] = {
|
||||
id,
|
||||
type: "compaction",
|
||||
status: "running",
|
||||
reason: event.data.reason,
|
||||
summary: existing.status === "running" ? existing.summary : "",
|
||||
recent: event.data.recent,
|
||||
metadata: existing.metadata,
|
||||
time: existing.time,
|
||||
}
|
||||
return
|
||||
}
|
||||
message.append(draft, index, {
|
||||
id: event.data.inputID ?? messageIDFromEvent(event.id),
|
||||
id,
|
||||
type: "compaction",
|
||||
status: "running",
|
||||
reason: event.data.reason,
|
||||
|
|
@ -836,9 +889,22 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
return position === undefined ? undefined : messages?.[position]
|
||||
},
|
||||
async refresh(sessionID: string) {
|
||||
const messages = (await sdk.api.message.list({ sessionID, limit: 200, order: "desc" })).data.toReversed()
|
||||
messageIndex.set(sessionID, new Map(messages.map((message, index) => [message.id, index])))
|
||||
setStore("session", "message", sessionID, reconcile(messages))
|
||||
const [projected, pending] = await Promise.all([
|
||||
sdk.api.message.list({ sessionID, limit: 200, order: "desc" }),
|
||||
sdk.api.session.pending.list({ sessionID }),
|
||||
])
|
||||
const messages = projected.data.toReversed()
|
||||
const projectedIDs = new Set(messages.map((message) => message.id))
|
||||
const pendingMessages = pending.filter((item) => !projectedIDs.has(item.id)).map(message.fromPending)
|
||||
const next = [...messages, ...pendingMessages]
|
||||
messageIndex.set(sessionID, new Map(next.map((message, index) => [message.id, index])))
|
||||
setStore(
|
||||
"session",
|
||||
"input",
|
||||
sessionID,
|
||||
pendingMessages.flatMap((item) => (item.type === "user" || item.type === "synthetic" ? [item.id] : [])),
|
||||
)
|
||||
setStore("session", "message", sessionID, reconcile(next))
|
||||
},
|
||||
},
|
||||
permission: {
|
||||
|
|
|
|||
|
|
@ -2091,6 +2091,110 @@ test("renders admitted prompts immediately and tracks them until promoted", asyn
|
|||
}
|
||||
})
|
||||
|
||||
test("refreshes pending session work into message state", async () => {
|
||||
const events = createEventStream()
|
||||
const sessionID = "session-pending"
|
||||
const calls = createFetch((url) => {
|
||||
if (url.pathname === `/api/session/${sessionID}/message`)
|
||||
return json({
|
||||
data: [{ id: "msg_projected", type: "user", text: "already visible", time: { created: 1 } }],
|
||||
cursor: {},
|
||||
})
|
||||
if (url.pathname === `/api/session/${sessionID}/pending`)
|
||||
return json({
|
||||
data: [
|
||||
{
|
||||
admittedSeq: 1,
|
||||
id: "msg_projected",
|
||||
sessionID,
|
||||
timeCreated: 1,
|
||||
type: "user",
|
||||
data: { text: "already visible" },
|
||||
delivery: "steer",
|
||||
},
|
||||
{
|
||||
admittedSeq: 2,
|
||||
id: "msg_pending_user",
|
||||
sessionID,
|
||||
timeCreated: 2,
|
||||
type: "user",
|
||||
data: { text: "queued user", metadata: { source: "test" } },
|
||||
delivery: "queue",
|
||||
},
|
||||
{
|
||||
admittedSeq: 3,
|
||||
id: "msg_pending_synthetic",
|
||||
sessionID,
|
||||
timeCreated: 3,
|
||||
type: "synthetic",
|
||||
data: { text: "internal", description: "Queued synthetic" },
|
||||
delivery: "steer",
|
||||
},
|
||||
{
|
||||
admittedSeq: 4,
|
||||
id: "msg_pending_compaction",
|
||||
sessionID,
|
||||
timeCreated: 4,
|
||||
type: "compaction",
|
||||
},
|
||||
],
|
||||
})
|
||||
}, events)
|
||||
let data!: ReturnType<typeof useData>
|
||||
|
||||
function Probe() {
|
||||
data = useData()
|
||||
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 data.session.message.refresh(sessionID)
|
||||
expect(data.session.message.ids(sessionID)).toEqual([
|
||||
"msg_projected",
|
||||
"msg_pending_user",
|
||||
"msg_pending_synthetic",
|
||||
"msg_pending_compaction",
|
||||
])
|
||||
expect(data.session.input.list(sessionID)).toEqual(["msg_pending_user", "msg_pending_synthetic"])
|
||||
expect(data.session.message.get(sessionID, "msg_pending_user")).toMatchObject({
|
||||
id: "msg_pending_user",
|
||||
type: "user",
|
||||
text: "queued user",
|
||||
metadata: { source: "test" },
|
||||
time: { created: 2 },
|
||||
})
|
||||
expect(data.session.message.get(sessionID, "msg_pending_synthetic")).toMatchObject({
|
||||
id: "msg_pending_synthetic",
|
||||
type: "synthetic",
|
||||
text: "internal",
|
||||
description: "Queued synthetic",
|
||||
time: { created: 3 },
|
||||
})
|
||||
expect(data.session.message.get(sessionID, "msg_pending_compaction")).toMatchObject({
|
||||
id: "msg_pending_compaction",
|
||||
type: "compaction",
|
||||
status: "running",
|
||||
summary: "",
|
||||
recent: "",
|
||||
time: { created: 4 },
|
||||
})
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
test("projects live instruction updates with their message ID", async () => {
|
||||
const events = createEventStream()
|
||||
const calls = createFetch(undefined, events)
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ export function createFetch(override?: FetchHandler, events?: ReturnType<typeof
|
|||
return json({ location: { directory, project: { id: "proj_test", directory: worktree } }, data: [] })
|
||||
if (url.pathname === "/api/session") return json({ data: [], cursor: {} })
|
||||
if (url.pathname === "/api/session/active") return json({ data: {} })
|
||||
if (/^\/api\/session\/[^/]+\/pending$/.test(url.pathname)) return json({ data: [] })
|
||||
if (url.pathname === "/api/permission/request")
|
||||
return json({ location: { directory, project: { id: "proj_test", directory: worktree } }, data: [] })
|
||||
if (url.pathname === "/api/form/request")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue