authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-02-09 00:16:23-08:00
committergravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-02-09 00:16:23-08:00
log99a906753ec9c0f86eff6fe306062f0c2c3d15df
tree48d9a4af7013057cd6b3c8fe74139fc5a08a3d08
parentde5df2690de58406a480071eb2c7ba7e2fd4e3bd
signature Commit is signed but in an unrecognized format.

approvals


16 files changed, 560 insertions(+), 33 deletions(-)

README.md+15-3
...@@ -26,6 +26,17 @@ CodeThing is a desktop shell for coding agents. This first implementation is:...@@ -26,6 +26,17 @@ CodeThing is a desktop shell for coding agents. This first implementation is:
26- Renderer talks only to `window.nativeApi` exposed by preload.26- Renderer talks only to `window.nativeApi` exposed by preload.
27- Preload and main both validate inputs using shared Zod schemas.27- Preload and main both validate inputs using shared Zod schemas.
2828
29`sandbox: true` above is Electron renderer sandboxing. It is separate from Codex execution sandbox policy (`read-only`, `workspace-write`, `danger-full-access`) used when starting provider sessions.
30
31## Runtime modes
32
33CodeThing has a global runtime mode switch in the sidebar:
34
35- `Full access` (default): starts new sessions with `approvalPolicy: never` and `sandboxMode: danger-full-access`.
36- `Approval required`: starts new sessions with `approvalPolicy: on-request` and `sandboxMode: workspace-write`, then prompts in-app for command/file approvals.
37
38Mode changes apply across all threads. Existing live sessions are restarted so old and new threads use the selected mode.
39
29## Scripts40## Scripts
3041
31- `bun run dev`: starts contract build/watch, renderer dev server, and Electron process.42- `bun run dev`: starts contract build/watch, renderer dev server, and Electron process.
...@@ -47,8 +58,9 @@ The renderer now depends on `nativeApi.providers.*`:...@@ -47,8 +58,9 @@ The renderer now depends on `nativeApi.providers.*`:
471. `startSession`581. `startSession`
482. `sendTurn`592. `sendTurn`
493. `interruptTurn`603. `interruptTurn`
504. `stopSession`614. `respondToRequest`
515. `listSessions`625. `stopSession`
526. `onEvent`636. `listSessions`
647. `onEvent`
5365
54Codex is the only implemented provider right now. `claudeCode` is reserved in contracts/UI but returns a not-implemented error in main-process dispatch.66Codex is the only implemented provider right now. `claudeCode` is reserved in contracts/UI but returns a not-implemented error in main-process dispatch.
TODO.md+2-4
...@@ -1,6 +1,4 @@...@@ -1,6 +1,4 @@
1## TODO1## TODO
22
3- [x] Remove model picker from new project menu3- [ ] 1 click copy in codeblocks
4- [x] CMD+Shift+O to create new thread in current project4- [ ] Pasting/adding images to submit with prompts
5- [x] Remove project title pill from input box and topnav
6- [x] Remove connection status from topnav
apps/desktop/src/codexAppServerManager.ts+92-13
...@@ -4,7 +4,9 @@ import { EventEmitter } from "node:events";...@@ -4,7 +4,9 @@ import { EventEmitter } from "node:events";
4import readline from "node:readline";4import readline from "node:readline";
55
6import type {6import type {
7 ProviderApprovalDecision,
7 ProviderEvent,8 ProviderEvent,
9 ProviderRequestKind,
8 ProviderSendTurnInput,10 ProviderSendTurnInput,
9 ProviderSession,11 ProviderSession,
10 ProviderSessionStartInput,12 ProviderSessionStartInput,
...@@ -21,11 +23,24 @@ interface PendingRequest {...@@ -21,11 +23,24 @@ interface PendingRequest {
21 reject: (error: Error) => void;23 reject: (error: Error) => void;
22}24}
2325
26interface PendingApprovalRequest {
27 requestId: string;
28 jsonRpcId: string | number;
29 method:
30 | "item/commandExecution/requestApproval"
31 | "item/fileChange/requestApproval";
32 requestKind: ProviderRequestKind;
33 threadId?: string;
34 turnId?: string;
35 itemId?: string;
36}
37
24interface CodexSessionContext {38interface CodexSessionContext {
25 session: ProviderSession;39 session: ProviderSession;
26 child: ChildProcessWithoutNullStreams;40 child: ChildProcessWithoutNullStreams;
27 output: readline.Interface;41 output: readline.Interface;
28 pending: Map<PendingRequestKey, PendingRequest>;42 pending: Map<PendingRequestKey, PendingRequest>;
43 pendingApprovals: Map<string, PendingApprovalRequest>;
29 nextRequestId: number;44 nextRequestId: number;
30 stopping: boolean;45 stopping: boolean;
31}46}
...@@ -137,6 +152,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve...@@ -137,6 +152,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve
137 child,152 child,
138 output,153 output,
139 pending: new Map(),154 pending: new Map(),
155 pendingApprovals: new Map(),
140 nextRequestId: 1,156 nextRequestId: 1,
141 stopping: false,157 stopping: false,
142 };158 };
...@@ -273,6 +289,45 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve...@@ -273,6 +289,45 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve
273 });289 });
274 }290 }
275291
292 async respondToRequest(
293 sessionId: string,
294 requestId: string,
295 decision: ProviderApprovalDecision,
296 ): Promise<void> {
297 const context = this.requireSession(sessionId);
298 const pendingRequest = context.pendingApprovals.get(requestId);
299 if (!pendingRequest) {
300 throw new Error(`Unknown pending approval request: ${requestId}`);
301 }
302
303 context.pendingApprovals.delete(requestId);
304 this.writeMessage(context, {
305 id: pendingRequest.jsonRpcId,
306 result: {
307 decision,
308 },
309 });
310
311 this.emitEvent({
312 id: randomUUID(),
313 kind: "notification",
314 provider: "codex",
315 sessionId: context.session.sessionId,
316 createdAt: new Date().toISOString(),
317 method: "item/requestApproval/decision",
318 threadId: pendingRequest.threadId,
319 turnId: pendingRequest.turnId,
320 itemId: pendingRequest.itemId,
321 requestId: pendingRequest.requestId,
322 requestKind: pendingRequest.requestKind,
323 payload: {
324 requestId: pendingRequest.requestId,
325 requestKind: pendingRequest.requestKind,
326 decision,
327 },
328 });
329 }
330
276 stopSession(sessionId: string): void {331 stopSession(sessionId: string): void {
277 const context = this.sessions.get(sessionId);332 const context = this.sessions.get(sessionId);
278 if (!context) {333 if (!context) {
...@@ -286,6 +341,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve...@@ -286,6 +341,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve
286 pending.reject(new Error("Session stopped before request completed."));341 pending.reject(new Error("Session stopped before request completed."));
287 }342 }
288 context.pending.clear();343 context.pending.clear();
344 context.pendingApprovals.clear();
289345
290 context.output.close();346 context.output.close();
291347
...@@ -498,6 +554,25 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve...@@ -498,6 +554,25 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve
498 request: JsonRpcRequest,554 request: JsonRpcRequest,
499 ): void {555 ): void {
500 const route = this.readRouteFields(request.params);556 const route = this.readRouteFields(request.params);
557 const requestKind = this.requestKindForMethod(request.method);
558 let requestId: string | undefined;
559 if (requestKind) {
560 requestId = randomUUID();
561 const pendingRequest: PendingApprovalRequest = {
562 requestId,
563 jsonRpcId: request.id,
564 method:
565 requestKind === "command"
566 ? "item/commandExecution/requestApproval"
567 : "item/fileChange/requestApproval",
568 requestKind,
569 ...(route.threadId ? { threadId: route.threadId } : {}),
570 ...(route.turnId ? { turnId: route.turnId } : {}),
571 ...(route.itemId ? { itemId: route.itemId } : {}),
572 };
573 context.pendingApprovals.set(requestId, pendingRequest);
574 }
575
501 this.emitEvent({576 this.emitEvent({
502 id: randomUUID(),577 id: randomUUID(),
503 kind: "request",578 kind: "request",
...@@ -508,22 +583,12 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve...@@ -508,22 +583,12 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve
508 threadId: route.threadId,583 threadId: route.threadId,
509 turnId: route.turnId,584 turnId: route.turnId,
510 itemId: route.itemId,585 itemId: route.itemId,
586 requestId,
587 requestKind,
511 payload: request.params,588 payload: request.params,
512 });589 });
513590
514 if (request.method === "item/commandExecution/requestApproval") {591 if (requestKind) {
515 this.writeMessage(context, {
516 id: request.id,
517 result: { decision: "decline" },
518 });
519 return;
520 }
521
522 if (request.method === "item/fileChange/requestApproval") {
523 this.writeMessage(context, {
524 id: request.id,
525 result: { decision: "decline" },
526 });
527 return;592 return;
528 }593 }
529594
...@@ -656,6 +721,20 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve...@@ -656,6 +721,20 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve
656 };721 };
657 }722 }
658723
724 private requestKindForMethod(
725 method: string,
726 ): ProviderRequestKind | undefined {
727 if (method === "item/commandExecution/requestApproval") {
728 return "command";
729 }
730
731 if (method === "item/fileChange/requestApproval") {
732 return "file-change";
733 }
734
735 return undefined;
736 }
737
659 private isServerRequest(value: unknown): value is JsonRpcRequest {738 private isServerRequest(value: unknown): value is JsonRpcRequest {
660 if (!value || typeof value !== "object") {739 if (!value || typeof value !== "object") {
661 return false;740 return false;
apps/desktop/src/main.ts+11
...@@ -13,6 +13,7 @@ import {...@@ -13,6 +13,7 @@ import {
13 agentSessionIdSchema,13 agentSessionIdSchema,
14 newTodoInputSchema,14 newTodoInputSchema,
15 providerInterruptTurnInputSchema,15 providerInterruptTurnInputSchema,
16 providerRespondToRequestInputSchema,
16 providerSendTurnInputSchema,17 providerSendTurnInputSchema,
17 providerSessionStartInputSchema,18 providerSessionStartInputSchema,
18 providerStopSessionInputSchema,19 providerStopSessionInputSchema,
...@@ -175,6 +176,16 @@ function registerIpcHandlers(): void {...@@ -175,6 +176,16 @@ function registerIpcHandlers(): void {
175 ),176 ),
176 );177 );
177178
179 ipcMain.handle(
180 IPC_CHANNELS.providerRequestRespond,
181 withParsedPayload(
182 providerRespondToRequestInputSchema,
183 async (_event, payload) => {
184 await providerManager.respondToRequest(payload);
185 },
186 ),
187 );
188
178 ipcMain.handle(189 ipcMain.handle(
179 IPC_CHANNELS.providerSessionStop,190 IPC_CHANNELS.providerSessionStop,
180 withParsedPayload(191 withParsedPayload(
apps/desktop/src/preload.ts+2
...@@ -41,6 +41,8 @@ const nativeApi: NativeApi = {...@@ -41,6 +41,8 @@ const nativeApi: NativeApi = {
41 ipcRenderer.invoke(IPC_CHANNELS.providerTurnStart, input),41 ipcRenderer.invoke(IPC_CHANNELS.providerTurnStart, input),
42 interruptTurn: (input) =>42 interruptTurn: (input) =>
43 ipcRenderer.invoke(IPC_CHANNELS.providerTurnInterrupt, input),43 ipcRenderer.invoke(IPC_CHANNELS.providerTurnInterrupt, input),
44 respondToRequest: (input) =>
45 ipcRenderer.invoke(IPC_CHANNELS.providerRequestRespond, input),
44 stopSession: (input) =>46 stopSession: (input) =>
45 ipcRenderer.invoke(IPC_CHANNELS.providerSessionStop, input),47 ipcRenderer.invoke(IPC_CHANNELS.providerSessionStop, input),
46 listSessions: () => ipcRenderer.invoke(IPC_CHANNELS.providerSessionList),48 listSessions: () => ipcRenderer.invoke(IPC_CHANNELS.providerSessionList),
apps/desktop/src/providerManager.test.ts+14
...@@ -48,4 +48,18 @@ describe("ProviderManager", () => {...@@ -48,4 +48,18 @@ describe("ProviderManager", () => {
48 manager.dispose();48 manager.dispose();
49 expect(() => manager.dispose()).not.toThrow();49 expect(() => manager.dispose()).not.toThrow();
50 });50 });
51
52 it("rejects request responses for unknown sessions", async () => {
53 const manager = new ProviderManager();
54
55 await expect(
56 manager.respondToRequest({
57 sessionId: "missing-session",
58 requestId: "req-1",
59 decision: "accept",
60 }),
61 ).rejects.toThrow("Unknown provider session: missing-session");
62
63 manager.dispose();
64 });
51});65});
apps/desktop/src/providerManager.ts+15
...@@ -5,12 +5,14 @@ import path from "node:path";...@@ -5,12 +5,14 @@ import path from "node:path";
5import {5import {
6 type ProviderEvent,6 type ProviderEvent,
7 type ProviderInterruptTurnInput,7 type ProviderInterruptTurnInput,
8 type ProviderRespondToRequestInput,
8 type ProviderSendTurnInput,9 type ProviderSendTurnInput,
9 type ProviderSession,10 type ProviderSession,
10 type ProviderSessionStartInput,11 type ProviderSessionStartInput,
11 type ProviderStopSessionInput,12 type ProviderStopSessionInput,
12 type ProviderTurnStartResult,13 type ProviderTurnStartResult,
13 providerInterruptTurnInputSchema,14 providerInterruptTurnInputSchema,
15 providerRespondToRequestInputSchema,
14 providerSendTurnInputSchema,16 providerSendTurnInputSchema,
15 providerSessionStartInputSchema,17 providerSessionStartInputSchema,
16 providerStopSessionInputSchema,18 providerStopSessionInputSchema,
...@@ -79,6 +81,19 @@ export class ProviderManager extends EventEmitter<ProviderManagerEvents> {...@@ -79,6 +81,19 @@ export class ProviderManager extends EventEmitter<ProviderManagerEvents> {
79 await this.codex.interruptTurn(input.sessionId, input.turnId);81 await this.codex.interruptTurn(input.sessionId, input.turnId);
80 }82 }
8183
84 async respondToRequest(raw: ProviderRespondToRequestInput): Promise<void> {
85 const input = providerRespondToRequestInputSchema.parse(raw);
86 if (!this.codex.hasSession(input.sessionId)) {
87 throw new Error(`Unknown provider session: ${input.sessionId}`);
88 }
89
90 await this.codex.respondToRequest(
91 input.sessionId,
92 input.requestId,
93 input.decision,
94 );
95 }
96
82 stopSession(raw: ProviderStopSessionInput): void {97 stopSession(raw: ProviderStopSessionInput): void {
83 const input = providerStopSessionInputSchema.parse(raw);98 const input = providerStopSessionInputSchema.parse(raw);
84 this.codex.stopSession(input.sessionId);99 this.codex.stopSession(input.sessionId);
apps/renderer/src/components/ChatView.tsx+190
...@@ -1,3 +1,4 @@...@@ -1,3 +1,4 @@
1import type { ProviderApprovalDecision, ProviderEvent } from "@acme/contracts";
1import {2import {
2 type FormEvent,3 type FormEvent,
3 Fragment,4 Fragment,
...@@ -39,6 +40,70 @@ function workToneClass(tone: "thinking" | "tool" | "info" | "error"): string {...@@ -39,6 +40,70 @@ function workToneClass(tone: "thinking" | "tool" | "info" | "error"): string {
39 return "text-[#606060]";40 return "text-[#606060]";
40}41}
4142
43interface PendingApprovalCard {
44 requestId: string;
45 requestKind: "command" | "file-change";
46 createdAt: string;
47 detail?: string;
48}
49
50function asRecord(value: unknown): Record<string, unknown> | undefined {
51 if (!value || typeof value !== "object") return undefined;
52 return value as Record<string, unknown>;
53}
54
55function asString(value: unknown): string | undefined {
56 return typeof value === "string" ? value : undefined;
57}
58
59function approvalDetail(event: ProviderEvent): string | undefined {
60 const payload = asRecord(event.payload);
61 const command = asString(payload?.command);
62 if (command) return command;
63 return asString(payload?.reason);
64}
65
66function derivePendingApprovals(
67 events: ProviderEvent[],
68): PendingApprovalCard[] {
69 const pending = new Map<string, PendingApprovalCard>();
70 const ordered = [...events].reverse();
71
72 for (const event of ordered) {
73 if (
74 event.method === "session/closed" ||
75 event.method === "session/exited"
76 ) {
77 pending.clear();
78 continue;
79 }
80
81 const requestId =
82 event.requestId ?? asString(asRecord(event.payload)?.requestId);
83 if (!requestId) continue;
84
85 if (
86 event.kind === "request" &&
87 (event.requestKind === "command" || event.requestKind === "file-change")
88 ) {
89 const detail = approvalDetail(event);
90 pending.set(requestId, {
91 requestId,
92 requestKind: event.requestKind,
93 createdAt: event.createdAt,
94 ...(detail ? { detail } : {}),
95 });
96 continue;
97 }
98
99 if (event.method === "item/requestApproval/decision") {
100 pending.delete(requestId);
101 }
102 }
103
104 return Array.from(pending.values());
105}
106
42export default function ChatView() {107export default function ChatView() {
43 const { state, dispatch } = useStore();108 const { state, dispatch } = useStore();
44 const api = useMemo(() => readNativeApi(), []);109 const api = useMemo(() => readNativeApi(), []);
...@@ -48,6 +113,9 @@ export default function ChatView() {...@@ -48,6 +113,9 @@ export default function ChatView() {
48 const [isModelMenuOpen, setIsModelMenuOpen] = useState(false);113 const [isModelMenuOpen, setIsModelMenuOpen] = useState(false);
49 const [selectedEffort, setSelectedEffort] =114 const [selectedEffort, setSelectedEffort] =
50 useState<string>(DEFAULT_REASONING);115 useState<string>(DEFAULT_REASONING);
116 const [respondingRequestIds, setRespondingRequestIds] = useState<string[]>(
117 [],
118 );
51 const [nowTick, setNowTick] = useState(() => Date.now());119 const [nowTick, setNowTick] = useState(() => Date.now());
52 const messagesEndRef = useRef<HTMLDivElement>(null);120 const messagesEndRef = useRef<HTMLDivElement>(null);
53 const textareaRef = useRef<HTMLTextAreaElement>(null);121 const textareaRef = useRef<HTMLTextAreaElement>(null);
...@@ -68,6 +136,10 @@ export default function ChatView() {...@@ -68,6 +136,10 @@ export default function ChatView() {
68 () => deriveWorkLogEntries(activeThread?.events ?? [], undefined),136 () => deriveWorkLogEntries(activeThread?.events ?? [], undefined),
69 [activeThread?.events],137 [activeThread?.events],
70 );138 );
139 const pendingApprovals = useMemo(
140 () => derivePendingApprovals(activeThread?.events ?? []),
141 [activeThread?.events],
142 );
71 const assistantCompletionByItemId = useMemo(() => {143 const assistantCompletionByItemId = useMemo(() => {
72 const map = new Map<string, string>();144 const map = new Map<string, string>();
73 const ordered = [...(activeThread?.events ?? [])].reverse();145 const ordered = [...(activeThread?.events ?? [])].reverse();
...@@ -127,6 +199,16 @@ export default function ChatView() {...@@ -127,6 +199,16 @@ export default function ChatView() {
127 timelineEntries,199 timelineEntries,
128 workLogEntries.length,200 workLogEntries.length,
129 ]);201 ]);
202 const runtimeSessionConfig =
203 state.runtimeMode === "full-access"
204 ? ({
205 approvalPolicy: "never",
206 sandboxMode: "danger-full-access",
207 } as const)
208 : ({
209 approvalPolicy: "on-request",
210 sandboxMode: "workspace-write",
211 } as const);
130212
131 // Auto-scroll on new messages213 // Auto-scroll on new messages
132 const messageCount = activeThread?.messages.length ?? 0;214 const messageCount = activeThread?.messages.length ?? 0;
...@@ -191,6 +273,8 @@ export default function ChatView() {...@@ -191,6 +273,8 @@ export default function ChatView() {
191 provider: "codex",273 provider: "codex",
192 cwd: activeProject.cwd || undefined,274 cwd: activeProject.cwd || undefined,
193 model: selectedModel || undefined,275 model: selectedModel || undefined,
276 approvalPolicy: runtimeSessionConfig.approvalPolicy,
277 sandboxMode: runtimeSessionConfig.sandboxMode,
194 });278 });
195 dispatch({279 dispatch({
196 type: "UPDATE_SESSION",280 type: "UPDATE_SESSION",
...@@ -270,6 +354,37 @@ export default function ChatView() {...@@ -270,6 +354,37 @@ export default function ChatView() {
270 });354 });
271 };355 };
272356
357 const onRespondToApproval = async (
358 requestId: string,
359 decision: ProviderApprovalDecision,
360 ) => {
361 if (!api || !activeThread?.session) return;
362
363 setRespondingRequestIds((existing) =>
364 existing.includes(requestId) ? existing : [...existing, requestId],
365 );
366 try {
367 await api.providers.respondToRequest({
368 sessionId: activeThread.session.sessionId,
369 requestId,
370 decision,
371 });
372 } catch (err) {
373 dispatch({
374 type: "SET_ERROR",
375 threadId: activeThread.id,
376 error:
377 err instanceof Error
378 ? err.message
379 : "Failed to submit approval decision.",
380 });
381 } finally {
382 setRespondingRequestIds((existing) =>
383 existing.filter((id) => id !== requestId),
384 );
385 }
386 };
387
273 const onModelSelect = (model: string) => {388 const onModelSelect = (model: string) => {
274 if (!activeThread) return;389 if (!activeThread) return;
275 dispatch({390 dispatch({
...@@ -335,6 +450,81 @@ export default function ChatView() {...@@ -335,6 +450,81 @@ export default function ChatView() {
335 </div>450 </div>
336 )}451 )}
337452
453 {pendingApprovals.length > 0 && (
454 <div className="mx-4 mt-3 space-y-2">
455 {pendingApprovals.map((approval) => {
456 const isResponding = respondingRequestIds.includes(
457 approval.requestId,
458 );
459 return (
460 <div
461 key={approval.requestId}
462 className="rounded-lg border border-amber-300/20 bg-amber-500/[0.07] px-3 py-2"
463 >
464 <p className="text-xs font-medium text-amber-100">
465 {approval.requestKind === "command"
466 ? "Command approval requested"
467 : "File-change approval requested"}
468 </p>
469 {approval.detail && (
470 <p
471 className="mt-1 truncate font-mono text-[11px] text-amber-100/75"
472 title={approval.detail}
473 >
474 {approval.detail}
475 </p>
476 )}
477 <div className="mt-2 flex flex-wrap gap-1.5">
478 <button
479 type="button"
480 className="rounded-md border border-white/[0.15] bg-white/[0.08] px-2 py-1 text-[11px] text-[#e8e8e8] transition-colors duration-150 hover:bg-white/[0.13] disabled:cursor-not-allowed disabled:opacity-50"
481 disabled={isResponding}
482 onClick={() =>
483 void onRespondToApproval(approval.requestId, "accept")
484 }
485 >
486 Approve once
487 </button>
488 <button
489 type="button"
490 className="rounded-md border border-sky-300/30 bg-sky-500/[0.15] px-2 py-1 text-[11px] text-sky-100 transition-colors duration-150 hover:bg-sky-500/[0.22] disabled:cursor-not-allowed disabled:opacity-50"
491 disabled={isResponding}
492 onClick={() =>
493 void onRespondToApproval(
494 approval.requestId,
495 "acceptForSession",
496 )
497 }
498 >
499 Always allow this session
500 </button>
501 <button
502 type="button"
503 className="rounded-md border border-white/[0.15] px-2 py-1 text-[11px] text-[#d8d8d8] transition-colors duration-150 hover:bg-white/[0.06] disabled:cursor-not-allowed disabled:opacity-50"
504 disabled={isResponding}
505 onClick={() =>
506 void onRespondToApproval(approval.requestId, "decline")
507 }
508 >
509 Decline
510 </button>
511 <button
512 type="button"
513 className="rounded-md border border-rose-300/30 bg-rose-500/[0.12] px-2 py-1 text-[11px] text-rose-100 transition-colors duration-150 hover:bg-rose-500/[0.2] disabled:cursor-not-allowed disabled:opacity-50"
514 disabled={isResponding}
515 onClick={() =>
516 void onRespondToApproval(approval.requestId, "cancel")
517 }
518 >
519 Cancel turn
520 </button>
521 </div>
522 </div>
523 );
524 })}
525 </div>
526 )}
527
338 {/* Messages */}528 {/* Messages */}
339 <div className="flex-1 overflow-y-auto px-5 py-4">529 <div className="flex-1 overflow-y-auto px-5 py-4">
340 {activeThread.messages.length === 0 && !isWorking ? (530 {activeThread.messages.length === 0 && !isWorking ? (
apps/renderer/src/components/Sidebar.tsx+75
...@@ -28,6 +28,7 @@ export default function Sidebar() {...@@ -28,6 +28,7 @@ export default function Sidebar() {
28 const [addingProject, setAddingProject] = useState(false);28 const [addingProject, setAddingProject] = useState(false);
29 const [newCwd, setNewCwd] = useState("");29 const [newCwd, setNewCwd] = useState("");
30 const [isPickingFolder, setIsPickingFolder] = useState(false);30 const [isPickingFolder, setIsPickingFolder] = useState(false);
31 const [isSwitchingRuntimeMode, setIsSwitchingRuntimeMode] = useState(false);
3132
32 const handleAddProject = () => {33 const handleAddProject = () => {
33 const cwd = newCwd.trim();34 const cwd = newCwd.trim();
...@@ -79,6 +80,43 @@ export default function Sidebar() {...@@ -79,6 +80,43 @@ export default function Sidebar() {
79 }80 }
80 };81 };
8182
83 const handleRuntimeModeChange = useCallback(
84 async (mode: "approval-required" | "full-access") => {
85 if (mode === state.runtimeMode) return;
86
87 dispatch({
88 type: "SET_RUNTIME_MODE",
89 mode,
90 });
91
92 if (!api) return;
93
94 const sessionIds = state.threads
95 .map((thread) => thread.session)
96 .filter(
97 (session): session is NonNullable<typeof session> =>
98 session !== null && session.status !== "closed",
99 )
100 .map((session) => session.sessionId);
101
102 if (sessionIds.length === 0) {
103 return;
104 }
105
106 setIsSwitchingRuntimeMode(true);
107 try {
108 await Promise.all(
109 sessionIds.map((sessionId) =>
110 api.providers.stopSession({ sessionId }).catch(() => undefined),
111 ),
112 );
113 } finally {
114 setIsSwitchingRuntimeMode(false);
115 }
116 },
117 [api, dispatch, state.runtimeMode, state.threads],
118 );
119
82 useEffect(() => {120 useEffect(() => {
83 const onWindowKeyDown = (event: KeyboardEvent) => {121 const onWindowKeyDown = (event: KeyboardEvent) => {
84 const isNewThreadShortcut =122 const isNewThreadShortcut =
...@@ -151,6 +189,43 @@ export default function Sidebar() {...@@ -151,6 +189,43 @@ export default function Sidebar() {
151 </button>189 </button>
152 </div>190 </div>
153191
192 <div className="px-3 pb-3">
193 <div className="rounded-lg border border-white/[0.08] bg-white/[0.03] p-2">
194 <p className="px-1 text-[10px] font-medium uppercase tracking-[0.08em] text-[#a0a0a0]/60">
195 Runtime mode
196 </p>
197 <p className="px-1 pt-0.5 pb-2 text-[10px] text-[#a0a0a0]/45">
198 Switching restarts live sessions.
199 </p>
200 <div className="grid grid-cols-2 gap-1">
201 <button
202 type="button"
203 className={`rounded-md px-2 py-1.5 text-[11px] transition-colors duration-150 ${
204 state.runtimeMode === "approval-required"
205 ? "bg-white/12 text-white"
206 : "text-[#a0a0a0]/70 hover:bg-white/[0.06]"
207 }`}
208 disabled={isSwitchingRuntimeMode}
209 onClick={() => void handleRuntimeModeChange("approval-required")}
210 >
211 Approval required
212 </button>
213 <button
214 type="button"
215 className={`rounded-md px-2 py-1.5 text-[11px] transition-colors duration-150 ${
216 state.runtimeMode === "full-access"
217 ? "bg-white/12 text-white"
218 : "text-[#a0a0a0]/70 hover:bg-white/[0.06]"
219 }`}
220 disabled={isSwitchingRuntimeMode}
221 onClick={() => void handleRuntimeModeChange("full-access")}
222 >
223 Full access
224 </button>
225 </div>
226 </div>
227 </div>
228
154 {/* Project list */}229 {/* Project list */}
155 <nav className="flex-1 overflow-y-auto px-2">230 <nav className="flex-1 overflow-y-auto px-2">
156 {state.projects.map((project) => {231 {state.projects.map((project) => {
apps/renderer/src/persistenceSchema.test.ts+27-2
...@@ -47,6 +47,7 @@ describe("hydratePersistedState", () => {...@@ -47,6 +47,7 @@ describe("hydratePersistedState", () => {
47 expect(hydrated?.projects[0]?.model).toBe(DEFAULT_MODEL);47 expect(hydrated?.projects[0]?.model).toBe(DEFAULT_MODEL);
48 expect(hydrated?.threads[0]?.model).toBe(DEFAULT_MODEL);48 expect(hydrated?.threads[0]?.model).toBe(DEFAULT_MODEL);
49 expect(hydrated?.threads[0]?.messages[0]?.streaming).toBe(false);49 expect(hydrated?.threads[0]?.messages[0]?.streaming).toBe(false);
50 expect(hydrated?.runtimeMode).toBe("full-access");
50 });51 });
5152
52 it("filters unknown project references and repairs active thread", () => {53 it("filters unknown project references and repairs active thread", () => {
...@@ -85,11 +86,33 @@ describe("hydratePersistedState", () => {...@@ -85,11 +86,33 @@ describe("hydratePersistedState", () => {
85 expect(hydrated).not.toBeNull();86 expect(hydrated).not.toBeNull();
86 expect(hydrated?.threads.map((thread) => thread.id)).toEqual(["t-1"]);87 expect(hydrated?.threads.map((thread) => thread.id)).toEqual(["t-1"]);
87 expect(hydrated?.activeThreadId).toBe("t-1");88 expect(hydrated?.activeThreadId).toBe("t-1");
89 expect(hydrated?.runtimeMode).toBe("full-access");
90 });
91
92 it("hydrates runtime mode from v3 payload", () => {
93 const payload = JSON.stringify({
94 version: 3,
95 runtimeMode: "approval-required",
96 projects: [
97 {
98 id: "p-1",
99 name: "Project",
100 cwd: "/tmp/project",
101 model: "gpt-5.3-codex",
102 expanded: false,
103 },
104 ],
105 threads: [],
106 activeThreadId: null,
107 });
108
109 const hydrated = hydratePersistedState(payload, false);
110 expect(hydrated?.runtimeMode).toBe("approval-required");
88 });111 });
89});112});
90113
91describe("toPersistedState", () => {114describe("toPersistedState", () => {
92 it("writes v2 payload and strips non-persisted thread fields", () => {115 it("writes v3 payload and strips non-persisted thread fields", () => {
93 const thread: Thread = {116 const thread: Thread = {
94 id: "t-1",117 id: "t-1",
95 projectId: "p-1",118 projectId: "p-1",
...@@ -122,9 +145,11 @@ describe("toPersistedState", () => {...@@ -122,9 +145,11 @@ describe("toPersistedState", () => {
122 ],145 ],
123 threads: [thread],146 threads: [thread],
124 activeThreadId: "t-1",147 activeThreadId: "t-1",
148 runtimeMode: "full-access",
125 });149 });
126150
127 expect(persisted.version).toBe(2);151 expect(persisted.version).toBe(3);
152 expect(persisted.runtimeMode).toBe("full-access");
128 expect(persisted.threads[0]).toEqual({153 expect(persisted.threads[0]).toEqual({
129 id: "t-1",154 id: "t-1",
130 projectId: "p-1",155 projectId: "p-1",
apps/renderer/src/persistenceSchema.ts+22-3
...@@ -1,7 +1,12 @@...@@ -1,7 +1,12 @@
1import { z } from "zod";1import { z } from "zod";
22
3import { DEFAULT_MODEL, resolveModelSlug } from "./model-logic";3import { DEFAULT_MODEL, resolveModelSlug } from "./model-logic";
4import type { Project, Thread } from "./types";4import {
5 DEFAULT_RUNTIME_MODE,
6 type Project,
7 type RuntimeMode,
8 type Thread,
9} from "./types";
510
6const LEGACY_DEFAULT_MODEL = "gpt-5.2-codex";11const LEGACY_DEFAULT_MODEL = "gpt-5.2-codex";
712
...@@ -36,6 +41,8 @@ const persistedStateBodySchema = z.object({...@@ -36,6 +41,8 @@ const persistedStateBodySchema = z.object({
36 activeThreadId: z.string().min(1).nullable(),41 activeThreadId: z.string().min(1).nullable(),
37});42});
3843
44const runtimeModeSchema = z.enum(["approval-required", "full-access"]);
45
39export const persistedStateV1Schema = persistedStateBodySchema.extend({46export const persistedStateV1Schema = persistedStateBodySchema.extend({
40 version: z.literal(1).optional(),47 version: z.literal(1).optional(),
41});48});
...@@ -44,7 +51,13 @@ export const persistedStateV2Schema = persistedStateBodySchema.extend({...@@ -44,7 +51,13 @@ export const persistedStateV2Schema = persistedStateBodySchema.extend({
44 version: z.literal(2).optional(),51 version: z.literal(2).optional(),
45});52});
4653
54export const persistedStateV3Schema = persistedStateBodySchema.extend({
55 runtimeMode: runtimeModeSchema.default(DEFAULT_RUNTIME_MODE),
56 version: z.literal(3).optional(),
57});
58
47const persistedStateSchema = z.union([59const persistedStateSchema = z.union([
60 persistedStateV3Schema,
48 persistedStateV2Schema,61 persistedStateV2Schema,
49 persistedStateV1Schema,62 persistedStateV1Schema,
50]);63]);
...@@ -53,6 +66,7 @@ export interface PersistedStoreSnapshot {...@@ -53,6 +66,7 @@ export interface PersistedStoreSnapshot {
53 projects: Project[];66 projects: Project[];
54 threads: Thread[];67 threads: Thread[];
55 activeThreadId: string | null;68 activeThreadId: string | null;
69 runtimeMode: RuntimeMode;
56}70}
5771
58function maybeMigrateLegacyModel(72function maybeMigrateLegacyModel(
...@@ -134,14 +148,18 @@ export function hydratePersistedState(...@@ -134,14 +148,18 @@ export function hydratePersistedState(
134 activeThreadId: hasActiveThread148 activeThreadId: hasActiveThread
135 ? parsedState.data.activeThreadId149 ? parsedState.data.activeThreadId
136 : (threads[0]?.id ?? null),150 : (threads[0]?.id ?? null),
151 runtimeMode:
152 "runtimeMode" in parsedState.data
153 ? parsedState.data.runtimeMode
154 : DEFAULT_RUNTIME_MODE,
137 };155 };
138}156}
139157
140export function toPersistedState(158export function toPersistedState(
141 state: PersistedStoreSnapshot,159 state: PersistedStoreSnapshot,
142): z.infer<typeof persistedStateV2Schema> {160): z.infer<typeof persistedStateV3Schema> {
143 return {161 return {
144 version: 2,162 version: 3,
145 projects: state.projects,163 projects: state.projects,
146 threads: state.threads.map((thread) => ({164 threads: state.threads.map((thread) => ({
147 id: thread.id,165 id: thread.id,
...@@ -152,5 +170,6 @@ export function toPersistedState(...@@ -152,5 +170,6 @@ export function toPersistedState(
152 createdAt: thread.createdAt,170 createdAt: thread.createdAt,
153 })),171 })),
154 activeThreadId: state.activeThreadId,172 activeThreadId: state.activeThreadId,
173 runtimeMode: state.runtimeMode,
155 };174 };
156}175}
apps/renderer/src/store.ts+29-8
...@@ -17,7 +17,12 @@ import {...@@ -17,7 +17,12 @@ import {
17 asString,17 asString,
18 evolveSession,18 evolveSession,
19} from "./session-logic";19} from "./session-logic";
20import type { Project, Thread } from "./types";20import {
21 DEFAULT_RUNTIME_MODE,
22 type Project,
23 type RuntimeMode,
24 type Thread,
25} from "./types";
2126
22// ── Actions ──────────────────────────────────────────────────────────27// ── Actions ──────────────────────────────────────────────────────────
2328
...@@ -36,7 +41,8 @@ type Action =...@@ -36,7 +41,8 @@ type Action =
36 | { type: "PUSH_USER_MESSAGE"; threadId: string; id: string; text: string }41 | { type: "PUSH_USER_MESSAGE"; threadId: string; id: string; text: string }
37 | { type: "SET_ERROR"; threadId: string; error: string | null }42 | { type: "SET_ERROR"; threadId: string; error: string | null }
38 | { type: "SET_THREAD_TITLE"; threadId: string; title: string }43 | { type: "SET_THREAD_TITLE"; threadId: string; title: string }
39 | { type: "SET_THREAD_MODEL"; threadId: string; model: string };44 | { type: "SET_THREAD_MODEL"; threadId: string; model: string }
45 | { type: "SET_RUNTIME_MODE"; mode: RuntimeMode };
4046
41// ── State ────────────────────────────────────────────────────────────47// ── State ────────────────────────────────────────────────────────────
4248
...@@ -44,16 +50,21 @@ export interface AppState {...@@ -44,16 +50,21 @@ export interface AppState {
44 projects: Project[];50 projects: Project[];
45 threads: Thread[];51 threads: Thread[];
46 activeThreadId: string | null;52 activeThreadId: string | null;
53 runtimeMode: RuntimeMode;
47 diffOpen: boolean;54 diffOpen: boolean;
48}55}
4956
50const PERSISTED_STATE_KEY = "codething:renderer-state:v2";57const PERSISTED_STATE_KEY = "codething:renderer-state:v3";
51const LEGACY_PERSISTED_STATE_KEY = "codething:renderer-state:v1";58const LEGACY_PERSISTED_STATE_KEYS = [
59 "codething:renderer-state:v2",
60 "codething:renderer-state:v1",
61] as const;
5262
53const initialState: AppState = {63const initialState: AppState = {
54 projects: [],64 projects: [],
55 threads: [],65 threads: [],
56 activeThreadId: null,66 activeThreadId: null,
67 runtimeMode: DEFAULT_RUNTIME_MODE,
57 diffOpen: false,68 diffOpen: false,
58};69};
5970
...@@ -64,12 +75,14 @@ function readPersistedState(): AppState {...@@ -64,12 +75,14 @@ function readPersistedState(): AppState {
6475
65 try {76 try {
66 const rawCurrent = window.localStorage.getItem(PERSISTED_STATE_KEY);77 const rawCurrent = window.localStorage.getItem(PERSISTED_STATE_KEY);
67 const rawLegacy = window.localStorage.getItem(LEGACY_PERSISTED_STATE_KEY);78 const [legacyV2Key, legacyV1Key] = LEGACY_PERSISTED_STATE_KEYS;
68 const raw = rawCurrent ?? rawLegacy;79 const rawLegacyV2 = window.localStorage.getItem(legacyV2Key);
80 const rawLegacyV1 = window.localStorage.getItem(legacyV1Key);
81 const raw = rawCurrent ?? rawLegacyV2 ?? rawLegacyV1;
69 if (!raw) return initialState;82 if (!raw) return initialState;
70 const hydrated = hydratePersistedState(83 const hydrated = hydratePersistedState(
71 raw,84 raw,
72 !rawCurrent && Boolean(rawLegacy),85 !rawCurrent && !rawLegacyV2 && Boolean(rawLegacyV1),
73 );86 );
74 if (!hydrated) return initialState;87 if (!hydrated) return initialState;
7588
...@@ -87,7 +100,9 @@ function persistState(state: AppState): void {...@@ -87,7 +100,9 @@ function persistState(state: AppState): void {
87 PERSISTED_STATE_KEY,100 PERSISTED_STATE_KEY,
88 JSON.stringify(toPersistedState(state)),101 JSON.stringify(toPersistedState(state)),
89 );102 );
90 window.localStorage.removeItem(LEGACY_PERSISTED_STATE_KEY);103 for (const legacyKey of LEGACY_PERSISTED_STATE_KEYS) {
104 window.localStorage.removeItem(legacyKey);
105 }
91 } catch {106 } catch {
92 // Ignore quota/storage errors to avoid breaking chat UX.107 // Ignore quota/storage errors to avoid breaking chat UX.
93 }108 }
...@@ -285,6 +300,12 @@ function reducer(state: AppState, action: Action): AppState {...@@ -285,6 +300,12 @@ function reducer(state: AppState, action: Action): AppState {
285 })),300 })),
286 };301 };
287302
303 case "SET_RUNTIME_MODE":
304 return {
305 ...state,
306 runtimeMode: action.mode,
307 };
308
288 default:309 default:
289 return state;310 return state;
290 }311 }
apps/renderer/src/types.ts+2
...@@ -1,6 +1,8 @@...@@ -1,6 +1,8 @@
1import type { ProviderEvent, ProviderSession } from "@acme/contracts";1import type { ProviderEvent, ProviderSession } from "@acme/contracts";
22
3export type SessionPhase = "disconnected" | "connecting" | "ready" | "running";3export type SessionPhase = "disconnected" | "connecting" | "ready" | "running";
4export type RuntimeMode = "approval-required" | "full-access";
5export const DEFAULT_RUNTIME_MODE: RuntimeMode = "full-access";
46
5export interface ChatMessage {7export interface ChatMessage {
6 id: string;8 id: string;
packages/contracts/src/ipc.ts+3
...@@ -2,6 +2,7 @@ import type { AgentConfig, AgentExit, OutputChunk } from "./agent";...@@ -2,6 +2,7 @@ import type { AgentConfig, AgentExit, OutputChunk } from "./agent";
2import type {2import type {
3 ProviderEvent,3 ProviderEvent,
4 ProviderInterruptTurnInput,4 ProviderInterruptTurnInput,
5 ProviderRespondToRequestInput,
5 ProviderSendTurnInput,6 ProviderSendTurnInput,
6 ProviderSession,7 ProviderSession,
7 ProviderSessionStartInput,8 ProviderSessionStartInput,
...@@ -28,6 +29,7 @@ export const IPC_CHANNELS = {...@@ -28,6 +29,7 @@ export const IPC_CHANNELS = {
28 providerTurnInterrupt: "provider:turn:interrupt",29 providerTurnInterrupt: "provider:turn:interrupt",
29 providerSessionStop: "provider:session:stop",30 providerSessionStop: "provider:session:stop",
30 providerSessionList: "provider:session:list",31 providerSessionList: "provider:session:list",
32 providerRequestRespond: "provider:request:respond",
31 providerEvent: "provider:event",33 providerEvent: "provider:event",
32} as const;34} as const;
3335
...@@ -59,6 +61,7 @@ export interface NativeApi {...@@ -59,6 +61,7 @@ export interface NativeApi {
59 input: ProviderSendTurnInput,61 input: ProviderSendTurnInput,
60 ) => Promise<ProviderTurnStartResult>;62 ) => Promise<ProviderTurnStartResult>;
61 interruptTurn: (input: ProviderInterruptTurnInput) => Promise<void>;63 interruptTurn: (input: ProviderInterruptTurnInput) => Promise<void>;
64 respondToRequest: (input: ProviderRespondToRequestInput) => Promise<void>;
62 stopSession: (input: ProviderStopSessionInput) => Promise<void>;65 stopSession: (input: ProviderStopSessionInput) => Promise<void>;
63 listSessions: () => Promise<ProviderSession[]>;66 listSessions: () => Promise<ProviderSession[]>;
64 onEvent: (callback: (event: ProviderEvent) => void) => () => void;67 onEvent: (callback: (event: ProviderEvent) => void) => () => void;
packages/contracts/src/provider.test.ts+37
...@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";...@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
22
3import {3import {
4 providerEventSchema,4 providerEventSchema,
5 providerRespondToRequestInputSchema,
5 providerSendTurnInputSchema,6 providerSendTurnInputSchema,
6 providerSessionStartInputSchema,7 providerSessionStartInputSchema,
7} from "./provider";8} from "./provider";
...@@ -45,4 +46,40 @@ describe("providerEventSchema", () => {...@@ -45,4 +46,40 @@ describe("providerEventSchema", () => {
45 });46 });
46 expect(parsed.method).toBe("item/agentMessage/delta");47 expect(parsed.method).toBe("item/agentMessage/delta");
47 });48 });
49
50 it("accepts request approval metadata", () => {
51 const parsed = providerEventSchema.parse({
52 id: "evt_2",
53 kind: "request",
54 provider: "codex",
55 sessionId: "sess_1",
56 createdAt: "2026-01-01T00:00:00.000Z",
57 method: "item/commandExecution/requestApproval",
58 requestId: "req_123",
59 requestKind: "command",
60 });
61 expect(parsed.requestId).toBe("req_123");
62 expect(parsed.requestKind).toBe("command");
63 });
64});
65
66describe("providerRespondToRequestInputSchema", () => {
67 it("accepts valid decisions", () => {
68 const parsed = providerRespondToRequestInputSchema.parse({
69 sessionId: "sess_1",
70 requestId: "req_1",
71 decision: "acceptForSession",
72 });
73 expect(parsed.decision).toBe("acceptForSession");
74 });
75
76 it("rejects unknown decisions", () => {
77 expect(() =>
78 providerRespondToRequestInputSchema.parse({
79 sessionId: "sess_1",
80 requestId: "req_1",
81 decision: "always",
82 }),
83 ).toThrow();
84 });
48});85});
packages/contracts/src/provider.ts+24
...@@ -15,6 +15,15 @@ export const providerSandboxModeSchema = z.enum([...@@ -15,6 +15,15 @@ export const providerSandboxModeSchema = z.enum([
15 "danger-full-access",15 "danger-full-access",
16]);16]);
1717
18export const providerRequestKindSchema = z.enum(["command", "file-change"]);
19
20export const providerApprovalDecisionSchema = z.enum([
21 "accept",
22 "acceptForSession",
23 "decline",
24 "cancel",
25]);
26
18export const providerSessionStatusSchema = z.enum([27export const providerSessionStatusSchema = z.enum([
19 "connecting",28 "connecting",
20 "ready",29 "ready",
...@@ -65,6 +74,12 @@ export const providerStopSessionInputSchema = z.object({...@@ -65,6 +74,12 @@ export const providerStopSessionInputSchema = z.object({
65 sessionId: z.string().min(1),74 sessionId: z.string().min(1),
66});75});
6776
77export const providerRespondToRequestInputSchema = z.object({
78 sessionId: z.string().min(1),
79 requestId: z.string().min(1),
80 decision: providerApprovalDecisionSchema,
81});
82
68export const providerEventKindSchema = z.enum([83export const providerEventKindSchema = z.enum([
69 "session",84 "session",
70 "notification",85 "notification",
...@@ -83,6 +98,8 @@ export const providerEventSchema = z.object({...@@ -83,6 +98,8 @@ export const providerEventSchema = z.object({
83 threadId: z.string().min(1).optional(),98 threadId: z.string().min(1).optional(),
84 turnId: z.string().min(1).optional(),99 turnId: z.string().min(1).optional(),
85 itemId: z.string().min(1).optional(),100 itemId: z.string().min(1).optional(),
101 requestId: z.string().min(1).optional(),
102 requestKind: providerRequestKindSchema.optional(),
86 textDelta: z.string().optional(),103 textDelta: z.string().optional(),
87 payload: z.unknown().optional(),104 payload: z.unknown().optional(),
88});105});
...@@ -92,6 +109,10 @@ export type ProviderApprovalPolicy = z.infer<...@@ -92,6 +109,10 @@ export type ProviderApprovalPolicy = z.infer<
92 typeof providerApprovalPolicySchema109 typeof providerApprovalPolicySchema
93>;110>;
94export type ProviderSandboxMode = z.infer<typeof providerSandboxModeSchema>;111export type ProviderSandboxMode = z.infer<typeof providerSandboxModeSchema>;
112export type ProviderRequestKind = z.infer<typeof providerRequestKindSchema>;
113export type ProviderApprovalDecision = z.infer<
114 typeof providerApprovalDecisionSchema
115>;
95export type ProviderSessionStatus = z.infer<typeof providerSessionStatusSchema>;116export type ProviderSessionStatus = z.infer<typeof providerSessionStatusSchema>;
96export type ProviderSession = z.infer<typeof providerSessionSchema>;117export type ProviderSession = z.infer<typeof providerSessionSchema>;
97export type ProviderSessionStartInput = z.input<118export type ProviderSessionStartInput = z.input<
...@@ -107,5 +128,8 @@ export type ProviderInterruptTurnInput = z.input<...@@ -107,5 +128,8 @@ export type ProviderInterruptTurnInput = z.input<
107export type ProviderStopSessionInput = z.input<128export type ProviderStopSessionInput = z.input<
108 typeof providerStopSessionInputSchema129 typeof providerStopSessionInputSchema
109>;130>;
131export type ProviderRespondToRequestInput = z.input<
132 typeof providerRespondToRequestInputSchema
133>;
110export type ProviderEventKind = z.infer<typeof providerEventKindSchema>;134export type ProviderEventKind = z.infer<typeof providerEventKindSchema>;
111export type ProviderEvent = z.infer<typeof providerEventSchema>;135export type ProviderEvent = z.infer<typeof providerEventSchema>;