Confirmation's approval type is a hand copy of the SDK's, and it has now drifted
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 2.4k
- Forks
- 283
- Avg merge
- 32m
- Merged PRs (30d)
- 1
Description
packages/elements/src/confirmation.tsx imports ToolUIPart from ai and then declares its own approval shape underneath it rather than deriving it:
type ToolUIPartApproval =
| { id: string; approved?: never; reason?: never }
| { id: string; approved: boolean; reason?: string }
| { id: string; approved: true; reason?: string }
| { id: string; approved: false; reason?: string }
| undefined;
Those four members line up with the states the SDK defines an approval on: approval-requested, approval-responded, output-available / output-error, and output-denied. In ai@7.0.84 each of those carries three more fields than the copy does. Taking approval-requested as the example:
approval: {
id: string;
approved?: never;
requestReason?: string; // missing from the copy
reason?: never;
isAutomatic?: boolean; // missing from the copy
signature?: string; // missing from the copy
}
requestReason is the one that shows the cost of the copy. It was added in vercel/ai#19658, shipped in ai@7.0.82, and it exists specifically so the explanation of why a call was routed to a person reaches that person. Anyone typing their approval object with ToolUIPartApproval cannot read it without a cast, so the component's own type hides the field that was added for the component's own job.
isAutomatic has the same problem and predates it. The tool approval docs branch on part.state === 'approval-requested' && !part.approval.isAutomatic, so that a person is not asked to approve something the SDK already approved on its own. The type here discourages the check the documentation asks for.
Flattening the per-state shapes into one union also made two members unreachable. { approved: boolean; reason?: string } already covers both { approved: true; reason?: string } and { approved: false; reason?: string }, so those two branches can never be discriminated on and add nothing.
Deriving it would fix all of this at once and keep the two in step:
type ToolUIPartApproval = NonNullable<ToolUIPart['approval']>;
Worth flagging that the shape is still moving. vercel/ai#18967 is open and adds context?: JSONValue to the user-approval status. Because the type here is a copy rather than a reference, nothing in this file will fail to compile when that lands either. It will just disagree quietly again, the same way it is disagreeing now.
For context, vercel/ai#19658 came out of an issue I filed. Happy to open a PR if deriving the type is the direction you want.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in packages/elements/src/confirmation.tsx, where ToolUIPart is imported and the local ToolUIPartApproval union is declared. Compare that alias with ToolUIPart['approval'] and confirm the component's approval usage still type-checks. Done means the confirmation type exposes the SDK's current fields and no longer duplicates its per-state shape.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- frontend
- Issue type
- Refactor
- Difficulty
- 1/5
- Estimated time
- Under an hour
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 88/100