diff --git a/apps/mobile/src/features/threads/PendingUserInputCard.tsx b/apps/mobile/src/features/threads/PendingUserInputCard.tsx index 8fe7fc186adb..762e00e42893 100644 --- a/apps/mobile/src/features/threads/PendingUserInputCard.tsx +++ b/apps/mobile/src/features/threads/PendingUserInputCard.tsx @@ -66,6 +66,8 @@ export interface PendingUserInputCardProps { customAnswer: string, ) => void; readonly onSubmit: () => Promise; + /** Closes an async question without a reply. Hidden for native callback questions. */ + readonly onDismiss: () => Promise; } /** @@ -338,6 +340,18 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) { Submit answers + {props.pendingUserInput.dismissible ? ( + void props.onDismiss()} + > + + Dismiss without answering + + + ) : null} ) : null; return ( diff --git a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx index 3392b534b634..9b8024ba915f 100644 --- a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx @@ -158,6 +158,7 @@ export interface ThreadDetailScreenProps { customAnswer: string, ) => void; readonly onSubmitUserInput: () => Promise; + readonly onDismissUserInput: () => Promise; readonly showContent?: boolean; } @@ -909,6 +910,7 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread onSelectOption={props.onSelectUserInputOption} onChangeCustomAnswer={props.onChangeUserInputCustomAnswer} onSubmit={props.onSubmitUserInput} + onDismiss={props.onDismissUserInput} /> ) : null} diff --git a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx index a59fa1a4450c..f0569cdb92ca 100644 --- a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx @@ -826,6 +826,7 @@ function ThreadRouteContent( onSelectUserInputOption={requests.onSelectUserInputOption} onChangeUserInputCustomAnswer={requests.onChangeUserInputCustomAnswer} onSubmitUserInput={requests.onSubmitUserInput} + onDismissUserInput={requests.onDismissUserInput} /> diff --git a/apps/mobile/src/lib/threadActivity.test.ts b/apps/mobile/src/lib/threadActivity.test.ts index 03fb7118b553..68f779f06579 100644 --- a/apps/mobile/src/lib/threadActivity.test.ts +++ b/apps/mobile/src/lib/threadActivity.test.ts @@ -100,6 +100,7 @@ describe("pending user input answers", () => { { requestId: "interaction_1", createdAt: requested.createdAt, + dismissible: false, questions: [nativeQuestion, singleSelectQuestion], }, ]); diff --git a/apps/mobile/src/state/use-selected-thread-requests.ts b/apps/mobile/src/state/use-selected-thread-requests.ts index 1b5209dec319..e816ac70e5ac 100644 --- a/apps/mobile/src/state/use-selected-thread-requests.ts +++ b/apps/mobile/src/state/use-selected-thread-requests.ts @@ -73,6 +73,10 @@ export function useSelectedThreadRequests() { threadEnvironment.respondToUserInput, "thread user input response", ); + const dismissUserInput = useAtomCommand( + threadEnvironment.dismissUserInput, + "thread user input dismissal", + ); const { selectedThread: selectedThreadShell } = useThreadSelection(); const selectedThread = useSelectedThreadDetail(); const userInputDraftsByRequestKey = useAtomValue(userInputDraftsByRequestKeyAtom); @@ -170,6 +174,26 @@ export function useSelectedThreadRequests() { selectedThreadShell, ]); + // Closes an async question without messaging the agent. + const onDismissUserInput = useCallback(async () => { + if (!selectedThreadShell || !activePendingUserInput) { + return; + } + + setRespondingUserInputId(activePendingUserInput.requestId); + const result = await dismissUserInput({ + environmentId: selectedThreadShell.environmentId, + input: { + threadId: selectedThreadShell.id, + requestId: activePendingUserInput.requestId, + }, + }); + setRespondingUserInputId((current) => + current === activePendingUserInput.requestId ? null : current, + ); + return result; + }, [activePendingUserInput, dismissUserInput, selectedThreadShell]); + return { activePendingApproval, activePendingUserInput, @@ -181,5 +205,6 @@ export function useSelectedThreadRequests() { onSelectUserInputOption, onChangeUserInputCustomAnswer, onSubmitUserInput, + onDismissUserInput, }; } diff --git a/apps/server/src/orchestration/Layers/OrchestrationEngine.ts b/apps/server/src/orchestration/Layers/OrchestrationEngine.ts index 4350d145810c..6557888c38ae 100644 --- a/apps/server/src/orchestration/Layers/OrchestrationEngine.ts +++ b/apps/server/src/orchestration/Layers/OrchestrationEngine.ts @@ -215,7 +215,8 @@ const makeOrchestrationEngine = Effect.gen(function* () { // Command snapshots omit activities at startup and cap them while running. // Read this request's durable state before deciding how to send the answer. const userInputActivity = - envelope.command.type === "thread.user-input.respond" + envelope.command.type === "thread.user-input.respond" || + envelope.command.type === "thread.user-input.dismiss" ? yield* projectionSnapshotQuery.getUserInputActivity(envelope.command) : Option.none(); const eventBase = yield* decideOrchestrationCommand({ diff --git a/apps/server/src/orchestration/decider.ts b/apps/server/src/orchestration/decider.ts index 9762d9bfd6da..37ab4730fb79 100644 --- a/apps/server/src/orchestration/decider.ts +++ b/apps/server/src/orchestration/decider.ts @@ -1289,6 +1289,51 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" }; } + case "thread.user-input.dismiss": { + yield* requireThread({ + readModel, + command, + threadId: command.threadId, + }); + const request = userInputActivity; + if (request === undefined || request.kind !== "user-input.requested") { + return yield* new OrchestrationCommandInvariantError({ + commandType: command.type, + detail: "This question has already been answered.", + }); + } + // Only async questions can be dropped silently. A native callback + // question leaves the provider blocked until it gets a reply, so it + // still needs an answer or an interrupted turn. + if (!Predicate.isObject(request.payload) || request.payload.responseMode !== "message") { + return yield* new OrchestrationCommandInvariantError({ + commandType: command.type, + detail: "This question needs an answer. Answer it or stop the turn.", + }); + } + return { + ...(yield* withEventBase({ + aggregateKind: "thread", + aggregateId: command.threadId, + occurredAt: command.createdAt, + commandId: command.commandId, + })), + type: "thread.activity-appended", + payload: { + threadId: command.threadId, + activity: { + id: EventId.make(`async-dismiss:${command.requestId}`), + kind: "user-input.resolved", + summary: "User input dismissed", + tone: "info", + turnId: request.turnId, + createdAt: command.createdAt, + payload: { requestId: command.requestId, responseMode: "message" }, + }, + }, + }; + } + case "thread.checkpoint.revert": { yield* requireThread({ readModel, diff --git a/apps/server/src/orchestration/decider.userInputDismiss.test.ts b/apps/server/src/orchestration/decider.userInputDismiss.test.ts new file mode 100644 index 000000000000..5c7b495bc245 --- /dev/null +++ b/apps/server/src/orchestration/decider.userInputDismiss.test.ts @@ -0,0 +1,142 @@ +import { + CommandId, + EventId, + ProjectId, + ProviderInstanceId, + ThreadId, + ApprovalRequestId, + type OrchestrationReadModel, + type OrchestrationThreadActivity, +} from "@t3tools/contracts"; +import * as NodeServices from "@effect/platform-node/NodeServices"; +import { expect, it } from "@effect/vitest"; +import * as Effect from "effect/Effect"; + +import { decideOrchestrationCommand } from "./decider.ts"; +import { projectEvent } from "./projector.ts"; + +const NOW = "2026-01-01T00:00:00.000Z"; +const threadId = ThreadId.make("thread-1"); +const requestId = ApprovalRequestId.make("question-1"); + +function makeRequest(responseMode: "message" | undefined): OrchestrationThreadActivity { + return { + id: EventId.make(requestId), + kind: "user-input.requested", + summary: "Question", + tone: "approval", + turnId: null, + createdAt: NOW, + payload: { + requestId, + ...(responseMode === undefined ? {} : { responseMode }), + questions: [{ id: "0", header: "Q", question: "Continue?", options: [] }], + }, + }; +} + +function makeReadModel( + activities: ReadonlyArray, +): OrchestrationReadModel { + return { + snapshotSequence: 0, + projects: [], + threads: [ + { + id: threadId, + projectId: ProjectId.make("project-1"), + title: "Thread", + modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.4" }, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + latestTurn: null, + createdAt: NOW, + updatedAt: NOW, + archivedAt: null, + settledOverride: null, + settledAt: null, + snoozedUntil: null, + snoozedAt: null, + pinnedAt: null, + deletedAt: null, + messages: [], + proposedPlans: [], + activities: [...activities], + checkpoints: [], + session: null, + }, + ], + updatedAt: NOW, + }; +} + +const command = { + type: "thread.user-input.dismiss" as const, + commandId: CommandId.make("dismiss-1"), + threadId, + requestId, + createdAt: NOW, +}; + +it.layer(NodeServices.layer)("user input dismiss decider", (it) => { + it.effect("closes an async question without sending a message or starting a turn", () => + Effect.gen(function* () { + const request = makeRequest("message"); + const readModel = makeReadModel([request]); + const result = yield* decideOrchestrationCommand({ + command, + readModel, + userInputActivity: request, + }); + const events = Array.isArray(result) ? result : [result]; + expect(events.map((event) => event.type)).toEqual(["thread.activity-appended"]); + expect(events[0]?.payload).toMatchObject({ + threadId, + activity: { + kind: "user-input.resolved", + summary: "User input dismissed", + payload: { requestId, responseMode: "message" }, + }, + }); + const projected = yield* projectEvent(readModel, { ...events[0]!, sequence: 1 }); + expect(projected.threads[0]?.messages).toEqual([]); + expect(projected.threads[0]?.latestTurn).toBeNull(); + }), + ); + + it.effect("rejects dismissing a native callback question", () => + Effect.gen(function* () { + const request = makeRequest(undefined); + const result = yield* decideOrchestrationCommand({ + command, + readModel: makeReadModel([request]), + userInputActivity: request, + }).pipe(Effect.flip); + expect(result).toMatchObject({ + _tag: "OrchestrationCommandInvariantError", + detail: "This question needs an answer. Answer it or stop the turn.", + }); + }), + ); + + it.effect("rejects dismissing a question that was already resolved", () => + Effect.gen(function* () { + const resolved: OrchestrationThreadActivity = { + ...makeRequest("message"), + id: EventId.make("resolved"), + kind: "user-input.resolved", + }; + const result = yield* decideOrchestrationCommand({ + command, + readModel: makeReadModel([makeRequest("message"), resolved]), + userInputActivity: resolved, + }).pipe(Effect.flip); + expect(result).toMatchObject({ + _tag: "OrchestrationCommandInvariantError", + detail: "This question has already been answered.", + }); + }), + ); +}); diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 2f2fe5a434eb..116536abccf2 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -1423,6 +1423,9 @@ export default function ChatView(props: ChatViewProps) { const respondToThreadUserInput = useAtomCommand(threadEnvironment.respondToUserInput, { reportFailure: false, }); + const dismissThreadUserInput = useAtomCommand(threadEnvironment.dismissUserInput, { + reportFailure: false, + }); const revertThreadCheckpoint = useAtomCommand(threadEnvironment.revertCheckpoint, { reportFailure: false, }); @@ -6960,6 +6963,32 @@ export default function ChatView(props: ChatViewProps) { [activeThreadId, environmentId, respondToThreadUserInput, setThreadError], ); + // Closes an async question without messaging the agent. The server records + // the dismissal so every client releases the composer. + const onDismissUserInput = useCallback( + async (requestId: ApprovalRequestId) => { + if (!activeThreadId) return; + + setRespondingUserInputRequestIds((existing) => + existing.includes(requestId) ? existing : [...existing, requestId], + ); + const result = await dismissThreadUserInput({ + environmentId, + input: { threadId: activeThreadId, requestId }, + }); + if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) { + const error = squashAtomCommandFailure(result); + setThreadError( + activeThreadId, + error instanceof Error ? error.message : "Failed to dismiss the question.", + ); + } + setRespondingUserInputRequestIds((existing) => existing.filter((id) => id !== requestId)); + return result; + }, + [activeThreadId, dismissThreadUserInput, environmentId, setThreadError], + ); + const setActivePendingUserInputQuestionIndex = useCallback( (nextQuestionIndex: number) => { if (!activePendingUserInput) { @@ -8055,6 +8084,7 @@ export default function ChatView(props: ChatViewProps) { onSelectActivePendingUserInputOption } onAdvanceActivePendingUserInput={onAdvanceActivePendingUserInput} + onDismissActivePendingUserInput={onDismissUserInput} onPreviousActivePendingUserInputQuestion={ onPreviousActivePendingUserInputQuestion } diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index 0a54a2a55abb..ce26f96515cc 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -1284,6 +1284,7 @@ export interface ChatComposerProps { ) => Promise; onSelectActivePendingUserInputOption: (questionId: string, optionValue: string) => void; onAdvanceActivePendingUserInput: () => void; + onDismissActivePendingUserInput: (requestId: ApprovalRequestId) => void; onPreviousActivePendingUserInputQuestion: () => void; onChangeActivePendingUserInputCustomAnswer: ( questionId: string, @@ -1384,6 +1385,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) onRespondToApproval, onSelectActivePendingUserInputOption, onAdvanceActivePendingUserInput, + onDismissActivePendingUserInput, onPreviousActivePendingUserInputQuestion, onChangeActivePendingUserInputCustomAnswer, onProviderModelSelect, @@ -4911,6 +4913,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) questionIndex={activePendingQuestionIndex} onToggleOption={onSelectActivePendingUserInputOption} onAdvance={onAdvanceActivePendingUserInput} + onDismiss={onDismissActivePendingUserInput} /> ) : !isComposerCollapsedMobile && showPlanFollowUpPrompt && activeProposedPlan ? ( {!isChoiceOnlyPendingQuestion || activePendingProgress?.activeQuestion?.multiSelect ? ( diff --git a/apps/web/src/components/chat/ComposerPendingUserInputPanel.test.tsx b/apps/web/src/components/chat/ComposerPendingUserInputPanel.test.tsx index 817182190b79..9b68cd438a2b 100644 --- a/apps/web/src/components/chat/ComposerPendingUserInputPanel.test.tsx +++ b/apps/web/src/components/chat/ComposerPendingUserInputPanel.test.tsx @@ -20,17 +20,19 @@ const prompt: PendingUserInput = { multiSelect: false, }, ], + dismissible: true, }; -function renderPanel() { +function renderPanel(pendingUserInput: PendingUserInput = prompt) { return renderToStaticMarkup( {}} onAdvance={() => {}} + onDismiss={() => {}} />, ); } @@ -50,6 +52,13 @@ describe("ComposerPendingUserInputPanel", () => { expect(markup).toMatch(new RegExp(`]*\\sid="${controlledId}"`)); }); + it("offers dismiss only for async questions", () => { + expect(renderPanel()).toContain("data-pending-user-input-dismiss"); + expect(renderPanel({ ...prompt, dismissible: false })).not.toContain( + "data-pending-user-input-dismiss", + ); + }); + it("starts expanded so the question and its options are visible", () => { const markup = renderPanel(); diff --git a/apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx b/apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx index 2a7df2488233..4803708d7943 100644 --- a/apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx +++ b/apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx @@ -17,6 +17,7 @@ interface PendingUserInputPanelProps { questionIndex: number; onToggleOption: (questionId: string, optionValue: string) => void; onAdvance: () => void; + onDismiss: (requestId: ApprovalRequestId) => void; } export const ComposerPendingUserInputPanel = memo(function ComposerPendingUserInputPanel({ @@ -26,6 +27,7 @@ export const ComposerPendingUserInputPanel = memo(function ComposerPendingUserIn questionIndex, onToggleOption, onAdvance, + onDismiss, }: PendingUserInputPanelProps) { if (pendingUserInputs.length === 0) return null; const activePrompt = pendingUserInputs[0]; @@ -40,6 +42,7 @@ export const ComposerPendingUserInputPanel = memo(function ComposerPendingUserIn questionIndex={questionIndex} onToggleOption={onToggleOption} onAdvance={onAdvance} + onDismiss={onDismiss} /> ); }); @@ -51,6 +54,7 @@ const ComposerPendingUserInputCard = memo(function ComposerPendingUserInputCard( questionIndex, onToggleOption, onAdvance, + onDismiss, }: { prompt: PendingUserInput; isResponding: boolean; @@ -58,6 +62,7 @@ const ComposerPendingUserInputCard = memo(function ComposerPendingUserInputCard( questionIndex: number; onToggleOption: (questionId: string, optionValue: string) => void; onAdvance: () => void; + onDismiss: (requestId: ApprovalRequestId) => void; }) { const progress = derivePendingUserInputProgress(prompt.questions, answers, questionIndex); const activeQuestion = progress.activeQuestion; @@ -198,6 +203,28 @@ const ComposerPendingUserInputCard = memo(function ComposerPendingUserInputCard( ) : null} + {prompt.dismissible ? ( + // Sits inside the trigger button, so stop the click from toggling + // the disclosure. Dismiss closes the question without a reply. + } + aria-label="Dismiss question without answering" + title="Dismiss question without answering" + disabled={isResponding} + data-pending-user-input-dismiss + onClick={(event) => { + event.preventDefault(); + event.stopPropagation(); + onDismiss(prompt.requestId); + }} + onKeyDown={(event) => { + if (event.key !== "Enter" && event.key !== " ") return; + event.preventDefault(); + event.stopPropagation(); + onDismiss(prompt.requestId); + }} + /> + ) : null} diff --git a/docs/user/providers-codex.md b/docs/user/providers-codex.md index 417287cc0032..085a8e55a257 100644 --- a/docs/user/providers-codex.md +++ b/docs/user/providers-codex.md @@ -54,7 +54,9 @@ contains a copied Codex setup. Use a fresh shadow directory and sign in again. Codex can ask a question and keep working. Answer it in the thread's question panel. The answer becomes a new message: it reaches the active turn, or starts another turn if Codex has finished. Unanswered questions survive reconnects. -This requires a Codex version that supports async questions. +If you do not want to answer, dismiss the question from its panel. Dismissing +closes it without sending anything to Codex. This requires a Codex version that +supports async questions. ## Approve app access diff --git a/packages/client-runtime/src/operations/commands.ts b/packages/client-runtime/src/operations/commands.ts index 9bf75c838a99..3118895ae182 100644 --- a/packages/client-runtime/src/operations/commands.ts +++ b/packages/client-runtime/src/operations/commands.ts @@ -50,6 +50,7 @@ export type StartThreadTurnInput = CommandInput<"thread.turn.start">; export type InterruptThreadTurnInput = CommandInput<"thread.turn.interrupt">; export type RespondToThreadApprovalInput = CommandInput<"thread.approval.respond">; export type RespondToThreadUserInputInput = CommandInput<"thread.user-input.respond">; +export type DismissThreadUserInputInput = CommandInput<"thread.user-input.dismiss">; export type RevertThreadCheckpointInput = CommandInput<"thread.checkpoint.revert">; export type StopThreadSessionInput = CommandInput<"thread.session.stop">; @@ -320,6 +321,17 @@ export const respondToThreadUserInput: (input: RespondToThreadUserInputInput) => }); }); +export const dismissThreadUserInput: (input: DismissThreadUserInputInput) => CommandEffect = + Effect.fn("EnvironmentCommands.dismissThreadUserInput")(function* (input) { + const metadata = yield* timestampedCommandMetadata(input); + return yield* dispatch({ + ...input, + type: "thread.user-input.dismiss", + commandId: metadata.commandId, + createdAt: metadata.createdAt, + }); + }); + export const revertThreadCheckpoint: (input: RevertThreadCheckpointInput) => CommandEffect = Effect.fn("EnvironmentCommands.revertThreadCheckpoint")(function* (input) { const metadata = yield* timestampedCommandMetadata(input); diff --git a/packages/client-runtime/src/pendingRequests.test.ts b/packages/client-runtime/src/pendingRequests.test.ts index 6ef4579044c2..6fc64fdc5d44 100644 --- a/packages/client-runtime/src/pendingRequests.test.ts +++ b/packages/client-runtime/src/pendingRequests.test.ts @@ -282,7 +282,25 @@ describe("pending questions", () => { payload: { requestId: "async-1", responseMode: "message", questions: [question] }, }), ]; - expect(derivePendingRequests(activities).userInputs[0]?.questions).toEqual([question]); + expect(derivePendingRequests(activities).userInputs[0]).toMatchObject({ + questions: [question], + dismissible: true, + }); + }); + + it("only marks async questions as dismissible", () => { + const question = { + id: "0", + header: "Question", + question: "Continue?", + options: [{ label: "Yes", description: "" }], + multiSelect: false, + }; + const native = makeActivity({ + kind: "user-input.requested", + payload: { requestId: "native-1", questions: [question] }, + }); + expect(derivePendingRequests([native]).userInputs[0]?.dismissible).toBe(false); }); it("preserves native choice values and the custom-answer restriction", () => { @@ -378,6 +396,7 @@ describe("pending questions", () => { { requestId: "req-user-input-1", createdAt: "2026-02-23T00:00:01.000Z", + dismissible: false, questions: [ { id: "sandbox_mode", diff --git a/packages/client-runtime/src/pendingRequests.ts b/packages/client-runtime/src/pendingRequests.ts index a94c49514e5d..54cac57e1af4 100644 --- a/packages/client-runtime/src/pendingRequests.ts +++ b/packages/client-runtime/src/pendingRequests.ts @@ -22,6 +22,8 @@ export interface PendingUserInput { readonly requestId: ApprovalRequestId; readonly createdAt: string; readonly questions: ReadonlyArray; + /** Async questions can be dismissed without a reply; native callbacks cannot. */ + readonly dismissible: boolean; } const isRequestId = Schema.is(ApprovalRequestId); @@ -160,7 +162,12 @@ export function derivePendingRequests(activities: ReadonlyArray( scheduler, concurrency, }), + dismissUserInput: createEnvironmentCommand(runtime, { + label: "environment-data:commands:thread:dismiss-user-input", + execute: (input: DismissThreadUserInputInput) => dismissThreadUserInput(input), + scheduler, + concurrency, + }), revertCheckpoint: createEnvironmentCommand(runtime, { label: "environment-data:commands:thread:revert-checkpoint", execute: (input: RevertThreadCheckpointInput) => revertThreadCheckpoint(input), diff --git a/packages/contracts/src/orchestration.ts b/packages/contracts/src/orchestration.ts index 37f5476fecc8..da4eac53d9e6 100644 --- a/packages/contracts/src/orchestration.ts +++ b/packages/contracts/src/orchestration.ts @@ -1033,6 +1033,17 @@ const ThreadUserInputRespondCommand = Schema.Struct({ createdAt: IsoDateTime, }); +// Closes an async question without answering it. The agent is not messaged; +// the composer is simply released. Native callback questions cannot be dismissed +// this way because the provider is blocked waiting on a reply. +const ThreadUserInputDismissCommand = Schema.Struct({ + type: Schema.Literal("thread.user-input.dismiss"), + commandId: CommandId, + threadId: ThreadId, + requestId: ApprovalRequestId, + createdAt: IsoDateTime, +}); + const ThreadCheckpointRevertCommand = Schema.Struct({ type: Schema.Literal("thread.checkpoint.revert"), commandId: CommandId, @@ -1077,6 +1088,7 @@ const DispatchableClientOrchestrationCommand = Schema.Union([ ThreadTurnInterruptCommand, ThreadApprovalRespondCommand, ThreadUserInputRespondCommand, + ThreadUserInputDismissCommand, ThreadCheckpointRevertCommand, ThreadSessionStopCommand, ]); @@ -1106,6 +1118,7 @@ export const ClientOrchestrationCommand = Schema.Union([ ThreadTurnInterruptCommand, ThreadApprovalRespondCommand, ThreadUserInputRespondCommand, + ThreadUserInputDismissCommand, ThreadCheckpointRevertCommand, ThreadSessionStopCommand, ]);