1import { useAtomValue } from "@effect/atom-react";
2import { AuthOrchestrationOperateScope, type ScopedThreadRef } from "@t3tools/contracts";
3import type { ChildThreadApproval } from "@t3tools/client-runtime/state/thread-requests";
4import { fullAccessOverrideExplanation } from "@t3tools/client-runtime/state/thread-requests";
5import {
6 isAtomCommandInterrupted,
7 squashAtomCommandFailure,
8} from "@t3tools/client-runtime/state/runtime";
9import { useState } from "react";
10import { useEnvironmentScope } from "../../state/session";
11import { childThreadApprovalsAtom, threadEnvironment } from "../../state/threads";
12import { useAtomCommand } from "../../state/use-atom-command";
13import { ComposerPendingApprovalActions } from "./ComposerPendingApprovalActions";
14
15function ChildApproval({ entry }: { entry: ChildThreadApproval }) {
16 const canRespond = useEnvironmentScope(entry.thread.environmentId, AuthOrchestrationOperateScope);
17 const respond = useAtomCommand(threadEnvironment.respondToApproval, { reportFailure: false });
18 const [responding, setResponding] = useState(false);
19 const [error, setError] = useState<string | null>(null);
20 const { approval } = entry;
21 return (
22 <section
23 aria-label={`Approval from ${entry.title}`}
24 className="rounded-lg border border-border bg-background p-3 text-xs"
25 >
26 <p className="mb-2 font-medium">Approval needed from {entry.title}</p>
27 <pre className="max-h-40 overflow-auto whitespace-pre-wrap break-words font-mono">
28 {approval.detail || approval.requestKind}
29 </pre>
30 {approval.appName ? <p className="mt-2 text-muted-foreground">{approval.appName}</p> : null}
31 {approval.responseCapability !== "live" ? (
32 <p className="mt-2 text-warning">
33 Provider process is gone. Interrupt or restart the child thread to respond.
34 </p>
35 ) : null}
36 {approval.reason ? <p className="mt-2 text-muted-foreground">{approval.reason}</p> : null}
37 {approval.fullAccessOverride ? (
38 <p className="mt-2 text-muted-foreground">
39 {fullAccessOverrideExplanation(approval.fullAccessOverride)}
40 </p>
41 ) : null}
42 <div className="mt-2 flex flex-wrap gap-2">
43 <ComposerPendingApprovalActions
44 presentation="inline"
45 requestId={approval.requestId}
46 isResponding={responding}
47 canRespond={approval.responseCapability === "live"}
48 disabled={!canRespond}
49 options={approval.options}
50 onRespondToApproval={async (requestId, decision) => {
51 if (!canRespond || responding) return;
52 setResponding(true);
53 setError(null);
54 const result = await respond({
55 environmentId: entry.thread.environmentId,
56 input: { threadId: entry.thread.threadId, requestId, decision },
57 });
58 if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) {
59 const failure = squashAtomCommandFailure(result);
60 setError(
61 failure instanceof Error ? failure.message : "Failed to submit approval decision.",
62 );
63 }
64 setResponding(false);
65 return result;
66 }}
67 />
68 </div>
69 {error !== null ? (
70 <p role="alert" className="mt-2 text-destructive">
71 {error}
72 </p>
73 ) : null}
74 </section>
75 );
76}
77
78export function ChildThreadApprovals({ threadRef }: { threadRef: ScopedThreadRef | null }) {
79 const approvals = useAtomValue(childThreadApprovalsAtom(threadRef));
80 if (approvals.length === 0) return null;
81 return (
82 <div className="mb-2 flex max-h-80 flex-col gap-2 overflow-auto">
83 {approvals.map((entry) => (
84 <ChildApproval
85 key={JSON.stringify([
86 entry.thread.environmentId,
87 entry.thread.threadId,
88 entry.approval.requestId,
89 ])}
90 entry={entry}
91 />
92 ))}
93 </div>
94 );
95}