fix(tui): restore queued compaction indicator (#36440)
This commit is contained in:
parent
c7ceccf869
commit
5945a8d429
5 changed files with 181 additions and 1 deletions
|
|
@ -63,6 +63,7 @@ type Data = {
|
||||||
status: Record<string, DataSessionStatus>
|
status: Record<string, DataSessionStatus>
|
||||||
message: Record<string, SessionMessageInfo[]>
|
message: Record<string, SessionMessageInfo[]>
|
||||||
input: Record<string, string[]>
|
input: Record<string, string[]>
|
||||||
|
compaction: Record<string, string[]>
|
||||||
permission: Record<string, PermissionV2Request[]>
|
permission: Record<string, PermissionV2Request[]>
|
||||||
// Pending forms keyed by owner: a session ID or the temporary "global" elicitation sentinel.
|
// Pending forms keyed by owner: a session ID or the temporary "global" elicitation sentinel.
|
||||||
form: Record<string, FormWithLocation[]>
|
form: Record<string, FormWithLocation[]>
|
||||||
|
|
@ -91,6 +92,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||||
status: {},
|
status: {},
|
||||||
message: {},
|
message: {},
|
||||||
input: {},
|
input: {},
|
||||||
|
compaction: {},
|
||||||
permission: {},
|
permission: {},
|
||||||
form: {},
|
form: {},
|
||||||
},
|
},
|
||||||
|
|
@ -112,6 +114,21 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||||
setStore("session", "status", sessionID, status)
|
setStore("session", "status", sessionID, status)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addCompaction(sessionID: string, inputID: string) {
|
||||||
|
if (store.session.compaction[sessionID]?.includes(inputID)) return
|
||||||
|
setStore("session", "compaction", sessionID, [...(store.session.compaction[sessionID] ?? []), inputID])
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeCompaction(sessionID: string, inputID?: string) {
|
||||||
|
if (!inputID || !store.session.compaction[sessionID]?.includes(inputID)) return
|
||||||
|
setStore(
|
||||||
|
"session",
|
||||||
|
"compaction",
|
||||||
|
sessionID,
|
||||||
|
store.session.compaction[sessionID].filter((id) => id !== inputID),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const message = {
|
const message = {
|
||||||
update(sessionID: string, fn: (messages: SessionMessageInfo[], index: Map<string, number>) => void) {
|
update(sessionID: string, fn: (messages: SessionMessageInfo[], index: Map<string, number>) => void) {
|
||||||
setStore(
|
setStore(
|
||||||
|
|
@ -221,6 +238,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||||
delete draft.status[sessionID]
|
delete draft.status[sessionID]
|
||||||
delete draft.message[sessionID]
|
delete draft.message[sessionID]
|
||||||
delete draft.input[sessionID]
|
delete draft.input[sessionID]
|
||||||
|
delete draft.compaction[sessionID]
|
||||||
delete draft.permission[sessionID]
|
delete draft.permission[sessionID]
|
||||||
delete draft.form[sessionID]
|
delete draft.form[sessionID]
|
||||||
for (const [rootID, family] of Object.entries(draft.family)) {
|
for (const [rootID, family] of Object.entries(draft.family)) {
|
||||||
|
|
@ -611,8 +629,10 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||||
setSessionStatus(event.data.sessionID, "running")
|
setSessionStatus(event.data.sessionID, "running")
|
||||||
break
|
break
|
||||||
case "session.compaction.admitted":
|
case "session.compaction.admitted":
|
||||||
|
addCompaction(event.data.sessionID, event.data.inputID)
|
||||||
break
|
break
|
||||||
case "session.compaction.started":
|
case "session.compaction.started":
|
||||||
|
removeCompaction(event.data.sessionID, event.data.inputID)
|
||||||
message.update(event.data.sessionID, (draft, index) => {
|
message.update(event.data.sessionID, (draft, index) => {
|
||||||
message.append(draft, index, {
|
message.append(draft, index, {
|
||||||
id: event.data.inputID ?? messageIDFromEvent(event.id),
|
id: event.data.inputID ?? messageIDFromEvent(event.id),
|
||||||
|
|
@ -689,6 +709,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case "session.compaction.failed":
|
case "session.compaction.failed":
|
||||||
|
removeCompaction(event.data.sessionID, event.data.inputID)
|
||||||
message.update(event.data.sessionID, (draft, index) => {
|
message.update(event.data.sessionID, (draft, index) => {
|
||||||
const position = draft.findLastIndex((item) => item.type === "compaction" && item.status === "running")
|
const position = draft.findLastIndex((item) => item.type === "compaction" && item.status === "running")
|
||||||
const current = draft[position]
|
const current = draft[position]
|
||||||
|
|
@ -821,6 +842,24 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||||
return store.session.input[sessionID]?.includes(inputID) ?? false
|
return store.session.input[sessionID]?.includes(inputID) ?? false
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
compaction: {
|
||||||
|
list(sessionID: string) {
|
||||||
|
return store.session.compaction[sessionID] ?? []
|
||||||
|
},
|
||||||
|
async refresh(sessionID: string) {
|
||||||
|
if (!store.session.compaction[sessionID]) setStore("session", "compaction", sessionID, [])
|
||||||
|
setStore(
|
||||||
|
"session",
|
||||||
|
"compaction",
|
||||||
|
sessionID,
|
||||||
|
reconcile(
|
||||||
|
(await sdk.api.session.pending.list({ sessionID }))
|
||||||
|
.filter((item) => item.type === "compaction")
|
||||||
|
.map((item) => item.id),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
async refresh(sessionID: string) {
|
async refresh(sessionID: string) {
|
||||||
setStore("session", "info", sessionID, await sdk.api.session.get({ sessionID }))
|
setStore("session", "info", sessionID, await sdk.api.session.get({ sessionID }))
|
||||||
registerSession(sessionID)
|
registerSession(sessionID)
|
||||||
|
|
@ -1098,9 +1137,14 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||||
sdk.event.listen(({ details }) => {
|
sdk.event.listen(({ details }) => {
|
||||||
if (details.type === "server.connected") {
|
if (details.type === "server.connected") {
|
||||||
const messages = connected ? Object.keys(store.session.message) : []
|
const messages = connected ? Object.keys(store.session.message) : []
|
||||||
|
const compactions = connected ? Object.keys(store.session.compaction) : []
|
||||||
connected = true
|
connected = true
|
||||||
refreshActive()
|
refreshActive()
|
||||||
void Promise.allSettled([bootstrap(), ...messages.map(result.session.message.refresh)])
|
void Promise.allSettled([
|
||||||
|
bootstrap(),
|
||||||
|
...messages.map(result.session.message.refresh),
|
||||||
|
...compactions.map(result.session.compaction.refresh),
|
||||||
|
])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
handleEvent(details)
|
handleEvent(details)
|
||||||
|
|
|
||||||
|
|
@ -1059,6 +1059,9 @@ function SessionRowView(props: { row: SessionRow; message: (messageID: string) =
|
||||||
<Show when={props.message(row().messageID)}>{(message) => <SessionMessageView message={message()} />}</Show>
|
<Show when={props.message(row().messageID)}>{(message) => <SessionMessageView message={message()} />}</Show>
|
||||||
)}
|
)}
|
||||||
</Match>
|
</Match>
|
||||||
|
<Match when={props.row.type === "compaction-queued"}>
|
||||||
|
<CompactionQueued />
|
||||||
|
</Match>
|
||||||
<Match when={props.row.type === "part" ? props.row : undefined}>
|
<Match when={props.row.type === "part" ? props.row : undefined}>
|
||||||
{(row) => <SessionPartView partRef={row().ref} message={props.message} />}
|
{(row) => <SessionPartView partRef={row().ref} message={props.message} />}
|
||||||
</Match>
|
</Match>
|
||||||
|
|
@ -1370,6 +1373,20 @@ function CompactionMessage(props: { message: Extract<SessionMessageInfo, { type:
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function CompactionQueued() {
|
||||||
|
const { theme } = useTheme()
|
||||||
|
return (
|
||||||
|
<box flexDirection="row" alignItems="center">
|
||||||
|
<box border={["top"]} borderColor={theme.border} flexGrow={1} />
|
||||||
|
<box flexDirection="row" gap={1} paddingLeft={1} paddingRight={1}>
|
||||||
|
<text fg={theme.textMuted}>◇</text>
|
||||||
|
<text fg={theme.textMuted}>Compaction queued</text>
|
||||||
|
</box>
|
||||||
|
<box border={["top"]} borderColor={theme.border} flexGrow={1} />
|
||||||
|
</box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function statusLabel(status: "added" | "modified" | "deleted") {
|
function statusLabel(status: "added" | "modified" | "deleted") {
|
||||||
if (status === "added") return "A"
|
if (status === "added") return "A"
|
||||||
if (status === "deleted") return "D"
|
if (status === "deleted") return "D"
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ export type PartRef = {
|
||||||
|
|
||||||
export type SessionRow =
|
export type SessionRow =
|
||||||
| { type: "message"; messageID: string }
|
| { type: "message"; messageID: string }
|
||||||
|
| { type: "compaction-queued"; inputID: string }
|
||||||
| { type: "part"; ref: PartRef }
|
| { type: "part"; ref: PartRef }
|
||||||
| {
|
| {
|
||||||
type: "group"
|
type: "group"
|
||||||
|
|
@ -31,6 +32,14 @@ export function createSessionRows(sessionID: Accessor<string>) {
|
||||||
const boundary = revertBoundary()
|
const boundary = revertBoundary()
|
||||||
const rows = reduceSessionRows(boundary ? messages.filter((message) => message.id < boundary) : messages, inputs)
|
const rows = reduceSessionRows(boundary ? messages.filter((message) => message.id < boundary) : messages, inputs)
|
||||||
partitionPending(rows, pendingPermissions())
|
partitionPending(rows, pendingPermissions())
|
||||||
|
const position = rows.findIndex((row) => row.type === "message" && inputs.has(row.messageID))
|
||||||
|
rows.splice(
|
||||||
|
position === -1 ? rows.length : position,
|
||||||
|
0,
|
||||||
|
...data.session.compaction
|
||||||
|
.list(sessionID())
|
||||||
|
.map((inputID): SessionRow => ({ type: "compaction-queued", inputID })),
|
||||||
|
)
|
||||||
return rows
|
return rows
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -54,6 +63,7 @@ export function createSessionRows(sessionID: Accessor<string>) {
|
||||||
createEffect(
|
createEffect(
|
||||||
on(sessionID, (id) => {
|
on(sessionID, (id) => {
|
||||||
setRows(reconcile(reduce()))
|
setRows(reconcile(reduce()))
|
||||||
|
void data.session.compaction.refresh(id).catch(() => undefined)
|
||||||
void data.session.message.refresh(id).then(
|
void data.session.message.refresh(id).then(
|
||||||
() => {
|
() => {
|
||||||
if (sessionID() !== id) return
|
if (sessionID() !== id) return
|
||||||
|
|
@ -71,6 +81,13 @@ export function createSessionRows(sessionID: Accessor<string>) {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
createEffect(
|
||||||
|
on(
|
||||||
|
() => data.session.compaction.list(sessionID()).map((inputID) => inputID),
|
||||||
|
() => setRows(reconcile(reduce())),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
() =>
|
() =>
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,7 @@ test("session lifecycle updates the terminal title and prints the epilogue after
|
||||||
})
|
})
|
||||||
if (url.pathname === "/api/session/dummy") return json({ data: session })
|
if (url.pathname === "/api/session/dummy") return json({ data: session })
|
||||||
if (url.pathname === "/api/session/dummy/message") return json({ data: [], cursor: {} })
|
if (url.pathname === "/api/session/dummy/message") return json({ data: [], cursor: {} })
|
||||||
|
if (url.pathname === "/api/session/dummy/pending") return json({ data: [] })
|
||||||
if (url.pathname === "/api/session/dummy/permission") return json({ data: [] })
|
if (url.pathname === "/api/session/dummy/permission") return json({ data: [] })
|
||||||
}, events)
|
}, events)
|
||||||
const server = Bun.serve({ port: 0, fetch: (request) => calls.fetch(request) })
|
const server = Bun.serve({ port: 0, fetch: (request) => calls.fetch(request) })
|
||||||
|
|
|
||||||
|
|
@ -1047,6 +1047,14 @@ test("tracks session status from active sessions and execution events", async ()
|
||||||
await wait(() => data.session.status("session-retry") === "idle")
|
await wait(() => data.session.status("session-retry") === "idle")
|
||||||
expect(data.session.message.get("session-retry", "message-retry")).not.toHaveProperty("retry")
|
expect(data.session.message.get("session-retry", "message-retry")).not.toHaveProperty("retry")
|
||||||
|
|
||||||
|
emitEvent(events, {
|
||||||
|
id: "evt_manual_compaction_admitted",
|
||||||
|
created: 0,
|
||||||
|
type: "session.compaction.admitted",
|
||||||
|
durable: durable("session-manual", 1),
|
||||||
|
data: { sessionID: "session-manual", inputID: "message-compaction" },
|
||||||
|
})
|
||||||
|
await wait(() => data.session.compaction.list("session-manual").includes("message-compaction"))
|
||||||
emitEvent(events, {
|
emitEvent(events, {
|
||||||
id: "evt_manual_compaction_started",
|
id: "evt_manual_compaction_started",
|
||||||
created: 1,
|
created: 1,
|
||||||
|
|
@ -1064,6 +1072,7 @@ test("tracks session status from active sessions and execution events", async ()
|
||||||
const message = data.session.message.get("session-manual", "message-compaction")
|
const message = data.session.message.get("session-manual", "message-compaction")
|
||||||
return message?.type === "compaction" && message.status === "running" && message.summary === "Streamed summary"
|
return message?.type === "compaction" && message.status === "running" && message.summary === "Streamed summary"
|
||||||
})
|
})
|
||||||
|
expect(data.session.compaction.list("session-manual")).toEqual([])
|
||||||
const compactionRow = manualRows.find(
|
const compactionRow = manualRows.find(
|
||||||
(row) => row.type === "message" && row.messageID === "message-compaction",
|
(row) => row.type === "message" && row.messageID === "message-compaction",
|
||||||
)
|
)
|
||||||
|
|
@ -1137,6 +1146,98 @@ test("tracks session status from active sessions and execution events", async ()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("restores queued compaction from durable pending input", async () => {
|
||||||
|
const events = createEventStream()
|
||||||
|
const sessionID = "session-compaction-queued"
|
||||||
|
let pending = [
|
||||||
|
{
|
||||||
|
admittedSeq: 3,
|
||||||
|
id: "message-compaction-queued",
|
||||||
|
sessionID,
|
||||||
|
timeCreated: 1,
|
||||||
|
type: "compaction" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
admittedSeq: 4,
|
||||||
|
id: "message-compaction-later",
|
||||||
|
sessionID,
|
||||||
|
timeCreated: 2,
|
||||||
|
type: "compaction" as const,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
const calls = createFetch((url) => {
|
||||||
|
if (url.pathname !== `/api/session/${sessionID}/pending`) return
|
||||||
|
return json({ data: pending })
|
||||||
|
}, events)
|
||||||
|
let data!: ReturnType<typeof useData>
|
||||||
|
let rows!: ReturnType<typeof createSessionRows>
|
||||||
|
|
||||||
|
function Probe() {
|
||||||
|
data = useData()
|
||||||
|
rows = createSessionRows(() => sessionID)
|
||||||
|
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 wait(() => data.session.compaction.list(sessionID).length === 2)
|
||||||
|
expect(data.session.compaction.list(sessionID)).toEqual([
|
||||||
|
"message-compaction-queued",
|
||||||
|
"message-compaction-later",
|
||||||
|
])
|
||||||
|
await wait(() => rows.filter((row) => row.type === "compaction-queued").length === 2)
|
||||||
|
expect(rows.filter((row) => row.type === "compaction-queued")).toEqual([
|
||||||
|
{ type: "compaction-queued", inputID: "message-compaction-queued" },
|
||||||
|
{ type: "compaction-queued", inputID: "message-compaction-later" },
|
||||||
|
])
|
||||||
|
|
||||||
|
emitEvent(events, {
|
||||||
|
id: "evt_compaction_started",
|
||||||
|
created: 2,
|
||||||
|
type: "session.compaction.started",
|
||||||
|
durable: durable(sessionID, 4),
|
||||||
|
data: {
|
||||||
|
sessionID,
|
||||||
|
reason: "manual",
|
||||||
|
recent: "",
|
||||||
|
inputID: "message-compaction-queued",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await wait(() => data.session.compaction.list(sessionID).length === 1)
|
||||||
|
expect(data.session.compaction.list(sessionID)).toEqual(["message-compaction-later"])
|
||||||
|
|
||||||
|
emitEvent(events, {
|
||||||
|
id: "evt_compaction_ended",
|
||||||
|
created: 3,
|
||||||
|
type: "session.compaction.ended",
|
||||||
|
durable: durable(sessionID, 5),
|
||||||
|
data: { sessionID, reason: "manual", text: "Summary", recent: "" },
|
||||||
|
})
|
||||||
|
expect(data.session.compaction.list(sessionID)).toEqual(["message-compaction-later"])
|
||||||
|
|
||||||
|
pending = []
|
||||||
|
emitEvent(events, {
|
||||||
|
id: "evt_reconnected",
|
||||||
|
type: "server.connected",
|
||||||
|
data: {},
|
||||||
|
})
|
||||||
|
await wait(() => data.session.compaction.list(sessionID).length === 0)
|
||||||
|
} finally {
|
||||||
|
app.renderer.destroy()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
test("refreshes integrations after integration updates", async () => {
|
test("refreshes integrations after integration updates", async () => {
|
||||||
const events = createEventStream()
|
const events = createEventStream()
|
||||||
const requests = { integration: 0, model: 0, provider: 0 }
|
const requests = { integration: 0, model: 0, provider: 0 }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue