From d7ffc7fec19a4507730066e668118a354ff7f6c7 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 30 Jul 2026 16:13:05 -0400 Subject: [PATCH] refactor(session): simplify pending withdrawal --- .../src/context/server-session-v2-reducer.ts | 2 +- packages/client/src/effect/api/api.ts | 60 ++++++++-------- .../client/src/effect/generated/client.ts | 50 ++++++------- .../client/src/promise/generated/client.ts | 4 +- .../client/src/promise/generated/types.ts | 14 ++-- packages/core/src/session/pending.ts | 71 +++++++++++-------- packages/protocol/src/groups/session.ts | 32 ++++----- packages/tui/src/context/data.tsx | 26 ++++--- .../tui/src/routes/session/dialog-message.tsx | 1 - packages/tui/src/routes/session/index.tsx | 1 - packages/tui/src/routes/session/undo.ts | 3 +- packages/tui/test/cli/tui/undo.test.ts | 9 ++- 12 files changed, 143 insertions(+), 130 deletions(-) diff --git a/packages/app/src/context/server-session-v2-reducer.ts b/packages/app/src/context/server-session-v2-reducer.ts index 53dc389b1e..b3db3c3647 100644 --- a/packages/app/src/context/server-session-v2-reducer.ts +++ b/packages/app/src/context/server-session-v2-reducer.ts @@ -31,7 +31,7 @@ export function createV2SessionReducer() { return result([...source]) case "session.input.withdrawn": pending.delete(key(sessionID, event.data.inputID)) - return result([...source]) + return case "session.input.promoted": { const input = pending.get(key(sessionID, event.data.inputID)) pending.delete(key(sessionID, event.data.inputID)) diff --git a/packages/client/src/effect/api/api.ts b/packages/client/src/effect/api/api.ts index 731444a177..65375b4e82 100644 --- a/packages/client/src/effect/api/api.ts +++ b/packages/client/src/effect/api/api.ts @@ -249,44 +249,38 @@ export type Endpoint5_21Input = { readonly sessionID: Session.ID } export type Endpoint5_21Output = ReadonlyArray export type SessionPendingListOperation = (input: Endpoint5_21Input) => Effect.Effect -export type Endpoint5_22Input = { readonly sessionID: Session.ID; readonly inputID: SessionMessage.ID } -export type Endpoint5_22Output = boolean -export type SessionPendingWithdrawOperation = ( +export type Endpoint5_22Input = { readonly sessionID: Session.ID } +export type Endpoint5_22Output = ReadonlyArray +export type SessionInstructionsEntryListOperation = ( input: Endpoint5_22Input, ) => Effect.Effect -export type Endpoint5_23Input = { readonly sessionID: Session.ID } -export type Endpoint5_23Output = ReadonlyArray -export type SessionInstructionsEntryListOperation = ( - input: Endpoint5_23Input, -) => Effect.Effect - -export type Endpoint5_24Input = { +export type Endpoint5_23Input = { readonly sessionID: Session.ID readonly key: InstructionEntry.Key readonly value: Schema.Json } -export type Endpoint5_24Output = void +export type Endpoint5_23Output = void export type SessionInstructionsEntryPutOperation = ( + input: Endpoint5_23Input, +) => Effect.Effect + +export type Endpoint5_24Input = { readonly sessionID: Session.ID; readonly key: InstructionEntry.Key } +export type Endpoint5_24Output = void +export type SessionInstructionsEntryRemoveOperation = ( input: Endpoint5_24Input, ) => Effect.Effect -export type Endpoint5_25Input = { readonly sessionID: Session.ID; readonly key: InstructionEntry.Key } -export type Endpoint5_25Output = void -export type SessionInstructionsEntryRemoveOperation = ( - input: Endpoint5_25Input, -) => Effect.Effect +export type Endpoint5_25Input = { readonly sessionID: Session.ID; readonly prompt: string } +export type Endpoint5_25Output = { readonly text: string } +export type SessionGenerateOperation = (input: Endpoint5_25Input) => Effect.Effect -export type Endpoint5_26Input = { readonly sessionID: Session.ID; readonly prompt: string } -export type Endpoint5_26Output = { readonly text: string } -export type SessionGenerateOperation = (input: Endpoint5_26Input) => Effect.Effect - -export type Endpoint5_27Input = { +export type Endpoint5_26Input = { readonly sessionID: Session.ID readonly after?: Event.Seq | undefined readonly follow?: boolean | undefined } -export type Endpoint5_27Output = +export type Endpoint5_26Output = | ( | { readonly id: Event.ID @@ -842,19 +836,25 @@ export type Endpoint5_27Output = } ) | EventLog.Synced -export type SessionLogOperation = (input: Endpoint5_27Input) => Stream.Stream +export type SessionLogOperation = (input: Endpoint5_26Input) => Stream.Stream + +export type Endpoint5_27Input = { readonly sessionID: Session.ID } +export type Endpoint5_27Output = void +export type SessionInterruptOperation = (input: Endpoint5_27Input) => Effect.Effect export type Endpoint5_28Input = { readonly sessionID: Session.ID } export type Endpoint5_28Output = void -export type SessionInterruptOperation = (input: Endpoint5_28Input) => Effect.Effect +export type SessionBackgroundOperation = (input: Endpoint5_28Input) => Effect.Effect -export type Endpoint5_29Input = { readonly sessionID: Session.ID } -export type Endpoint5_29Output = void -export type SessionBackgroundOperation = (input: Endpoint5_29Input) => Effect.Effect +export type Endpoint5_29Input = { readonly sessionID: Session.ID; readonly messageID: SessionMessage.ID } +export type Endpoint5_29Output = SessionMessage.Info +export type SessionMessageOperation = (input: Endpoint5_29Input) => Effect.Effect -export type Endpoint5_30Input = { readonly sessionID: Session.ID; readonly messageID: SessionMessage.ID } -export type Endpoint5_30Output = SessionMessage.Info -export type SessionMessageOperation = (input: Endpoint5_30Input) => Effect.Effect +export type Endpoint5_30Input = { readonly sessionID: Session.ID; readonly inputID: SessionMessage.ID } +export type Endpoint5_30Output = boolean +export type SessionPendingWithdrawOperation = ( + input: Endpoint5_30Input, +) => Effect.Effect export interface SessionApi { readonly list: SessionListOperation diff --git a/packages/client/src/effect/generated/client.ts b/packages/client/src/effect/generated/client.ts index 4985abc549..b7540fed34 100644 --- a/packages/client/src/effect/generated/client.ts +++ b/packages/client/src/effect/generated/client.ts @@ -492,7 +492,7 @@ const Endpoint5_21 = (raw: RawClient["server.session"]) => (input: Endpoint5_21I const Endpoint5_22 = (raw: RawClient["server.session"]) => (input: Endpoint5_22Input) => preserveEffect()( - raw["session.pending.withdraw"]({ params: { sessionID: input["sessionID"], inputID: input["inputID"] } }).pipe( + raw["session.instructions.entry.list"]({ params: { sessionID: input["sessionID"] } }).pipe( Effect.mapError(mapClientError), Effect.map((value) => value.data), ), @@ -500,37 +500,29 @@ const Endpoint5_22 = (raw: RawClient["server.session"]) => (input: Endpoint5_22I const Endpoint5_23 = (raw: RawClient["server.session"]) => (input: Endpoint5_23Input) => preserveEffect()( - raw["session.instructions.entry.list"]({ params: { sessionID: input["sessionID"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ), - ) - -const Endpoint5_24 = (raw: RawClient["server.session"]) => (input: Endpoint5_24Input) => - preserveEffect()( raw["session.instructions.entry.put"]({ params: { sessionID: input["sessionID"], key: input["key"] }, payload: { value: input["value"] }, }).pipe(Effect.mapError(mapClientError)), ) -const Endpoint5_25 = (raw: RawClient["server.session"]) => (input: Endpoint5_25Input) => - preserveEffect()( +const Endpoint5_24 = (raw: RawClient["server.session"]) => (input: Endpoint5_24Input) => + preserveEffect()( raw["session.instructions.entry.remove"]({ params: { sessionID: input["sessionID"], key: input["key"] } }).pipe( Effect.mapError(mapClientError), ), ) -const Endpoint5_26 = (raw: RawClient["server.session"]) => (input: Endpoint5_26Input) => - preserveEffect()( +const Endpoint5_25 = (raw: RawClient["server.session"]) => (input: Endpoint5_25Input) => + preserveEffect()( raw["session.generate"]({ params: { sessionID: input["sessionID"] }, payload: { prompt: input["prompt"] } }).pipe( Effect.mapError(mapClientError), Effect.map((value) => value.data), ), ) -const Endpoint5_27 = (raw: RawClient["server.session"]) => (input: Endpoint5_27Input) => - preserveStream()( +const Endpoint5_26 = (raw: RawClient["server.session"]) => (input: Endpoint5_26Input) => + preserveStream()( Stream.unwrap( raw["session.log"]({ params: { sessionID: input["sessionID"] }, @@ -542,19 +534,27 @@ const Endpoint5_27 = (raw: RawClient["server.session"]) => (input: Endpoint5_27I ), ) +const Endpoint5_27 = (raw: RawClient["server.session"]) => (input: Endpoint5_27Input) => + preserveEffect()( + raw["session.interrupt"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)), + ) + const Endpoint5_28 = (raw: RawClient["server.session"]) => (input: Endpoint5_28Input) => preserveEffect()( - raw["session.interrupt"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)), + raw["session.background"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)), ) const Endpoint5_29 = (raw: RawClient["server.session"]) => (input: Endpoint5_29Input) => preserveEffect()( - raw["session.background"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)), + raw["session.message"]({ params: { sessionID: input["sessionID"], messageID: input["messageID"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), ) const Endpoint5_30 = (raw: RawClient["server.session"]) => (input: Endpoint5_30Input) => preserveEffect()( - raw["session.message"]({ params: { sessionID: input["sessionID"], messageID: input["messageID"] } }).pipe( + raw["session.pending.withdraw"]({ params: { sessionID: input["sessionID"], inputID: input["inputID"] } }).pipe( Effect.mapError(mapClientError), Effect.map((value) => value.data), ), @@ -580,13 +580,13 @@ const adaptGroup5 = (raw: RawClient["server.session"]) => ({ wait: Endpoint5_16(raw), revert: { stage: Endpoint5_17(raw), clear: Endpoint5_18(raw), commit: Endpoint5_19(raw) }, context: Endpoint5_20(raw), - pending: { list: Endpoint5_21(raw), withdraw: Endpoint5_22(raw) }, - instructions: { entry: { list: Endpoint5_23(raw), put: Endpoint5_24(raw), remove: Endpoint5_25(raw) } }, - generate: Endpoint5_26(raw), - log: Endpoint5_27(raw), - interrupt: Endpoint5_28(raw), - background: Endpoint5_29(raw), - message: Endpoint5_30(raw), + pending: { list: Endpoint5_21(raw), withdraw: Endpoint5_30(raw) }, + instructions: { entry: { list: Endpoint5_22(raw), put: Endpoint5_23(raw), remove: Endpoint5_24(raw) } }, + generate: Endpoint5_25(raw), + log: Endpoint5_26(raw), + interrupt: Endpoint5_27(raw), + background: Endpoint5_28(raw), + message: Endpoint5_29(raw), }) const Endpoint6_0 = (raw: RawClient["server.message"]) => (input: Endpoint6_0Input) => diff --git a/packages/client/src/promise/generated/client.ts b/packages/client/src/promise/generated/client.ts index 16a15c5162..6ca36522fb 100644 --- a/packages/client/src/promise/generated/client.ts +++ b/packages/client/src/promise/generated/client.ts @@ -54,8 +54,6 @@ import type { SessionContextOutput, SessionPendingListInput, SessionPendingListOutput, - SessionPendingWithdrawInput, - SessionPendingWithdrawOutput, SessionInstructionsEntryListInput, SessionInstructionsEntryListOutput, SessionInstructionsEntryPutInput, @@ -72,6 +70,8 @@ import type { SessionBackgroundOutput, SessionMessageInput, SessionMessageOutput, + SessionPendingWithdrawInput, + SessionPendingWithdrawOutput, MessageListInput, MessageListOutput, ModelListInput, diff --git a/packages/client/src/promise/generated/types.ts b/packages/client/src/promise/generated/types.ts index 1f12ed8caa..a8bd9e12a4 100644 --- a/packages/client/src/promise/generated/types.ts +++ b/packages/client/src/promise/generated/types.ts @@ -3170,13 +3170,6 @@ export type SessionPendingListInput = { readonly sessionID: { readonly sessionID export type SessionPendingListOutput = { data: Array }["data"] -export type SessionPendingWithdrawInput = { - readonly sessionID: { readonly sessionID: string; readonly inputID: string }["sessionID"] - readonly inputID: { readonly sessionID: string; readonly inputID: string }["inputID"] -} - -export type SessionPendingWithdrawOutput = { data: boolean }["data"] - export type SessionInstructionsEntryListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } export type SessionInstructionsEntryListOutput = { data: Array }["data"] @@ -3226,6 +3219,13 @@ export type SessionMessageInput = { export type SessionMessageOutput = { data: SessionMessageInfo }["data"] +export type SessionPendingWithdrawInput = { + readonly sessionID: { readonly sessionID: string; readonly inputID: string }["sessionID"] + readonly inputID: { readonly sessionID: string; readonly inputID: string }["inputID"] +} + +export type SessionPendingWithdrawOutput = { data: boolean }["data"] + export type MessageListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] readonly limit?: { diff --git a/packages/core/src/session/pending.ts b/packages/core/src/session/pending.ts index 2b7d7c303a..a54f5d2492 100644 --- a/packages/core/src/session/pending.ts +++ b/packages/core/src/session/pending.ts @@ -1,6 +1,6 @@ export * as SessionPending from "./pending" -import { and, asc, eq, or } from "drizzle-orm" +import { and, asc, eq, or, sql } from "drizzle-orm" import { DateTime, Effect, Schema } from "effect" import { Compaction, @@ -113,20 +113,24 @@ const admittedFromHistory = Effect.fn("SessionPending.admittedFromHistory")(func sessionID: SessionSchema.ID, id: SessionMessage.ID, ) { - const rows = yield* db + const row = yield* db .select() .from(EventTable) - .where(and(eq(EventTable.aggregate_id, sessionID), eq(EventTable.type, admittedEventType))) - .all() + .where( + and( + eq(EventTable.aggregate_id, sessionID), + eq(EventTable.type, admittedEventType), + sql`json_extract(${EventTable.data}, '$.inputID') = ${id}`, + ), + ) + .limit(1) + .get() .pipe(Effect.orDie) - for (const row of rows) { + if (row) { const decoded = decodeAdmittedEvent(row.data) - if (decoded._tag !== "Some" || decoded.value.inputID !== id) continue - const base = { - id, - sessionID, - timeCreated: DateTime.makeUnsafe(row.created), - } + if (decoded._tag !== "Some" || decoded.value.inputID !== id) + return yield* Effect.die(new LifecycleConflict({ id })) + const base = { id, sessionID, timeCreated: DateTime.makeUnsafe(row.created) } return decoded.value.input.type === "user" ? User.make({ ...base, ...decoded.value.input }) : Synthetic.make({ ...base, ...decoded.value.input }) @@ -164,16 +168,22 @@ const wasWithdrawn = Effect.fn("SessionPending.wasWithdrawn")(function* ( sessionID: SessionSchema.ID, id: SessionMessage.ID, ) { - const rows = yield* db + const row = yield* db .select({ data: EventTable.data }) .from(EventTable) - .where(and(eq(EventTable.aggregate_id, sessionID), eq(EventTable.type, withdrawnEventType))) - .all() + .where( + and( + eq(EventTable.aggregate_id, sessionID), + eq(EventTable.type, withdrawnEventType), + sql`json_extract(${EventTable.data}, '$.inputID') = ${id}`, + ), + ) + .limit(1) + .get() .pipe(Effect.orDie) - return rows.some((row) => { - const decoded = decodeWithdrawnEvent(row.data) - return decoded._tag === "Some" && decoded.value.inputID === id - }) + if (!row) return false + const decoded = decodeWithdrawnEvent(row.data) + return decoded._tag === "Some" && decoded.value.inputID === id }) export const admit = Effect.fn("SessionPending.admit")(function* ( @@ -341,14 +351,13 @@ export const projectCompactionAdmitted = Effect.fn("SessionPending.projectCompac * message insert inside the same event transaction; the deleted row is what * makes the table pending-only. */ -export const projectPromoted = Effect.fn("SessionPending.projectPromoted")(function* ( +const consumeInput = Effect.fn("SessionPending.consumeInput")(function* ( db: DatabaseService, input: { readonly id: SessionMessage.ID readonly sessionID: SessionSchema.ID }, ) { - if (yield* compaction(db, input.sessionID)) return yield* Effect.die(new LifecycleConflict({ id: input.id })) const deleted = yield* db .delete(SessionPendingTable) .where(and(eq(SessionPendingTable.id, input.id), eq(SessionPendingTable.session_id, input.sessionID))) @@ -361,6 +370,17 @@ export const projectPromoted = Effect.fn("SessionPending.projectPromoted")(funct return stored }) +export const projectPromoted = Effect.fn("SessionPending.projectPromoted")(function* ( + db: DatabaseService, + input: { + readonly id: SessionMessage.ID + readonly sessionID: SessionSchema.ID + }, +) { + if (yield* compaction(db, input.sessionID)) return yield* Effect.die(new LifecycleConflict({ id: input.id })) + return yield* consumeInput(db, input) +}) + export const projectWithdrawn = Effect.fn("SessionPending.projectWithdrawn")(function* ( db: DatabaseService, input: { @@ -368,16 +388,7 @@ export const projectWithdrawn = Effect.fn("SessionPending.projectWithdrawn")(fun readonly sessionID: SessionSchema.ID }, ) { - const deleted = yield* db - .delete(SessionPendingTable) - .where(and(eq(SessionPendingTable.id, input.id), eq(SessionPendingTable.session_id, input.sessionID))) - .returning() - .get() - .pipe(Effect.orDie) - if (!deleted) return yield* Effect.die(new LifecycleConflict({ id: input.id })) - const stored = fromRow(deleted) - if (stored.type === "compaction") return yield* Effect.die(new LifecycleConflict({ id: input.id })) - return stored + return yield* consumeInput(db, input) }) export const settleCompaction = Effect.fn("SessionPending.settleCompaction")(function* ( diff --git a/packages/protocol/src/groups/session.ts b/packages/protocol/src/groups/session.ts index 61eeff3bce..8524fe9e7f 100644 --- a/packages/protocol/src/groups/session.ts +++ b/packages/protocol/src/groups/session.ts @@ -494,22 +494,6 @@ export const makeSessionGroup = (sessionLo }), ), ) - .add( - HttpApiEndpoint.post("session.pending.withdraw", "/api/session/:sessionID/pending/:inputID/withdraw", { - params: { sessionID: Session.ID, inputID: SessionMessage.ID }, - success: Schema.Struct({ data: Schema.Boolean }), - error: SessionNotFoundError, - }) - .middleware(sessionLocationMiddleware) - .annotateMerge( - OpenApi.annotations({ - identifier: "v2.session.pending.withdraw", - summary: "Withdraw pending session input", - description: - "Withdraw one admitted input before promotion. Returns true when the input was withdrawn or had already been withdrawn, and false when it is no longer pending so callers can fall back to reverting projected history.", - }), - ), - ) .add( HttpApiEndpoint.get("session.instructions.entry.list", "/api/session/:sessionID/instructions/entries", { params: { sessionID: Session.ID }, @@ -644,6 +628,22 @@ export const makeSessionGroup = (sessionLo }), ), ) + .add( + HttpApiEndpoint.post("session.pending.withdraw", "/api/session/:sessionID/pending/:inputID/withdraw", { + params: { sessionID: Session.ID, inputID: SessionMessage.ID }, + success: Schema.Struct({ data: Schema.Boolean }), + error: SessionNotFoundError, + }) + .middleware(sessionLocationMiddleware) + .annotateMerge( + OpenApi.annotations({ + identifier: "v2.session.pending.withdraw", + summary: "Withdraw pending session input", + description: + "Withdraw one admitted input before promotion. Returns true when the input was withdrawn or had already been withdrawn, and false when it is no longer pending so callers can fall back to reverting projected history.", + }), + ), + ) .annotateMerge( OpenApi.annotations({ title: "session", diff --git a/packages/tui/src/context/data.tsx b/packages/tui/src/context/data.tsx index 1bd684c02c..a6dc6287b3 100644 --- a/packages/tui/src/context/data.tsx +++ b/packages/tui/src/context/data.tsx @@ -188,6 +188,12 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ index.set(item.id, messages.length) messages.push(item) }, + reindex(messages: SessionMessageInfo[], index: Map, start: number) { + for (let position = start; position < messages.length; position++) { + const item = messages[position] + if (item) index.set(item.id, position) + } + }, activeAssistant(messages: SessionMessageInfo[]) { const item = messages.findLast((item) => item.type === "assistant" && !item.time.completed) return item?.type === "assistant" ? item : undefined @@ -395,8 +401,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ existing.time.created = event.created draft.splice(position, 1) draft.push(existing) - index.clear() - draft.forEach((message, indexValue) => index.set(message.id, indexValue)) + message.reindex(draft, index, position) }) setStore( "session", @@ -445,15 +450,16 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ const position = index.get(event.data.inputID) if (position === undefined) return draft.splice(position, 1) - index.clear() - draft.forEach((message, indexValue) => index.set(message.id, indexValue)) + index.delete(event.data.inputID) + message.reindex(draft, index, position) }) - setStore( - "session", - "input", - event.data.sessionID, - (store.session.input[event.data.sessionID] ?? []).filter((id) => id !== event.data.inputID), - ) + if (store.session.input[event.data.sessionID]?.includes(event.data.inputID)) + setStore( + "session", + "input", + event.data.sessionID, + (store.session.input[event.data.sessionID] ?? []).filter((id) => id !== event.data.inputID), + ) break } case "session.instructions.updated": diff --git a/packages/tui/src/routes/session/dialog-message.tsx b/packages/tui/src/routes/session/dialog-message.tsx index 69c3e62cc4..5707fb5f8e 100644 --- a/packages/tui/src/routes/session/dialog-message.tsx +++ b/packages/tui/src/routes/session/dialog-message.tsx @@ -46,7 +46,6 @@ export function DialogMessage(props: { void undoMessage(client.api, { sessionID: props.sessionID, messageID: props.messageID, - pending: data.session.input.has(props.sessionID, props.messageID), }).catch((error) => toast.show({ message: errorMessage(error), variant: "error", duration: 5000 })) dialog.clear() }, diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index fbd24ea18c..92719c306c 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -594,7 +594,6 @@ export function Session() { void undoMessage(client.api, { sessionID: route.sessionID, messageID: message.id, - pending: data.session.input.has(route.sessionID, message.id), }).catch((error) => toast.show({ message: errorMessage(error), variant: "error", duration: 5000 })) prompt()?.set({ ...projectedPromptInput(message), diff --git a/packages/tui/src/routes/session/undo.ts b/packages/tui/src/routes/session/undo.ts index 6a65daa0ed..c32cdd3e7b 100644 --- a/packages/tui/src/routes/session/undo.ts +++ b/packages/tui/src/routes/session/undo.ts @@ -2,10 +2,9 @@ import type { OpenCodeClient } from "@opencode-ai/client" export function undoMessage( client: OpenCodeClient, - input: { readonly sessionID: string; readonly messageID: string; readonly pending: boolean }, + input: { readonly sessionID: string; readonly messageID: string }, ) { const revert = () => client.session.revert.stage(input).then(() => undefined) - if (!input.pending) return revert() return client.session.pending .withdraw({ sessionID: input.sessionID, inputID: input.messageID }) .then((withdrawn) => (withdrawn ? undefined : revert())) diff --git a/packages/tui/test/cli/tui/undo.test.ts b/packages/tui/test/cli/tui/undo.test.ts index a2e52e9996..5b38823e96 100644 --- a/packages/tui/test/cli/tui/undo.test.ts +++ b/packages/tui/test/cli/tui/undo.test.ts @@ -3,10 +3,9 @@ import { OpenCode } from "@opencode-ai/client" import { undoMessage } from "../../../src/routes/session/undo" test.each([ - { pending: true, withdrawn: true, expected: ["withdraw"] }, - { pending: true, withdrawn: false, expected: ["withdraw", "revert"] }, - { pending: false, withdrawn: false, expected: ["revert"] }, -])("routes undo for pending=$pending withdrawn=$withdrawn", async ({ pending, withdrawn, expected }) => { + { withdrawn: true, expected: ["withdraw"] }, + { withdrawn: false, expected: ["withdraw", "revert"] }, +])("routes undo for withdrawn=$withdrawn", async ({ withdrawn, expected }) => { const calls: string[] = [] const client = OpenCode.make({ baseUrl: "http://localhost:3000", @@ -21,7 +20,7 @@ test.each([ ), }) - await undoMessage(client, { sessionID: "ses_test", messageID: "msg_user", pending }) + await undoMessage(client, { sessionID: "ses_test", messageID: "msg_user" }) expect(calls).toEqual([...expected]) })