From 2bce84781bb678e33ba45f3feef7647e82513d6c Mon Sep 17 00:00:00 2001 From: gavrielc Date: Sat, 4 Jul 2026 16:04:24 +0300 Subject: [PATCH] feat(approvals): colored buttons on approval cards (Slack primary/danger) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Approval cards render every button with the same neutral style, so Approve and Reject read identically at a glance. The chat SDK's Button already accepts style ('primary' | 'danger' | 'default') and @chat-adapter/slack maps primary→green and danger→red Block Kit styles (Telegram ignores it), but the ask_question pipeline dropped the field. - ask-question.ts: add OptionStyle and an optional style field to OptionInput/NormalizedOption; normalizeOption whitelists the value. Bare-string options stay unstyled. - chat-sdk-bridge.ts: forward opt.style into Button() on ask_question cards. Persisted options_json tolerates the extra key (getAskQuestionRender only reads label/selectedLabel/value). - Producers: module approvals (Approve/Reject), sender approvals (Allow/Deny), channel approvals (Connect/Reject), and OneCLI credential cards (Approve/Reject) annotate primary/danger. Multi-choice picker options stay unstyled — a list of equals. Purely additive: options without style render exactly as before. Co-Authored-By: Claude Fable 5 --- src/channels/ask-question.ts | 7 +++++++ src/channels/chat-sdk-bridge.ts | 2 +- src/modules/approvals/onecli-approvals.ts | 4 ++-- src/modules/approvals/primitive.ts | 4 ++-- src/modules/permissions/channel-approval.ts | 2 ++ src/modules/permissions/sender-approval.ts | 4 ++-- 6 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/channels/ask-question.ts b/src/channels/ask-question.ts index 9f71417e8..7a85c2394 100644 --- a/src/channels/ask-question.ts +++ b/src/channels/ask-question.ts @@ -7,10 +7,15 @@ * and rendering. */ +/** Chat SDK button styles — Slack maps primary→green, danger→red; platforms + * without button colors (Telegram) ignore it. */ +export type OptionStyle = 'primary' | 'danger' | 'default'; + export interface OptionInput { label: string; selectedLabel?: string; value?: string; + style?: OptionStyle; } export type RawOption = string | OptionInput; @@ -19,6 +24,7 @@ export interface NormalizedOption { label: string; selectedLabel: string; value: string; + style?: OptionStyle; } export function normalizeOption(raw: RawOption): NormalizedOption { @@ -30,6 +36,7 @@ export function normalizeOption(raw: RawOption): NormalizedOption { label, selectedLabel: raw.selectedLabel ?? label, value: raw.value ?? label, + style: raw.style === 'primary' || raw.style === 'danger' || raw.style === 'default' ? raw.style : undefined, }; } diff --git a/src/channels/chat-sdk-bridge.ts b/src/channels/chat-sdk-bridge.ts index 41df8443f..fdf587532 100644 --- a/src/channels/chat-sdk-bridge.ts +++ b/src/channels/chat-sdk-bridge.ts @@ -439,7 +439,7 @@ export function createChatSdkBridge(config: ChatSdkBridgeConfig): ChannelAdapter // well past that. The onAction handlers resolve the index back // to the real value via getAskQuestionRender(questionId). options.map((opt, idx) => - Button({ id: `ncq:${questionId}:${idx}`, label: opt.label, value: String(idx) }), + Button({ id: `ncq:${questionId}:${idx}`, label: opt.label, value: String(idx), style: opt.style }), ), ), ], diff --git a/src/modules/approvals/onecli-approvals.ts b/src/modules/approvals/onecli-approvals.ts index eec05c0b4..3056c216e 100644 --- a/src/modules/approvals/onecli-approvals.ts +++ b/src/modules/approvals/onecli-approvals.ts @@ -147,8 +147,8 @@ async function handleRequest(request: ApprovalRequest): Promise { const onecliTitle = 'Credentials Request'; const onecliOptions = [ - { label: 'Approve', selectedLabel: '✅ Approved', value: 'approve' }, - { label: 'Reject', selectedLabel: '❌ Rejected', value: 'reject' }, + { label: 'Approve', selectedLabel: '✅ Approved', value: 'approve', style: 'primary' as const }, + { label: 'Reject', selectedLabel: '❌ Rejected', value: 'reject', style: 'danger' as const }, ]; let platformMessageId: string | undefined; try { diff --git a/src/modules/approvals/primitive.ts b/src/modules/approvals/primitive.ts index 4a0fc3fa7..2d3028a64 100644 --- a/src/modules/approvals/primitive.ts +++ b/src/modules/approvals/primitive.ts @@ -46,8 +46,8 @@ export const REJECT_WITH_REASON_VALUE = 'reject_with_reason'; * keep their own two-button set in onecli-approvals.ts. */ const APPROVAL_OPTIONS: RawOption[] = [ - { label: 'Approve', selectedLabel: '✅ Approved', value: 'approve' }, - { label: 'Reject', selectedLabel: '❌ Rejected', value: 'reject' }, + { label: 'Approve', selectedLabel: '✅ Approved', value: 'approve', style: 'primary' }, + { label: 'Reject', selectedLabel: '❌ Rejected', value: 'reject', style: 'danger' }, { label: 'Reject with reason…', selectedLabel: '📝 Rejected (awaiting reason)', value: REJECT_WITH_REASON_VALUE }, ]; diff --git a/src/modules/permissions/channel-approval.ts b/src/modules/permissions/channel-approval.ts index 9352b8903..8dec14029 100644 --- a/src/modules/permissions/channel-approval.ts +++ b/src/modules/permissions/channel-approval.ts @@ -93,6 +93,7 @@ function buildApprovalOptions(agentGroups: AgentGroup[], approverUserId?: string label: `Connect to ${visibleAgentGroups[0].name}`, selectedLabel: `✅ Connected to ${visibleAgentGroups[0].name}`, value: `${CONNECT_PREFIX}${visibleAgentGroups[0].id}`, + style: 'primary', }); } else if (visibleAgentGroups.length > 1) { options.push({ @@ -110,6 +111,7 @@ function buildApprovalOptions(agentGroups: AgentGroup[], approverUserId?: string label: 'Reject', selectedLabel: '🙅 Rejected', value: REJECT_VALUE, + style: 'danger', }); return options; } diff --git a/src/modules/permissions/sender-approval.ts b/src/modules/permissions/sender-approval.ts index fb3e24e01..24919ade4 100644 --- a/src/modules/permissions/sender-approval.ts +++ b/src/modules/permissions/sender-approval.ts @@ -35,8 +35,8 @@ import { pickApprovalDelivery, pickApprover } from '../approvals/primitive.js'; import { createPendingSenderApproval, hasInFlightSenderApproval } from './db/pending-sender-approvals.js'; const APPROVAL_OPTIONS: RawOption[] = [ - { label: 'Allow', selectedLabel: '✅ Allowed', value: 'approve' }, - { label: 'Deny', selectedLabel: '❌ Denied', value: 'reject' }, + { label: 'Allow', selectedLabel: '✅ Allowed', value: 'approve', style: 'primary' }, + { label: 'Deny', selectedLabel: '❌ Denied', value: 'reject', style: 'danger' }, ]; function generateId(): string {