Repository navigation
Releases: dgesteves/signoff-ui
Release list
signoff-ui@0.7.1
Patch Changes
- #102
03fa6adThanks @dgesteves! - In narrow containers with longer labels, such as a translation,DiffReviewwraps its file buttons, its footer and (under 30rem) its lines instead of cutting them off, keeps "+n −n" together and shows a decided hunk's badge under its range, andApprovalCardshows four or five choices in two columns with Approve across the last row;RunMeterreaches new numbers within 200 ms and starts each run from "–", its cost too, instead of counting down from the last run.
signoff-ui@0.7.0
Minor Changes
-
#84
cb0bae9Thanks @dgesteves! -AgentMessageandReasoningcan render text with your own Markdown renderer, such as Streamdown, through arenderMarkdownprop orMarkdownRendererProvider, andsignoff-ui/custom-markdownexports them without react-markdown, so it stays out of your bundle. -
#85
2420e62Thanks @dgesteves! -DiffReviewtakes anactionsprop: your own controls, such as a "Copy as a patch" button, in its header next to the layout toggle. They sit in adata-signoff-slotelement, sostyles.cssleaves them to your CSS, and keys pressed on them don't trigger the review's shortcuts. -
#83
70c341aThanks @dgesteves! -reviewToolOutputandreviewResumeEntrynow tell the agent which files the reviewer marked viewed: each such file hasviewed: true, and the others have noviewedkey. -
#86
d29ff27Thanks @dgesteves! - To send a person's approval rules with each request,policy.getRules()returns the latest rules as plain data for the transport'sbody, andparseApprovalRules(rules)fromsignoff-ui/corechecks them in the route beforetoToolApproval.getRulesis a new member of theApprovalPolicyinterface. Code that builds its ownApprovalPolicyobject, rather than usinguseApprovalPolicy, needs to add it. -
#72
5972edcThanks @dgesteves! - Sub-agents nest in the timeline. The AG-UI adapter mapsSUBAGENT_STARTED,SUBAGENT_FINISHEDandSUBAGENT_ERROR, and the messages and events that carry asubagentRunId, into onedata-signoff-subagentpart on the parent message, after the call that started it:{ subagentRunId, parentToolCallId, name, status, parts }, with the sub-agent's tool calls and text in the same shapes as the parent's.ToolCallTimeline, inAgentMessagetoo, shows it under that call as a collapsed row with its name, its number of calls and its state, which opens to its calls, with the same keys, names and announcements as the others. A sub-agent's call can wait on approval like any: its row opens to the card,deriveAgentStatereadsawaiting-approvaland "Approve all" includes it. The run state lists the sub-agents (subagents), events may arrive in any order, and the part's types (SubagentPart,SubagentData,SubagentStatus) andisSubagentPartare exported, so other sources can stream their own. The labels gaintoolCallTimeline.subagent. -
#82
7a0aa14Thanks @dgesteves! -toToolApproval(rules, { reason })words the reason for a call a rule decides, so the server's rules and a person's own can each say whose rule it was; withoutreason, the reason is the same as before.
Patch Changes
-
#92
1e9ad7dThanks @dgesteves! -ApprovalCard(andToolApprovalCard, and the cards inAgentMessagethroughapprovalProps) no longer approves or denies when Y, S, A, N, Shift+N or ⌘/Ctrl+Enter is pressed on a control in a custompreview, or in a menu thatpreviewopens in a portal. Those keys go to the control. WithglobalShortcut, ⌘/Ctrl+Enter still approves from anywhere on the page. -
#81
ad50100Thanks @dgesteves! -useApprovalPolicy: analwaysrule fromdefaultRulesthat a person removes now stays removed after a reload, instead of coming back on every load. A default your app adds in a later version still appears.webStorageRules()keeps the removed ids under<key>:removed-defaultsandmemoryRuleStorage()keeps them in memory; a storage of your own can keep them with the new optionalloadRemovedDefaults()andsaveRemovedDefaults(ids). Rules saved by earlier versions load as before. -
#94
9d8d48aThanks @dgesteves! -DiffReviewrenders no actions slot foractions={true}oractions="", as forundefined,nullandfalse: React renders nothing for them, and the empty slot narrowed the title. -
#91
bcfda1cThanks @dgesteves! - Approval rules match an argument in time proportional to its length times the pattern's.matchesGlob, and with itevaluateRules,toToolApprovalanduseApprovalPolicy, used a regular expression that backtracked: a pattern with a few*, such as*a*a*a*b, took seconds on an argument of a few hundred characters that it didn't match, and held up the server or the page meanwhile. It now reads the value once and gives the same answers.globToRegExpstill returns the sameRegExp, which backtracks. -
#69
34ff527Thanks @dgesteves! - npm's homepage link for the package is now the website, signoff-ui.vercel.app, instead of the GitHub README. -
#66
89e4314Thanks @dgesteves! -RunMeterno longer shows a negative number for a moment as a value animates, such as "↑ -89" or "$-0.0002" at the start of a run. A frame that began before the animation counts as its start.
signoff-ui@0.6.0
Minor Changes
-
#59
782ed22Thanks @dgesteves! -signoff-ui/acp: render Agent Client Protocol sessions with the components. Pure functions, no React, nothing new to install.-
A prompt turn as a message.
createAcpTurn(), thenreduceAcpTurn(turn, notification)for eachsession/update: message and thought chunks become text and reasoning, and tool calls and their updates merge by id.requestAcpPermission(turn, request)marks a call as waiting on itssession/request_permission,answerAcpPermission(turn, toolCallId, decision)records the answer, andendAcpTurn(turn, response)ends the turn with itsstopReason(or an error).toAcpMessage(turn)is the assistantUIMessageforAgentMessageandToolCallTimeline: a call waiting on a person isapproval-requested, its tool call id the approval id, and a denied or cancelled one isoutput-denied. -
Diffs as review files.
fromAcpDiffs(content, { root })maps a tool call's{ type: 'diff', path, oldText, newText }content toDiffReviewfiles, relative to the session'scwd.toAcpDiffsgoes back. -
The permission converters (
decisionsFromAcpOptions,toAcpPermissionResponse,fromAcpPermissionResponse,fromAcpPermissionRequest) are re-exported here.fromAcpPermissionRequestnow names the tool by ACP 1.8'snamewhen the agent sends one, then bykindandtitleas before (acpToolName). Rules saved against a call's kind (execute) no longer match when the agent sends a name (Bash): those calls are asked about again, and adeny-alwaysrule on the kind needs a rule on the name added to keep blocking them. -
ApprovalCard'sruleScope={false}hides the argument patterns a session or always decision would cover, for when the agent keeps what "always" means, as ACP'sallow_alwaysdoes. The decision then comes withoutargs.
The types mirror what is read, and a type test checks that
@agentclientprotocol/sdk1.8's notifications, permission requests and diff content fit them, and that the responses fit the SDK's. The components page has a demo with a scripted ACP agent. -
-
#57
66d5c2fThanks @dgesteves! -signoff-ui/assistant-ui: review and approve assistant-ui tool calls.signoffTools({ review })goes inMessagePrimitive.Parts'components.tools. A call to one of thereviewtools gets aDiffReviewonce its arguments are in, and the review goes back as the call's result throughaddResult(reviewToolOutput(review)), then stays on the page read-only. Any other call at an approval gate gets the approval card, answered throughrespondToApproval.Fallbackrenders the calls with no gate.SignoffToolsProvider, optional, passes approval rules fromuseApprovalPolicy(once, this session or always, and a call a rule decides is answered without a card once the run has paused for it), labels and risk levels by tool, props for everyDiffReviewand approval card, andreviewToolOutput's options.ReviewToolUIandApprovalToolUIare the two components, for your own tool components.
It works with any assistant-ui runtime; it ran end to end with the AI SDK runtime (
@assistant-ui/react-ai-sdk1.4) and the local runtime.@assistant-ui/react(^0.15.0) is an optional peer dependency: the entry imports only its types, and nothing of it reaches the main entry. The shadcn registry has it as theassistant-uiitem.ToolApprovalCardwith apolicyno longer answers a call twice when a person's session or always decision adds a rule while the call still reads pending, as it does with a runtime that records the answer a moment later. -
#63
2a7cff8Thanks @dgesteves! -ToolCallTimelinekeeps up with very long runs. At 2,000 calls, appending one took about 105 ms of the main thread until its frame was painted; it now takes about 2.5 ms (12 ms with the CPU six times slower). With a call running, frames dropped to about 50 ms; they now keep the display's pace. The first render goes from about 440 ms to 40 ms.- A long run renders only the calls near the screen. Past 150 calls, the timeline renders the calls within 800 px of the viewport, in chunks of 25. A chunk away from it is an empty item as tall as its calls, hidden from assistive tech. A chunk holding an open call, a call still going (running, streaming its input or waiting on approval) or focus stays rendered. The calls that render carry
aria-posinsetandaria-setsize. Arrow Up and Down, Home and End move between all the calls, scrolling a call that isn't rendered into view and focusing it. Announcements are unchanged. What a call far from the screen renders, includingrenderExtra's content, unmounts until it comes back near. Shorter timelines render every call, as before. - Only the calls that change render again. A new call, or a running call's clock, no longer renders every call. The waterfall's bars are placed from each call's times and the timeline's span, set on the bars that are rendered.
- The enter animation leaves nothing behind.
--animate-signoff-enterfillsbackwards, notboth: the end state is the same, but a finished animation no longer stays in effect on every element it ran on. A call that renders again as its chunk comes back near the screen doesn't slide in.
- A long run renders only the calls near the screen. Past 150 calls, the timeline renders the calls within 800 px of the viewport, in chunks of 25. A chunk away from it is an empty item as tall as its calls, hidden from assistive tech. A chunk holding an open call, a call still going (running, streaming its input or waiting on approval) or focus stays rendered. The calls that render carry
Patch Changes
- #60
ae3d758Thanks @dgesteves! - The website and the hosted shadcn registry moved to signoff-ui.vercel.app; the registry URL is nowhttps://signoff-ui.vercel.app/r/{name}.json. The old address redirects, so existingcomponents.jsonfiles and links keep working.
signoff-ui@0.5.0
Minor Changes
-
#53
ebf330dThanks @dgesteves! - Approval rules: a person approves once, for the session or always, by tool and argument pattern, and the calls that follow are decided by those rules.useApprovalPolicy()holds the rules for the page. Each rule covers a tool (a name or a glob), narrowed by argument globs ({ command: 'npm test*' }, dotted names for nested arguments) or a test of your own (when), and a deny rule wins over an allow rule. In a pattern,*never runs past a shell operator (;,&,|, backticks,<,>,$(, line breaks), sonpm test*does not allownpm test && rm -rf ~;**matches anything. Session rules stay in memory.alwaysrules go to a pluggablestorage: memory by default,webStorageRules()forlocalStorage, or your own{ load, save }, which may be async.onAuditreceives every decision (by a person or by which rule, with the reason and edited arguments), every rule added or removed, and every session cleared.ApprovalCardoffers the choices.decisionssets which ofallow-once,allow-session,allow-always,deny-onceanddeny-alwaysit shows, each a button with a key (Y, S, A, N, Shift+N). A lasting choice shows the argument patterns it will cover, editable, or "Any arguments".onDecidereceives{ decision, reason, input, args }.ToolApprovalCardwith apolicyoffers every choice, records the rules, answers a call a rule covers without a prompt, and shows it as "Allowed by your rule" with the rule.- Edit the arguments, then approve.
editableonApprovalCard, oronEditInputonToolApprovalCard(onToolInputEditonAgentMessage), opens the arguments as a form (an object of strings, numbers and booleans) or as JSON that must parse. With AI SDK 7,setToolInputputs the edited input in the messages, and the server runs the call with it after checking it against the tool's schema andtoolApprovalagain.useAgUiAgentgainseditInput, and the resume payload carries the edit asinput. - Approve all pending.
ToolApprovalBatchanswers every approval a run waits on at once, with a second press when one of them is critical.AgentMessageshows it when two or more are waiting. - The same rules elsewhere.
toToolApproval(rules)is AI SDK 7'stoolApprovalfunction: a matching rule approves or denies on the server, and anything else asks a person.toToolApprovalResponseanswersaddToolApprovalResponse. For the Agent Client Protocol'ssession/request_permission,decisionsFromAcpOptions,toAcpPermissionResponse,fromAcpPermissionResponseandfromAcpPermissionRequestmap to and from itsallow_once/allow_always/reject_once/reject_alwaysoptions.allow-sessionanswersallow_once, and the session rule stays on the client. These are types and functions only, with nothing new to install.
onApproveandonDenywork as before. When you passonDecide, they are not called. -
#34
39d503fThanks @dgesteves! - Approval cards claim only what you told them, and read better with a screen reader.riskhas no default. It used to default to'medium', so every card, and everyToolApprovalCardfor a tool withoutriskin its meta, said "Medium risk" when nobody had rated it. Withoutrisk, the card now shows no risk badge, uses its neutral colors and has nodata-risk.- A neutral reason placeholder. The denial reason field said "e.g. Use the existing Redis client instead", copy from the demo. It now says "What should the agent do instead?", and the new
reasonPlaceholderprop sets your own. - Not a landmark.
ApprovalCardis a group named by its title (role="group"), not a region landmark, so a run with many approvals no longer fills landmark navigation. Passrole="region"to make it one. Tests that find the card withgetByRole('region')needgetByRole('group'). - "Approved" once. A decided card read "Approved, Approved": the announcement stayed in the card next to the outcome. The announcement now clears 3 seconds after it is made.
ToolCallTimeline.- Calls that settle together, such as parallel calls, are announced in one message, after a 300 ms pause. Before, only the last one was announced.
- Calls that had already settled when the timeline mounted are not announced.
- The awaiting-approval "!" beside a call is hidden from screen readers; the call's button already says "Needs approval".
-
#50
34dfe90Thanks @dgesteves! -DiffReviewstays responsive on large files. A 5,000-line rewrite used to block the page for 3.3 s, leave 260,000 elements and take 77 ms per keypress; it is now ready in under 0.3 s, with no task over 60 ms, about 4,000 elements and under 1 ms per keypress (Chrome, Apple M1 Max).- A limit on the diff.
maxEditLength(default 2,000 lines added plus removed) caps the diff, whose cost grows with the square of that number. Past it, the changed region (from its first changed line to its last) is one hunk that replaces it, with no word-level highlights, and the review says so;ParsedFileDiff.fallbackis'replace'and the file carriesdata-fallback="replace".parseFileChangetakes the same option, andDEFAULT_MAX_EDIT_LENGTHis exported fromsignoff-ui/core. The common lines before and after the change are set aside before diffing, so only the changed region counts, and lines only added or only removed never fall back. - A worker. A file that needs more than 300 edits is diffed in a worker and reads "Comparing changes…" until it is done; Apply and the bulk buttons wait for it. Webpack 5, Next.js (webpack and Turbopack) and Vite emit the package's worker as they build. The new
diffWorkerprop takes a function that starts your ownWorker, orfalse; without a working worker, files are diffed on the main thread after the first paint. A server render diffs what a render may and shows the rest as being compared, so hydration matches. - Only what is near the screen. A review with more than 400 rows renders, of each hunk, the rows near the viewport; the rest keep their height and their lines as visually hidden text, so screen readers and find-in-page still have every line, read with "Added:" and "Removed:". Hunks always render, so focus, J and K and their names do not change, and a decision re-renders only the hunks it touches.
Where two diffs are equally small, hunks can now differ from jsdiff's own: the region between the common first and last lines is what gets diffed. The edits are as few as before, and
applyHunksgives the same files. - A limit on the diff.
-
#53
4d23065Thanks @dgesteves! - Labels: every word the components show or announce can be replaced, so the kit can be translated whole.SignoffLabelsProvidertakes a translation ofdefaultLabels, whole or in part, for every component inside. Providers nest, the inner one winning key by key. Every component also takes alabelsprop, which wins over the providers for it and what it renders.useSignoffLabels()returns the labels in effect, for markup of your own, andmergeLabels()builds them anywhere.- 207 labels in twelve sections, 140 strings and 67 functions: visible text, accessible names, live-region announcements, keyboard hints, titles and placeholders. Text built from values (counts, paths, line numbers, tool names) is a function, so a translation gets plurals and word order right;
formatholds how durations, token counts, cost and percentages read. Type a translation asSignoffLabelsand the compiler lists what it misses. The docs have a complete one in Portuguese. useDiffReviewanduseApprovalPolicytakelabelstoo, andderiveAgentStateawritingResponsedetail.- Each component bundles the English of the sections it reads only: 0.2 to 1.5 kB gzipped more per import, 2.4 kB for the whole main entry.
Small changes that come with it:
Sourcesis now a client module, as it reads the labels. It still renders from a Server Component: its props are data.- The diff review's view toggle is named "Unified" and "Split", capitalized for screen readers too (before, only on screen).
-
#32
bf53438Thanks @dgesteves! -useRunTimingtimes each turn on its own. Pass the chat's messages,useRunTiming(status, messages): a run then starts with each user message (or a regenerated reply) and spans its approval round trips. Without them, a run now starts with each request submitted after the last one ended. Before, every turn was added to the first: the second turn of a chat showed the first turn's time to first token and the sum of both turns' active time. Time to first token is also no longer 0 by mistake: for a run that was already streaming when the hook first saw it (keyed by a counter that changed while a stopped run was still settling, for example) it is unknown, and so, given the messages, for a request stopped before any reply arrived. The README quickstart now passesmessages. -
#37 [
c459d3f](https://github.com/dgesteves/...
@dgesteves/agent-ui-kit@0.4.3
Patch Changes
- #30
f8dae5eThanks @dgesteves! - The README opens with the demo, on GitHub as a video and on npm animated.
@dgesteves/agent-ui-kit@0.4.2
Patch Changes
- #28
13f7fecThanks @dgesteves! - The npm page now shows the repository's README, the same one as on GitHub (every component with its screenshot, AG-UI, theming and the full quickstart), instead of a shorter separate README.
@dgesteves/agent-ui-kit@0.4.1
Patch Changes
-
#19
afc5071Thanks @dgesteves! - Content that scrolls sideways is now reachable from the keyboard: a diff hunk's code, a code block or table inMarkdown, an approval card's command preview and the compactRunMeterstrip. While their content is wider than they are, they join the tab order as named groups ("Hunk 2 code, app/api/chat/route.ts", "Code, ts"), so the arrow keys scroll them, with a visible focus ring; once everything fits, they add no tab stop. Fixes axe'sscrollable-region-focusable(WCAG 2.1.1). -
#18
b3e4361Thanks @dgesteves! -ToolCallTimelinefits narrow columns: a long tool label truncates instead of pushing the row wider than its container, and the waterfall bar shows from a 24rem-wide timeline rather than from a 640px-wide screen, so a timeline in a sidebar or a phone-width panel no longer scrolls sideways.
@dgesteves/agent-ui-kit@0.4.0
A quickstart that runs, and works in more apps
An adopter audit copied the 0.3.0 README into a fresh Next.js 16 app. This release fixes everything that got in the way.
The quickstart runs (#7)
- Complete code. The README now has the complete client, page and route: real tools with one behind an approval,
stopWhen: stepCountIs(10)(without it AI SDK 7 stops after one step, so the approval card never showed), a composer, andonError. There's an AI SDK 6 variant too. - The same code, as an app.
examples/nextjs-minimalis that exact code as a Next.js 16 app with a mock model, so it runs with no API key. CI builds it and clicks through a run: a tool call, a failed tool, approve, resume, sources and the meter. - Next.js 16
cacheComponents(the default in new apps): components no longer read the clock while rendering on the server, which failednext build. Every component is now tested withreact-dom/server.
Works in more apps (#8)
- Tailwind v3 and plain CSS:
styles.csshas no cascade layers any more. Those broke Tailwind v3 builds, and lost to global resets like create-next-app's* { padding: 0 }.- Every rule is scoped to the kit's own elements with zero specificity, so your theme and utility classes are left alone.
styles.layered.cssis still available.
- React 18: peers are now
react@^18.2.0 || ^19.0.0, tested in CI. - Dark mode from the OS: import
theme.auto.css..lightor[data-theme=light]still forces light. - Your app's font: the kit falls back through
--font-sans, so shadcn/ui apps get their own font. - Lighter install: the kit depends on the two Radix primitives it uses, instead of the
radix-uimeta-package (about 60 packages). - shadcn items lint clean under a fresh Next.js ESLint config (24 warnings before).
Docs and playground (#9)
- shadcn: the shortest install,
npx shadcn@latest add @agent-ui-kit/agent-message, and the shadcn MCP setup. - Sizes: a gzip table per import, measured with
pnpm size. - New recipes: LangGraph via AG-UI, and a preset that maps the kit's tokens to shadcn/ui's.
- Screenshots: light and dark component shots.
- Playground: social preview images, a light/dark toggle on the gallery,
/llms.txt, and no more jump past the headline on phones. - Contributing: a contributing guide, a security policy and issue forms.
Full changes: https://github.com/dgesteves/agent-ui-kit/compare/@dgesteves/agent-ui-kit@0.3.0...@dgesteves/agent-ui-kit@0.4.0
@dgesteves/agent-ui-kit@0.3.0
Render AG-UI agents
AG-UI is the protocol LangGraph, CrewAI, Mastra, Pydantic AI, Google ADK and other agent frameworks use to stream runs to a frontend. The new @dgesteves/agent-ui-kit/ag-ui entry renders those agents with the same components as AI SDK apps:
import { HttpAgent } from '@ag-ui/client';
import { useAgUiAgent } from '@dgesteves/agent-ui-kit/ag-ui';
const agent = new HttpAgent({ url: '/api/agent' });
const { messages, status, usage, step, respond } = useAgUiAgent(agent);
<AgentMessage message={last} streaming={status === 'streaming'} onToolApproval={respond} />
<RunMeter usage={usage} />- Messages become AI SDK parts: text and reasoning with their streaming state, and tool calls as
dynamic-toolparts whose arguments are parsed as they stream in, with their results. Each turn is one assistant message, as with a multi-step AI SDK run. - Interrupts bound to a tool call become approval cards. AG-UI resumes every open interrupt in one run, so the hook waits until each one has an answer, then calls
runAgent({ resume })with{ approved, reason }. Other interrupts are ininterrupts; answer them withresolve(). status,usageandstepcome from the event stream. Usage is summed across runs, cache and reasoning tokens included, and the step is e.g. the LangGraph node that's running.- The pure pieces (
fromAgUiMessages,reduceAgUiRun,answerAgUiInterrupt,getAgUiResume) work with your own store. Nothing from@ag-ui/*is a dependency: their objects are read structurally. - Also a shadcn registry item:
npx shadcn@latest add https://agent-ui-kit-demo.vercel.app/r/ag-ui.json.
Try it: the gallery runs a real @ag-ui/client agent replaying a LangGraph-style run, with no API key needed. Approve or deny the install and watch the run resume: https://agent-ui-kit-demo.vercel.app/gallery#ag-ui
The gallery also shows how to install each component, from npm or as a shadcn registry item (#4).
Full changes: https://github.com/dgesteves/agent-ui-kit/compare/@dgesteves/agent-ui-kit@0.2.0...@dgesteves/agent-ui-kit@0.3.0
@dgesteves/agent-ui-kit@0.2.0
AI SDK 6 support
The ai peer range is now ^6.0.0 || ^7.0.102. AI SDK 6 had more installs than 7 last week, and the old range turned all of those projects away.
- On AI SDK 6: the kit reads the two approval fields AI SDK 7 added (
approval.isAutomaticandapproval.requestReason) only when they are present. Approvals read as a person's decision, and the card uses thedescriptionyou pass. - AI SDK 7 only:
toolApprovalpolicies, and the "Auto-approved" and "Blocked by policy" labels. - CI: the typecheck and the full suite now run against AI SDK 6.0.0, the latest 6.x and 7 (#2).
Also, the README's comparison with AI Elements and assistant-ui is rewritten to reflect assistant-ui's new Elements (#1).
npm: @dgesteves/agent-ui-kit@0.2.0.