| author | |
| committer | |
| log | 7112697e8be2d6f231726a91eb565e00ddbeebec |
| tree | a5536a5c8ba180076b21abca9511fcc935cf4996 |
| parent | ec36176e4f25fac7e3c380f6a4b646116e4ccf3d |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>18 files changed, 369 insertions(+), 6 deletions(-)
apps/mobile/src/features/threads/PendingUserInputCard.tsx+14| ... | @@ -66,6 +66,8 @@ export interface PendingUserInputCardProps { | ... | @@ -66,6 +66,8 @@ export interface PendingUserInputCardProps { |
| 66 | customAnswer: string, | 66 | customAnswer: string, |
| 67 | ) => void; | 67 | ) => void; |
| 68 | readonly onSubmit: () => Promise<unknown>; | 68 | readonly onSubmit: () => Promise<unknown>; |
| 69 | /** Closes an async question without a reply. Hidden for native callback questions. */ | ||
| 70 | readonly onDismiss: () => Promise<unknown>; | ||
| 69 | } | 71 | } |
| 70 | 72 | ||
| 71 | /** | 73 | /** |
| ... | @@ -338,6 +340,18 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) { | ... | @@ -338,6 +340,18 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) { |
| 338 | Submit answers | 340 | Submit answers |
| 339 | </Text> | 341 | </Text> |
| 340 | </Pressable> | 342 | </Pressable> |
| 343 | {props.pendingUserInput.dismissible ? ( | ||
| 344 | <Pressable | ||
| 345 | accessibilityRole="button" | ||
| 346 | className="items-center justify-center rounded-2xl px-4 py-2.5 active:opacity-70" | ||
| 347 | disabled={props.respondingUserInputId === props.pendingUserInput.requestId} | ||
| 348 | onPress={() => void props.onDismiss()} | ||
| 349 | > | ||
| 350 | <Text className="font-t3-bold text-sm text-foreground-muted"> | ||
| 351 | Dismiss without answering | ||
| 352 | </Text> | ||
| 353 | </Pressable> | ||
| 354 | ) : null} | ||
| 341 | </Animated.View> | 355 | </Animated.View> |
| 342 | ) : null; | 356 | ) : null; |
| 343 | return ( | 357 | return ( |
apps/mobile/src/features/threads/ThreadDetailScreen.tsx+2| ... | @@ -158,6 +158,7 @@ export interface ThreadDetailScreenProps { | ... | @@ -158,6 +158,7 @@ export interface ThreadDetailScreenProps { |
| 158 | customAnswer: string, | 158 | customAnswer: string, |
| 159 | ) => void; | 159 | ) => void; |
| 160 | readonly onSubmitUserInput: () => Promise<unknown>; | 160 | readonly onSubmitUserInput: () => Promise<unknown>; |
| 161 | readonly onDismissUserInput: () => Promise<unknown>; | ||
| 161 | readonly showContent?: boolean; | 162 | readonly showContent?: boolean; |
| 162 | } | 163 | } |
| 163 | 164 | ||
| ... | @@ -909,6 +910,7 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread | ... | @@ -909,6 +910,7 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread |
| 909 | onSelectOption={props.onSelectUserInputOption} | 910 | onSelectOption={props.onSelectUserInputOption} |
| 910 | onChangeCustomAnswer={props.onChangeUserInputCustomAnswer} | 911 | onChangeCustomAnswer={props.onChangeUserInputCustomAnswer} |
| 911 | onSubmit={props.onSubmitUserInput} | 912 | onSubmit={props.onSubmitUserInput} |
| 913 | onDismiss={props.onDismissUserInput} | ||
| 912 | /> | 914 | /> |
| 913 | ) : null} | 915 | ) : null} |
| 914 | </Animated.View> | 916 | </Animated.View> |
apps/mobile/src/features/threads/ThreadRouteScreen.tsx+1| ... | @@ -826,6 +826,7 @@ function ThreadRouteContent( | ... | @@ -826,6 +826,7 @@ function ThreadRouteContent( |
| 826 | onSelectUserInputOption={requests.onSelectUserInputOption} | 826 | onSelectUserInputOption={requests.onSelectUserInputOption} |
| 827 | onChangeUserInputCustomAnswer={requests.onChangeUserInputCustomAnswer} | 827 | onChangeUserInputCustomAnswer={requests.onChangeUserInputCustomAnswer} |
| 828 | onSubmitUserInput={requests.onSubmitUserInput} | 828 | onSubmitUserInput={requests.onSubmitUserInput} |
| 829 | onDismissUserInput={requests.onDismissUserInput} | ||
| 829 | /> | 830 | /> |
| 830 | </View> | 831 | </View> |
| 831 | </> | 832 | </> |
apps/mobile/src/lib/threadActivity.test.ts+1| ... | @@ -100,6 +100,7 @@ describe("pending user input answers", () => { | ... | @@ -100,6 +100,7 @@ describe("pending user input answers", () => { |
| 100 | { | 100 | { |
| 101 | requestId: "interaction_1", | 101 | requestId: "interaction_1", |
| 102 | createdAt: requested.createdAt, | 102 | createdAt: requested.createdAt, |
| 103 | dismissible: false, | ||
| 103 | questions: [nativeQuestion, singleSelectQuestion], | 104 | questions: [nativeQuestion, singleSelectQuestion], |
| 104 | }, | 105 | }, |
| 105 | ]); | 106 | ]); |
apps/mobile/src/state/use-selected-thread-requests.ts+25| ... | @@ -73,6 +73,10 @@ export function useSelectedThreadRequests() { | ... | @@ -73,6 +73,10 @@ export function useSelectedThreadRequests() { |
| 73 | threadEnvironment.respondToUserInput, | 73 | threadEnvironment.respondToUserInput, |
| 74 | "thread user input response", | 74 | "thread user input response", |
| 75 | ); | 75 | ); |
| 76 | const dismissUserInput = useAtomCommand( | ||
| 77 | threadEnvironment.dismissUserInput, | ||
| 78 | "thread user input dismissal", | ||
| 79 | ); | ||
| 76 | const { selectedThread: selectedThreadShell } = useThreadSelection(); | 80 | const { selectedThread: selectedThreadShell } = useThreadSelection(); |
| 77 | const selectedThread = useSelectedThreadDetail(); | 81 | const selectedThread = useSelectedThreadDetail(); |
| 78 | const userInputDraftsByRequestKey = useAtomValue(userInputDraftsByRequestKeyAtom); | 82 | const userInputDraftsByRequestKey = useAtomValue(userInputDraftsByRequestKeyAtom); |
| ... | @@ -170,6 +174,26 @@ export function useSelectedThreadRequests() { | ... | @@ -170,6 +174,26 @@ export function useSelectedThreadRequests() { |
| 170 | selectedThreadShell, | 174 | selectedThreadShell, |
| 171 | ]); | 175 | ]); |
| 172 | 176 | ||
| 177 | // Closes an async question without messaging the agent. | ||
| 178 | const onDismissUserInput = useCallback(async () => { | ||
| 179 | if (!selectedThreadShell || !activePendingUserInput) { | ||
| 180 | return; | ||
| 181 | } | ||
| 182 | |||
| 183 | setRespondingUserInputId(activePendingUserInput.requestId); | ||
| 184 | const result = await dismissUserInput({ | ||
| 185 | environmentId: selectedThreadShell.environmentId, | ||
| 186 | input: { | ||
| 187 | threadId: selectedThreadShell.id, | ||
| 188 | requestId: activePendingUserInput.requestId, | ||
| 189 | }, | ||
| 190 | }); | ||
| 191 | setRespondingUserInputId((current) => | ||
| 192 | current === activePendingUserInput.requestId ? null : current, | ||
| 193 | ); | ||
| 194 | return result; | ||
| 195 | }, [activePendingUserInput, dismissUserInput, selectedThreadShell]); | ||
| 196 | |||
| 173 | return { | 197 | return { |
| 174 | activePendingApproval, | 198 | activePendingApproval, |
| 175 | activePendingUserInput, | 199 | activePendingUserInput, |
| ... | @@ -181,5 +205,6 @@ export function useSelectedThreadRequests() { | ... | @@ -181,5 +205,6 @@ export function useSelectedThreadRequests() { |
| 181 | onSelectUserInputOption, | 205 | onSelectUserInputOption, |
| 182 | onChangeUserInputCustomAnswer, | 206 | onChangeUserInputCustomAnswer, |
| 183 | onSubmitUserInput, | 207 | onSubmitUserInput, |
| 208 | onDismissUserInput, | ||
| 184 | }; | 209 | }; |
| 185 | } | 210 | } |
apps/server/src/orchestration/Layers/OrchestrationEngine.ts+2-1| ... | @@ -215,7 +215,8 @@ const makeOrchestrationEngine = Effect.gen(function* () { | ... | @@ -215,7 +215,8 @@ const makeOrchestrationEngine = Effect.gen(function* () { |
| 215 | // Command snapshots omit activities at startup and cap them while running. | 215 | // Command snapshots omit activities at startup and cap them while running. |
| 216 | // Read this request's durable state before deciding how to send the answer. | 216 | // Read this request's durable state before deciding how to send the answer. |
| 217 | const userInputActivity = | 217 | const userInputActivity = |
| 218 | envelope.command.type === "thread.user-input.respond" | 218 | envelope.command.type === "thread.user-input.respond" || |
| 219 | envelope.command.type === "thread.user-input.dismiss" | ||
| 219 | ? yield* projectionSnapshotQuery.getUserInputActivity(envelope.command) | 220 | ? yield* projectionSnapshotQuery.getUserInputActivity(envelope.command) |
| 220 | : Option.none(); | 221 | : Option.none(); |
| 221 | const eventBase = yield* decideOrchestrationCommand({ | 222 | const eventBase = yield* decideOrchestrationCommand({ |
apps/server/src/orchestration/decider.ts+45| ... | @@ -1289,6 +1289,51 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" | ... | @@ -1289,6 +1289,51 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" |
| 1289 | }; | 1289 | }; |
| 1290 | } | 1290 | } |
| 1291 | 1291 | ||
| 1292 | case "thread.user-input.dismiss": { | ||
| 1293 | yield* requireThread({ | ||
| 1294 | readModel, | ||
| 1295 | command, | ||
| 1296 | threadId: command.threadId, | ||
| 1297 | }); | ||
| 1298 | const request = userInputActivity; | ||
| 1299 | if (request === undefined || request.kind !== "user-input.requested") { | ||
| 1300 | return yield* new OrchestrationCommandInvariantError({ | ||
| 1301 | commandType: command.type, | ||
| 1302 | detail: "This question has already been answered.", | ||
| 1303 | }); | ||
| 1304 | } | ||
| 1305 | // Only async questions can be dropped silently. A native callback | ||
| 1306 | // question leaves the provider blocked until it gets a reply, so it | ||
| 1307 | // still needs an answer or an interrupted turn. | ||
| 1308 | if (!Predicate.isObject(request.payload) || request.payload.responseMode !== "message") { | ||
| 1309 | return yield* new OrchestrationCommandInvariantError({ | ||
| 1310 | commandType: command.type, | ||
| 1311 | detail: "This question needs an answer. Answer it or stop the turn.", | ||
| 1312 | }); | ||
| 1313 | } | ||
| 1314 | return { | ||
| 1315 | ...(yield* withEventBase({ | ||
| 1316 | aggregateKind: "thread", | ||
| 1317 | aggregateId: command.threadId, | ||
| 1318 | occurredAt: command.createdAt, | ||
| 1319 | commandId: command.commandId, | ||
| 1320 | })), | ||
| 1321 | type: "thread.activity-appended", | ||
| 1322 | payload: { | ||
| 1323 | threadId: command.threadId, | ||
| 1324 | activity: { | ||
| 1325 | id: EventId.make(`async-dismiss:${command.requestId}`), | ||
| 1326 | kind: "user-input.resolved", | ||
| 1327 | summary: "User input dismissed", | ||
| 1328 | tone: "info", | ||
| 1329 | turnId: request.turnId, | ||
| 1330 | createdAt: command.createdAt, | ||
| 1331 | payload: { requestId: command.requestId, responseMode: "message" }, | ||
| 1332 | }, | ||
| 1333 | }, | ||
| 1334 | }; | ||
| 1335 | } | ||
| 1336 | |||
| 1292 | case "thread.checkpoint.revert": { | 1337 | case "thread.checkpoint.revert": { |
| 1293 | yield* requireThread({ | 1338 | yield* requireThread({ |
| 1294 | readModel, | 1339 | readModel, |
apps/server/src/orchestration/decider.userInputDismiss.test.ts created+142| ... | @@ -0,0 +1,142 @@ | ||
| 1 | import { | ||
| 2 | CommandId, | ||
| 3 | EventId, | ||
| 4 | ProjectId, | ||
| 5 | ProviderInstanceId, | ||
| 6 | ThreadId, | ||
| 7 | ApprovalRequestId, | ||
| 8 | type OrchestrationReadModel, | ||
| 9 | type OrchestrationThreadActivity, | ||
| 10 | } from "@t3tools/contracts"; | ||
| 11 | import * as NodeServices from "@effect/platform-node/NodeServices"; | ||
| 12 | import { expect, it } from "@effect/vitest"; | ||
| 13 | import * as Effect from "effect/Effect"; | ||
| 14 | |||
| 15 | import { decideOrchestrationCommand } from "./decider.ts"; | ||
| 16 | import { projectEvent } from "./projector.ts"; | ||
| 17 | |||
| 18 | const NOW = "2026-01-01T00:00:00.000Z"; | ||
| 19 | const threadId = ThreadId.make("thread-1"); | ||
| 20 | const requestId = ApprovalRequestId.make("question-1"); | ||
| 21 | |||
| 22 | function makeRequest(responseMode: "message" | undefined): OrchestrationThreadActivity { | ||
| 23 | return { | ||
| 24 | id: EventId.make(requestId), | ||
| 25 | kind: "user-input.requested", | ||
| 26 | summary: "Question", | ||
| 27 | tone: "approval", | ||
| 28 | turnId: null, | ||
| 29 | createdAt: NOW, | ||
| 30 | payload: { | ||
| 31 | requestId, | ||
| 32 | ...(responseMode === undefined ? {} : { responseMode }), | ||
| 33 | questions: [{ id: "0", header: "Q", question: "Continue?", options: [] }], | ||
| 34 | }, | ||
| 35 | }; | ||
| 36 | } | ||
| 37 | |||
| 38 | function makeReadModel( | ||
| 39 | activities: ReadonlyArray<OrchestrationThreadActivity>, | ||
| 40 | ): OrchestrationReadModel { | ||
| 41 | return { | ||
| 42 | snapshotSequence: 0, | ||
| 43 | projects: [], | ||
| 44 | threads: [ | ||
| 45 | { | ||
| 46 | id: threadId, | ||
| 47 | projectId: ProjectId.make("project-1"), | ||
| 48 | title: "Thread", | ||
| 49 | modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.4" }, | ||
| 50 | runtimeMode: "full-access", | ||
| 51 | interactionMode: "default", | ||
| 52 | branch: null, | ||
| 53 | worktreePath: null, | ||
| 54 | latestTurn: null, | ||
| 55 | createdAt: NOW, | ||
| 56 | updatedAt: NOW, | ||
| 57 | archivedAt: null, | ||
| 58 | settledOverride: null, | ||
| 59 | settledAt: null, | ||
| 60 | snoozedUntil: null, | ||
| 61 | snoozedAt: null, | ||
| 62 | pinnedAt: null, | ||
| 63 | deletedAt: null, | ||
| 64 | messages: [], | ||
| 65 | proposedPlans: [], | ||
| 66 | activities: [...activities], | ||
| 67 | checkpoints: [], | ||
| 68 | session: null, | ||
| 69 | }, | ||
| 70 | ], | ||
| 71 | updatedAt: NOW, | ||
| 72 | }; | ||
| 73 | } | ||
| 74 | |||
| 75 | const command = { | ||
| 76 | type: "thread.user-input.dismiss" as const, | ||
| 77 | commandId: CommandId.make("dismiss-1"), | ||
| 78 | threadId, | ||
| 79 | requestId, | ||
| 80 | createdAt: NOW, | ||
| 81 | }; | ||
| 82 | |||
| 83 | it.layer(NodeServices.layer)("user input dismiss decider", (it) => { | ||
| 84 | it.effect("closes an async question without sending a message or starting a turn", () => | ||
| 85 | Effect.gen(function* () { | ||
| 86 | const request = makeRequest("message"); | ||
| 87 | const readModel = makeReadModel([request]); | ||
| 88 | const result = yield* decideOrchestrationCommand({ | ||
| 89 | command, | ||
| 90 | readModel, | ||
| 91 | userInputActivity: request, | ||
| 92 | }); | ||
| 93 | const events = Array.isArray(result) ? result : [result]; | ||
| 94 | expect(events.map((event) => event.type)).toEqual(["thread.activity-appended"]); | ||
| 95 | expect(events[0]?.payload).toMatchObject({ | ||
| 96 | threadId, | ||
| 97 | activity: { | ||
| 98 | kind: "user-input.resolved", | ||
| 99 | summary: "User input dismissed", | ||
| 100 | payload: { requestId, responseMode: "message" }, | ||
| 101 | }, | ||
| 102 | }); | ||
| 103 | const projected = yield* projectEvent(readModel, { ...events[0]!, sequence: 1 }); | ||
| 104 | expect(projected.threads[0]?.messages).toEqual([]); | ||
| 105 | expect(projected.threads[0]?.latestTurn).toBeNull(); | ||
| 106 | }), | ||
| 107 | ); | ||
| 108 | |||
| 109 | it.effect("rejects dismissing a native callback question", () => | ||
| 110 | Effect.gen(function* () { | ||
| 111 | const request = makeRequest(undefined); | ||
| 112 | const result = yield* decideOrchestrationCommand({ | ||
| 113 | command, | ||
| 114 | readModel: makeReadModel([request]), | ||
| 115 | userInputActivity: request, | ||
| 116 | }).pipe(Effect.flip); | ||
| 117 | expect(result).toMatchObject({ | ||
| 118 | _tag: "OrchestrationCommandInvariantError", | ||
| 119 | detail: "This question needs an answer. Answer it or stop the turn.", | ||
| 120 | }); | ||
| 121 | }), | ||
| 122 | ); | ||
| 123 | |||
| 124 | it.effect("rejects dismissing a question that was already resolved", () => | ||
| 125 | Effect.gen(function* () { | ||
| 126 | const resolved: OrchestrationThreadActivity = { | ||
| 127 | ...makeRequest("message"), | ||
| 128 | id: EventId.make("resolved"), | ||
| 129 | kind: "user-input.resolved", | ||
| 130 | }; | ||
| 131 | const result = yield* decideOrchestrationCommand({ | ||
| 132 | command, | ||
| 133 | readModel: makeReadModel([makeRequest("message"), resolved]), | ||
| 134 | userInputActivity: resolved, | ||
| 135 | }).pipe(Effect.flip); | ||
| 136 | expect(result).toMatchObject({ | ||
| 137 | _tag: "OrchestrationCommandInvariantError", | ||
| 138 | detail: "This question has already been answered.", | ||
| 139 | }); | ||
| 140 | }), | ||
| 141 | ); | ||
| 142 | }); | ||
apps/web/src/components/ChatView.tsx+30| ... | @@ -1423,6 +1423,9 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -1423,6 +1423,9 @@ export default function ChatView(props: ChatViewProps) { |
| 1423 | const respondToThreadUserInput = useAtomCommand(threadEnvironment.respondToUserInput, { | 1423 | const respondToThreadUserInput = useAtomCommand(threadEnvironment.respondToUserInput, { |
| 1424 | reportFailure: false, | 1424 | reportFailure: false, |
| 1425 | }); | 1425 | }); |
| 1426 | const dismissThreadUserInput = useAtomCommand(threadEnvironment.dismissUserInput, { | ||
| 1427 | reportFailure: false, | ||
| 1428 | }); | ||
| 1426 | const revertThreadCheckpoint = useAtomCommand(threadEnvironment.revertCheckpoint, { | 1429 | const revertThreadCheckpoint = useAtomCommand(threadEnvironment.revertCheckpoint, { |
| 1427 | reportFailure: false, | 1430 | reportFailure: false, |
| 1428 | }); | 1431 | }); |
| ... | @@ -6960,6 +6963,32 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -6960,6 +6963,32 @@ export default function ChatView(props: ChatViewProps) { |
| 6960 | [activeThreadId, environmentId, respondToThreadUserInput, setThreadError], | 6963 | [activeThreadId, environmentId, respondToThreadUserInput, setThreadError], |
| 6961 | ); | 6964 | ); |
| 6962 | 6965 | ||
| 6966 | // Closes an async question without messaging the agent. The server records | ||
| 6967 | // the dismissal so every client releases the composer. | ||
| 6968 | const onDismissUserInput = useCallback( | ||
| 6969 | async (requestId: ApprovalRequestId) => { | ||
| 6970 | if (!activeThreadId) return; | ||
| 6971 | |||
| 6972 | setRespondingUserInputRequestIds((existing) => | ||
| 6973 | existing.includes(requestId) ? existing : [...existing, requestId], | ||
| 6974 | ); | ||
| 6975 | const result = await dismissThreadUserInput({ | ||
| 6976 | environmentId, | ||
| 6977 | input: { threadId: activeThreadId, requestId }, | ||
| 6978 | }); | ||
| 6979 | if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) { | ||
| 6980 | const error = squashAtomCommandFailure(result); | ||
| 6981 | setThreadError( | ||
| 6982 | activeThreadId, | ||
| 6983 | error instanceof Error ? error.message : "Failed to dismiss the question.", | ||
| 6984 | ); | ||
| 6985 | } | ||
| 6986 | setRespondingUserInputRequestIds((existing) => existing.filter((id) => id !== requestId)); | ||
| 6987 | return result; | ||
| 6988 | }, | ||
| 6989 | [activeThreadId, dismissThreadUserInput, environmentId, setThreadError], | ||
| 6990 | ); | ||
| 6991 | |||
| 6963 | const setActivePendingUserInputQuestionIndex = useCallback( | 6992 | const setActivePendingUserInputQuestionIndex = useCallback( |
| 6964 | (nextQuestionIndex: number) => { | 6993 | (nextQuestionIndex: number) => { |
| 6965 | if (!activePendingUserInput) { | 6994 | if (!activePendingUserInput) { |
| ... | @@ -8055,6 +8084,7 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -8055,6 +8084,7 @@ export default function ChatView(props: ChatViewProps) { |
| 8055 | onSelectActivePendingUserInputOption | 8084 | onSelectActivePendingUserInputOption |
| 8056 | } | 8085 | } |
| 8057 | onAdvanceActivePendingUserInput={onAdvanceActivePendingUserInput} | 8086 | onAdvanceActivePendingUserInput={onAdvanceActivePendingUserInput} |
| 8087 | onDismissActivePendingUserInput={onDismissUserInput} | ||
| 8058 | onPreviousActivePendingUserInputQuestion={ | 8088 | onPreviousActivePendingUserInputQuestion={ |
| 8059 | onPreviousActivePendingUserInputQuestion | 8089 | onPreviousActivePendingUserInputQuestion |
| 8060 | } | 8090 | } |
apps/web/src/components/chat/ChatComposer.tsx+4| ... | @@ -1284,6 +1284,7 @@ export interface ChatComposerProps { | ... | @@ -1284,6 +1284,7 @@ export interface ChatComposerProps { |
| 1284 | ) => Promise<unknown>; | 1284 | ) => Promise<unknown>; |
| 1285 | onSelectActivePendingUserInputOption: (questionId: string, optionValue: string) => void; | 1285 | onSelectActivePendingUserInputOption: (questionId: string, optionValue: string) => void; |
| 1286 | onAdvanceActivePendingUserInput: () => void; | 1286 | onAdvanceActivePendingUserInput: () => void; |
| 1287 | onDismissActivePendingUserInput: (requestId: ApprovalRequestId) => void; | ||
| 1287 | onPreviousActivePendingUserInputQuestion: () => void; | 1288 | onPreviousActivePendingUserInputQuestion: () => void; |
| 1288 | onChangeActivePendingUserInputCustomAnswer: ( | 1289 | onChangeActivePendingUserInputCustomAnswer: ( |
| 1289 | questionId: string, | 1290 | questionId: string, |
| ... | @@ -1384,6 +1385,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) | ... | @@ -1384,6 +1385,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) |
| 1384 | onRespondToApproval, | 1385 | onRespondToApproval, |
| 1385 | onSelectActivePendingUserInputOption, | 1386 | onSelectActivePendingUserInputOption, |
| 1386 | onAdvanceActivePendingUserInput, | 1387 | onAdvanceActivePendingUserInput, |
| 1388 | onDismissActivePendingUserInput, | ||
| 1387 | onPreviousActivePendingUserInputQuestion, | 1389 | onPreviousActivePendingUserInputQuestion, |
| 1388 | onChangeActivePendingUserInputCustomAnswer, | 1390 | onChangeActivePendingUserInputCustomAnswer, |
| 1389 | onProviderModelSelect, | 1391 | onProviderModelSelect, |
| ... | @@ -4911,6 +4913,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) | ... | @@ -4911,6 +4913,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) |
| 4911 | questionIndex={activePendingQuestionIndex} | 4913 | questionIndex={activePendingQuestionIndex} |
| 4912 | onToggleOption={onSelectActivePendingUserInputOption} | 4914 | onToggleOption={onSelectActivePendingUserInputOption} |
| 4913 | onAdvance={onAdvanceActivePendingUserInput} | 4915 | onAdvance={onAdvanceActivePendingUserInput} |
| 4916 | onDismiss={onDismissActivePendingUserInput} | ||
| 4914 | /> | 4917 | /> |
| 4915 | ) : !isComposerCollapsedMobile && showPlanFollowUpPrompt && activeProposedPlan ? ( | 4918 | ) : !isComposerCollapsedMobile && showPlanFollowUpPrompt && activeProposedPlan ? ( |
| 4916 | <ComposerPlanFollowUpBanner | 4919 | <ComposerPlanFollowUpBanner |
| ... | @@ -4926,6 +4929,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) | ... | @@ -4926,6 +4929,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) |
| 4926 | questionIndex={activePendingQuestionIndex} | 4929 | questionIndex={activePendingQuestionIndex} |
| 4927 | onToggleOption={onSelectActivePendingUserInputOption} | 4930 | onToggleOption={onSelectActivePendingUserInputOption} |
| 4928 | onAdvance={onAdvanceActivePendingUserInput} | 4931 | onAdvance={onAdvanceActivePendingUserInput} |
| 4932 | onDismiss={onDismissActivePendingUserInput} | ||
| 4929 | /> | 4933 | /> |
| 4930 | {!isChoiceOnlyPendingQuestion || | 4934 | {!isChoiceOnlyPendingQuestion || |
| 4931 | activePendingProgress?.activeQuestion?.multiSelect ? ( | 4935 | activePendingProgress?.activeQuestion?.multiSelect ? ( |
apps/web/src/components/chat/ComposerPendingUserInputPanel.test.tsx+11-2| ... | @@ -20,17 +20,19 @@ const prompt: PendingUserInput = { | ... | @@ -20,17 +20,19 @@ const prompt: PendingUserInput = { |
| 20 | multiSelect: false, | 20 | multiSelect: false, |
| 21 | }, | 21 | }, |
| 22 | ], | 22 | ], |
| 23 | dismissible: true, | ||
| 23 | }; | 24 | }; |
| 24 | 25 | ||
| 25 | function renderPanel() { | 26 | function renderPanel(pendingUserInput: PendingUserInput = prompt) { |
| 26 | return renderToStaticMarkup( | 27 | return renderToStaticMarkup( |
| 27 | <ComposerPendingUserInputPanel | 28 | <ComposerPendingUserInputPanel |
| 28 | pendingUserInputs={[prompt]} | 29 | pendingUserInputs={[pendingUserInput]} |
| 29 | respondingRequestIds={[]} | 30 | respondingRequestIds={[]} |
| 30 | answers={{}} | 31 | answers={{}} |
| 31 | questionIndex={0} | 32 | questionIndex={0} |
| 32 | onToggleOption={() => {}} | 33 | onToggleOption={() => {}} |
| 33 | onAdvance={() => {}} | 34 | onAdvance={() => {}} |
| 35 | onDismiss={() => {}} | ||
| 34 | />, | 36 | />, |
| 35 | ); | 37 | ); |
| 36 | } | 38 | } |
| ... | @@ -50,6 +52,13 @@ describe("ComposerPendingUserInputPanel", () => { | ... | @@ -50,6 +52,13 @@ describe("ComposerPendingUserInputPanel", () => { |
| 50 | expect(markup).toMatch(new RegExp(`<div[^>]*\\sid="${controlledId}"`)); | 52 | expect(markup).toMatch(new RegExp(`<div[^>]*\\sid="${controlledId}"`)); |
| 51 | }); | 53 | }); |
| 52 | 54 | ||
| 55 | it("offers dismiss only for async questions", () => { | ||
| 56 | expect(renderPanel()).toContain("data-pending-user-input-dismiss"); | ||
| 57 | expect(renderPanel({ ...prompt, dismissible: false })).not.toContain( | ||
| 58 | "data-pending-user-input-dismiss", | ||
| 59 | ); | ||
| 60 | }); | ||
| 61 | |||
| 53 | it("starts expanded so the question and its options are visible", () => { | 62 | it("starts expanded so the question and its options are visible", () => { |
| 54 | const markup = renderPanel(); | 63 | const markup = renderPanel(); |
| 55 | 64 |
apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx+27| ... | @@ -17,6 +17,7 @@ interface PendingUserInputPanelProps { | ... | @@ -17,6 +17,7 @@ interface PendingUserInputPanelProps { |
| 17 | questionIndex: number; | 17 | questionIndex: number; |
| 18 | onToggleOption: (questionId: string, optionValue: string) => void; | 18 | onToggleOption: (questionId: string, optionValue: string) => void; |
| 19 | onAdvance: () => void; | 19 | onAdvance: () => void; |
| 20 | onDismiss: (requestId: ApprovalRequestId) => void; | ||
| 20 | } | 21 | } |
| 21 | 22 | ||
| 22 | export const ComposerPendingUserInputPanel = memo(function ComposerPendingUserInputPanel({ | 23 | export const ComposerPendingUserInputPanel = memo(function ComposerPendingUserInputPanel({ |
| ... | @@ -26,6 +27,7 @@ export const ComposerPendingUserInputPanel = memo(function ComposerPendingUserIn | ... | @@ -26,6 +27,7 @@ export const ComposerPendingUserInputPanel = memo(function ComposerPendingUserIn |
| 26 | questionIndex, | 27 | questionIndex, |
| 27 | onToggleOption, | 28 | onToggleOption, |
| 28 | onAdvance, | 29 | onAdvance, |
| 30 | onDismiss, | ||
| 29 | }: PendingUserInputPanelProps) { | 31 | }: PendingUserInputPanelProps) { |
| 30 | if (pendingUserInputs.length === 0) return null; | 32 | if (pendingUserInputs.length === 0) return null; |
| 31 | const activePrompt = pendingUserInputs[0]; | 33 | const activePrompt = pendingUserInputs[0]; |
| ... | @@ -40,6 +42,7 @@ export const ComposerPendingUserInputPanel = memo(function ComposerPendingUserIn | ... | @@ -40,6 +42,7 @@ export const ComposerPendingUserInputPanel = memo(function ComposerPendingUserIn |
| 40 | questionIndex={questionIndex} | 42 | questionIndex={questionIndex} |
| 41 | onToggleOption={onToggleOption} | 43 | onToggleOption={onToggleOption} |
| 42 | onAdvance={onAdvance} | 44 | onAdvance={onAdvance} |
| 45 | onDismiss={onDismiss} | ||
| 43 | /> | 46 | /> |
| 44 | ); | 47 | ); |
| 45 | }); | 48 | }); |
| ... | @@ -51,6 +54,7 @@ const ComposerPendingUserInputCard = memo(function ComposerPendingUserInputCard( | ... | @@ -51,6 +54,7 @@ const ComposerPendingUserInputCard = memo(function ComposerPendingUserInputCard( |
| 51 | questionIndex, | 54 | questionIndex, |
| 52 | onToggleOption, | 55 | onToggleOption, |
| 53 | onAdvance, | 56 | onAdvance, |
| 57 | onDismiss, | ||
| 54 | }: { | 58 | }: { |
| 55 | prompt: PendingUserInput; | 59 | prompt: PendingUserInput; |
| 56 | isResponding: boolean; | 60 | isResponding: boolean; |
| ... | @@ -58,6 +62,7 @@ const ComposerPendingUserInputCard = memo(function ComposerPendingUserInputCard( | ... | @@ -58,6 +62,7 @@ const ComposerPendingUserInputCard = memo(function ComposerPendingUserInputCard( |
| 58 | questionIndex: number; | 62 | questionIndex: number; |
| 59 | onToggleOption: (questionId: string, optionValue: string) => void; | 63 | onToggleOption: (questionId: string, optionValue: string) => void; |
| 60 | onAdvance: () => void; | 64 | onAdvance: () => void; |
| 65 | onDismiss: (requestId: ApprovalRequestId) => void; | ||
| 61 | }) { | 66 | }) { |
| 62 | const progress = derivePendingUserInputProgress(prompt.questions, answers, questionIndex); | 67 | const progress = derivePendingUserInputProgress(prompt.questions, answers, questionIndex); |
| 63 | const activeQuestion = progress.activeQuestion; | 68 | const activeQuestion = progress.activeQuestion; |
| ... | @@ -198,6 +203,28 @@ const ComposerPendingUserInputCard = memo(function ComposerPendingUserInputCard( | ... | @@ -198,6 +203,28 @@ const ComposerPendingUserInputCard = memo(function ComposerPendingUserInputCard( |
| 198 | </span> | 203 | </span> |
| 199 | ) : null} | 204 | ) : null} |
| 200 | <ComposerBanner.ToggleIcon expanded={!isCollapsed} /> | 205 | <ComposerBanner.ToggleIcon expanded={!isCollapsed} /> |
| 206 | {prompt.dismissible ? ( | ||
| 207 | // Sits inside the trigger button, so stop the click from toggling | ||
| 208 | // the disclosure. Dismiss closes the question without a reply. | ||
| 209 | <ComposerBanner.Dismiss | ||
| 210 | render={<span role="button" tabIndex={0} />} | ||
| 211 | aria-label="Dismiss question without answering" | ||
| 212 | title="Dismiss question without answering" | ||
| 213 | disabled={isResponding} | ||
| 214 | data-pending-user-input-dismiss | ||
| 215 | onClick={(event) => { | ||
| 216 | event.preventDefault(); | ||
| 217 | event.stopPropagation(); | ||
| 218 | onDismiss(prompt.requestId); | ||
| 219 | }} | ||
| 220 | onKeyDown={(event) => { | ||
| 221 | if (event.key !== "Enter" && event.key !== " ") return; | ||
| 222 | event.preventDefault(); | ||
| 223 | event.stopPropagation(); | ||
| 224 | onDismiss(prompt.requestId); | ||
| 225 | }} | ||
| 226 | /> | ||
| 227 | ) : null} | ||
| 201 | </ComposerBanner.Actions> | 228 | </ComposerBanner.Actions> |
| 202 | </CollapsibleTrigger> | 229 | </CollapsibleTrigger> |
| 203 | <CollapsiblePanel> | 230 | <CollapsiblePanel> |
docs/user/providers-codex.md+3-1| ... | @@ -54,7 +54,9 @@ contains a copied Codex setup. Use a fresh shadow directory and sign in again. | ... | @@ -54,7 +54,9 @@ contains a copied Codex setup. Use a fresh shadow directory and sign in again. |
| 54 | Codex can ask a question and keep working. Answer it in the thread's question | 54 | Codex can ask a question and keep working. Answer it in the thread's question |
| 55 | panel. The answer becomes a new message: it reaches the active turn, or starts | 55 | panel. The answer becomes a new message: it reaches the active turn, or starts |
| 56 | another turn if Codex has finished. Unanswered questions survive reconnects. | 56 | another turn if Codex has finished. Unanswered questions survive reconnects. |
| 57 | This requires a Codex version that supports async questions. | 57 | If you do not want to answer, dismiss the question from its panel. Dismissing |
| 58 | closes it without sending anything to Codex. This requires a Codex version that | ||
| 59 | supports async questions. | ||
| 58 | 60 | ||
| 59 | ## Approve app access | 61 | ## Approve app access |
| 60 | 62 |
packages/client-runtime/src/operations/commands.ts+12| ... | @@ -50,6 +50,7 @@ export type StartThreadTurnInput = CommandInput<"thread.turn.start">; | ... | @@ -50,6 +50,7 @@ export type StartThreadTurnInput = CommandInput<"thread.turn.start">; |
| 50 | export type InterruptThreadTurnInput = CommandInput<"thread.turn.interrupt">; | 50 | export type InterruptThreadTurnInput = CommandInput<"thread.turn.interrupt">; |
| 51 | export type RespondToThreadApprovalInput = CommandInput<"thread.approval.respond">; | 51 | export type RespondToThreadApprovalInput = CommandInput<"thread.approval.respond">; |
| 52 | export type RespondToThreadUserInputInput = CommandInput<"thread.user-input.respond">; | 52 | export type RespondToThreadUserInputInput = CommandInput<"thread.user-input.respond">; |
| 53 | export type DismissThreadUserInputInput = CommandInput<"thread.user-input.dismiss">; | ||
| 53 | export type RevertThreadCheckpointInput = CommandInput<"thread.checkpoint.revert">; | 54 | export type RevertThreadCheckpointInput = CommandInput<"thread.checkpoint.revert">; |
| 54 | export type StopThreadSessionInput = CommandInput<"thread.session.stop">; | 55 | export type StopThreadSessionInput = CommandInput<"thread.session.stop">; |
| 55 | 56 | ||
| ... | @@ -320,6 +321,17 @@ export const respondToThreadUserInput: (input: RespondToThreadUserInputInput) => | ... | @@ -320,6 +321,17 @@ export const respondToThreadUserInput: (input: RespondToThreadUserInputInput) => |
| 320 | }); | 321 | }); |
| 321 | }); | 322 | }); |
| 322 | 323 | ||
| 324 | export const dismissThreadUserInput: (input: DismissThreadUserInputInput) => CommandEffect = | ||
| 325 | Effect.fn("EnvironmentCommands.dismissThreadUserInput")(function* (input) { | ||
| 326 | const metadata = yield* timestampedCommandMetadata(input); | ||
| 327 | return yield* dispatch({ | ||
| 328 | ...input, | ||
| 329 | type: "thread.user-input.dismiss", | ||
| 330 | commandId: metadata.commandId, | ||
| 331 | createdAt: metadata.createdAt, | ||
| 332 | }); | ||
| 333 | }); | ||
| 334 | |||
| 323 | export const revertThreadCheckpoint: (input: RevertThreadCheckpointInput) => CommandEffect = | 335 | export const revertThreadCheckpoint: (input: RevertThreadCheckpointInput) => CommandEffect = |
| 324 | Effect.fn("EnvironmentCommands.revertThreadCheckpoint")(function* (input) { | 336 | Effect.fn("EnvironmentCommands.revertThreadCheckpoint")(function* (input) { |
| 325 | const metadata = yield* timestampedCommandMetadata(input); | 337 | const metadata = yield* timestampedCommandMetadata(input); |
packages/client-runtime/src/pendingRequests.test.ts+20-1| ... | @@ -282,7 +282,25 @@ describe("pending questions", () => { | ... | @@ -282,7 +282,25 @@ describe("pending questions", () => { |
| 282 | payload: { requestId: "async-1", responseMode: "message", questions: [question] }, | 282 | payload: { requestId: "async-1", responseMode: "message", questions: [question] }, |
| 283 | }), | 283 | }), |
| 284 | ]; | 284 | ]; |
| 285 | expect(derivePendingRequests(activities).userInputs[0]?.questions).toEqual([question]); | 285 | expect(derivePendingRequests(activities).userInputs[0]).toMatchObject({ |
| 286 | questions: [question], | ||
| 287 | dismissible: true, | ||
| 288 | }); | ||
| 289 | }); | ||
| 290 | |||
| 291 | it("only marks async questions as dismissible", () => { | ||
| 292 | const question = { | ||
| 293 | id: "0", | ||
| 294 | header: "Question", | ||
| 295 | question: "Continue?", | ||
| 296 | options: [{ label: "Yes", description: "" }], | ||
| 297 | multiSelect: false, | ||
| 298 | }; | ||
| 299 | const native = makeActivity({ | ||
| 300 | kind: "user-input.requested", | ||
| 301 | payload: { requestId: "native-1", questions: [question] }, | ||
| 302 | }); | ||
| 303 | expect(derivePendingRequests([native]).userInputs[0]?.dismissible).toBe(false); | ||
| 286 | }); | 304 | }); |
| 287 | 305 | ||
| 288 | it("preserves native choice values and the custom-answer restriction", () => { | 306 | it("preserves native choice values and the custom-answer restriction", () => { |
| ... | @@ -378,6 +396,7 @@ describe("pending questions", () => { | ... | @@ -378,6 +396,7 @@ describe("pending questions", () => { |
| 378 | { | 396 | { |
| 379 | requestId: "req-user-input-1", | 397 | requestId: "req-user-input-1", |
| 380 | createdAt: "2026-02-23T00:00:01.000Z", | 398 | createdAt: "2026-02-23T00:00:01.000Z", |
| 399 | dismissible: false, | ||
| 381 | questions: [ | 400 | questions: [ |
| 382 | { | 401 | { |
| 383 | id: "sandbox_mode", | 402 | id: "sandbox_mode", |
packages/client-runtime/src/pendingRequests.ts+8-1| ... | @@ -22,6 +22,8 @@ export interface PendingUserInput { | ... | @@ -22,6 +22,8 @@ export interface PendingUserInput { |
| 22 | readonly requestId: ApprovalRequestId; | 22 | readonly requestId: ApprovalRequestId; |
| 23 | readonly createdAt: string; | 23 | readonly createdAt: string; |
| 24 | readonly questions: ReadonlyArray<UserInputQuestion>; | 24 | readonly questions: ReadonlyArray<UserInputQuestion>; |
| 25 | /** Async questions can be dismissed without a reply; native callbacks cannot. */ | ||
| 26 | readonly dismissible: boolean; | ||
| 25 | } | 27 | } |
| 26 | 28 | ||
| 27 | const isRequestId = Schema.is(ApprovalRequestId); | 29 | const isRequestId = Schema.is(ApprovalRequestId); |
| ... | @@ -160,7 +162,12 @@ export function derivePendingRequests(activities: ReadonlyArray<OrchestrationThr | ... | @@ -160,7 +162,12 @@ export function derivePendingRequests(activities: ReadonlyArray<OrchestrationThr |
| 160 | if (closedUserInputs.has(requestId)) continue; | 162 | if (closedUserInputs.has(requestId)) continue; |
| 161 | const questions = parseQuestions(payload.questions); | 163 | const questions = parseQuestions(payload.questions); |
| 162 | if (questions.length === 0) continue; | 164 | if (questions.length === 0) continue; |
| 163 | userInputs.set(requestId, { requestId, createdAt: activity.createdAt, questions }); | 165 | userInputs.set(requestId, { |
| 166 | requestId, | ||
| 167 | createdAt: activity.createdAt, | ||
| 168 | questions, | ||
| 169 | dismissible: payload.responseMode === "message", | ||
| 170 | }); | ||
| 164 | } else if ( | 171 | } else if ( |
| 165 | activity.kind === "approval.resolved" || | 172 | activity.kind === "approval.resolved" || |
| 166 | (activity.kind === "provider.approval.respond.failed" && | 173 | (activity.kind === "provider.approval.respond.failed" && |
packages/client-runtime/src/state/threadCommands.ts+9| ... | @@ -14,6 +14,7 @@ import { | ... | @@ -14,6 +14,7 @@ import { |
| 14 | type InterruptThreadTurnInput, | 14 | type InterruptThreadTurnInput, |
| 15 | type RespondToThreadApprovalInput, | 15 | type RespondToThreadApprovalInput, |
| 16 | type RespondToThreadUserInputInput, | 16 | type RespondToThreadUserInputInput, |
| 17 | type DismissThreadUserInputInput, | ||
| 17 | type RevertThreadCheckpointInput, | 18 | type RevertThreadCheckpointInput, |
| 18 | type SetThreadInteractionModeInput, | 19 | type SetThreadInteractionModeInput, |
| 19 | type SetThreadRuntimeModeInput, | 20 | type SetThreadRuntimeModeInput, |
| ... | @@ -35,6 +36,7 @@ import { | ... | @@ -35,6 +36,7 @@ import { |
| 35 | interruptThreadTurn, | 36 | interruptThreadTurn, |
| 36 | respondToThreadApproval, | 37 | respondToThreadApproval, |
| 37 | respondToThreadUserInput, | 38 | respondToThreadUserInput, |
| 39 | dismissThreadUserInput, | ||
| 38 | revertThreadCheckpoint, | 40 | revertThreadCheckpoint, |
| 39 | setThreadInteractionMode, | 41 | setThreadInteractionMode, |
| 40 | setThreadRuntimeMode, | 42 | setThreadRuntimeMode, |
| ... | @@ -60,6 +62,7 @@ export type { | ... | @@ -60,6 +62,7 @@ export type { |
| 60 | InterruptThreadTurnInput, | 62 | InterruptThreadTurnInput, |
| 61 | RespondToThreadApprovalInput, | 63 | RespondToThreadApprovalInput, |
| 62 | RespondToThreadUserInputInput, | 64 | RespondToThreadUserInputInput, |
| 65 | DismissThreadUserInputInput, | ||
| 63 | RevertThreadCheckpointInput, | 66 | RevertThreadCheckpointInput, |
| 64 | SetThreadInteractionModeInput, | 67 | SetThreadInteractionModeInput, |
| 65 | SetThreadRuntimeModeInput, | 68 | SetThreadRuntimeModeInput, |
| ... | @@ -201,6 +204,12 @@ export function createThreadEnvironmentAtoms<R, E>( | ... | @@ -201,6 +204,12 @@ export function createThreadEnvironmentAtoms<R, E>( |
| 201 | scheduler, | 204 | scheduler, |
| 202 | concurrency, | 205 | concurrency, |
| 203 | }), | 206 | }), |
| 207 | dismissUserInput: createEnvironmentCommand(runtime, { | ||
| 208 | label: "environment-data:commands:thread:dismiss-user-input", | ||
| 209 | execute: (input: DismissThreadUserInputInput) => dismissThreadUserInput(input), | ||
| 210 | scheduler, | ||
| 211 | concurrency, | ||
| 212 | }), | ||
| 204 | revertCheckpoint: createEnvironmentCommand(runtime, { | 213 | revertCheckpoint: createEnvironmentCommand(runtime, { |
| 205 | label: "environment-data:commands:thread:revert-checkpoint", | 214 | label: "environment-data:commands:thread:revert-checkpoint", |
| 206 | execute: (input: RevertThreadCheckpointInput) => revertThreadCheckpoint(input), | 215 | execute: (input: RevertThreadCheckpointInput) => revertThreadCheckpoint(input), |
packages/contracts/src/orchestration.ts+13| ... | @@ -1033,6 +1033,17 @@ const ThreadUserInputRespondCommand = Schema.Struct({ | ... | @@ -1033,6 +1033,17 @@ const ThreadUserInputRespondCommand = Schema.Struct({ |
| 1033 | createdAt: IsoDateTime, | 1033 | createdAt: IsoDateTime, |
| 1034 | }); | 1034 | }); |
| 1035 | 1035 | ||
| 1036 | // Closes an async question without answering it. The agent is not messaged; | ||
| 1037 | // the composer is simply released. Native callback questions cannot be dismissed | ||
| 1038 | // this way because the provider is blocked waiting on a reply. | ||
| 1039 | const ThreadUserInputDismissCommand = Schema.Struct({ | ||
| 1040 | type: Schema.Literal("thread.user-input.dismiss"), | ||
| 1041 | commandId: CommandId, | ||
| 1042 | threadId: ThreadId, | ||
| 1043 | requestId: ApprovalRequestId, | ||
| 1044 | createdAt: IsoDateTime, | ||
| 1045 | }); | ||
| 1046 | |||
| 1036 | const ThreadCheckpointRevertCommand = Schema.Struct({ | 1047 | const ThreadCheckpointRevertCommand = Schema.Struct({ |
| 1037 | type: Schema.Literal("thread.checkpoint.revert"), | 1048 | type: Schema.Literal("thread.checkpoint.revert"), |
| 1038 | commandId: CommandId, | 1049 | commandId: CommandId, |
| ... | @@ -1077,6 +1088,7 @@ const DispatchableClientOrchestrationCommand = Schema.Union([ | ... | @@ -1077,6 +1088,7 @@ const DispatchableClientOrchestrationCommand = Schema.Union([ |
| 1077 | ThreadTurnInterruptCommand, | 1088 | ThreadTurnInterruptCommand, |
| 1078 | ThreadApprovalRespondCommand, | 1089 | ThreadApprovalRespondCommand, |
| 1079 | ThreadUserInputRespondCommand, | 1090 | ThreadUserInputRespondCommand, |
| 1091 | ThreadUserInputDismissCommand, | ||
| 1080 | ThreadCheckpointRevertCommand, | 1092 | ThreadCheckpointRevertCommand, |
| 1081 | ThreadSessionStopCommand, | 1093 | ThreadSessionStopCommand, |
| 1082 | ]); | 1094 | ]); |
| ... | @@ -1106,6 +1118,7 @@ export const ClientOrchestrationCommand = Schema.Union([ | ... | @@ -1106,6 +1118,7 @@ export const ClientOrchestrationCommand = Schema.Union([ |
| 1106 | ThreadTurnInterruptCommand, | 1118 | ThreadTurnInterruptCommand, |
| 1107 | ThreadApprovalRespondCommand, | 1119 | ThreadApprovalRespondCommand, |
| 1108 | ThreadUserInputRespondCommand, | 1120 | ThreadUserInputRespondCommand, |
| 1121 | ThreadUserInputDismissCommand, | ||
| 1109 | ThreadCheckpointRevertCommand, | 1122 | ThreadCheckpointRevertCommand, |
| 1110 | ThreadSessionStopCommand, | 1123 | ThreadSessionStopCommand, |
| 1111 | ]); | 1124 | ]); |