-
Notifications
You must be signed in to change notification settings - Fork 351
Add Approval Navigation Banner and Related Functionality #474
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
c6c6d93
b3b55ec
2c31a1b
ea55f91
e50e635
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@truefoundry/trueforge-ui": minor | ||
| --- | ||
|
|
||
| Add a composer approval-nav banner (overridable `ApprovalNavBanner` slot) that counts pending tool approvals, pauses the composer, and jumps/expands/flashes the focused approval — including nested subagent tools. | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,87 @@ | ||||||
| 'use client'; | ||||||
|
|
||||||
| import { Icon } from '../icons/Icon.js'; | ||||||
| import { cn } from './lib/cn.js'; | ||||||
|
|
||||||
| export type ApprovalNavBannerProps = { | ||||||
| count: number; | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| /** 1-based index for display, e.g. 1 in "(1/4)". */ | ||||||
| current: number; | ||||||
| canPrev: boolean; | ||||||
| canNext: boolean; | ||||||
| onPrev: () => void; | ||||||
| onNext: () => void; | ||||||
| /** Scroll the thread to the currently selected approval. */ | ||||||
| onFocusCurrent: () => void; | ||||||
| className?: string; | ||||||
| }; | ||||||
|
|
||||||
| export function ApprovalNavBanner({ | ||||||
| count, | ||||||
| current, | ||||||
| canPrev, | ||||||
| canNext, | ||||||
| onPrev, | ||||||
| onNext, | ||||||
| onFocusCurrent, | ||||||
| className, | ||||||
| }: ApprovalNavBannerProps) { | ||||||
| // Figma Agents node 6747:4232 — "N tools need your input" | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same |
||||||
| const label = count === 1 ? '1 tool needs your input' : `${String(count)} tools need your input`; | ||||||
|
|
||||||
| return ( | ||||||
| <div | ||||||
| data-slot="aui_approval-nav-banner" | ||||||
| role="status" | ||||||
| aria-live="polite" | ||||||
| aria-label={`${label}. Click to go to approval ${String(current)} of ${String(count)}.`} | ||||||
| onClick={onFocusCurrent} | ||||||
| className={cn( | ||||||
| // Figma light 6747:4232 / dark 6748:1977 — top border only; sides & bottom open into the composer. | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can remove comment, not useful |
||||||
| 'aui-approval-nav-banner border-approval-banner-border bg-approval-banner-bg text-approval-banner-fg flex w-full cursor-pointer items-center justify-between gap-2 border-t px-4 py-1.5 text-sm', | ||||||
| 'rounded-t-[var(--composer-radius,1.5rem)]', | ||||||
| className, | ||||||
| )} | ||||||
| > | ||||||
| <div className="flex min-w-0 items-center gap-3"> | ||||||
| <span className="bg-approval-banner-accent size-2 shrink-0 rounded-full" aria-hidden /> | ||||||
| <span className="truncate font-medium leading-[1.4]">{label}</span> | ||||||
| </div> | ||||||
| <div className="flex shrink-0 items-center gap-2"> | ||||||
| <span className="font-medium tabular-nums leading-[1.4]"> | ||||||
| ({current}/{count}) | ||||||
| </span> | ||||||
| <button | ||||||
| type="button" | ||||||
| aria-label="Next approval" | ||||||
| disabled={!canNext} | ||||||
| onClick={event => { | ||||||
| event.stopPropagation(); | ||||||
| onNext(); | ||||||
| }} | ||||||
| className="text-approval-banner-fg hover:bg-approval-banner-border/10 inline-flex size-6 items-center justify-center rounded-sm disabled:pointer-events-none disabled:opacity-40" | ||||||
| > | ||||||
| <Icon name="arrow-down" size="0.875rem" /> | ||||||
| </button> | ||||||
| <button | ||||||
| type="button" | ||||||
| aria-label="Previous approval" | ||||||
| disabled={!canPrev} | ||||||
| onClick={event => { | ||||||
| event.stopPropagation(); | ||||||
| onPrev(); | ||||||
| }} | ||||||
| className="text-approval-banner-fg hover:bg-approval-banner-border/10 inline-flex size-6 items-center justify-center rounded-sm disabled:pointer-events-none disabled:opacity-40" | ||||||
| > | ||||||
| <Icon name="arrow-up" size="0.875rem" /> | ||||||
| </button> | ||||||
| </div> | ||||||
| </div> | ||||||
| ); | ||||||
| } | ||||||
|
|
||||||
| declare module '../theme/SlotsProvider.js' { | ||||||
| interface AtomSlots { | ||||||
| ApprovalNavBanner: typeof ApprovalNavBanner; | ||||||
| } | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,8 @@ | ||
| 'use client'; | ||
|
|
||
| import type { ReactNode } from 'react'; | ||
| import { useRef, type ReactNode } from 'react'; | ||
|
|
||
| import { useOptionalApprovalFocus, useRegisterApprovalTarget } from '../containers/approvalFocus.js'; | ||
| import { AgentStepRow, type AgentStepStatus } from './agent-chat/AgentStepRow.js'; | ||
| import { cn } from './lib/cn.js'; | ||
|
|
||
|
|
@@ -22,6 +23,8 @@ export type ToolCallCardProps = { | |
| showResponseLine?: boolean; | ||
| responseIcon?: ReactNode; | ||
| approvalSlot?: ReactNode; | ||
| /** When set, this card is the scroll/flash target for approval banner navigation. */ | ||
| approvalId?: string; | ||
| requestSlot?: ReactNode; | ||
| responseSlot?: ReactNode; | ||
| highlightCard?: boolean; | ||
|
|
@@ -46,18 +49,24 @@ export function ToolCallCard({ | |
| showExpandChevron = true, | ||
| showResponseLine = false, | ||
| approvalSlot, | ||
| approvalId, | ||
| requestSlot, | ||
| responseSlot, | ||
| highlightCard = false, | ||
| className, | ||
| mcpServerName: _mcpServerName, | ||
| dataTestPrefix, | ||
| }: ToolCallCardProps) { | ||
| const rootRef = useRef<HTMLDivElement>(null); | ||
| const focusApi = useOptionalApprovalFocus(); | ||
| useRegisterApprovalTarget(approvalId, () => rootRef.current); | ||
|
|
||
| const hasApproval = !!approvalSlot; | ||
| const hasRequest = !!requestSlot; | ||
| const hasResponse = !!responseSlot; | ||
| const showConnector = hasApproval || hasRequest || hasResponse; | ||
| const isExpandable = showExpandChevron && (hasRequest || hasResponse); | ||
| const isFlashing = approvalId != null && focusApi?.flashingApprovalId === approvalId; | ||
|
|
||
| let derivedStatus: AgentStepStatus = 'idle'; | ||
| if (explicitStatus) derivedStatus = explicitStatus; | ||
|
|
@@ -73,9 +82,14 @@ export function ToolCallCard({ | |
|
|
||
| return ( | ||
| <div | ||
| ref={rootRef} | ||
| data-approval-id={approvalId} | ||
| className={cn( | ||
| 'aui-tool-call-card flex min-w-0 flex-col', | ||
| highlightCard ? 'rounded-md p-1 -mx-1' : 'mx-0 mt-2 p-0', | ||
| // Pending-approval cards keep top/right padding always so flash doesn't jump the layout. | ||
| // Left stays unpadded so the step rail stays aligned with siblings. | ||
|
Comment on lines
+89
to
+90
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can remove |
||
| approvalId != null || highlightCard ? 'mx-0 mt-2 rounded-md pt-1.5 pr-2 pb-1' : 'mx-0 mt-2 p-0', | ||
| isFlashing && 'aui-approval-flash', | ||
| className, | ||
| )} | ||
| data-testid={dataTestPrefix ? `${dataTestPrefix}-tool-call-card` : undefined} | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| 'use client'; | ||
|
|
||
| import { useApprovalNav } from '../hooks/useApprovalNav.js'; | ||
| import { useSlot } from '../theme/SlotsProvider.js'; | ||
|
|
||
| /** Composer banner that navigates pending tool approvals. */ | ||
| export function ApprovalNavContainer() { | ||
| const ApprovalNavBanner = useSlot('ApprovalNavBanner'); | ||
| const { count, index, canPrev, canNext, goPrev, goNext, focusCurrent } = useApprovalNav(); | ||
|
|
||
| if (count === 0) return null; | ||
|
|
||
| return ( | ||
| <ApprovalNavBanner | ||
| count={count} | ||
| current={index + 1} | ||
| canPrev={canPrev} | ||
| canNext={canNext} | ||
| onPrev={goPrev} | ||
| onNext={goNext} | ||
| onFocusCurrent={focusCurrent} | ||
| /> | ||
| ); | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.