feat: name the action on chat tool confirmation buttons (#11795)

* feat: name the action on chat tool confirmation buttons

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: size chat confirmation footer buttons with unifiedSize and the UI font

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Guilhem
2026-10-19 23:13:53 +02:00
committed by GitHub
co-authored by Claude Opus 5.5
parent ed589963f4
commit 7d01d252b5
15 changed files with 62 additions and 11 deletions
@@ -977,7 +977,7 @@ the panel, or the Escape-to-stop focus check would wrongly reject them. -->
<div class="absolute -top-10 w-full flex flex-row justify-center gap-2">
<Button
startIcon={{ icon: CheckIcon }}
size="xs"
unifiedSize="sm"
variant="default"
btnClasses="bg-green-500 hover:bg-green-600 text-white hover:text-white"
onclick={() => {
@@ -989,7 +989,7 @@ the panel, or the Escape-to-stop focus check would wrongly reject them. -->
<div class="rounded bg-surface">
<Button
startIcon={{ icon: XIcon }}
size="xs"
unifiedSize="sm"
variant="default"
btnClasses="dark:opacity-50 opacity-60 hover:opacity-100"
onclick={() => {
@@ -34,10 +34,11 @@
}
</script>
<div class={twMerge('flex flex-row items-center justify-end gap-2', className)}>
<!-- font-main: the tool cards around it are font-mono, which reads as bold on a button label. -->
<div class={twMerge('flex flex-row items-center justify-end gap-2 font-main', className)}>
<Button
variant="default"
size="xs"
unifiedSize="sm"
destructive={rejectDestructive}
startIcon={rejectIcon ? { icon: rejectIcon } : undefined}
on:click={() => respond(false)}
@@ -46,7 +47,7 @@
</Button>
<Button
variant="accent"
size="xs"
unifiedSize="sm"
startIcon={confirmIcon ? { icon: confirmIcon } : undefined}
on:click={() => respond(true)}
>
@@ -3,7 +3,6 @@
Loader2,
ChevronRight,
XCircle,
Play,
ClipboardList,
Check,
CircleMinus,
@@ -43,6 +42,7 @@
import RunScriptCard from './RunScriptCard.svelte'
import ToolDiffCard from './ToolDiffCard.svelte'
import { hasToolCodeDiff } from './toolCodeDiff'
import { TOOL_CONFIRM_ACTIONS } from './toolConfirmAction'
import WebSearchSourcesDisplay from './WebSearchSourcesDisplay.svelte'
import ExpandableImage from '$lib/components/common/image/ExpandableImage.svelte'
import McpServerIcon from '$lib/components/mcp/McpServerIcon.svelte'
@@ -92,6 +92,8 @@
// The preview pane's renderers, so a model-written link opens in a new tab either way.
const planPlugins = [gfmPlugin(), { renderer: { pre: CodeDisplay, a: LinkRenderer } }]
const confirmAction = $derived(TOOL_CONFIRM_ACTIONS[message.confirmAction ?? 'confirm'])
const hasParameters = $derived(
message.parameters !== undefined && Object.keys(message.parameters).length > 0
)
@@ -118,7 +120,7 @@
// stream, while it waits on the user, and once it has an error or details to keep in view.
// The user's toggle lives in state keyed by call id: the `message` prop is replaced on every
// chat update, so an override written onto a derived would be recomputed away. A pending
// confirmation wins over it, since the Run/Reject footer renders inside the card.
// confirmation wins over it, since the confirm/reject footer renders inside the card.
let toggled = $state<{ id: string | undefined; open: boolean } | undefined>(undefined)
const isExpanded = $derived(
Boolean(message.isLoading && message.needsConfirmation) ||
@@ -285,7 +287,7 @@
</div>
<!-- Collapsed: the buttons stand on their own rather than boxing empty space. -->
{:else if message.needsConfirmation}
{@render confirmFooter('py-1 font-main')}
{@render confirmFooter('py-1')}
{/if}
</div>
{:else}
@@ -373,8 +375,8 @@
toolCallId={message.tool_call_id}
rejectIcon={XCircle}
rejectDestructive
confirmLabel="Run"
confirmIcon={Play}
confirmLabel={confirmAction.label}
confirmIcon={confirmAction.icon}
/>
<!-- Logs and Result - hide while streaming -->
@@ -109,6 +109,7 @@ export function createApiTools(
def: chatTool,
planModeSafe: !!endpoint && ['GET', 'HEAD', 'OPTIONS'].includes(method),
requiresConfirmation: needsConfirmation,
confirmAction: method === 'DELETE' ? 'delete' : 'confirm',
confirmationMessage: `Run ${toolName}`,
showDetails: true,
showFade: true,
@@ -213,6 +213,7 @@ export function getDatatableMigrationTools(): SessionTool<{}>[] {
requires: RUN_PREVIEW,
def: getCreateMigrationToolDef(),
requiresConfirmation: true,
confirmAction: (args) => (args?.apply === false ? 'create' : 'apply'),
confirmationMessage: (args) =>
`${args?.version === undefined ? 'Create' : 'Rewrite'}${args?.apply === false ? '' : ' and apply'} data table migration`,
queuedLabel: (args) => `Write migration "${args?.name ?? ''}"`,
@@ -341,6 +342,7 @@ export function getDatatableMigrationTools(): SessionTool<{}>[] {
requires: RUN_PREVIEW,
def: getRunMigrationsToolDef(),
requiresConfirmation: true,
confirmAction: 'apply',
confirmationMessage: 'Apply pending data table migrations',
queuedLabel: (args) => `Apply migrations on "${args?.datatable_name ?? 'datatable'}"`,
showDetails: true,
@@ -376,6 +378,7 @@ export function getDatatableMigrationTools(): SessionTool<{}>[] {
requires: RUN_PREVIEW,
def: getRollbackMigrationToolDef(),
requiresConfirmation: true,
confirmAction: 'rollback',
confirmationMessage: (args) => `Roll back data table migration ${args?.version ?? ''}`,
queuedLabel: (args) => `Roll back migration ${args?.version ?? ''}`,
showDetails: true,
@@ -344,6 +344,7 @@ export function getDatatableTools(): SessionTool<{}>[] {
requires: RUN_PREVIEW,
def: getExecDatatableSqlToolDef(),
requiresConfirmation: true,
confirmAction: 'run',
confirmationMessage: 'Execute SQL on datatable',
queuedLabel: (args) => `Execute SQL on "${args?.datatable_name ?? 'datatable'}"`,
showDetails: true,
@@ -441,6 +441,7 @@ export const flowTools: Tool<FlowAIChatHelpers>[] = [
return def
},
requiresConfirmation: true,
confirmAction: 'run',
confirmationMessage: 'Run a test of the current flow',
showDetails: true,
autoCollapseDetails: false
@@ -486,6 +487,7 @@ export const flowTools: Tool<FlowAIChatHelpers>[] = [
})
},
requiresConfirmation: true,
confirmAction: 'run',
confirmationMessage: (args) => `Run a test of step "${args?.stepId ?? ''}"`,
showDetails: true,
autoCollapseDetails: false
@@ -4134,6 +4134,7 @@ export const globalTools: SessionTool<{}>[] = [
'Create a new shared folder (addressable as f/<name>/) in the workspace. The current user is added as an owner.'
),
requiresConfirmation: true,
confirmAction: 'create',
confirmationMessage: 'Create folder',
showDetails: true,
fn: async ({ args, workspace, toolId, toolCallbacks }) => {
@@ -4568,6 +4569,7 @@ export const globalTools: SessionTool<{}>[] = [
showDetails: true,
showFade: true,
requiresConfirmation: true,
confirmAction: 'deploy',
confirmationMessage: 'Deploy draft to workspace',
fn: async (ctx) => {
const parsed = deployWorkspaceItemSchema.parse(ctx.args)
@@ -4650,6 +4652,7 @@ export const globalTools: SessionTool<{}>[] = [
showDetails: true,
showFade: true,
requiresConfirmation: true,
confirmAction: 'delete',
confirmationMessage: 'Delete workspace item',
validateBeforeConfirmation: validateDeleteWorkspaceItemTarget,
fn: async (ctx) => {
@@ -4669,6 +4672,7 @@ export const globalTools: SessionTool<{}>[] = [
showDetails: true,
showFade: true,
requiresConfirmation: true,
confirmAction: 'discard',
confirmationMessage: 'Discard draft',
fn: async (ctx) => {
const parsed = discardLocalDraftSchema.parse(ctx.args)
@@ -4764,6 +4768,7 @@ export const globalTools: SessionTool<{}>[] = [
),
showDetails: true,
requiresConfirmation: true,
confirmAction: 'create',
confirmationMessage: 'Create resource type',
fn: async ({ args, workspace, toolId, toolCallbacks }) => {
const parsed = createResourceTypeSchema.parse(args)
@@ -4944,6 +4949,7 @@ export const globalTools: SessionTool<{}>[] = [
return testRunAppRunnable(parsed, ctx)
},
requiresConfirmation: true,
confirmAction: 'run',
confirmationMessage: (args) =>
`Run the backend runnable "${args?.key ?? ''}" of ${pathLeaf(args?.path, 'the app')}`,
queuedLabel: (args) => `Test runnable "${args?.key ?? ''}" of ${args?.path ?? 'the app'}`,
@@ -366,6 +366,7 @@ function createCallTool(servers: McpServer[], mode: 'read' | 'write'): SessionTo
? { planModeSafe: true }
: {
requiresConfirmation: true,
confirmAction: 'run',
confirmationMessage: (args: any) => `Call ${args?.tool ?? ''} on ${args?.server ?? ''}`
}),
fn: async ({ args, workspace, toolId, toolCallbacks }) => {
@@ -105,6 +105,7 @@ export const workspaceSettingsTools: SessionTool<{}>[] = [
'Change workspace settings. Keys the yaml omits are left as they are. The user reviews the changes before they apply. Read get_instructions subject "workspace_settings" before your first change.'
),
requiresConfirmation: true,
confirmAction: 'apply',
confirmationMessage: 'Change workspace settings',
cancellationMessage: 'The user declined: no setting was changed.',
showDetails: true,
@@ -568,6 +568,7 @@ export const pipelineTools: SessionTool<PipelineToolHelpers>[] = [
requires: RUN_PREVIEW,
def: testPipelineNodeToolDef,
requiresConfirmation: true,
confirmAction: 'run',
confirmationMessage: 'Run pipeline node',
showDetails: true,
autoCollapseDetails: false,
@@ -1007,6 +1007,7 @@ export const testRunScriptTool: Tool<ScriptChatHelpers> = {
})
},
requiresConfirmation: true,
confirmAction: 'run',
confirmationMessage: 'Run a test of the current script',
showDetails: true,
autoCollapseDetails: false
@@ -642,6 +642,16 @@ export type ToolCodeDiff = {
beforeLang?: string
}
export type ToolConfirmAction =
| 'run'
| 'deploy'
| 'create'
| 'apply'
| 'delete'
| 'discard'
| 'rollback'
| 'confirm'
export type ToolDisplayMessage = {
role: 'tool'
tool_call_id: string
@@ -657,6 +667,8 @@ export type ToolDisplayMessage = {
isQueued?: boolean
error?: string
needsConfirmation?: boolean
/** What the confirm button says the call will do. */
confirmAction?: ToolConfirmAction
showDetails?: boolean
autoCollapseDetails?: boolean
isStreamingArguments?: boolean
@@ -1103,6 +1115,8 @@ export async function processToolCall<T>({
typeof tool?.confirmationMessage === 'function'
? tool.confirmationMessage(args)
: tool?.confirmationMessage
const confirmAction =
typeof tool?.confirmAction === 'function' ? tool.confirmAction(args) : tool?.confirmAction
// preAction fires at promotion, not stream time, so its "-ing" label covers
// only the execution window — queued cards keep their imperative header.
@@ -1111,7 +1125,7 @@ export async function processToolCall<T>({
toolCallbacks.setToolStatus(toolCall.id, {
...(requiresConfirmation
? { content: confirmationContent ?? 'Waiting for confirmation...' }
? { content: confirmationContent ?? 'Waiting for confirmation...', confirmAction }
: {}),
parameters: args,
isLoading: true,
@@ -1287,6 +1301,8 @@ export interface Tool<T> {
/** Header shown on the confirmation card before the tool runs. Pass a function
* to derive it from the parsed arguments (e.g. name the script being tested). */
confirmationMessage?: string | ((args: any) => string)
/** The verb on the confirm button; `confirm` when omitted. */
confirmAction?: ToolConfirmAction | ((args: any) => ToolConfirmAction)
/** Only when a card gates the call, so `fn` must not rely on it. Not awaited, so it may
* not throw, and must be safe for a call the user then declines. */
onConfirmationRequested?: (p: { args: any; toolCallbacks: ToolCallbacks; toolId: string }) => void
@@ -0,0 +1,13 @@
import { Check, Play, Plus, Save, Trash2, Undo2 } from 'lucide-svelte'
import type { ToolConfirmAction } from './shared'
export const TOOL_CONFIRM_ACTIONS: Record<ToolConfirmAction, { label: string; icon: any }> = {
run: { label: 'Run', icon: Play },
deploy: { label: 'Deploy', icon: Save },
create: { label: 'Create', icon: Plus },
apply: { label: 'Apply', icon: Check },
delete: { label: 'Delete', icon: Trash2 },
discard: { label: 'Discard', icon: Trash2 },
rollback: { label: 'Roll back', icon: Undo2 },
confirm: { label: 'Confirm', icon: Check }
}
@@ -309,6 +309,7 @@ function setToolError(toolCallbacks: ToolCallbacks, toolId: string, error: unkno
const createScheduleTool: Tool<any> = {
def: createScheduleToolDef,
requiresConfirmation: true,
confirmAction: 'create',
confirmationMessage: 'Create schedule',
showDetails: true,
showFade: true,
@@ -418,6 +419,7 @@ async function resolveEmailTriggerAvailability(): Promise<EmailTriggerAvailabili
const createTriggerTool: Tool<any> = {
def: createTriggerToolDef,
requiresConfirmation: true,
confirmAction: 'create',
confirmationMessage: 'Create trigger',
showDetails: true,
showFade: true,