From 8b5f139626d12bffe4a8ffc964373e81eb44b16b Mon Sep 17 00:00:00 2001
From: GCWing
Date: Sat, 29 Aug 2026 00:45:19 +0800
Subject: [PATCH] refactor(flow-chat): migrate cards into design system
---
design-system/README.md | 21 +
design-system/apps/design-lab/src/App.tsx | 82 +-
.../design-lab/src/i18n/componentMetadata.ts | 28 +
.../apps/design-lab/src/i18n/messages.ts | 315 +++++
.../src/pages/ComponentDetailPage.tsx | 62 +-
.../design-lab/src/pages/ComponentsPage.tsx | 88 +-
.../src/preview/FlowChatPreviewRegistry.css | 248 ++++
.../src/preview/FlowChatPreviewRegistry.tsx | 1242 +++++++++++++++++
.../src/preview/FlowChatToolGallery.css | 208 +++
.../src/preview/FlowChatToolGallery.tsx | 115 ++
design-system/apps/design-lab/src/styles.css | 75 +
design-system/apps/design-lab/vite.config.ts | 4 +
.../vite/component-detail-contract.test.mjs | 145 ++
.../design-tokens/src/system.tokens.json | 31 +-
.../design-tokens/tests/contract.test.mjs | 39 +-
design-system/packages/ui/README.md | 61 +
design-system/packages/ui/package.json | 4 +
design-system/packages/ui/src/flow-chat.ts | 3 +
.../ui/src/flow-chat/ask-user/AskUser.meta.ts | 77 +
.../src/flow-chat/ask-user/AskUser.module.css | 454 ++++++
.../ui/src/flow-chat/ask-user/AskUser.tsx | 477 +++++++
.../ui/src/flow-chat/ask-user/index.ts | 10 +
.../flow-chat/composer/ChatComposer.meta.ts | 46 +
.../composer/ChatComposer.module.css | 145 ++
.../src/flow-chat/composer/ChatComposer.tsx | 150 ++
.../ui/src/flow-chat/composer/index.ts | 9 +
.../packages/ui/src/flow-chat/index.ts | 3 +
.../tool-cards/ActivityToolCards.module.css | 14 +
.../tool-cards/ActivityToolCards.tsx | 109 ++
.../tool-cards/AmbientToolCard.meta.ts | 34 +
.../tool-cards/CommandToolCard.meta.ts | 34 +
.../tool-cards/CommandToolCard.module.css | 196 +++
.../flow-chat/tool-cards/CommandToolCard.tsx | 233 ++++
.../ContextCompressionToolCard.meta.ts | 23 +
.../ContextCompressionToolCard.module.css | 34 +
.../tool-cards/ContextCompressionToolCard.tsx | 72 +
.../tool-cards/FileOperationToolCard.meta.ts | 31 +
.../FileOperationToolCard.module.css | 79 ++
.../tool-cards/FileOperationToolCard.tsx | 223 +++
.../tool-cards/FlowChatToolCard.module.css | 539 +++++++
.../flow-chat/tool-cards/FlowChatToolCard.tsx | 871 ++++++++++++
.../tool-cards/ProminentToolCard.meta.ts | 45 +
.../tool-cards/ProminentToolCards.meta.ts | 92 ++
.../tool-cards/ProminentToolCards.module.css | 335 +++++
.../tool-cards/ProminentToolCards.tsx | 427 ++++++
.../tool-cards/ReadFileToolCard.meta.ts | 24 +
.../flow-chat/tool-cards/ReadFileToolCard.tsx | 52 +
.../SearchResultsToolCards.module.css | 137 ++
.../tool-cards/SearchResultsToolCards.tsx | 169 +++
.../tool-cards/SessionToolCards.module.css | 105 ++
.../flow-chat/tool-cards/SessionToolCards.tsx | 127 ++
.../StandardAmbientToolCards.module.css | 311 +++++
.../tool-cards/StandardAmbientToolCards.tsx | 456 ++++++
.../tool-cards/StandardToolCards.meta.ts | 181 +++
.../tool-cards/ToolCardStatusSlot.module.css | 58 +
.../tool-cards/ToolCardStatusSlot.tsx | 80 ++
.../tool-cards/ToolProcessingDots.module.css | 65 +
.../tool-cards/ToolProcessingDots.tsx | 29 +
.../ui/src/flow-chat/tool-cards/index.ts | 105 ++
design-system/packages/ui/src/registry.ts | 62 +
.../packages/ui/src/registry.types.ts | 2 +-
.../packages/ui/tests/ask-user.test.mjs | 92 ++
.../packages/ui/tests/chat-composer.test.mjs | 118 ++
.../ui/tests/flow-chat-tool-card.test.mjs | 445 ++++++
.../packages/ui/tests/registry.test.mjs | 61 +-
design-system/packages/ui/vite.config.ts | 1 +
scripts/audit-appearance-contracts.mjs | 50 +-
scripts/audit-theme-colors.test.mjs | 24 +-
scripts/theme-color-governance-baseline.json | 2 +-
scripts/theme-css-var-contract.mjs | 12 -
.../BaseToolCard/BaseToolCard.scss | 296 ----
.../BaseToolCard/BaseToolCard.tsx | 216 ---
.../FlowChatCards/BaseToolCard/index.ts | 2 -
.../ContextCompressionCard.scss | 226 ---
.../ContextCompressionCard.tsx | 166 ---
.../ContextCompressionCard/appearance.ts | 13 -
.../ContextCompressionCard/index.ts | 3 -
.../components/FlowChatCards/README.md | 80 --
.../components/FlowChatCards/README.zh-CN.md | 80 --
.../ReadFileCard/ReadFileCard.scss | 83 --
.../FlowChatCards/SearchCard/SearchCard.scss | 281 ----
.../FlowChatCards/SearchCard/SearchCard.tsx | 206 ---
.../FlowChatCards/SearchCard/index.ts | 2 -
.../SnapshotCard/SnapshotCard.scss | 296 ----
.../SnapshotCard/SnapshotCard.tsx | 241 ----
.../FlowChatCards/SnapshotCard/index.ts | 2 -
.../FlowChatCards/TaskCard/TaskCard.scss | 188 ---
.../FlowChatCards/TaskCard/TaskCard.tsx | 99 --
.../FlowChatCards/TaskCard/index.ts | 2 -
.../FlowChatCards/TodoCard/TodoCard.scss | 184 ---
.../FlowChatCards/TodoCard/TodoCard.tsx | 114 --
.../FlowChatCards/TodoCard/index.ts | 2 -
.../ToolProcessingDots.scss | 83 --
.../ToolProcessingDots/ToolProcessingDots.tsx | 31 -
.../FlowChatCards/ToolProcessingDots/index.ts | 2 -
.../WebSearchCard/WebSearchCard.scss | 251 ----
.../WebSearchCard/WebSearchCard.tsx | 154 --
.../FlowChatCards/WebSearchCard/index.ts | 2 -
.../FlowChatCards/_shared-styles.scss | 72 -
.../components/FlowChatCards/appearance.ts | 15 -
.../components/FlowChatCards/index.ts | 180 ---
.../component-library/components/README.md | 13 +-
.../components/README.zh-CN.md | 13 +-
.../src/component-library/components/index.ts | 2 -
.../preview/flowchat-cards-preview.css | 119 --
.../src/component-library/preview/main.tsx | 1 -
.../components/ChatInput.appearance.ts | 1 -
.../src/flow_chat/components/ChatInput.scss | 575 +++-----
.../src/flow_chat/components/ChatInput.tsx | 252 ++--
.../components/ChatInputWorkspaceStrip.scss | 27 +-
.../components/ChatInputWorkspaceStrip.tsx | 2 +-
.../ChatInputWorkspaceStripLayout.test.ts | 201 ++-
.../FlowChatTypographyRoles.test.ts | 24 +-
.../flow_chat/components/FlowTextBlock.scss | 2 +-
.../components/FlowToolCard.appearance.ts | 27 +
.../flow_chat/components/FlowToolCard.scss | 33 +
.../src/flow_chat/components/FlowToolCard.tsx | 17 +-
.../components/FlowToolCardErrorBoundary.tsx | 6 +-
.../HarnessProfileSelector.appearance.ts | 5 +
.../components/HarnessProfileSelector.scss | 48 +
.../HarnessProfileSelector.test.tsx | 53 +
.../components/HarnessProfileSelector.tsx | 71 +-
.../flow_chat/components/ModelSelector.tsx | 6 +
.../ReasoningPresetSelector.appearance.ts | 7 +
.../components/ReasoningPresetSelector.scss | 18 +
.../components/ReasoningPresetSelector.tsx | 11 +-
.../components/ToolStatusIndicator.tsx | 2 +-
.../components/modern/ExportImageButton.scss | 14 +-
.../components/modern/ExportImageButton.tsx | 9 +-
.../modern/FlowChatCollapseAlignment.test.ts | 49 +-
.../components/modern/ModelRoundItem.scss | 56 +-
.../components/modern/ModelRoundItem.tsx | 28 +-
.../components/modern/UserMessageItem.tsx | 3 +-
.../components/modern/VirtualItemRenderer.tsx | 6 +-
.../components/modern/VirtualMessageList.scss | 6 +-
.../usage/SessionUsageReportCard.tsx | 3 +-
.../voice/ComposerVoiceInputButton.tsx | 2 +-
.../AgentControlToolCard.appearance.ts | 25 -
.../tool-cards/AgentControlToolCard.scss | 191 ---
.../tool-cards/AgentControlToolCard.test.tsx | 23 +-
.../tool-cards/AgentControlToolCard.tsx | 142 +-
.../AgentWaitToolCard.appearance.ts | 11 -
.../tool-cards/AgentWaitToolCard.scss | 17 -
.../tool-cards/AgentWaitToolCard.test.tsx | 6 +-
.../tool-cards/AgentWaitToolCard.tsx | 60 +-
.../AskUserQuestionCard.appearance.ts | 15 -
.../tool-cards/AskUserQuestionCard.scss | 587 --------
.../tool-cards/AskUserQuestionCard.test.tsx | 35 +-
.../tool-cards/AskUserQuestionCard.tsx | 704 +++++-----
.../tool-cards/BaseToolCard.appearance.ts | 25 -
.../flow_chat/tool-cards/BaseToolCard.scss | 344 -----
.../src/flow_chat/tool-cards/BaseToolCard.tsx | 247 ----
.../flow_chat/tool-cards/CanvasToolCard.tsx | 9 +-
.../tool-cards/CodeReviewToolCard.scss | 8 +-
.../tool-cards/CodeReviewToolCard.tsx | 47 +-
.../tool-cards/CompactToolCard.appearance.ts | 23 -
.../flow_chat/tool-cards/CompactToolCard.scss | 452 ------
.../flow_chat/tool-cards/CompactToolCard.tsx | 213 ---
.../tool-cards/ComputerUseToolCard.tsx | 10 +-
.../ContextCompressionDisplay.appearance.ts | 13 -
.../tool-cards/ContextCompressionDisplay.scss | 73 -
.../ContextCompressionDisplay.test.tsx | 84 ++
.../tool-cards/ContextCompressionDisplay.tsx | 148 +-
.../tool-cards/DefaultToolCard.appearance.ts | 15 -
.../flow_chat/tool-cards/DefaultToolCard.scss | 215 ---
.../tool-cards/DefaultToolCard.test.tsx | 4 +-
.../flow_chat/tool-cards/DefaultToolCard.tsx | 61 +-
.../tool-cards/ExecProcessToolCard.scss | 153 --
.../ExecProcessToolCardView.appearance.ts | 16 -
.../ExecProcessToolCardView.test.tsx | 70 +-
.../tool-cards/ExecProcessToolCardView.tsx | 423 ++----
.../FileOperationToolCard.appearance.ts | 21 -
.../tool-cards/FileOperationToolCard.scss | 439 ------
.../tool-cards/FileOperationToolCard.test.tsx | 194 ++-
.../tool-cards/FileOperationToolCard.tsx | 696 ++-------
.../tool-cards/GenerativeWidgetToolCard.scss | 6 +-
.../tool-cards/GenerativeWidgetToolCard.tsx | 44 +-
.../GetFileDiffDisplay.appearance.ts | 14 -
.../tool-cards/GetFileDiffDisplay.scss | 91 --
.../tool-cards/GetFileDiffDisplay.test.tsx | 110 ++
.../tool-cards/GetFileDiffDisplay.tsx | 167 +--
.../tool-cards/GetToolSpecCard.appearance.ts | 11 -
.../tool-cards/GetToolSpecCard.test.tsx | 12 +-
.../flow_chat/tool-cards/GetToolSpecCard.tsx | 24 +-
.../tool-cards/GitToolDisplay.appearance.ts | 9 -
.../flow_chat/tool-cards/GitToolDisplay.scss | 361 -----
.../tool-cards/GitToolDisplay.test.tsx | 4 +-
.../flow_chat/tool-cards/GitToolDisplay.tsx | 221 +--
.../tool-cards/GlobSearchDisplay.tsx | 102 +-
.../tool-cards/GrepSearchDisplay.tsx | 67 +-
.../src/flow_chat/tool-cards/LSDisplay.tsx | 99 +-
.../flow_chat/tool-cards/MCPToolDisplay.tsx | 26 +-
.../tool-cards/MiniAppToolDisplay.scss | 16 +-
.../tool-cards/MiniAppToolDisplay.tsx | 8 +-
.../tool-cards/PageDeployToolDisplay.tsx | 113 +-
.../tool-cards/PagePublishToolDisplay.tsx | 157 +--
.../tool-cards/ReadFileDisplay.test.tsx | 79 +-
.../flow_chat/tool-cards/ReadFileDisplay.tsx | 29 +-
.../ReviewSessionSummaryCard.appearance.ts | 9 -
.../tool-cards/ReviewSessionSummaryCard.scss | 63 -
.../tool-cards/ReviewSessionSummaryCard.tsx | 98 +-
.../tool-cards/RunCodeToolCard.appearance.ts | 13 -
.../flow_chat/tool-cards/RunCodeToolCard.scss | 30 -
.../tool-cards/RunCodeToolCard.test.tsx | 4 +-
.../flow_chat/tool-cards/RunCodeToolCard.tsx | 93 +-
.../SessionControlToolCard.test.tsx | 15 -
.../tool-cards/SessionControlToolCard.tsx | 166 +--
.../tool-cards/SessionMessageToolCard.tsx | 88 +-
.../src/flow_chat/tool-cards/SkillDisplay.tsx | 53 +-
.../flow_chat/tool-cards/TaskToolDisplay.scss | 57 +-
.../tool-cards/TaskToolDisplay.test.tsx | 12 +-
.../flow_chat/tool-cards/TaskToolDisplay.tsx | 19 +-
.../tool-cards/TerminalControlDisplay.tsx | 68 +-
.../tool-cards/TerminalToolCard.appearance.ts | 26 -
.../tool-cards/TerminalToolCard.scss | 638 ---------
.../flow_chat/tool-cards/TerminalToolCard.tsx | 439 ++----
.../tool-cards/TodoWriteDisplay.appearance.ts | 16 -
.../tool-cards/TodoWriteDisplay.scss | 259 ----
.../tool-cards/TodoWriteDisplay.test.tsx | 21 +-
.../flow_chat/tool-cards/TodoWriteDisplay.tsx | 179 +--
.../tool-cards/ToolCardCopyAction.tsx | 50 +
.../ToolCardHeaderActions.appearance.ts | 6 -
.../tool-cards/ToolCardHeaderActions.scss | 31 -
.../tool-cards/ToolCardHeaderActions.tsx | 71 -
.../tool-cards/ToolCardHeaderLayoutContext.ts | 19 -
.../flow_chat/tool-cards/ToolCardIconSlot.tsx | 76 -
.../tool-cards/ToolCardStatusIcon.tsx | 30 -
.../ToolCardStatusSlot.appearance.ts | 7 -
.../tool-cards/ToolCardStatusSlot.scss | 72 -
.../tool-cards/ToolCardStatusSlot.tsx | 82 --
.../ViewImageToolCard.appearance.ts | 13 -
.../tool-cards/ViewImageToolCard.scss | 68 -
.../tool-cards/ViewImageToolCard.test.tsx | 3 +-
.../tool-cards/ViewImageToolCard.tsx | 95 +-
.../tool-cards/WebFetchCard.appearance.ts | 14 -
.../flow_chat/tool-cards/WebFetchCard.scss | 60 -
.../tool-cards/WebFetchCard.test.tsx | 20 +-
.../src/flow_chat/tool-cards/WebFetchCard.tsx | 129 +-
.../flow_chat/tool-cards/WebSearchCard.tsx | 76 +-
.../tool-cards/_tool-card-common.scss | 143 --
.../src/flow_chat/tool-cards/index.test.ts | 58 +
src/web-ui/src/flow_chat/tool-cards/index.ts | 78 +-
.../flow_chat/tool-cards/toolCardMetadata.ts | 81 +-
src/web-ui/src/flow_chat/types/flow-chat.ts | 2 +
.../utils/flowChatScrollLayout.test.ts | 4 +
.../flow_chat/utils/flowChatScrollLayout.ts | 4 +-
.../flow_chat/utils/tokenUsageDisplay.test.ts | 32 +-
.../src/flow_chat/utils/tokenUsageDisplay.ts | 39 +-
.../compiler/AppearanceCompiler.test.ts | 25 -
.../registry/defaultAppearanceRegistry.ts | 46 +-
.../config/components/McpToolsConfig.tsx | 3 +-
src/web-ui/src/locales/en-US/flow-chat.json | 3 +-
src/web-ui/src/locales/zh-CN/flow-chat.json | 3 +-
src/web-ui/src/locales/zh-TW/flow-chat.json | 3 +-
.../LazyTerminalOutputRenderer.test.tsx | 2 +
.../components/LazyTerminalOutputRenderer.tsx | 3 +
.../components/TerminalOutputRenderer.scss | 43 +
src/web-ui/vite.config.ts | 4 +
258 files changed, 13275 insertions(+), 13763 deletions(-)
create mode 100644 design-system/apps/design-lab/src/preview/FlowChatPreviewRegistry.css
create mode 100644 design-system/apps/design-lab/src/preview/FlowChatPreviewRegistry.tsx
create mode 100644 design-system/apps/design-lab/src/preview/FlowChatToolGallery.css
create mode 100644 design-system/apps/design-lab/src/preview/FlowChatToolGallery.tsx
create mode 100644 design-system/packages/ui/src/flow-chat.ts
create mode 100644 design-system/packages/ui/src/flow-chat/ask-user/AskUser.meta.ts
create mode 100644 design-system/packages/ui/src/flow-chat/ask-user/AskUser.module.css
create mode 100644 design-system/packages/ui/src/flow-chat/ask-user/AskUser.tsx
create mode 100644 design-system/packages/ui/src/flow-chat/ask-user/index.ts
create mode 100644 design-system/packages/ui/src/flow-chat/composer/ChatComposer.meta.ts
create mode 100644 design-system/packages/ui/src/flow-chat/composer/ChatComposer.module.css
create mode 100644 design-system/packages/ui/src/flow-chat/composer/ChatComposer.tsx
create mode 100644 design-system/packages/ui/src/flow-chat/composer/index.ts
create mode 100644 design-system/packages/ui/src/flow-chat/index.ts
create mode 100644 design-system/packages/ui/src/flow-chat/tool-cards/ActivityToolCards.module.css
create mode 100644 design-system/packages/ui/src/flow-chat/tool-cards/ActivityToolCards.tsx
create mode 100644 design-system/packages/ui/src/flow-chat/tool-cards/AmbientToolCard.meta.ts
create mode 100644 design-system/packages/ui/src/flow-chat/tool-cards/CommandToolCard.meta.ts
create mode 100644 design-system/packages/ui/src/flow-chat/tool-cards/CommandToolCard.module.css
create mode 100644 design-system/packages/ui/src/flow-chat/tool-cards/CommandToolCard.tsx
create mode 100644 design-system/packages/ui/src/flow-chat/tool-cards/ContextCompressionToolCard.meta.ts
create mode 100644 design-system/packages/ui/src/flow-chat/tool-cards/ContextCompressionToolCard.module.css
create mode 100644 design-system/packages/ui/src/flow-chat/tool-cards/ContextCompressionToolCard.tsx
create mode 100644 design-system/packages/ui/src/flow-chat/tool-cards/FileOperationToolCard.meta.ts
create mode 100644 design-system/packages/ui/src/flow-chat/tool-cards/FileOperationToolCard.module.css
create mode 100644 design-system/packages/ui/src/flow-chat/tool-cards/FileOperationToolCard.tsx
create mode 100644 design-system/packages/ui/src/flow-chat/tool-cards/FlowChatToolCard.module.css
create mode 100644 design-system/packages/ui/src/flow-chat/tool-cards/FlowChatToolCard.tsx
create mode 100644 design-system/packages/ui/src/flow-chat/tool-cards/ProminentToolCard.meta.ts
create mode 100644 design-system/packages/ui/src/flow-chat/tool-cards/ProminentToolCards.meta.ts
create mode 100644 design-system/packages/ui/src/flow-chat/tool-cards/ProminentToolCards.module.css
create mode 100644 design-system/packages/ui/src/flow-chat/tool-cards/ProminentToolCards.tsx
create mode 100644 design-system/packages/ui/src/flow-chat/tool-cards/ReadFileToolCard.meta.ts
create mode 100644 design-system/packages/ui/src/flow-chat/tool-cards/ReadFileToolCard.tsx
create mode 100644 design-system/packages/ui/src/flow-chat/tool-cards/SearchResultsToolCards.module.css
create mode 100644 design-system/packages/ui/src/flow-chat/tool-cards/SearchResultsToolCards.tsx
create mode 100644 design-system/packages/ui/src/flow-chat/tool-cards/SessionToolCards.module.css
create mode 100644 design-system/packages/ui/src/flow-chat/tool-cards/SessionToolCards.tsx
create mode 100644 design-system/packages/ui/src/flow-chat/tool-cards/StandardAmbientToolCards.module.css
create mode 100644 design-system/packages/ui/src/flow-chat/tool-cards/StandardAmbientToolCards.tsx
create mode 100644 design-system/packages/ui/src/flow-chat/tool-cards/StandardToolCards.meta.ts
create mode 100644 design-system/packages/ui/src/flow-chat/tool-cards/ToolCardStatusSlot.module.css
create mode 100644 design-system/packages/ui/src/flow-chat/tool-cards/ToolCardStatusSlot.tsx
create mode 100644 design-system/packages/ui/src/flow-chat/tool-cards/ToolProcessingDots.module.css
create mode 100644 design-system/packages/ui/src/flow-chat/tool-cards/ToolProcessingDots.tsx
create mode 100644 design-system/packages/ui/src/flow-chat/tool-cards/index.ts
create mode 100644 design-system/packages/ui/tests/ask-user.test.mjs
create mode 100644 design-system/packages/ui/tests/chat-composer.test.mjs
create mode 100644 design-system/packages/ui/tests/flow-chat-tool-card.test.mjs
delete mode 100644 src/web-ui/src/component-library/components/FlowChatCards/BaseToolCard/BaseToolCard.scss
delete mode 100644 src/web-ui/src/component-library/components/FlowChatCards/BaseToolCard/BaseToolCard.tsx
delete mode 100644 src/web-ui/src/component-library/components/FlowChatCards/BaseToolCard/index.ts
delete mode 100644 src/web-ui/src/component-library/components/FlowChatCards/ContextCompressionCard/ContextCompressionCard.scss
delete mode 100644 src/web-ui/src/component-library/components/FlowChatCards/ContextCompressionCard/ContextCompressionCard.tsx
delete mode 100644 src/web-ui/src/component-library/components/FlowChatCards/ContextCompressionCard/appearance.ts
delete mode 100644 src/web-ui/src/component-library/components/FlowChatCards/ContextCompressionCard/index.ts
delete mode 100644 src/web-ui/src/component-library/components/FlowChatCards/README.md
delete mode 100644 src/web-ui/src/component-library/components/FlowChatCards/README.zh-CN.md
delete mode 100644 src/web-ui/src/component-library/components/FlowChatCards/ReadFileCard/ReadFileCard.scss
delete mode 100644 src/web-ui/src/component-library/components/FlowChatCards/SearchCard/SearchCard.scss
delete mode 100644 src/web-ui/src/component-library/components/FlowChatCards/SearchCard/SearchCard.tsx
delete mode 100644 src/web-ui/src/component-library/components/FlowChatCards/SearchCard/index.ts
delete mode 100644 src/web-ui/src/component-library/components/FlowChatCards/SnapshotCard/SnapshotCard.scss
delete mode 100644 src/web-ui/src/component-library/components/FlowChatCards/SnapshotCard/SnapshotCard.tsx
delete mode 100644 src/web-ui/src/component-library/components/FlowChatCards/SnapshotCard/index.ts
delete mode 100644 src/web-ui/src/component-library/components/FlowChatCards/TaskCard/TaskCard.scss
delete mode 100644 src/web-ui/src/component-library/components/FlowChatCards/TaskCard/TaskCard.tsx
delete mode 100644 src/web-ui/src/component-library/components/FlowChatCards/TaskCard/index.ts
delete mode 100644 src/web-ui/src/component-library/components/FlowChatCards/TodoCard/TodoCard.scss
delete mode 100644 src/web-ui/src/component-library/components/FlowChatCards/TodoCard/TodoCard.tsx
delete mode 100644 src/web-ui/src/component-library/components/FlowChatCards/TodoCard/index.ts
delete mode 100644 src/web-ui/src/component-library/components/FlowChatCards/ToolProcessingDots/ToolProcessingDots.scss
delete mode 100644 src/web-ui/src/component-library/components/FlowChatCards/ToolProcessingDots/ToolProcessingDots.tsx
delete mode 100644 src/web-ui/src/component-library/components/FlowChatCards/ToolProcessingDots/index.ts
delete mode 100644 src/web-ui/src/component-library/components/FlowChatCards/WebSearchCard/WebSearchCard.scss
delete mode 100644 src/web-ui/src/component-library/components/FlowChatCards/WebSearchCard/WebSearchCard.tsx
delete mode 100644 src/web-ui/src/component-library/components/FlowChatCards/WebSearchCard/index.ts
delete mode 100644 src/web-ui/src/component-library/components/FlowChatCards/_shared-styles.scss
delete mode 100644 src/web-ui/src/component-library/components/FlowChatCards/appearance.ts
delete mode 100644 src/web-ui/src/component-library/components/FlowChatCards/index.ts
delete mode 100644 src/web-ui/src/component-library/preview/flowchat-cards-preview.css
create mode 100644 src/web-ui/src/flow_chat/components/FlowToolCard.appearance.ts
create mode 100644 src/web-ui/src/flow_chat/components/FlowToolCard.scss
delete mode 100644 src/web-ui/src/flow_chat/tool-cards/AgentControlToolCard.appearance.ts
delete mode 100644 src/web-ui/src/flow_chat/tool-cards/AgentControlToolCard.scss
delete mode 100644 src/web-ui/src/flow_chat/tool-cards/AgentWaitToolCard.appearance.ts
delete mode 100644 src/web-ui/src/flow_chat/tool-cards/AgentWaitToolCard.scss
delete mode 100644 src/web-ui/src/flow_chat/tool-cards/AskUserQuestionCard.appearance.ts
delete mode 100644 src/web-ui/src/flow_chat/tool-cards/AskUserQuestionCard.scss
delete mode 100644 src/web-ui/src/flow_chat/tool-cards/BaseToolCard.appearance.ts
delete mode 100644 src/web-ui/src/flow_chat/tool-cards/BaseToolCard.scss
delete mode 100644 src/web-ui/src/flow_chat/tool-cards/BaseToolCard.tsx
delete mode 100644 src/web-ui/src/flow_chat/tool-cards/CompactToolCard.appearance.ts
delete mode 100644 src/web-ui/src/flow_chat/tool-cards/CompactToolCard.scss
delete mode 100644 src/web-ui/src/flow_chat/tool-cards/CompactToolCard.tsx
delete mode 100644 src/web-ui/src/flow_chat/tool-cards/ContextCompressionDisplay.appearance.ts
delete mode 100644 src/web-ui/src/flow_chat/tool-cards/ContextCompressionDisplay.scss
create mode 100644 src/web-ui/src/flow_chat/tool-cards/ContextCompressionDisplay.test.tsx
delete mode 100644 src/web-ui/src/flow_chat/tool-cards/DefaultToolCard.appearance.ts
delete mode 100644 src/web-ui/src/flow_chat/tool-cards/DefaultToolCard.scss
delete mode 100644 src/web-ui/src/flow_chat/tool-cards/ExecProcessToolCard.scss
delete mode 100644 src/web-ui/src/flow_chat/tool-cards/ExecProcessToolCardView.appearance.ts
delete mode 100644 src/web-ui/src/flow_chat/tool-cards/FileOperationToolCard.appearance.ts
delete mode 100644 src/web-ui/src/flow_chat/tool-cards/FileOperationToolCard.scss
delete mode 100644 src/web-ui/src/flow_chat/tool-cards/GetFileDiffDisplay.appearance.ts
delete mode 100644 src/web-ui/src/flow_chat/tool-cards/GetFileDiffDisplay.scss
create mode 100644 src/web-ui/src/flow_chat/tool-cards/GetFileDiffDisplay.test.tsx
delete mode 100644 src/web-ui/src/flow_chat/tool-cards/GetToolSpecCard.appearance.ts
delete mode 100644 src/web-ui/src/flow_chat/tool-cards/GitToolDisplay.appearance.ts
delete mode 100644 src/web-ui/src/flow_chat/tool-cards/GitToolDisplay.scss
delete mode 100644 src/web-ui/src/flow_chat/tool-cards/ReviewSessionSummaryCard.appearance.ts
delete mode 100644 src/web-ui/src/flow_chat/tool-cards/ReviewSessionSummaryCard.scss
delete mode 100644 src/web-ui/src/flow_chat/tool-cards/RunCodeToolCard.appearance.ts
delete mode 100644 src/web-ui/src/flow_chat/tool-cards/RunCodeToolCard.scss
delete mode 100644 src/web-ui/src/flow_chat/tool-cards/TerminalToolCard.appearance.ts
delete mode 100644 src/web-ui/src/flow_chat/tool-cards/TerminalToolCard.scss
delete mode 100644 src/web-ui/src/flow_chat/tool-cards/TodoWriteDisplay.appearance.ts
delete mode 100644 src/web-ui/src/flow_chat/tool-cards/TodoWriteDisplay.scss
create mode 100644 src/web-ui/src/flow_chat/tool-cards/ToolCardCopyAction.tsx
delete mode 100644 src/web-ui/src/flow_chat/tool-cards/ToolCardHeaderActions.appearance.ts
delete mode 100644 src/web-ui/src/flow_chat/tool-cards/ToolCardHeaderActions.scss
delete mode 100644 src/web-ui/src/flow_chat/tool-cards/ToolCardHeaderActions.tsx
delete mode 100644 src/web-ui/src/flow_chat/tool-cards/ToolCardHeaderLayoutContext.ts
delete mode 100644 src/web-ui/src/flow_chat/tool-cards/ToolCardIconSlot.tsx
delete mode 100644 src/web-ui/src/flow_chat/tool-cards/ToolCardStatusIcon.tsx
delete mode 100644 src/web-ui/src/flow_chat/tool-cards/ToolCardStatusSlot.appearance.ts
delete mode 100644 src/web-ui/src/flow_chat/tool-cards/ToolCardStatusSlot.scss
delete mode 100644 src/web-ui/src/flow_chat/tool-cards/ToolCardStatusSlot.tsx
delete mode 100644 src/web-ui/src/flow_chat/tool-cards/ViewImageToolCard.appearance.ts
delete mode 100644 src/web-ui/src/flow_chat/tool-cards/ViewImageToolCard.scss
delete mode 100644 src/web-ui/src/flow_chat/tool-cards/WebFetchCard.appearance.ts
delete mode 100644 src/web-ui/src/flow_chat/tool-cards/WebFetchCard.scss
create mode 100644 src/web-ui/src/tools/terminal/components/TerminalOutputRenderer.scss
diff --git a/design-system/README.md b/design-system/README.md
index 28c5c651d0..884f5f503f 100644
--- a/design-system/README.md
+++ b/design-system/README.md
@@ -18,6 +18,27 @@ Design Lab exposes an Overview, a searchable component catalog with interactive
`@bitfun/ui/registry` is the component source of truth. Design Lab derives its component navigation, counts, token scopes, and detail routes from that package export; adding Lab copy or a preview cannot publish a component or keep a removed component alive.
+The same entry publishes `ChatComposer`, the shared context-band and
+compact/expanded input anatomy used by product composers without importing
+their stores, localized menus, editor, model, voice, or host actions.
+
+FlowChat tool-card frameworks are published from `@bitfun/ui/flow-chat` as
+`AmbientToolCard` and `ProminentToolCard`. The names describe whether a tool
+result should stay in the conversational background or receive deliberate user
+attention. Concrete package views cover the reusable FlowChat families for file
+and command execution, search and web results, agent and session activity, Git
+and review summaries, page lifecycle, code execution, todos, images, and other
+routine tool traces. Product adapters continue to own parsing, stores,
+localization, host capabilities, and heavy renderers passed through semantic
+slots; the package owns each migrated card's anatomy, state presentation, and
+interaction structure.
+
+Design Lab exposes FlowChat as its own library category. Both framework and
+tool-view entries come from `@bitfun/ui/registry`; its typed preview registry is
+exhaustive over those package entries. The tool gallery renders only real
+public views used by migrated adapters and lists bespoke product cards
+separately instead of substituting generic previews for them.
+
## Commands
```bash
diff --git a/design-system/apps/design-lab/src/App.tsx b/design-system/apps/design-lab/src/App.tsx
index 1ec3aa9d3c..f662ab53be 100644
--- a/design-system/apps/design-lab/src/App.tsx
+++ b/design-system/apps/design-lab/src/App.tsx
@@ -22,6 +22,7 @@ import {
PanelTop,
Search,
Settings2,
+ SquareTerminal,
Sun,
ToggleLeft,
X,
@@ -71,6 +72,7 @@ type LabRoute =
| { page: "overview" }
| { page: "getting-started" }
| { page: "components" }
+ | { page: "flow-chat" }
| { page: "colors" }
| { page: "resources" }
| { page: "tokens" }
@@ -85,12 +87,21 @@ interface SearchDestination {
}
const componentIcons: Record = {
+ AmbientToolCard: SquareTerminal,
Button: MousePointerClick,
Modal: AppWindow,
+ ProminentToolCard: SquareTerminal,
Switch: ToggleLeft,
TabGroup: PanelTop,
};
+const flowChatComponents = componentRegistry.filter(
+ (component) => component.category === "flow-chat",
+);
+const standardComponents = componentRegistry.filter(
+ (component) => component.category !== "flow-chat",
+);
+
function getThemeDataName(
colorScheme: ColorScheme,
contrast: ContrastMode,
@@ -118,6 +129,9 @@ function parseRoute(hash: string): LabRoute {
if (route === "components") {
return { page: "components" };
}
+ if (route === "flow-chat") {
+ return { page: "flow-chat" };
+ }
if (route === "resources") {
return { page: "resources" };
}
@@ -201,6 +215,13 @@ export function App() {
label: t("nav.components"),
route: { page: "components" },
},
+ {
+ detail: t("search.flowChatDetail", { count: flowChatComponents.length }),
+ icon: SquareTerminal,
+ keywords: `FlowChat tool cards ambient prominent ${t("nav.flowChat")}`,
+ label: t("nav.flowChat"),
+ route: { page: "flow-chat" },
+ },
{
detail: t("search.resourcesDetail"),
icon: FileText,
@@ -343,6 +364,10 @@ export function App() {
const activeComponent = route.page === "component"
? componentRegistry.find((component) => component.name === route.componentName)
: undefined;
+ const isFlowChatRoute = route.page === "flow-chat"
+ || activeComponent?.category === "flow-chat";
+ const isStandardComponentRoute = route.page === "components"
+ || Boolean(activeComponent && activeComponent.category !== "flow-chat");
return (
{t("nav.library")}
{
event.preventDefault();
@@ -432,7 +457,7 @@ export function App() {
{t("nav.components")}
- {componentRegistry.map((component) => {
+ {standardComponents.map((component) => {
const Icon = componentIcons[component.name] ?? Blocks;
const active = route.page === "component" && route.componentName === component.name;
return (
@@ -451,6 +476,39 @@ export function App() {
);
})}
+
{
+ event.preventDefault();
+ navigate({ page: "flow-chat" });
+ }}
+ >
+
+ {t("nav.flowChat")}
+ {flowChatComponents.length}
+
+
@@ -643,6 +701,18 @@ export function App() {
/>
)}
+ {route.page === "flow-chat" && (
+
openTokenPage()}
+ onOpenComponent={(name) => navigate({ componentName: name, page: "component" })}
+ tokenOverrides={tokenOverrides}
+ />
+ )}
+
{route.page === "component" && activeComponent && (
navigate({ page: "components" })}
+ onBack={() => navigate({
+ page: activeComponent.category === "flow-chat"
+ ? "flow-chat"
+ : "components",
+ })}
onInspectTokens={openTokenPage}
tokenOverrides={tokenOverrides}
/>
diff --git a/design-system/apps/design-lab/src/i18n/componentMetadata.ts b/design-system/apps/design-lab/src/i18n/componentMetadata.ts
index 9d0aac1a60..834840cb1a 100644
--- a/design-system/apps/design-lab/src/i18n/componentMetadata.ts
+++ b/design-system/apps/design-lab/src/i18n/componentMetadata.ts
@@ -6,6 +6,7 @@ type Translate = (key: MessageKey, params?: TranslateParams) => string;
const categoryKeys: Readonly> = {
action: "meta.category.action",
feedback: "meta.category.feedback",
+ "flow-chat": "meta.category.flowChat",
form: "meta.category.form",
navigation: "meta.category.navigation",
primitive: "meta.category.primitive",
@@ -15,24 +16,51 @@ const descriptionKeys: Readonly> = {
ActionCard: "component.ActionCard.description",
ActionItem: "component.ActionItem.description",
ActivityItem: "component.ActivityItem.description",
+ AgentControlToolCard: "component.AgentControlToolCard.description",
+ AgentWaitToolCard: "component.AgentWaitToolCard.description",
+ AmbientToolCard: "component.AmbientToolCard.description",
Button: "component.Button.description",
Card: "component.Card.description",
+ CommandToolCard: "component.CommandToolCard.description",
Composer: "component.Composer.description",
ConfirmDialog: "component.ConfirmDialog.description",
+ ContextCompressionToolCard: "component.ContextCompressionToolCard.description",
+ DefaultToolCard: "component.DefaultToolCard.description",
+ DirectoryListToolCard: "component.DirectoryListToolCard.description",
Field: "component.Field.description",
FieldGroup: "component.FieldGroup.description",
+ FileDiffToolCard: "component.FileDiffToolCard.description",
+ FileOperationToolCard: "component.FileOperationToolCard.description",
+ GetToolSpecToolCard: "component.GetToolSpecToolCard.description",
+ GitToolCard: "component.GitToolCard.description",
+ GlobSearchToolCard: "component.GlobSearchToolCard.description",
+ GrepSearchToolCard: "component.GrepSearchToolCard.description",
IconButton: "component.IconButton.description",
Input: "component.Input.description",
KeyHint: "component.KeyHint.description",
Menu: "component.Menu.description",
Modal: "component.Modal.description",
NavigationPanel: "component.NavigationPanel.description",
+ PageDeployToolCard: "component.PageDeployToolCard.description",
PageHeader: "component.PageHeader.description",
+ PagePublishToolCard: "component.PagePublishToolCard.description",
+ ProminentToolCard: "component.ProminentToolCard.description",
+ ReadFileToolCard: "component.ReadFileToolCard.description",
+ ReviewSummaryToolCard: "component.ReviewSummaryToolCard.description",
+ RunCodeToolCard: "component.RunCodeToolCard.description",
SearchField: "component.SearchField.description",
SegmentedControl: "component.SegmentedControl.description",
+ SessionControlToolCard: "component.SessionControlToolCard.description",
+ SessionMessageToolCard: "component.SessionMessageToolCard.description",
+ SkillToolCard: "component.SkillToolCard.description",
Switch: "component.Switch.description",
TabGroup: "component.TabGroup.description",
+ TerminalControlToolCard: "component.TerminalControlToolCard.description",
+ TodoToolCard: "component.TodoToolCard.description",
Toolbar: "component.Toolbar.description",
+ ViewImageToolCard: "component.ViewImageToolCard.description",
+ WebFetchToolCard: "component.WebFetchToolCard.description",
+ WebSearchToolCard: "component.WebSearchToolCard.description",
};
export function getComponentCategoryLabel(category: string, t: Translate): string {
diff --git a/design-system/apps/design-lab/src/i18n/messages.ts b/design-system/apps/design-lab/src/i18n/messages.ts
index eb0fc9a8ac..ec18d6eda6 100644
--- a/design-system/apps/design-lab/src/i18n/messages.ts
+++ b/design-system/apps/design-lab/src/i18n/messages.ts
@@ -13,6 +13,7 @@ export const enUSMessages = {
"nav.colors": "Colors",
"nav.library": "Library",
"nav.components": "Components",
+ "nav.flowChat": "FlowChat",
"nav.docs": "Docs",
"nav.resources": "Resources",
"search.label": "Search Design Lab",
@@ -23,6 +24,7 @@ export const enUSMessages = {
"search.tokensDetail": "{count} editable contracts",
"search.colorsDetail": "{count} semantic color tokens and real reference scales",
"search.componentsDetail": "{count} registered components",
+ "search.flowChatDetail": "{count} shared templates and standard tool-card specimens",
"search.resourcesDetail": "Package documentation and release policy",
"search.componentDetail": "{category}",
"theme.switchToDark": "Switch to dark scheme",
@@ -46,31 +48,60 @@ export const enUSMessages = {
"meta.category.action": "Action",
"meta.category.form": "Form",
"meta.category.feedback": "Feedback",
+ "meta.category.flowChat": "Flow Chat",
"meta.category.navigation": "Navigation",
"meta.category.primitive": "Primitive",
"meta.category.other": "Component",
"component.ActionCard.description": "An actionable card with independent leading, title, description, selection, and sibling action areas.",
"component.ActionItem.description": "A native button row with independent leading, shortcut, and sibling action areas.",
"component.ActivityItem.description": "A compact activity row with independent identity, description, metadata, and sibling action areas.",
+ "component.AgentControlToolCard.description": "A prominent subagent card with an openable identity, lifecycle state, and expandable prompt.",
+ "component.AgentWaitToolCard.description": "An ambient trace for background-agent wait progress and completion summaries.",
+ "component.AmbientToolCard.description": "A low-attention tool trace that stays glanceable in the conversation and can reveal supporting detail.",
+ "component.AskUser.description": "A controlled question card with responsive choices, custom answers, submission feedback, and an answered disclosure summary.",
"component.Button.description": "A compact action with outline, filled, primary, and text presentation variants.",
"component.Card.description": "A composable content surface with independent media, header, body, footer, spacing, clipping, and elevation contracts.",
+ "component.CommandToolCard.description": "A prominent command view with controlled output disclosure, process metadata, and capability-driven actions.",
"component.Composer.description": "A composable message editor shell with independent context, editor, and action regions.",
"component.ConfirmDialog.description": "A semantic confirmation dialog with optional preview content, destructive emphasis, and async actions.",
+ "component.ContextCompressionToolCard.description": "A prominent context-compression result with one compact length-and-ratio summary, plus progress and failure states.",
+ "component.DefaultToolCard.description": "The ambient fallback for an unregistered tool summary, sanitized input, result, and confirmation state.",
+ "component.DirectoryListToolCard.description": "An ambient directory-list card with structured metadata and file results.",
"component.Field.description": "Associates label content, optional actions, description, and adjacent controls with one primary form control.",
"component.FieldGroup.description": "Composes titled form sections, grouped field surfaces, and independently padded rows.",
+ "component.FileDiffToolCard.description": "A prominent file-diff card with a compact file path, change summary, message, and renderer slot.",
+ "component.FileOperationToolCard.description": "An adaptive file-operation view: ambient for deletion, prominent and expandable for writes and edits.",
+ "component.GetToolSpecToolCard.description": "An ambient trace for loading a deferred tool specification into runtime context.",
+ "component.GitToolCard.description": "A prominent Git command card with output, warning, failure, and execution metadata.",
+ "component.GlobSearchToolCard.description": "An ambient file-pattern search card with reusable result-list anatomy.",
+ "component.GrepSearchToolCard.description": "An ambient text-search card with query metadata and a scrollable result preview.",
"component.IconButton.description": "A labeled icon-only action with quiet, filled, and primary presentation variants.",
"component.Input.description": "A native text field with independent leading and trailing content slots.",
"component.KeyHint.description": "A semantic keyboard hint with optional decorative icon content.",
"component.Menu.description": "A grouped, scrollable menu surface with heading actions, native item roles, and keyboard navigation.",
"component.Modal.description": "A portaled dialog with focus management, configurable geometry, semantic header content, and a floating action footer.",
"component.NavigationPanel.description": "A scrollable application navigation panel with independent header, grouped content, selected items, and footer slots.",
+ "component.PageDeployToolCard.description": "A prominent page-deploy card with version metadata and host-provided actions.",
"component.PageHeader.description": "A title composition with independent leading content, semantic level, visual size, description, action, and alignment.",
+ "component.PagePublishToolCard.description": "A prominent page-publish card with production and preview metadata and actions.",
+ "component.ProminentToolCard.description": "An attention-worthy tool result with a stable summary region, hover-revealed actions, and controlled detail disclosure.",
+ "component.ReadFileToolCard.description": "An ambient read-file trace with progress, permission-aware interaction, completion, and failure summaries.",
+ "component.ReviewSummaryToolCard.description": "A prominent review summary with changed files, status, and a host navigation action.",
+ "component.RunCodeToolCard.description": "An ambient code-execution card with host-rendered program and output slots.",
"component.ScrollArea.description": "A native scroll viewport with configurable direction and scrollbar visibility.",
"component.SearchField.description": "A search input with optional leading icon, shortcut hint, and Enter submission.",
"component.SegmentedControl.description": "A compact pill-shaped single-choice control that switches between a small set of exclusive modes.",
+ "component.SessionControlToolCard.description": "An ambient session-lifecycle card with semantic detail fields and session rows.",
+ "component.SessionMessageToolCard.description": "An ambient cross-session message card with target details and message content.",
+ "component.SkillToolCard.description": "An ambient trace for skill loading progress and outcomes.",
"component.Switch.description": "Toggles a single setting between on and off with native checkbox semantics.",
"component.TabGroup.description": "A compact single-selection tab list with optional leading icons, end actions, and automatic keyboard activation.",
+ "component.TerminalControlToolCard.description": "An ambient trace for interrupting or terminating a terminal session.",
+ "component.TodoToolCard.description": "An ambient task-progress card with compact and expandable list presentations.",
"component.Toolbar.description": "A responsive bar with independent leading, centered, trailing, and horizontally scrollable composition regions.",
+ "component.ViewImageToolCard.description": "An ambient image-result card with an expandable preview and accessible lightbox.",
+ "component.WebFetchToolCard.description": "An ambient web-fetch card with source metadata, result actions, and content preview.",
+ "component.WebSearchToolCard.description": "An ambient web-search card with link, snippet, URL, and summary result presentations.",
"overview.intro": "A modern design system for building consistent, accessible, and delightful BitFun products.",
"overview.getStarted": "Get started",
"overview.viewTokens": "View design tokens",
@@ -132,6 +163,54 @@ export const enUSMessages = {
"components.preview.activityCopy": "Copy command",
"components.preview.activityDownload": "Download output",
"components.preview.activityOpen": "Open details",
+ "components.preview.flowChat.askUserAnswered": "1 question answered",
+ "components.preview.flowChat.askUserBetaDescription": "Latest Beta pre-release — newer and has passed basic testing",
+ "components.preview.flowChat.askUserError": "Questions could not be loaded",
+ "components.preview.flowChat.askUserHeader": "1 question",
+ "components.preview.flowChat.askUserLoading": "Loading questions…",
+ "components.preview.flowChat.askUserNightly": "Nightly build",
+ "components.preview.flowChat.askUserNightlyDescription": "Daily automatic builds — contain the latest code but are untested",
+ "components.preview.flowChat.askUserOther": "Other",
+ "components.preview.flowChat.askUserOtherDescription": "Provide custom text input",
+ "components.preview.flowChat.askUserOtherInputLabel": "Custom version",
+ "components.preview.flowChat.askUserOtherPlaceholder": "Enter a version",
+ "components.preview.flowChat.askUserQuestion": "BitFun has three versions — which would you like me to pull?",
+ "components.preview.flowChat.askUserStableDescription": "The stable release marked as Latest on GitHub — the most stable one",
+ "components.preview.flowChat.askUserSubmit": "Submit",
+ "components.preview.flowChat.askUserSubmitting": "Submitting…",
+ "components.preview.flowChat.askUserSummaryPrefix": "Choose version",
+ "components.preview.flowChat.askUserWaiting": "Waiting for your answer",
+ "components.preview.flowChat.changeSummary": "6 additions and 0 deletions",
+ "components.preview.flowChat.commandFailed": "Command exited with code 1.",
+ "components.preview.flowChat.commandWaiting": "Waiting for process output…",
+ "components.preview.flowChat.completed": "Completed",
+ "components.preview.flowChat.contextCompression": "Compress context",
+ "components.preview.flowChat.contextDetail": "Older conversation segments were summarized while recent tool context was retained.",
+ "components.preview.flowChat.contextError": "Context compression could not be completed.",
+ "components.preview.flowChat.contextMeta": "93k tokens removed",
+ "components.preview.flowChat.contextProcessing": "Analyzing conversation context…",
+ "components.preview.flowChat.contextSummary": "Compressed context length 31k (compression ratio 75%)",
+ "components.preview.flowChat.copy": "Copy command",
+ "components.preview.flowChat.deleteFile": "Delete file",
+ "components.preview.flowChat.download": "Download output",
+ "components.preview.flowChat.duration": "Duration",
+ "components.preview.flowChat.editFile": "Edit file",
+ "components.preview.flowChat.emptyCommand": "No command available",
+ "components.preview.flowChat.exitCode": "Exit code",
+ "components.preview.flowChat.failed": "Failed",
+ "components.preview.flowChat.fileChangeSummary": "12 additions and 4 deletions",
+ "components.preview.flowChat.fileErrorMessage": "The target changed before the operation could be applied.",
+ "components.preview.flowChat.fileErrorTitle": "File operation failed",
+ "components.preview.flowChat.fileLoading": "Applying changes…",
+ "components.preview.flowChat.openFile": "Open file",
+ "components.preview.flowChat.interruptProcess": "Interrupt process",
+ "components.preview.flowChat.pollProcess": "Poll process",
+ "components.preview.flowChat.readFile": "Read file",
+ "components.preview.flowChat.readFailed": "Could not read src/flow_chat/tool-cards/index.ts",
+ "components.preview.flowChat.readingFile": "Reading src/flow_chat/tool-cards/index.ts…",
+ "components.preview.flowChat.runCommand": "Run command",
+ "components.preview.flowChat.running": "Running",
+ "components.preview.flowChat.writeFile": "Write file",
"components.preview.add": "Add",
"components.preview.more": "More actions",
"components.preview.appearance": "Appearance",
@@ -206,6 +285,26 @@ export const enUSMessages = {
"components.kicker": "Library",
"components.title": "Components",
"components.description": "Production package exports, rendered against the active theme and density contract.",
+ "components.flowChat.kicker": "FlowChat library",
+ "components.flowChat.title": "FlowChat",
+ "components.flowChat.description": "Shared attention-based card frameworks and standard tool-card specimens built from those public contracts.",
+ "components.flowChat.templatesKicker": "Shared frameworks",
+ "components.flowChat.templatesTitle": "General card templates",
+ "components.flowChat.templatesDescription": "Ambient and Prominent define the stable anatomy, behavior, accessibility, and disclosure rules used by standard FlowChat tools.",
+ "components.flowChat.toolsKicker": "Tool cards",
+ "components.flowChat.toolsTitle": "Migrated tool cards",
+ "components.flowChat.toolsDescription": "Every specimen below renders the actual public component used by its Web UI adapter. Product-owned cards are listed separately and are not represented by substitute previews.",
+ "components.flowChat.ambientTitle": "Ambient tools",
+ "components.flowChat.ambientDescription": "Routine execution traces that remain lightweight and easy to scan.",
+ "components.flowChat.prominentTitle": "Prominent tools",
+ "components.flowChat.prominentDescription": "Results and operations that warrant deliberate user attention.",
+ "components.flowChat.toolCount": "{count} cards",
+ "components.flowChat.attention.adaptive": "Adaptive",
+ "components.flowChat.attention.ambient": "Ambient",
+ "components.flowChat.attention.prominent": "Prominent",
+ "components.flowChat.openComponent": "Open {name} component details",
+ "components.flowChat.dedicatedTitle": "Dedicated cards remain product-owned",
+ "components.flowChat.dedicatedDescription": "Distinct product workflows keep their own presentation and are intentionally excluded from the shared templates.",
"components.inspectAllTokens": "Inspect all tokens",
"components.summaryLabel": "Component library summary",
"components.registeredCount": "registered components",
@@ -216,6 +315,7 @@ export const enUSMessages = {
"components.primitivesTitle": "ThemeRoot and Stack hold the canvas together.",
"components.primitivesDescription": "These primitives remain intentionally small: ThemeRoot establishes scheme, contrast, density, and token overrides; Stack owns only layout direction, alignment, wrapping, and system spacing.",
"detail.back": "Components",
+ "detail.backFlowChat": "FlowChat",
"detail.breadcrumbLabel": "Component breadcrumb",
"detail.livePlayground": "Live playground",
"detail.allStates": "Preview states",
@@ -263,6 +363,7 @@ export const enUSMessages = {
"detail.option.md": "Medium",
"detail.option.lg": "Large",
"detail.option.default": "Default",
+ "detail.option.asking": "Asking",
"detail.option.hover": "Hover",
"detail.option.info": "Information",
"detail.option.warning": "Warning",
@@ -291,12 +392,16 @@ export const enUSMessages = {
"detail.option.focus-visible": "Focus visible",
"detail.option.disabled": "Disabled",
"detail.option.display": "Display",
+ "detail.option.completed": "Completed",
+ "detail.option.expanded": "Expanded",
"detail.option.loading": "Loading",
+ "detail.option.submitting": "Submitting",
"detail.option.none": "None",
"detail.option.chevron": "Chevron",
"detail.option.center": "Center",
"detail.option.left": "Left",
"detail.option.right": "Right",
+ "detail.option.confirmation": "Confirmation",
"detail.option.off": "Off",
"detail.option.on": "On",
"detail.option.selected": "Selected",
@@ -503,6 +608,7 @@ export const zhCNMessages = {
"nav.colors": "Colors",
"nav.library": "组件库",
"nav.components": "组件",
+ "nav.flowChat": "FlowChat",
"nav.docs": "文档",
"nav.resources": "资源",
"search.label": "搜索 Design Lab",
@@ -513,6 +619,7 @@ export const zhCNMessages = {
"search.tokensDetail": "{count} 个可编辑契约",
"search.colorsDetail": "{count} 个语义颜色 Token 与真实基础色阶",
"search.componentsDetail": "{count} 个已注册组件",
+ "search.flowChatDetail": "{count} 个通用模板及标准工具卡片样例",
"search.resourcesDetail": "包文档与发布策略",
"search.componentDetail": "{category}",
"theme.switchToDark": "切换到深色模式",
@@ -536,31 +643,60 @@ export const zhCNMessages = {
"meta.category.action": "操作",
"meta.category.form": "表单",
"meta.category.feedback": "反馈",
+ "meta.category.flowChat": "FlowChat",
"meta.category.navigation": "导航",
"meta.category.primitive": "基础组件",
"meta.category.other": "组件",
"component.ActionCard.description": "可独立配置前置内容、标题、说明、选中状态和同级尾部操作的可交互卡片。",
"component.ActionItem.description": "使用原生按钮语义,并拆分前置、快捷键与同级尾部操作区域的行项目。",
"component.ActivityItem.description": "紧凑的活动行,可独立配置标识、描述、统计信息和同级尾部操作。",
+ "component.AgentControlToolCard.description": "关注型子 Agent 卡片,统一呈现可打开的身份、生命周期状态和可展开提示词。",
+ "component.AgentWaitToolCard.description": "低关注等待轨迹,用于呈现后台 Agent 的等待进度与完成摘要。",
+ "component.AmbientToolCard.description": "用于低关注工具轨迹,在对话中保持轻量易扫读,并可按需展开辅助详情。",
+ "component.AskUser.description": "受控的提问卡片,包含响应式选项、自定义答案、提交反馈与已回答展开摘要。",
"component.Button.description": "紧凑的操作组件,支持描边、填充、主要和文字呈现方式。",
"component.Card.description": "媒体、头部、主体、底部、间距、裁切与层级彼此独立的可组合内容表面。",
+ "component.CommandToolCard.description": "关注型命令视图,统一承载受控输出展开、进程元数据与能力驱动的操作。",
"component.Composer.description": "可组合的消息编辑外壳,提供彼此独立的上下文、编辑和操作区域。",
"component.ConfirmDialog.description": "支持可选预览、危险操作强调和异步操作的语义化确认对话框。",
+ "component.ContextCompressionToolCard.description": "关注型上下文压缩结果,以一条紧凑信息呈现压缩后长度与压缩比例,并保留处理和失败状态。",
+ "component.DefaultToolCard.description": "未注册工具的低关注兜底卡片,统一承载摘要、净化后的输入、结果与确认状态。",
+ "component.DirectoryListToolCard.description": "带结构化元数据和文件结果的低关注目录列表卡片。",
"component.Field.description": "将标签内容、可选操作、说明和相邻控件与一个主要表单控件关联。",
"component.FieldGroup.description": "组合带标题的表单区段、分组字段表面和可独立调整内边距的字段行。",
+ "component.FileDiffToolCard.description": "关注型文件差异卡片,以紧凑文件路径和变更统计承载说明与渲染插槽。",
+ "component.FileOperationToolCard.description": "自适应文件操作视图:删除采用低关注呈现,写入与编辑采用可展开的关注型呈现。",
+ "component.GetToolSpecToolCard.description": "低关注工具规范轨迹,用于将延迟加载的工具说明载入运行时上下文。",
+ "component.GitToolCard.description": "关注型 Git 命令卡片,统一呈现输出、警告、失败与执行元数据。",
+ "component.GlobSearchToolCard.description": "复用结果列表结构的低关注文件模式搜索卡片。",
+ "component.GrepSearchToolCard.description": "带查询元数据和可滚动结果预览的低关注文本搜索卡片。",
"component.IconButton.description": "带无障碍标签的纯图标操作组件,支持安静、填充和主要呈现方式。",
"component.Input.description": "使用原生输入语义,并提供独立前置与后置内容槽的文本字段。",
"component.KeyHint.description": "使用键盘提示语义,并支持可选装饰图标的基础组件。",
"component.Menu.description": "支持分组标题、标题操作、原生项目语义、键盘导航与滚动的菜单表面。",
"component.Modal.description": "具备 Portal、焦点管理、可配置表面几何、语义标题内容和浮动操作栏的对话框。",
"component.NavigationPanel.description": "具备独立头部、分组内容、选中项、滚动主体与底栏槽位的应用导航面板。",
+ "component.PageDeployToolCard.description": "关注型页面部署卡片,统一呈现版本元数据与宿主操作。",
"component.PageHeader.description": "前置内容、标题语义层级、视觉大小、说明、操作区与对齐方式彼此独立的标题组合。",
+ "component.PagePublishToolCard.description": "关注型页面发布卡片,统一呈现生产与预览元数据和操作。",
+ "component.ProminentToolCard.description": "用于需要用户关注的工具结果,左侧摘要始终稳定,右侧操作悬停出现,并支持受控展开。",
+ "component.ReadFileToolCard.description": "低关注读取文件轨迹,统一呈现进度、权限相关交互、完成与失败摘要。",
+ "component.ReviewSummaryToolCard.description": "关注型审查摘要,统一呈现变更文件、状态和宿主导航操作。",
+ "component.RunCodeToolCard.description": "低关注代码执行卡片,通过插槽承载宿主渲染的程序与输出。",
"component.ScrollArea.description": "支持配置滚动方向与滚动条可见性的原生滚动视口。",
"component.SearchField.description": "支持可选前置图标、快捷键提示和回车提交的搜索输入。",
"component.SegmentedControl.description": "紧凑的胶囊式单选控件,用于在少量互斥模式之间切换。",
+ "component.SessionControlToolCard.description": "低关注会话生命周期卡片,包含语义详情字段与会话行。",
+ "component.SessionMessageToolCard.description": "低关注跨会话消息卡片,包含目标详情与消息内容。",
+ "component.SkillToolCard.description": "低关注 Skill 轨迹,用于呈现加载进度和执行结果。",
"component.Switch.description": "通过原生复选框语义在开启与关闭之间切换单项设置。",
"component.TabGroup.description": "紧凑的单选标签组,支持前置图标、尾部操作与方向键自动切换。",
+ "component.TerminalControlToolCard.description": "低关注终端控制轨迹,用于呈现中断或终止终端会话的结果。",
+ "component.TodoToolCard.description": "低关注任务进度卡片,支持紧凑摘要与可展开列表两种呈现。",
"component.Toolbar.description": "具备独立前置、居中、尾部与横向滚动区域的响应式工具栏。",
+ "component.ViewImageToolCard.description": "低关注图片结果卡片,包含可展开预览和无障碍灯箱。",
+ "component.WebFetchToolCard.description": "低关注网页读取卡片,包含来源元数据、结果操作与内容预览。",
+ "component.WebSearchToolCard.description": "低关注网页搜索卡片,统一呈现链接、摘要、网址与结果列表。",
"overview.intro": "用于构建一致、易用且令人愉悦的 BitFun 产品的现代设计系统。",
"overview.getStarted": "开始使用",
"overview.viewTokens": "查看 Design Token",
@@ -622,6 +758,54 @@ export const zhCNMessages = {
"components.preview.activityCopy": "复制命令",
"components.preview.activityDownload": "下载输出",
"components.preview.activityOpen": "打开详情",
+ "components.preview.flowChat.askUserAnswered": "1 个问题已回答",
+ "components.preview.flowChat.askUserBetaDescription": "最新 Beta 预发布版——版本更新,且已通过基础测试",
+ "components.preview.flowChat.askUserError": "无法加载问题",
+ "components.preview.flowChat.askUserHeader": "1 个问题",
+ "components.preview.flowChat.askUserLoading": "正在加载问题…",
+ "components.preview.flowChat.askUserNightly": "Nightly 构建",
+ "components.preview.flowChat.askUserNightlyDescription": "每日自动构建——包含最新代码,但尚未测试",
+ "components.preview.flowChat.askUserOther": "其他",
+ "components.preview.flowChat.askUserOtherDescription": "提供自定义文本输入",
+ "components.preview.flowChat.askUserOtherInputLabel": "自定义版本",
+ "components.preview.flowChat.askUserOtherPlaceholder": "输入版本",
+ "components.preview.flowChat.askUserQuestion": "BitFun 有三个版本——你希望我拉取哪一个?",
+ "components.preview.flowChat.askUserStableDescription": "GitHub 标记为 Latest 的稳定版本——稳定性最佳",
+ "components.preview.flowChat.askUserSubmit": "提交",
+ "components.preview.flowChat.askUserSubmitting": "正在提交…",
+ "components.preview.flowChat.askUserSummaryPrefix": "选择版本",
+ "components.preview.flowChat.askUserWaiting": "等待你的回答",
+ "components.preview.flowChat.changeSummary": "新增 6 行,删除 0 行",
+ "components.preview.flowChat.commandFailed": "命令退出,状态码为 1。",
+ "components.preview.flowChat.commandWaiting": "正在等待进程输出…",
+ "components.preview.flowChat.completed": "已完成",
+ "components.preview.flowChat.contextCompression": "压缩上下文",
+ "components.preview.flowChat.contextDetail": "较早的对话片段已被摘要,并保留近期工具上下文。",
+ "components.preview.flowChat.contextError": "无法完成上下文压缩。",
+ "components.preview.flowChat.contextMeta": "已减少 93k Token",
+ "components.preview.flowChat.contextProcessing": "正在分析对话上下文…",
+ "components.preview.flowChat.contextSummary": "压缩后上下文长度 31k(压缩比例 75%)",
+ "components.preview.flowChat.copy": "复制命令",
+ "components.preview.flowChat.deleteFile": "删除文件",
+ "components.preview.flowChat.download": "下载输出",
+ "components.preview.flowChat.duration": "耗时",
+ "components.preview.flowChat.editFile": "编辑文件",
+ "components.preview.flowChat.emptyCommand": "没有可用命令",
+ "components.preview.flowChat.exitCode": "退出码",
+ "components.preview.flowChat.failed": "失败",
+ "components.preview.flowChat.fileChangeSummary": "新增 12 行,删除 4 行",
+ "components.preview.flowChat.fileErrorMessage": "应用操作前目标文件已发生变化。",
+ "components.preview.flowChat.fileErrorTitle": "文件操作失败",
+ "components.preview.flowChat.fileLoading": "正在应用变更…",
+ "components.preview.flowChat.openFile": "打开文件",
+ "components.preview.flowChat.interruptProcess": "中断进程",
+ "components.preview.flowChat.pollProcess": "轮询进程",
+ "components.preview.flowChat.readFile": "读取文件",
+ "components.preview.flowChat.readFailed": "无法读取 src/flow_chat/tool-cards/index.ts",
+ "components.preview.flowChat.readingFile": "正在读取 src/flow_chat/tool-cards/index.ts…",
+ "components.preview.flowChat.runCommand": "运行命令",
+ "components.preview.flowChat.running": "运行中",
+ "components.preview.flowChat.writeFile": "写入文件",
"components.preview.add": "添加",
"components.preview.more": "更多操作",
"components.preview.appearance": "外观",
@@ -696,6 +880,26 @@ export const zhCNMessages = {
"components.kicker": "组件库",
"components.title": "组件",
"components.description": "真实的生产包导出,并使用当前主题与密度契约渲染。",
+ "components.flowChat.kicker": "FlowChat 组件库",
+ "components.flowChat.title": "FlowChat",
+ "components.flowChat.description": "以用户关注程度组织的通用卡片框架,以及基于这些公共契约构建的标准工具卡片样例。",
+ "components.flowChat.templatesKicker": "通用框架",
+ "components.flowChat.templatesTitle": "通用卡片模板",
+ "components.flowChat.templatesDescription": "Ambient 与 Prominent 定义标准 FlowChat 工具共享的稳定结构、交互、无障碍与详情展开规则。",
+ "components.flowChat.toolsKicker": "工具卡片",
+ "components.flowChat.toolsTitle": "已迁移工具卡片",
+ "components.flowChat.toolsDescription": "下方每个样例均渲染 Web UI 适配器实际使用的公共组件。产品层卡片单独列出,不再使用替代预览伪装为已组件化。",
+ "components.flowChat.ambientTitle": "低关注工具",
+ "components.flowChat.ambientDescription": "保持轻量、易扫读的常规执行轨迹。",
+ "components.flowChat.prominentTitle": "关注型工具",
+ "components.flowChat.prominentDescription": "需要用户明确关注其结果或操作过程的工具。",
+ "components.flowChat.toolCount": "{count} 张卡片",
+ "components.flowChat.attention.adaptive": "自适应关注型",
+ "components.flowChat.attention.ambient": "低关注型",
+ "components.flowChat.attention.prominent": "关注型",
+ "components.flowChat.openComponent": "打开 {name} 组件详情",
+ "components.flowChat.dedicatedTitle": "定制卡片继续由产品层维护",
+ "components.flowChat.dedicatedDescription": "具有独特产品流程的卡片保留自己的呈现方式,不纳入通用模板。",
"components.inspectAllTokens": "检查全部 Token",
"components.summaryLabel": "组件库摘要",
"components.registeredCount": "个已注册组件",
@@ -706,6 +910,7 @@ export const zhCNMessages = {
"components.primitivesTitle": "ThemeRoot 与 Stack 共同支撑整个画布。",
"components.primitivesDescription": "这些原语刻意保持精简:ThemeRoot 建立模式、对比度、密度和 Token 覆盖;Stack 只负责布局方向、对齐、换行与系统间距。",
"detail.back": "组件",
+ "detail.backFlowChat": "FlowChat",
"detail.breadcrumbLabel": "组件面包屑",
"detail.livePlayground": "实时工作台",
"detail.allStates": "状态预览",
@@ -753,6 +958,7 @@ export const zhCNMessages = {
"detail.option.md": "中",
"detail.option.lg": "大",
"detail.option.default": "默认",
+ "detail.option.asking": "提问中",
"detail.option.hover": "悬停",
"detail.option.info": "信息",
"detail.option.warning": "警告",
@@ -781,12 +987,16 @@ export const zhCNMessages = {
"detail.option.focus-visible": "焦点可见",
"detail.option.disabled": "禁用",
"detail.option.display": "展示",
+ "detail.option.completed": "已完成",
+ "detail.option.expanded": "展开",
"detail.option.loading": "加载中",
+ "detail.option.submitting": "提交中",
"detail.option.none": "无",
"detail.option.chevron": "箭头",
"detail.option.center": "居中",
"detail.option.left": "左侧",
"detail.option.right": "右侧",
+ "detail.option.confirmation": "待确认",
"detail.option.off": "关闭",
"detail.option.on": "开启",
"detail.option.selected": "选中",
@@ -991,6 +1201,7 @@ export const zhTWMessages = {
"nav.colors": "Colors",
"nav.library": "元件庫",
"nav.components": "元件",
+ "nav.flowChat": "FlowChat",
"nav.docs": "文件",
"nav.resources": "資源",
"search.label": "搜尋 Design Lab",
@@ -1001,6 +1212,7 @@ export const zhTWMessages = {
"search.tokensDetail": "{count} 個可編輯契約",
"search.colorsDetail": "{count} 個語意顏色 Token 與真實基礎色階",
"search.componentsDetail": "{count} 個已註冊元件",
+ "search.flowChatDetail": "{count} 個通用模板及標準工具卡片範例",
"search.resourcesDetail": "套件文件與發佈策略",
"theme.switchToDark": "切換至深色模式",
"theme.switchToLight": "切換至淺色模式",
@@ -1023,28 +1235,57 @@ export const zhTWMessages = {
"meta.category.action": "操作",
"meta.category.form": "表單",
"meta.category.feedback": "回饋",
+ "meta.category.flowChat": "FlowChat",
"meta.category.navigation": "導覽",
"meta.category.primitive": "基礎元件",
"meta.category.other": "元件",
"component.ActionCard.description": "可獨立設定前置內容、標題、說明、選取狀態和同級尾端操作的可互動卡片。",
"component.ActionItem.description": "使用原生按鈕語意,並拆分前置、快捷鍵與同級尾端操作區域的列項目。",
"component.ActivityItem.description": "緊湊的活動列,可獨立設定標識、描述、統計資訊和同級尾端操作。",
+ "component.AgentControlToolCard.description": "關注型子 Agent 卡片,統一呈現可開啟的身分、生命週期狀態和可展開提示詞。",
+ "component.AgentWaitToolCard.description": "低關注等待軌跡,用於呈現背景 Agent 的等待進度與完成摘要。",
+ "component.AmbientToolCard.description": "用於低關注工具軌跡,在對話中保持輕量易掃讀,並可按需展開輔助詳情。",
+ "component.AskUser.description": "受控的提問卡片,包含響應式選項、自訂答案、提交回饋與已回答展開摘要。",
"component.Button.description": "緊湊的中性操作元件,支援描邊和填滿兩種呈現方式。",
"component.Card.description": "媒體、頂部、主體、底部、間距、裁切與層級彼此獨立的可組合內容表面。",
+ "component.CommandToolCard.description": "關注型命令檢視,統一承載受控輸出展開、程序中繼資料與能力驅動操作。",
"component.Composer.description": "可組合的訊息編輯外殼,提供彼此獨立的上下文、編輯和操作區域。",
"component.ConfirmDialog.description": "支援可選預覽、危險操作強調和非同步操作的語意化確認對話框。",
+ "component.ContextCompressionToolCard.description": "關注型上下文壓縮結果,以一條緊湊資訊呈現壓縮後長度與壓縮比例,並保留處理和失敗狀態。",
+ "component.DefaultToolCard.description": "未註冊工具的低關注預設卡片,統一承載摘要、淨化後的輸入、結果與確認狀態。",
+ "component.DirectoryListToolCard.description": "帶結構化中繼資料和檔案結果的低關注目錄列表卡片。",
"component.Field.description": "將標籤內容、可選操作、說明和相鄰控制項與一個主要表單控制項關聯。",
"component.FieldGroup.description": "組合帶標題的表單區段、分組欄位表面和可獨立調整內距的欄位列。",
+ "component.FileDiffToolCard.description": "關注型檔案差異卡片,以緊湊檔案路徑和變更統計承載說明與呈現插槽。",
+ "component.FileOperationToolCard.description": "自適應檔案操作檢視:刪除採低關注呈現,寫入與編輯採可展開的關注型呈現。",
+ "component.GetToolSpecToolCard.description": "低關注工具規範軌跡,用於將延遲載入的工具說明載入執行階段上下文。",
+ "component.GitToolCard.description": "關注型 Git 命令卡片,統一呈現輸出、警告、失敗與執行中繼資料。",
+ "component.GlobSearchToolCard.description": "重用結果列表結構的低關注檔案模式搜尋卡片。",
+ "component.GrepSearchToolCard.description": "帶查詢中繼資料和可捲動結果預覽的低關注文字搜尋卡片。",
"component.KeyHint.description": "使用鍵盤提示語意,並支援可選裝飾圖示的基礎元件。",
"component.Menu.description": "支援分組標題、標題操作、原生項目語意、鍵盤導覽與捲動的選單表面。",
"component.Modal.description": "具備 Portal、焦點管理、可設定表面幾何、語意標題內容和浮動操作列的對話框。",
"component.NavigationPanel.description": "具備獨立頂部、分組內容、選取項目、捲動主體與底欄插槽的應用導覽面板。",
+ "component.PageDeployToolCard.description": "關注型頁面部署卡片,統一呈現版本中繼資料與宿主操作。",
"component.PageHeader.description": "前置內容、標題語意層級、視覺大小、說明、操作區與對齊方式彼此獨立的標題組合。",
+ "component.PagePublishToolCard.description": "關注型頁面發佈卡片,統一呈現正式環境與預覽中繼資料和操作。",
+ "component.ProminentToolCard.description": "用於需要使用者關注的工具結果,左側摘要始終穩定,右側操作懸停出現,並支援受控展開。",
+ "component.ReadFileToolCard.description": "低關注讀取檔案軌跡,統一呈現進度、權限相關互動、完成與失敗摘要。",
+ "component.ReviewSummaryToolCard.description": "關注型審查摘要,統一呈現變更檔案、狀態和宿主導覽操作。",
+ "component.RunCodeToolCard.description": "低關注程式碼執行卡片,透過插槽承載宿主呈現的程式與輸出。",
"component.ScrollArea.description": "支援設定捲動方向與捲動條可見性的原生捲動視口。",
"component.SegmentedControl.description": "緊湊的膠囊式單選控制項,用於在少量互斥模式之間切換。",
+ "component.SessionControlToolCard.description": "低關注工作階段生命週期卡片,包含語意詳情欄位與工作階段列。",
+ "component.SessionMessageToolCard.description": "低關注跨工作階段訊息卡片,包含目標詳情與訊息內容。",
+ "component.SkillToolCard.description": "低關注 Skill 軌跡,用於呈現載入進度和執行結果。",
"component.Switch.description": "以原生核取方塊語意在開啟與關閉之間切換單一設定。",
"component.TabGroup.description": "緊湊的單選標籤群組,支援前置圖示、尾端操作與方向鍵自動切換。",
+ "component.TerminalControlToolCard.description": "低關注終端控制軌跡,用於呈現中斷或終止終端工作階段的結果。",
+ "component.TodoToolCard.description": "低關注任務進度卡片,支援緊湊摘要與可展開列表兩種呈現。",
"component.Toolbar.description": "具備獨立前置、置中、尾端與橫向捲動區域的響應式工具列。",
+ "component.ViewImageToolCard.description": "低關注圖片結果卡片,包含可展開預覽和無障礙燈箱。",
+ "component.WebFetchToolCard.description": "低關注網頁讀取卡片,包含來源中繼資料、結果操作與內容預覽。",
+ "component.WebSearchToolCard.description": "低關注網頁搜尋卡片,統一呈現連結、摘要、網址與結果列表。",
"overview.intro": "用於建構一致、易用且令人愉悅的 BitFun 產品的現代設計系統。",
"overview.getStarted": "開始使用",
"overview.viewTokens": "檢視 Design Token",
@@ -1106,6 +1347,54 @@ export const zhTWMessages = {
"components.preview.activityCopy": "複製命令",
"components.preview.activityDownload": "下載輸出",
"components.preview.activityOpen": "開啟詳情",
+ "components.preview.flowChat.askUserAnswered": "1 個問題已回答",
+ "components.preview.flowChat.askUserBetaDescription": "最新 Beta 預發佈版——版本更新,且已通過基礎測試",
+ "components.preview.flowChat.askUserError": "無法載入問題",
+ "components.preview.flowChat.askUserHeader": "1 個問題",
+ "components.preview.flowChat.askUserLoading": "正在載入問題…",
+ "components.preview.flowChat.askUserNightly": "Nightly 建置",
+ "components.preview.flowChat.askUserNightlyDescription": "每日自動建置——包含最新程式碼,但尚未測試",
+ "components.preview.flowChat.askUserOther": "其他",
+ "components.preview.flowChat.askUserOtherDescription": "提供自訂文字輸入",
+ "components.preview.flowChat.askUserOtherInputLabel": "自訂版本",
+ "components.preview.flowChat.askUserOtherPlaceholder": "輸入版本",
+ "components.preview.flowChat.askUserQuestion": "BitFun 有三個版本——你希望我拉取哪一個?",
+ "components.preview.flowChat.askUserStableDescription": "GitHub 標記為 Latest 的穩定版本——穩定性最佳",
+ "components.preview.flowChat.askUserSubmit": "提交",
+ "components.preview.flowChat.askUserSubmitting": "正在提交…",
+ "components.preview.flowChat.askUserSummaryPrefix": "選擇版本",
+ "components.preview.flowChat.askUserWaiting": "等待你的回答",
+ "components.preview.flowChat.changeSummary": "新增 6 行,刪除 0 行",
+ "components.preview.flowChat.commandFailed": "命令結束,狀態碼為 1。",
+ "components.preview.flowChat.commandWaiting": "正在等待程序輸出…",
+ "components.preview.flowChat.completed": "已完成",
+ "components.preview.flowChat.contextCompression": "壓縮上下文",
+ "components.preview.flowChat.contextDetail": "較早的對話片段已被摘要,並保留近期工具上下文。",
+ "components.preview.flowChat.contextError": "無法完成上下文壓縮。",
+ "components.preview.flowChat.contextMeta": "已減少 93k Token",
+ "components.preview.flowChat.contextProcessing": "正在分析對話上下文…",
+ "components.preview.flowChat.contextSummary": "壓縮後上下文長度 31k(壓縮比例 75%)",
+ "components.preview.flowChat.copy": "複製命令",
+ "components.preview.flowChat.deleteFile": "刪除檔案",
+ "components.preview.flowChat.download": "下載輸出",
+ "components.preview.flowChat.duration": "耗時",
+ "components.preview.flowChat.editFile": "編輯檔案",
+ "components.preview.flowChat.emptyCommand": "沒有可用命令",
+ "components.preview.flowChat.exitCode": "結束碼",
+ "components.preview.flowChat.failed": "失敗",
+ "components.preview.flowChat.fileChangeSummary": "新增 12 行,刪除 4 行",
+ "components.preview.flowChat.fileErrorMessage": "套用操作前目標檔案已發生變化。",
+ "components.preview.flowChat.fileErrorTitle": "檔案操作失敗",
+ "components.preview.flowChat.fileLoading": "正在套用變更…",
+ "components.preview.flowChat.openFile": "開啟檔案",
+ "components.preview.flowChat.interruptProcess": "中斷進程",
+ "components.preview.flowChat.pollProcess": "輪詢進程",
+ "components.preview.flowChat.readFile": "讀取檔案",
+ "components.preview.flowChat.readFailed": "無法讀取 src/flow_chat/tool-cards/index.ts",
+ "components.preview.flowChat.readingFile": "正在讀取 src/flow_chat/tool-cards/index.ts…",
+ "components.preview.flowChat.runCommand": "執行命令",
+ "components.preview.flowChat.running": "執行中",
+ "components.preview.flowChat.writeFile": "寫入檔案",
"components.preview.add": "新增",
"components.preview.more": "更多操作",
"components.preview.appearance": "外觀",
@@ -1175,6 +1464,26 @@ export const zhTWMessages = {
"components.kicker": "元件庫",
"components.title": "元件",
"components.description": "真實的正式套件匯出,並使用目前主題與密度契約呈現。",
+ "components.flowChat.kicker": "FlowChat 元件庫",
+ "components.flowChat.title": "FlowChat",
+ "components.flowChat.description": "依使用者關注程度組織的通用卡片框架,以及基於這些公開契約建構的標準工具卡片範例。",
+ "components.flowChat.templatesKicker": "通用框架",
+ "components.flowChat.templatesTitle": "通用卡片模板",
+ "components.flowChat.templatesDescription": "Ambient 與 Prominent 定義標準 FlowChat 工具共用的穩定結構、互動、無障礙與詳情展開規則。",
+ "components.flowChat.toolsKicker": "工具卡片",
+ "components.flowChat.toolsTitle": "已遷移工具卡片",
+ "components.flowChat.toolsDescription": "下方每個範例均呈現 Web UI 介接層實際使用的公開元件。產品層卡片單獨列出,不再使用替代預覽偽裝為已元件化。",
+ "components.flowChat.ambientTitle": "低關注工具",
+ "components.flowChat.ambientDescription": "保持輕量、易掃讀的日常執行軌跡。",
+ "components.flowChat.prominentTitle": "關注型工具",
+ "components.flowChat.prominentDescription": "需要使用者明確關注其結果或操作過程的工具。",
+ "components.flowChat.toolCount": "{count} 張卡片",
+ "components.flowChat.attention.adaptive": "自適應關注型",
+ "components.flowChat.attention.ambient": "低關注型",
+ "components.flowChat.attention.prominent": "關注型",
+ "components.flowChat.openComponent": "開啟 {name} 元件詳情",
+ "components.flowChat.dedicatedTitle": "定製卡片繼續由產品層維護",
+ "components.flowChat.dedicatedDescription": "具有獨特產品流程的卡片保留自己的呈現方式,不納入通用模板。",
"components.inspectAllTokens": "檢查全部 Token",
"components.summaryLabel": "元件庫摘要",
"components.registeredCount": "個已註冊元件",
@@ -1185,6 +1494,7 @@ export const zhTWMessages = {
"components.primitivesTitle": "ThemeRoot 與 Stack 共同支撐整個畫布。",
"components.primitivesDescription": "這些原語刻意保持精簡:ThemeRoot 建立模式、對比度、密度和 Token 覆寫;Stack 只負責版面方向、對齊、換行與系統間距。",
"detail.back": "元件",
+ "detail.backFlowChat": "FlowChat",
"detail.breadcrumbLabel": "元件麵包屑",
"detail.livePlayground": "即時工作台",
"detail.allStates": "狀態預覽",
@@ -1227,6 +1537,7 @@ export const zhTWMessages = {
"detail.option.md": "中",
"detail.option.lg": "大",
"detail.option.default": "預設",
+ "detail.option.asking": "提問中",
"detail.option.hover": "懸停",
"detail.option.info": "資訊",
"detail.option.warning": "警告",
@@ -1254,12 +1565,16 @@ export const zhTWMessages = {
"detail.option.focus-visible": "焦點可見",
"detail.option.disabled": "停用",
"detail.option.display": "展示",
+ "detail.option.completed": "已完成",
+ "detail.option.expanded": "展開",
"detail.option.loading": "載入中",
+ "detail.option.submitting": "提交中",
"detail.option.none": "無",
"detail.option.chevron": "箭頭",
"detail.option.center": "置中",
"detail.option.left": "左側",
"detail.option.right": "右側",
+ "detail.option.confirmation": "待確認",
"detail.option.off": "關閉",
"detail.option.on": "開啟",
"detail.option.selected": "已選取",
diff --git a/design-system/apps/design-lab/src/pages/ComponentDetailPage.tsx b/design-system/apps/design-lab/src/pages/ComponentDetailPage.tsx
index 64c51603a9..1c9c88d3e6 100644
--- a/design-system/apps/design-lab/src/pages/ComponentDetailPage.tsx
+++ b/design-system/apps/design-lab/src/pages/ComponentDetailPage.tsx
@@ -91,6 +91,10 @@ import {
getComponentCategoryLabel,
getComponentDescription,
} from "../i18n/componentMetadata";
+import {
+ FlowChatComponentPreview,
+ getFlowChatPreviewDefinition,
+} from "../preview/FlowChatPreviewRegistry";
interface ComponentDetailPageProps {
colorScheme: ColorScheme;
@@ -127,6 +131,7 @@ const iconTones = ["inherit", "primary", "secondary", "muted", "disabled", "info
const optionLabelKeys: Readonly> = {
active: "detail.option.active",
always: "detail.option.always",
+ asking: "detail.option.asking",
auto: "detail.option.auto",
both: "detail.option.both",
chevron: "detail.option.chevron",
@@ -134,6 +139,8 @@ const optionLabelKeys: Readonly> = {
default: "detail.option.default",
disabled: "detail.option.disabled",
display: "detail.option.display",
+ error: "detail.option.error",
+ expanded: "detail.option.expanded",
fill: "detail.option.fill",
"focus-visible": "detail.option.focus-visible",
hover: "detail.option.hover",
@@ -163,18 +170,20 @@ const optionLabelKeys: Readonly> = {
quiet: "detail.option.quiet",
raised: "detail.option.raised",
right: "detail.option.right",
+ confirmation: "detail.option.confirmation",
+ completed: "detail.option.completed",
selected: "detail.option.selected",
sm: "detail.option.sm",
start: "detail.option.start",
surface: "detail.option.surface",
subtle: "detail.option.subtle",
+ submitting: "detail.option.submitting",
success: "detail.option.success",
text: "detail.option.text",
media: "detail.option.media",
unselected: "detail.option.unselected",
vertical: "detail.option.vertical",
warning: "detail.option.warning",
- error: "detail.option.error",
};
function InspectorSelect({
@@ -287,8 +296,14 @@ export function ComponentDetailPage({
const [navigationPanelShowScrollbar, setNavigationPanelShowScrollbar] = useState(true);
const [composerShowContext, setComposerShowContext] = useState(false);
const [composerShowToolbar, setComposerShowToolbar] = useState(true);
+ const flowChatPreview = getFlowChatPreviewDefinition(component.name);
+ const isFlowChatComponent = Boolean(flowChatPreview);
const states = useMemo(() => {
+ if (flowChatPreview) {
+ return component.states;
+ }
+
switch (component.name) {
case "ActionCard":
return ["default", "hover", "active", "focus-visible", "selected", "disabled"] as const;
@@ -333,12 +348,16 @@ export function ComponentDetailPage({
default:
return ["off", "on", "focus-visible", "disabled"] as const;
}
- }, [component.name]);
+ }, [component.name, component.states, flowChatPreview]);
const inspectorStates = component.name === "Button" || component.name === "IconButton"
? buttonInspectorStates
: states;
const codeSample = useMemo(() => {
+ if (flowChatPreview) {
+ return flowChatPreview.codeSample(t);
+ }
+
if (component.name === "ActionCard") {
return `import { ActionCard } from "@bitfun/ui";\nimport { MessageCircle, MoreHorizontal } from "lucide-react";\n\n, label: "${t("components.preview.more")}" },\n ]}\n description="${t("components.preview.actionCardDescription")}"\n leading={}\n size="${actionCardSize}"\n>\n ${t("components.preview.actionCardTitle")}\n`;
}
@@ -475,6 +494,7 @@ export function ComponentDetailPage({
fieldShowControlLeading,
fieldShowControlTrailing,
fieldShowLabelAction,
+ flowChatPreview,
iconButtonVariant,
iconName,
iconSize,
@@ -676,6 +696,16 @@ export function ComponentDetailPage({
previewVariant = variant,
applyInspectorControls = false,
) {
+ if (isFlowChatComponent) {
+ return (
+
+ );
+ }
+
if (component.name === "Icon") {
return ;
}
@@ -1417,7 +1447,9 @@ export function ComponentDetailPage({
return (
@@ -1488,6 +1520,30 @@ export function ComponentDetailPage({
{renderPreview(previewState)}
+ ) : isFlowChatComponent ? (
+
+ {states.map((state) => (
+
+
+ {t(optionLabelKeys[state] ?? "detail.option.default")}
+ {state}
+
+
+ {renderPreview(state)}
+
+
+ ))}
+
) : component.name === "Button" ? (
+ );
+ }
switch (component.name) {
case "ActionCard":
@@ -441,6 +458,7 @@ function ComponentCardPreview({ component }: { component: ComponentMeta }) {
}
export function ComponentsPage({
+ category,
colorScheme,
contrast,
density,
@@ -449,14 +467,31 @@ export function ComponentsPage({
tokenOverrides,
}: ComponentsPageProps) {
const { t } = useI18n();
+ const isFlowChatCategory = category === "flow-chat";
+ const visibleComponents = componentRegistry.filter((component) =>
+ isFlowChatCategory
+ ? component.category === "flow-chat"
+ : component.category !== "flow-chat",
+ );
+ const catalogComponents = isFlowChatCategory
+ ? flowChatPreviewRegistry
+ .filter(({ definition }) => definition.section === "framework")
+ .map(({ component }) => component)
+ : visibleComponents;
return (
-
+
- {componentRegistry.length} {t("components.registeredCount")}
- {componentRegistry.reduce((total, item) => total + item.states.length, 0)} {t("components.statesCount")}
+ {visibleComponents.length} {t("components.registeredCount")}
+ {visibleComponents.reduce((total, item) => total + item.states.length, 0)} {t("components.statesCount")}
{t("components.accessibilityContracts")}
+ {isFlowChatCategory && (
+
+
+ {t("components.flowChat.templatesKicker")}
+
{t("components.flowChat.templatesTitle")}
+
+ {t("components.flowChat.templatesDescription")}
+
+ )}
+
- {componentRegistry.map((component) => {
- const Icon = componentIcons[component.name as keyof typeof componentIcons];
+ {catalogComponents.map((component) => {
+ const Icon = getFlowChatPreviewDefinition(component.name)?.icon
+ ?? componentIcons[component.name as keyof typeof componentIcons];
return (
);
}
diff --git a/design-system/apps/design-lab/src/preview/FlowChatPreviewRegistry.css b/design-system/apps/design-lab/src/preview/FlowChatPreviewRegistry.css
new file mode 100644
index 0000000000..6ad4b12e9b
--- /dev/null
+++ b/design-system/apps/design-lab/src/preview/FlowChatPreviewRegistry.css
@@ -0,0 +1,248 @@
+.flow-chat-tool-card-preview {
+ inline-size: 100%;
+ min-inline-size: 0;
+ text-align: start;
+}
+
+.flow-chat-composer-preview {
+ inline-size: 100%;
+ min-inline-size: 0;
+ text-align: start;
+}
+
+.flow-chat-composer-preview__context {
+ display: flex;
+ min-inline-size: 0;
+ inline-size: 100%;
+ align-items: center;
+ gap: var(--bf-space-3);
+ overflow: hidden;
+ color: var(--bf-color-content-secondary);
+ font-family: var(--bf-font-family-control);
+ font-size: var(--bf-font-size-meta);
+ white-space: nowrap;
+}
+
+.flow-chat-composer-preview__context > span {
+ display: inline-flex;
+ min-inline-size: 0;
+ align-items: center;
+ gap: var(--bf-space-1);
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.flow-chat-composer-preview__context svg {
+ inline-size: var(--bf-font-size-body);
+ block-size: var(--bf-font-size-body);
+ flex: 0 0 auto;
+}
+
+.flow-chat-composer-preview__permission {
+ margin-inline-start: auto;
+ padding: var(--bf-space-1) var(--bf-space-2);
+ border-radius: var(--bf-radius-pill);
+ color: var(--bf-color-status-success-content);
+ background: var(--bf-color-status-success-surface);
+}
+
+.flow-chat-composer-preview textarea {
+ min-inline-size: 0;
+ inline-size: 100%;
+ block-size: 100%;
+ resize: none;
+ margin: 0;
+ padding: 0;
+ border: 0;
+ outline: 0;
+ color: var(--bf-color-content-primary);
+ background: transparent;
+ font: inherit;
+ line-height: var(--bf-line-height-base);
+}
+
+.flow-chat-composer-preview textarea::placeholder {
+ color: var(--bf-color-content-muted);
+ opacity: 1;
+}
+
+.flow-chat-composer-preview [data-bf-layout="compact"] textarea {
+ line-height: var(--bf-control-chat-composer-compact-track-height);
+}
+
+.flow-chat-composer-preview__action,
+.flow-chat-composer-preview__model {
+ display: inline-flex;
+ min-inline-size: 0;
+ block-size: var(--bf-control-chat-composer-control-height);
+ align-items: center;
+ justify-content: center;
+ margin: 0;
+ padding: 0;
+ border: 0;
+ color: var(--bf-color-action-neutral-content);
+ background: transparent;
+ cursor: pointer;
+}
+
+.flow-chat-composer-preview__action {
+ inline-size: var(--bf-control-chat-composer-control-height);
+ aspect-ratio: 1;
+ border-radius: var(--bf-radius-pill);
+}
+
+.flow-chat-composer-preview [data-bf-layout="compact"]
+ :is(.flow-chat-composer-preview__action, .flow-chat-composer-preview__model) {
+ block-size: 100%;
+}
+
+.flow-chat-composer-preview [data-bf-layout="compact"]
+ .flow-chat-composer-preview__action {
+ inline-size: auto;
+}
+
+.flow-chat-composer-preview__action--fill {
+ background: var(--bf-color-action-neutral-surface);
+}
+
+.flow-chat-composer-preview__action--primary {
+ color: var(--bf-color-action-primary-content);
+ background: var(--bf-color-action-primary-background);
+}
+
+.flow-chat-composer-preview__action > svg {
+ inline-size: var(--bf-font-size-body);
+ block-size: var(--bf-font-size-body);
+}
+
+.flow-chat-composer-preview__model {
+ max-inline-size: 180px;
+ gap: var(--bf-space-1);
+ padding-inline: var(--bf-space-2);
+ border-radius: var(--bf-radius-pill);
+ font-family: var(--bf-font-family-control);
+ font-size: var(--bf-font-size-small);
+}
+
+.flow-chat-composer-preview__model > span {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.flow-chat-composer-preview__model > small {
+ color: var(--bf-color-content-muted);
+ font: inherit;
+}
+
+.flow-chat-composer-preview__action:hover:not(:disabled),
+.flow-chat-composer-preview__model:hover:not(:disabled) {
+ background: var(--bf-color-action-neutral-surface-hover);
+}
+
+.flow-chat-composer-preview__action:focus-visible,
+.flow-chat-composer-preview__model:focus-visible {
+ outline: var(--bf-focus-width) solid var(--bf-color-focus-ring);
+ outline-offset: var(--bf-focus-offset);
+}
+
+.flow-chat-tool-card-preview__command {
+ display: block;
+ overflow: hidden;
+ min-inline-size: 0;
+ color: inherit;
+ font-family: var(--bf-font-family-mono);
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.flow-chat-tool-card-preview__action {
+ display: inline-grid;
+ inline-size: var(--bf-space-6);
+ block-size: var(--bf-space-6);
+ place-items: center;
+ margin: 0;
+ padding: 0;
+ border: 0;
+ border-radius: var(--bf-radius-sm);
+ color: var(--bf-color-content-muted);
+ background: transparent;
+ cursor: pointer;
+}
+
+.flow-chat-tool-card-preview__action:hover {
+ color: var(--bf-color-content-primary);
+ background: var(--bf-color-action-neutral-surface);
+}
+
+.flow-chat-tool-card-preview__action:focus-visible {
+ outline: var(--bf-focus-width) solid var(--bf-color-focus-ring);
+ outline-offset: var(--bf-focus-offset);
+}
+
+.flow-chat-tool-card-preview__action > svg {
+ inline-size: var(--bf-font-size-base);
+ block-size: var(--bf-font-size-base);
+}
+
+.flow-chat-tool-card-preview__details {
+ display: grid;
+ gap: var(--bf-space-2);
+ min-inline-size: 0;
+}
+
+.flow-chat-tool-card-preview__details code {
+ overflow: hidden;
+ color: var(--bf-color-content-primary);
+ font-family: var(--bf-font-family-mono);
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.flow-chat-tool-card-preview__details span {
+ color: var(--bf-color-content-muted);
+}
+
+.flow-chat-tool-card-preview__duration {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--bf-space-1);
+ color: var(--bf-color-content-muted);
+ font-family: var(--bf-font-family-mono);
+ font-variant-numeric: tabular-nums;
+}
+
+.flow-chat-tool-card-preview__duration[data-status="completed"] {
+ color: var(--bf-color-status-success-content);
+}
+
+.flow-chat-tool-card-preview__duration > svg {
+ inline-size: var(--bf-font-size-small);
+ block-size: var(--bf-font-size-small);
+}
+
+.flow-chat-tool-card-preview__output,
+.flow-chat-tool-card-preview__diff {
+ display: grid;
+ gap: var(--bf-space-1);
+ margin: 0;
+ color: var(--bf-color-content-secondary);
+ font-family: var(--bf-font-family-mono);
+ font-size: var(--bf-font-size-small);
+ line-height: var(--bf-line-height-body);
+ white-space: pre-wrap;
+}
+
+.flow-chat-tool-card-preview__diff [data-change="added"] {
+ color: var(--bf-color-status-success-content);
+}
+
+.flow-chat-tool-card-preview__diff [data-change="removed"] {
+ color: var(--bf-color-status-danger-content);
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .flow-chat-tool-card-preview__action {
+ transition: none;
+ }
+}
diff --git a/design-system/apps/design-lab/src/preview/FlowChatPreviewRegistry.tsx b/design-system/apps/design-lab/src/preview/FlowChatPreviewRegistry.tsx
new file mode 100644
index 0000000000..fefa250fed
--- /dev/null
+++ b/design-system/apps/design-lab/src/preview/FlowChatPreviewRegistry.tsx
@@ -0,0 +1,1242 @@
+import {
+ Fragment,
+ useState,
+ type ReactNode,
+} from "react";
+import {
+ Archive,
+ ArrowDownToLine,
+ ArrowUp,
+ Bot,
+ CheckCircle2,
+ Circle,
+ Code2,
+ Copy,
+ FileEdit,
+ FileText,
+ FolderOpen,
+ FolderSearch,
+ GitBranch,
+ GitCompare,
+ Globe,
+ Hourglass,
+ Image as ImageIcon,
+ Info,
+ Layers,
+ ListTodo,
+ MessageSquare,
+ Mic,
+ Monitor,
+ Plus,
+ Rocket,
+ Search,
+ SearchCheck,
+ Shield,
+ SquareTerminal,
+ Terminal,
+ Timer,
+ Zap,
+ type LucideIcon,
+} from "lucide-react";
+import {
+ AgentControlToolCard,
+ AgentWaitToolCard,
+ AmbientToolCard,
+ AmbientToolCardHeader,
+ AskUser,
+ ChatComposer,
+ CommandToolCard,
+ ContextCompressionToolCard,
+ DefaultToolCard,
+ DirectoryListToolCard,
+ FileDiffToolCard,
+ FileOperationToolCard,
+ GetToolSpecToolCard,
+ GitToolCard,
+ GlobSearchToolCard,
+ GrepSearchToolCard,
+ PageDeployToolCard,
+ PagePublishToolCard,
+ ProminentToolCard,
+ ProminentToolCardHeader,
+ ReadFileToolCard,
+ ReviewSummaryToolCard,
+ RunCodeToolCard,
+ SessionControlToolCard,
+ SessionMessageToolCard,
+ SkillToolCard,
+ TerminalControlToolCard,
+ TodoToolCard,
+ ToolCardChangeSummary,
+ ToolCardHeaderActions,
+ ViewImageToolCard,
+ WebFetchToolCard,
+ WebSearchToolCard,
+ type FileOperationKind,
+ type FlowChatToolStatus,
+ type AskUserAnswers,
+ type AskUserState,
+} from "@bitfun/ui/flow-chat";
+import { componentRegistry } from "@bitfun/ui/registry";
+import { useI18n } from "../i18n";
+import "./FlowChatPreviewRegistry.css";
+
+type RegisteredComponent = (typeof componentRegistry)[number];
+
+export type FlowChatComponentMeta = Extract<
+ RegisteredComponent,
+ { readonly category: "flow-chat" }
+>;
+export type FlowChatComponentName = FlowChatComponentMeta["name"];
+export type FlowChatPreviewAttention = "adaptive" | "ambient" | "prominent";
+export type FlowChatPreviewSection = "framework" | "tool-card";
+export type FlowChatPreviewState = FlowChatComponentMeta["states"][number];
+
+export interface FlowChatToolSpecimen {
+ tool: string;
+}
+
+interface FlowChatPreviewRenderOptions {
+ interactive: boolean;
+ specimen?: FlowChatToolSpecimen;
+ state: FlowChatPreviewState;
+}
+
+type Translate = ReturnType["t"];
+
+export interface FlowChatPreviewDefinition {
+ attention: FlowChatPreviewAttention;
+ codeSample: (t: Translate) => string;
+ icon: LucideIcon;
+ render: (options: FlowChatPreviewRenderOptions) => ReactNode;
+ section: FlowChatPreviewSection;
+ specimens: readonly FlowChatToolSpecimen[];
+}
+
+type FlowChatPreviewDefinitionMap = {
+ readonly [Name in FlowChatComponentName]: FlowChatPreviewDefinition;
+};
+
+type PreviewProps = FlowChatPreviewRenderOptions;
+
+function ChatComposerPreview({ interactive, state }: PreviewProps) {
+ const [value, setValue] = useState("");
+ const expanded = state === "expanded";
+
+ return (
+
+
+ This computer
+ BitFun
+ 1.0.0-explore
+ worktree
+
+ Ask
+
+
+ )}
+ disabled={state === "disabled"}
+ endActions={(
+ <>
+
+
+
+ >
+ )}
+ layout={expanded ? "expanded" : "compact"}
+ startActions={(
+
+ )}
+ >
+
+ );
+}
+
+function resolveStatus(state: FlowChatPreviewState): FlowChatToolStatus {
+ if (state === "loading") return "running";
+ if (state === "error") return "error";
+ if (state === "confirmation") return "pending_confirmation";
+ return "completed";
+}
+
+function FrameworkPreview({
+ interactive,
+ kind,
+ state,
+}: PreviewProps & { kind: "ambient" | "prominent" }) {
+ const { t } = useI18n();
+ const [isExpanded, setIsExpanded] = useState(state === "expanded");
+ const status = resolveStatus(state);
+ const previewState = state === "hover" ? "hover" : undefined;
+ const toggleExpanded = interactive
+ ? () => setIsExpanded((expanded) => !expanded)
+ : undefined;
+ const expandedContent = (
+
+ HTTP 200 · size=2,486 bytes
+ {t("components.preview.flowChat.completed")}
+
+ );
+
+ if (kind === "ambient") {
+ return (
+
+
}
+ />
+ )}
+ isExpanded={isExpanded}
+ onClick={toggleExpanded}
+ status={status}
+ />
+
+ );
+ }
+
+ const actions = interactive ? (
+
+
+
+
+ ) : undefined;
+
+ return (
+
+
+ curl -s -o /dev/null -w "HTTP %{http_code}" https://openbitfun.com
+
+ )}
+ extra={(
+
+ )}
+ icon={}
+ />
+ )}
+ isExpanded={isExpanded}
+ onClick={toggleExpanded}
+ requiresConfirmation={state === "confirmation"}
+ status={status}
+ />
+
+ );
+}
+
+function ReadFilePreview({ interactive, state }: PreviewProps) {
+ const { t } = useI18n();
+ const status = resolveStatus(state);
+ const summary = state === "loading"
+ ? t("components.preview.flowChat.readingFile")
+ : state === "error"
+ ? t("components.preview.flowChat.readFailed")
+ : "src/flow_chat/tool-cards/index.ts · 128 lines";
+
+ return (
+
+ undefined : undefined}
+ status={status}
+ summary={summary}
+ />
+
+ );
+}
+
+function ContextCompressionPreview({ state }: PreviewProps) {
+ const { t } = useI18n();
+ const status = resolveStatus(state);
+
+ return (
+
+
+
+ );
+}
+
+const COMMAND_SAMPLES = {
+ Bash: {
+ actionKey: "components.preview.flowChat.runCommand",
+ command: "pnpm run design-system:check",
+ output: "✓ packages built\n✓ public contracts verified",
+ },
+ ExecCommand: {
+ actionKey: "components.preview.flowChat.runCommand",
+ command: "pnpm --dir design-system --filter @bitfun/ui test",
+ output: "57 tests passed",
+ },
+ ExecControl: {
+ actionKey: "components.preview.flowChat.interruptProcess",
+ command: "interrupt session 9182",
+ output: "Process interrupted · exit 130",
+ },
+ WriteStdin: {
+ actionKey: "components.preview.flowChat.pollProcess",
+ command: "poll session 9182",
+ output: "Process is still running",
+ },
+} as const;
+
+function CommandPreview({ interactive, specimen, state }: PreviewProps) {
+ const { t } = useI18n();
+ const sample = COMMAND_SAMPLES[
+ specimen?.tool && specimen.tool in COMMAND_SAMPLES
+ ? specimen.tool as keyof typeof COMMAND_SAMPLES
+ : "Bash"
+ ];
+ const [isExpanded, setIsExpanded] = useState(
+ state === "expanded" || state === "loading",
+ );
+ const status = resolveStatus(state);
+ const loading = state === "loading";
+ const completed = status === "completed";
+ const footerItems = completed ? [
+ {
+ label: t("components.preview.flowChat.exitCode"),
+ monospace: true,
+ tone: "success" as const,
+ value: "0",
+ },
+ {
+ label: t("components.preview.flowChat.duration"),
+ value: "1.24s",
+ },
+ ] : [];
+ const statusSummary = completed || loading ? (
+
+ {completed
+ ?
+ : }
+ {completed ? "1.24s" : "1.2s"}
+
+ ) : undefined;
+
+ return (
+
+
undefined,
+ } : undefined}
+ data-bf-preview-state={state === "hover" ? "hover" : undefined}
+ emptyCommand={t("components.preview.flowChat.emptyCommand")}
+ error={state === "error"
+ ? t("components.preview.flowChat.commandFailed")
+ : undefined}
+ footerItems={footerItems}
+ isExpanded={isExpanded}
+ onToggle={interactive ? () => setIsExpanded((expanded) => !expanded) : undefined}
+ output={completed ? (
+ {sample.output}
+ ) : undefined}
+ outputDensity={loading ? "compact" : "expanded"}
+ requiresConfirmation={state === "confirmation"}
+ status={status}
+ statusLabel={state === "error"
+ ? t("components.preview.flowChat.failed")
+ : undefined}
+ statusSummary={statusSummary}
+ statusTone={state === "error" ? "danger" : undefined}
+ waitingContent={loading
+ ? t("components.preview.flowChat.commandWaiting")
+ : undefined}
+ />
+
+ );
+}
+
+function resolveFileOperation(tool?: string): FileOperationKind {
+ if (tool === "Delete") return "delete";
+ if (tool === "Write") return "write";
+ return "edit";
+}
+
+function FileOperationPreview({ interactive, specimen, state }: PreviewProps) {
+ const { t } = useI18n();
+ const operation = resolveFileOperation(specimen?.tool);
+ const [isExpanded, setIsExpanded] = useState(state === "expanded");
+ const status = resolveStatus(state);
+ const path = operation === "write"
+ ? "src/flow_chat/tool-cards/NewCard.tsx"
+ : operation === "delete"
+ ? "dist/stale-preview.js"
+ : "src/flow_chat/components/FlowToolCard.tsx";
+ const actionLabel = operation === "write"
+ ? t("components.preview.flowChat.writeFile")
+ : operation === "delete"
+ ? t("components.preview.flowChat.deleteFile")
+ : t("components.preview.flowChat.editFile");
+ const preview = operation === "delete" ? undefined : (
+
+ - legacyToolCard
+ + {operation === "write" ? "NewCard" : "FlowChatComponentPreview"}
+
+ );
+
+ return (
+
+ undefined,
+ } : undefined}
+ onToggle={interactive ? () => setIsExpanded((expanded) => !expanded) : undefined}
+ operation={operation}
+ path={path}
+ pathLabel={path}
+ preview={preview}
+ requiresConfirmation={state === "confirmation"}
+ status={status}
+ statusDetail={state === "loading"
+ ? t("components.preview.flowChat.fileLoading")
+ : undefined}
+ />
+
+ );
+}
+
+type ActivityPreviewKind = "agent-wait" | "get-tool-spec" | "skill" | "terminal-control";
+
+function ActivityPreview({
+ kind,
+ state,
+}: PreviewProps & { kind: ActivityPreviewKind }) {
+ const { t } = useI18n();
+ const status = resolveStatus(state);
+ const shared = {
+ "data-bf-preview-state": state === "hover" ? "hover" : undefined,
+ status,
+ summary: state === "error"
+ ? t("components.preview.flowChat.failed")
+ : state === "loading"
+ ? t("components.preview.flowChat.running")
+ : t("components.preview.flowChat.completed"),
+ };
+
+ const card = kind === "agent-wait"
+ ?
+ : kind === "get-tool-spec"
+ ?
+ : kind === "skill"
+ ?
+ : ;
+
+ return {card}
;
+}
+
+type SearchPreviewKind = "directory" | "glob" | "grep" | "web";
+
+function SearchPreview({
+ interactive,
+ kind,
+ state,
+}: PreviewProps & { kind: SearchPreviewKind }) {
+ const { t } = useI18n();
+ const [isExpanded, setIsExpanded] = useState(state === "expanded");
+ const status = resolveStatus(state);
+ const Component = kind === "directory"
+ ? DirectoryListToolCard
+ : kind === "glob"
+ ? GlobSearchToolCard
+ : kind === "grep"
+ ? GrepSearchToolCard
+ : WebSearchToolCard;
+ const summary = state === "loading"
+ ? t("components.preview.flowChat.running")
+ : state === "error"
+ ? t("components.preview.flowChat.failed")
+ : kind === "directory"
+ ? "List src/ · 18 entries"
+ : kind === "glob"
+ ? "Find **/*.tsx · 42 files"
+ : kind === "grep"
+ ? "Search ToolCard · 27 matches"
+ : "Search FlowChat component architecture · 3 results";
+
+ return (
+
+ setIsExpanded((value) => !value) : undefined}
+ resultText={kind === "grep" ? "FlowToolCard.tsx:42\nindex.ts:88\nREADME.md:17" : undefined}
+ results={kind === "grep" ? undefined : [
+ {
+ description: kind === "web" ? "Reusable FlowChat card anatomy and migration boundary." : undefined,
+ icon: kind === "web" ? "link" : "file",
+ key: "one",
+ onOpen: interactive && kind === "web" ? () => undefined : undefined,
+ title: kind === "web" ? "FlowChat tool cards" : "src/flow_chat/tool-cards/index.ts",
+ url: kind === "web" ? "https://openbitfun.com/docs/flow-chat" : undefined,
+ },
+ {
+ icon: kind === "directory" ? "directory" : "file",
+ key: "two",
+ title: kind === "directory" ? "components/" : "design-system/packages/ui/src/flow-chat.ts",
+ },
+ ]}
+ status={status}
+ summary={summary}
+ />
+
+ );
+}
+
+function SessionPreview({
+ interactive,
+ kind,
+ state,
+}: PreviewProps & { kind: "control" | "message" }) {
+ const { t } = useI18n();
+ const [isExpanded, setIsExpanded] = useState(state === "expanded");
+ const status = resolveStatus(state);
+ const common = {
+ "data-bf-preview-state": state === "hover" ? "hover" : undefined,
+ action: kind === "control" ? "SessionControl" : "SessionMessage",
+ error: state === "error" ? t("components.preview.flowChat.failed") : undefined,
+ fields: [
+ { label: "Session", monospace: true, value: "review-42" },
+ { label: "Workspace", monospace: true, value: "BitFun" },
+ ],
+ isExpanded,
+ onToggle: interactive ? () => setIsExpanded((value) => !value) : undefined,
+ status,
+ summary: state === "loading"
+ ? t("components.preview.flowChat.running")
+ : t("components.preview.flowChat.completed"),
+ };
+
+ return (
+
+ {kind === "control" ? (
+
+ ) : (
+
+ )}
+
+ );
+}
+
+const PREVIEW_IMAGE = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='320' height='320' viewBox='0 0 320 320'%3E%3Crect width='320' height='320' rx='32' fill='%23232a35'/%3E%3Cpath d='M60 235l64-76 42 45 31-36 63 67z' fill='%237e8ca3'/%3E%3Ccircle cx='224' cy='91' r='25' fill='%23c6cfdb'/%3E%3C/svg%3E";
+
+type StandardAmbientPreviewKind = "default" | "run-code" | "todo" | "view-image" | "web-fetch";
+
+function StandardAmbientPreview({
+ interactive,
+ kind,
+ specimen,
+ state,
+}: PreviewProps & { kind: StandardAmbientPreviewKind }) {
+ const { t } = useI18n();
+ const [isExpanded, setIsExpanded] = useState(state === "expanded");
+ const status = resolveStatus(state);
+ const common = {
+ "data-bf-preview-state": state === "hover" ? "hover" : undefined,
+ isExpanded,
+ onToggle: interactive ? () => setIsExpanded((value) => !value) : undefined,
+ status,
+ };
+
+ let card: ReactNode;
+ if (kind === "run-code") {
+ card = (
+ await verifyDesignSystem();}
+ programLabel="Program"
+ summary="Verify the independent FlowChat package"
+ />
+ );
+ } else if (kind === "web-fetch") {
+ card = (
+ undefined : undefined}
+ openUrlLabel="Open source"
+ title="FlowChat architecture"
+ url="https://openbitfun.com/docs/flow-chat"
+ />
+ );
+ } else if (kind === "default") {
+ const toolName = specimen?.tool ?? "UnregisteredTool";
+ card = (
+
+ );
+ } else if (kind === "view-image") {
+ card = (
+
+ );
+ } else {
+ card = (
+
+ );
+ }
+
+ return {card}
;
+}
+
+type ProminentPreviewKind = "agent" | "diff" | "git" | "page-deploy" | "page-publish" | "review";
+
+function ConcreteProminentPreview({
+ interactive,
+ kind,
+ state,
+}: PreviewProps & { kind: ProminentPreviewKind }) {
+ const { t } = useI18n();
+ const [isExpanded, setIsExpanded] = useState(state === "expanded");
+ const status = resolveStatus(state);
+ const common = {
+ "data-bf-preview-state": state === "hover" ? "hover" : undefined,
+ isExpanded,
+ onToggle: interactive ? () => setIsExpanded((value) => !value) : undefined,
+ status,
+ };
+
+ let card: ReactNode;
+ if (kind === "agent") {
+ card = (
+ Review the FlowChat migration boundary and public contracts.
}
+ statusLabel={state === "loading" ? t("components.preview.flowChat.running") : t("components.preview.flowChat.completed")}
+ statusTone={state === "error" ? "danger" : state === "loading" ? "success" : "neutral"}
+ />
+ );
+ } else if (kind === "diff") {
+ card = (
+
+ );
+ } else if (kind === "git") {
+ card = (
+
+ );
+ } else if (kind === "review") {
+ card = (
+
+ );
+ } else {
+ const Component = kind === "page-deploy" ? PageDeployToolCard : PagePublishToolCard;
+ card = (
+
+ );
+ }
+
+ return {card}
;
+}
+
+function concreteCodeSample(name: string) {
+ return () => `import { ${name} } from "@bitfun/ui/flow-chat";\n\n<${name} status="completed" />`;
+}
+
+function AskUserPreview({ interactive, state }: PreviewProps) {
+ const { t } = useI18n();
+ const completed = state === "completed" || state === "expanded";
+ const startsSelected = completed
+ || state === "selected"
+ || state === "submitting"
+ || state === "disabled";
+ const [answers, setAnswers] = useState(
+ startsSelected ? { version: ["beta"] } : {},
+ );
+ const [customAnswers, setCustomAnswers] = useState>({});
+ const [isExpanded, setIsExpanded] = useState(state === "expanded");
+ const componentState: AskUserState = state === "loading"
+ ? "loading"
+ : state === "error"
+ ? "error"
+ : state === "submitting"
+ ? "submitting"
+ : completed
+ ? "completed"
+ : "asking";
+ const questions = [{
+ customOption: {
+ description: t("components.preview.flowChat.askUserOtherDescription"),
+ inputLabel: t("components.preview.flowChat.askUserOtherInputLabel"),
+ label: t("components.preview.flowChat.askUserOther"),
+ placeholder: t("components.preview.flowChat.askUserOtherPlaceholder"),
+ value: "other",
+ },
+ id: "version",
+ options: [
+ {
+ description: t("components.preview.flowChat.askUserBetaDescription"),
+ label: "v0.2.19-beta.1 (Recommended)",
+ value: "beta",
+ },
+ {
+ description: t("components.preview.flowChat.askUserStableDescription"),
+ label: "v0.2.18",
+ value: "stable",
+ },
+ {
+ description: t("components.preview.flowChat.askUserNightlyDescription"),
+ label: t("components.preview.flowChat.askUserNightly"),
+ value: "nightly",
+ },
+ ],
+ prompt: t("components.preview.flowChat.askUserQuestion"),
+ selectionMode: "single" as const,
+ }];
+ const selectedValue = answers.version?.[0];
+ const selectedLabel = selectedValue === "stable"
+ ? "v0.2.18"
+ : selectedValue === "nightly"
+ ? t("components.preview.flowChat.askUserNightly")
+ : selectedValue === "other"
+ ? customAnswers.version || t("components.preview.flowChat.askUserOther")
+ : "v0.2.19-beta.1 (Recommended)";
+ const statusLabel = componentState === "loading"
+ ? t("components.preview.flowChat.askUserLoading")
+ : componentState === "error"
+ ? t("components.preview.flowChat.askUserError")
+ : componentState === "submitting"
+ ? t("components.preview.flowChat.askUserSubmitting")
+ : t("components.preview.flowChat.askUserWaiting");
+
+ return (
+
+
{
+ setAnswers((current) => ({ ...current, [questionId]: values }));
+ } : undefined}
+ onCustomAnswerChange={interactive ? (questionId, value) => {
+ setCustomAnswers((current) => ({ ...current, [questionId]: value }));
+ } : undefined}
+ onExpandedChange={interactive ? setIsExpanded : undefined}
+ onSubmit={interactive ? () => undefined : undefined}
+ questions={componentState === "error" ? [] : questions}
+ state={componentState}
+ statusLabel={completed ? undefined : statusLabel}
+ submitDisabled={!answers.version?.length}
+ submitLabel={completed || componentState === "loading" || componentState === "error"
+ ? undefined
+ : t("components.preview.flowChat.askUserSubmit")}
+ submittingLabel={t("components.preview.flowChat.askUserSubmitting")}
+ summaryDetail={completed
+ ? `${t("components.preview.flowChat.askUserSummaryPrefix")}: ${selectedLabel}`
+ : undefined}
+ summaryLabel={completed
+ ? t("components.preview.flowChat.askUserAnswered")
+ : undefined}
+ />
+
+ );
+}
+
+export const flowChatPreviewDefinitions = {
+ AgentControlToolCard: {
+ attention: "prominent",
+ codeSample: concreteCodeSample("AgentControlToolCard"),
+ icon: Bot,
+ render: (options) => ,
+ section: "tool-card",
+ specimens: [
+ { tool: "AgentSpawn" },
+ { tool: "AgentSendInput" },
+ ],
+ },
+ AgentWaitToolCard: {
+ attention: "ambient",
+ codeSample: concreteCodeSample("AgentWaitToolCard"),
+ icon: Hourglass,
+ render: (options) => ,
+ section: "tool-card",
+ specimens: [{ tool: "AgentWait" }],
+ },
+ AmbientToolCard: {
+ attention: "ambient",
+ codeSample: (t) => `import { AmbientToolCard, AmbientToolCardHeader } from "@bitfun/ui/flow-chat";\nimport { FileText } from "lucide-react";\n\n}\n action="${t("components.preview.flowChat.readFile")}"\n content="src/flow_chat/tool-cards/index.ts"\n />\n )}\n/>`,
+ icon: FileText,
+ render: (options) => ,
+ section: "framework",
+ specimens: [],
+ },
+ AskUser: {
+ attention: "prominent",
+ codeSample: (t) => `import { AskUser } from "@bitfun/ui/flow-chat";\n\n`,
+ icon: MessageSquare,
+ render: (options) => ,
+ section: "tool-card",
+ specimens: [{ tool: "AskUserQuestion" }],
+ },
+ ChatComposer: {
+ attention: "adaptive",
+ codeSample: () => `import { ChatComposer } from "@bitfun/ui/flow-chat";\n\n}\n layout={multiline ? "expanded" : "compact"}\n startActions={}\n endActions={}\n>\n \n`,
+ icon: MessageSquare,
+ render: (options) => ,
+ section: "framework",
+ specimens: [],
+ },
+ CommandToolCard: {
+ attention: "prominent",
+ codeSample: (t) => `import { CommandToolCard } from "@bitfun/ui/flow-chat";\n\n setIsExpanded((value) => !value)}\n output={}\n status="completed"\n/>`,
+ icon: Terminal,
+ render: (options) => ,
+ section: "tool-card",
+ specimens: [
+ { tool: "Bash" },
+ { tool: "ExecCommand" },
+ { tool: "WriteStdin" },
+ { tool: "ExecControl" },
+ ],
+ },
+ ContextCompressionToolCard: {
+ attention: "prominent",
+ codeSample: (t) => `import { ContextCompressionToolCard } from "@bitfun/ui/flow-chat";\n\n`,
+ icon: Archive,
+ render: (options) => ,
+ section: "tool-card",
+ specimens: [{ tool: "ContextCompression" }],
+ },
+ DefaultToolCard: {
+ attention: "ambient",
+ codeSample: concreteCodeSample("DefaultToolCard"),
+ icon: Info,
+ render: (options) => ,
+ section: "tool-card",
+ specimens: [
+ { tool: "ControlHub" },
+ { tool: "FinalizeMiniApp" },
+ { tool: "PublishMiniApp" },
+ { tool: "PublishAppearance" },
+ { tool: "UnregisteredTool" },
+ ],
+ },
+ DirectoryListToolCard: {
+ attention: "ambient",
+ codeSample: concreteCodeSample("DirectoryListToolCard"),
+ icon: FolderOpen,
+ render: (options) => ,
+ section: "tool-card",
+ specimens: [{ tool: "LS" }],
+ },
+ FileDiffToolCard: {
+ attention: "prominent",
+ codeSample: (t) => `import { FileDiffToolCard } from "@bitfun/ui/flow-chat";\n\n`,
+ icon: GitCompare,
+ render: (options) => ,
+ section: "tool-card",
+ specimens: [{ tool: "GetFileDiff" }],
+ },
+ FileOperationToolCard: {
+ attention: "adaptive",
+ codeSample: (t) => `import { FileOperationToolCard } from "@bitfun/ui/flow-chat";\n\n setIsExpanded((value) => !value)}\n operation="edit"\n path="src/flow_chat/components/FlowToolCard.tsx"\n pathLabel="FlowToolCard.tsx"\n preview={}\n status="completed"\n/>`,
+ icon: FileEdit,
+ render: (options) => ,
+ section: "tool-card",
+ specimens: [
+ { tool: "Write" },
+ { tool: "Edit" },
+ { tool: "Delete" },
+ ],
+ },
+ GetToolSpecToolCard: {
+ attention: "ambient",
+ codeSample: concreteCodeSample("GetToolSpecToolCard"),
+ icon: SearchCheck,
+ render: (options) => ,
+ section: "tool-card",
+ specimens: [{ tool: "GetToolSpec" }],
+ },
+ GitToolCard: {
+ attention: "prominent",
+ codeSample: concreteCodeSample("GitToolCard"),
+ icon: GitBranch,
+ render: (options) => ,
+ section: "tool-card",
+ specimens: [{ tool: "Git" }],
+ },
+ GlobSearchToolCard: {
+ attention: "ambient",
+ codeSample: concreteCodeSample("GlobSearchToolCard"),
+ icon: FolderSearch,
+ render: (options) => ,
+ section: "tool-card",
+ specimens: [{ tool: "Glob" }],
+ },
+ GrepSearchToolCard: {
+ attention: "ambient",
+ codeSample: concreteCodeSample("GrepSearchToolCard"),
+ icon: Search,
+ render: (options) => ,
+ section: "tool-card",
+ specimens: [{ tool: "Grep" }],
+ },
+ PageDeployToolCard: {
+ attention: "prominent",
+ codeSample: concreteCodeSample("PageDeployToolCard"),
+ icon: ArrowDownToLine,
+ render: (options) => ,
+ section: "tool-card",
+ specimens: [{ tool: "PageDeploy" }],
+ },
+ PagePublishToolCard: {
+ attention: "prominent",
+ codeSample: concreteCodeSample("PagePublishToolCard"),
+ icon: Rocket,
+ render: (options) => ,
+ section: "tool-card",
+ specimens: [{ tool: "PagePublish" }],
+ },
+ ProminentToolCard: {
+ attention: "prominent",
+ codeSample: (t) => `import { ProminentToolCard, ProminentToolCardHeader } from "@bitfun/ui/flow-chat";\nimport { SquareTerminal } from "lucide-react";\n\n setIsExpanded((value) => !value)}\n header={(\n }\n action="${t("components.preview.flowChat.runCommand")}"\n content={curl https://openbitfun.com}\n />\n )}\n expandedContent={}\n/>`,
+ icon: SquareTerminal,
+ render: (options) => ,
+ section: "framework",
+ specimens: [],
+ },
+ ReadFileToolCard: {
+ attention: "ambient",
+ codeSample: (t) => `import { ReadFileToolCard } from "@bitfun/ui/flow-chat";\n\n`,
+ icon: FileText,
+ render: (options) => ,
+ section: "tool-card",
+ specimens: [{ tool: "Read" }],
+ },
+ ReviewSummaryToolCard: {
+ attention: "prominent",
+ codeSample: concreteCodeSample("ReviewSummaryToolCard"),
+ icon: SearchCheck,
+ render: (options) => ,
+ section: "tool-card",
+ specimens: [{ tool: "ReviewSessionSummary" }],
+ },
+ RunCodeToolCard: {
+ attention: "ambient",
+ codeSample: concreteCodeSample("RunCodeToolCard"),
+ icon: Code2,
+ render: (options) => ,
+ section: "tool-card",
+ specimens: [{ tool: "RunCode" }],
+ },
+ SessionControlToolCard: {
+ attention: "ambient",
+ codeSample: concreteCodeSample("SessionControlToolCard"),
+ icon: Layers,
+ render: (options) => ,
+ section: "tool-card",
+ specimens: [{ tool: "SessionControl" }],
+ },
+ SessionMessageToolCard: {
+ attention: "ambient",
+ codeSample: concreteCodeSample("SessionMessageToolCard"),
+ icon: MessageSquare,
+ render: (options) => ,
+ section: "tool-card",
+ specimens: [{ tool: "SessionMessage" }],
+ },
+ SkillToolCard: {
+ attention: "ambient",
+ codeSample: concreteCodeSample("SkillToolCard"),
+ icon: Zap,
+ render: (options) => ,
+ section: "tool-card",
+ specimens: [{ tool: "Skill" }],
+ },
+ TerminalControlToolCard: {
+ attention: "ambient",
+ codeSample: concreteCodeSample("TerminalControlToolCard"),
+ icon: SquareTerminal,
+ render: (options) => ,
+ section: "tool-card",
+ specimens: [{ tool: "TerminalControl" }],
+ },
+ TodoToolCard: {
+ attention: "ambient",
+ codeSample: concreteCodeSample("TodoToolCard"),
+ icon: ListTodo,
+ render: (options) => ,
+ section: "tool-card",
+ specimens: [{ tool: "TodoWrite" }],
+ },
+ ViewImageToolCard: {
+ attention: "ambient",
+ codeSample: concreteCodeSample("ViewImageToolCard"),
+ icon: ImageIcon,
+ render: (options) => ,
+ section: "tool-card",
+ specimens: [{ tool: "view_image" }],
+ },
+ WebFetchToolCard: {
+ attention: "ambient",
+ codeSample: concreteCodeSample("WebFetchToolCard"),
+ icon: Globe,
+ render: (options) => ,
+ section: "tool-card",
+ specimens: [{ tool: "WebFetch" }],
+ },
+ WebSearchToolCard: {
+ attention: "ambient",
+ codeSample: concreteCodeSample("WebSearchToolCard"),
+ icon: Search,
+ render: (options) => ,
+ section: "tool-card",
+ specimens: [{ tool: "WebSearch" }],
+ },
+} as const satisfies FlowChatPreviewDefinitionMap;
+
+function isFlowChatComponent(
+ component: RegisteredComponent,
+): component is FlowChatComponentMeta {
+ return component.category === "flow-chat";
+}
+
+export const flowChatPreviewRegistry = componentRegistry
+ .filter(isFlowChatComponent)
+ .map((component) => ({
+ component,
+ definition: flowChatPreviewDefinitions[component.name],
+ }));
+
+export function getFlowChatPreviewDefinition(
+ componentName: string,
+): FlowChatPreviewDefinition | undefined {
+ if (!Object.prototype.hasOwnProperty.call(flowChatPreviewDefinitions, componentName)) {
+ return undefined;
+ }
+
+ return flowChatPreviewDefinitions[componentName as FlowChatComponentName];
+}
+
+export function FlowChatComponentPreview({
+ componentName,
+ interactive = true,
+ specimen,
+ state,
+}: {
+ componentName: string;
+ interactive?: boolean;
+ specimen?: FlowChatToolSpecimen;
+ state?: string;
+}) {
+ const entry = flowChatPreviewRegistry.find(
+ ({ component }) => component.name === componentName,
+ );
+ const definition = getFlowChatPreviewDefinition(componentName);
+
+ if (!entry || !definition) {
+ return null;
+ }
+
+ const resolvedState = (state ?? entry.component.states[0]) as FlowChatPreviewState;
+
+ return (
+
+ {definition.render({
+ interactive,
+ specimen,
+ state: resolvedState,
+ })}
+
+ );
+}
diff --git a/design-system/apps/design-lab/src/preview/FlowChatToolGallery.css b/design-system/apps/design-lab/src/preview/FlowChatToolGallery.css
new file mode 100644
index 0000000000..b174f0b079
--- /dev/null
+++ b/design-system/apps/design-lab/src/preview/FlowChatToolGallery.css
@@ -0,0 +1,208 @@
+.flow-chat-tool-gallery-theme {
+ display: block;
+ color: var(--bf-color-content-primary);
+ background: transparent;
+}
+
+.flow-chat-tool-gallery {
+ display: grid;
+ gap: 34px;
+ margin-top: 42px;
+}
+
+.flow-chat-tool-gallery > .component-library-section-heading {
+ margin-block: 0 -8px;
+ padding-top: 34px;
+ border-top: var(--bf-border-width-default) solid var(--bf-color-border-subtle);
+}
+
+.flow-chat-tool-group {
+ display: grid;
+ gap: 16px;
+}
+
+.flow-chat-tool-group__heading {
+ display: flex;
+ align-items: end;
+ justify-content: space-between;
+ gap: var(--bf-space-5);
+}
+
+.flow-chat-tool-group__heading > div {
+ display: grid;
+ gap: var(--bf-space-1);
+}
+
+.flow-chat-tool-group__attention {
+ color: var(--bf-color-content-muted);
+ font-size: var(--bf-font-size-caption);
+ font-weight: var(--bf-font-weight-medium);
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+}
+
+.flow-chat-tool-group__heading h3,
+.flow-chat-tool-group__heading p {
+ margin: 0;
+}
+
+.flow-chat-tool-group__heading h3 {
+ color: var(--bf-color-content-primary);
+ font-size: var(--bf-font-size-title);
+}
+
+.flow-chat-tool-group__heading p,
+.flow-chat-tool-group__heading > button {
+ color: var(--bf-color-content-muted);
+ font-size: var(--bf-font-size-caption);
+}
+
+.flow-chat-tool-group__heading > button {
+ display: inline-flex;
+ flex: 0 0 auto;
+ align-items: center;
+ gap: var(--bf-space-2);
+ margin: 0;
+ padding: var(--bf-space-2) var(--bf-space-3);
+ border: 0;
+ border-radius: var(--bf-radius-md);
+ background: transparent;
+ cursor: pointer;
+}
+
+.flow-chat-tool-group__heading > button:hover {
+ color: var(--bf-color-content-primary);
+ background: var(--bf-color-action-neutral-surface);
+}
+
+.flow-chat-tool-group__heading > button:focus-visible {
+ outline: var(--bf-focus-width) solid var(--bf-color-focus-ring);
+ outline-offset: var(--bf-focus-offset);
+}
+
+.flow-chat-tool-group__heading > button > svg {
+ inline-size: var(--bf-font-size-base);
+ block-size: var(--bf-font-size-base);
+}
+
+.flow-chat-tool-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 12px;
+}
+
+.flow-chat-tool-specimen {
+ display: grid;
+ align-content: start;
+ gap: var(--bf-space-3);
+ min-width: 0;
+ padding: var(--bf-space-4);
+ border: var(--bf-border-width-default) solid var(--bf-color-border-subtle);
+ border-radius: var(--bf-radius-lg);
+ background: var(--bf-color-surface-panel);
+}
+
+.flow-chat-tool-specimen__heading {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: var(--bf-space-3);
+}
+
+.flow-chat-tool-specimen__heading code {
+ overflow: hidden;
+ min-width: 0;
+ color: var(--bf-color-content-primary);
+ font-family: var(--bf-font-family-mono);
+ font-size: var(--bf-font-size-small);
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.flow-chat-tool-specimen__heading span {
+ flex: 0 0 auto;
+ color: var(--bf-color-content-muted);
+ font-size: var(--bf-font-size-caption);
+}
+
+.flow-chat-dedicated-note {
+ display: grid;
+ grid-template-columns: auto minmax(0, 1fr) auto;
+ align-items: center;
+ gap: var(--bf-space-4);
+ padding: var(--bf-space-4);
+ border: var(--bf-border-width-default) solid var(--bf-color-border-subtle);
+ border-radius: var(--bf-radius-lg);
+ color: var(--bf-color-content-secondary);
+ background: var(--bf-color-surface-subtle);
+}
+
+.flow-chat-dedicated-note > svg {
+ inline-size: var(--bf-control-height-sm);
+ block-size: var(--bf-control-height-sm);
+ padding: var(--bf-space-2);
+ border-radius: var(--bf-radius-md);
+ color: var(--bf-color-content-primary);
+ background: var(--bf-color-surface-panel);
+}
+
+.flow-chat-dedicated-note > div {
+ display: grid;
+ gap: var(--bf-space-1);
+}
+
+.flow-chat-dedicated-note strong,
+.flow-chat-dedicated-note p {
+ margin: 0;
+}
+
+.flow-chat-dedicated-note strong {
+ color: var(--bf-color-content-primary);
+ font-size: var(--bf-font-size-small);
+}
+
+.flow-chat-dedicated-note p,
+.flow-chat-dedicated-note code {
+ font-size: var(--bf-font-size-caption);
+}
+
+.flow-chat-dedicated-note code {
+ max-inline-size: 360px;
+ color: var(--bf-color-content-muted);
+ font-family: var(--bf-font-family-mono);
+ line-height: var(--bf-line-height-body);
+ text-align: end;
+}
+
+@media (max-width: 1080px) {
+ .flow-chat-tool-grid {
+ grid-template-columns: 1fr;
+ }
+}
+
+@media (max-width: 720px) {
+ .component-library-section-heading {
+ grid-template-columns: 1fr;
+ align-items: start;
+ gap: 12px;
+ }
+
+ .flow-chat-tool-group__heading {
+ align-items: start;
+ flex-direction: column;
+ }
+
+ .flow-chat-dedicated-note {
+ grid-template-columns: 1fr;
+ align-items: start;
+ }
+
+ .flow-chat-dedicated-note > svg {
+ display: none;
+ }
+
+ .flow-chat-dedicated-note code {
+ max-inline-size: none;
+ text-align: start;
+ }
+}
diff --git a/design-system/apps/design-lab/src/preview/FlowChatToolGallery.tsx b/design-system/apps/design-lab/src/preview/FlowChatToolGallery.tsx
new file mode 100644
index 0000000000..9d8f1e2947
--- /dev/null
+++ b/design-system/apps/design-lab/src/preview/FlowChatToolGallery.tsx
@@ -0,0 +1,115 @@
+import {
+ ArrowRight,
+ Eye,
+} from "lucide-react";
+import { useI18n } from "../i18n";
+import { getComponentDescription } from "../i18n/componentMetadata";
+import {
+ FlowChatComponentPreview,
+ flowChatPreviewRegistry,
+ type FlowChatToolSpecimen,
+} from "./FlowChatPreviewRegistry";
+import "./FlowChatToolGallery.css";
+
+interface FlowChatToolGalleryProps {
+ onOpenComponent: (name: string) => void;
+}
+
+const productOwnedToolExamples = [
+ "CreatePlan",
+ "Task",
+ "LaunchReviewAgent",
+ "submit_code_review",
+ "MCP",
+ "InitMiniApp",
+ "GenerativeUI",
+ "ComputerUse",
+ "CreateCanvas",
+ "ReadCanvas",
+ "UpdateCanvas",
+ "PatchCanvas",
+] as const;
+
+function ToolSpecimenCard({
+ componentName,
+ specimen,
+}: {
+ componentName: string;
+ specimen: FlowChatToolSpecimen;
+}) {
+ return (
+
+
+ {specimen.tool}
+ {componentName}
+
+
+
+ );
+}
+
+export function FlowChatToolGallery({
+ onOpenComponent,
+}: FlowChatToolGalleryProps) {
+ const { t } = useI18n();
+ const toolCardEntries = flowChatPreviewRegistry.filter(
+ ({ definition }) => definition.section === "tool-card",
+ );
+
+ return (
+
+
+
+ {t("components.flowChat.toolsKicker")}
+
{t("components.flowChat.toolsTitle")}
+
+ {t("components.flowChat.toolsDescription")}
+
+
+ {toolCardEntries.map(({ component, definition }) => (
+
+
+
+ {definition.specimens.map((specimen) => (
+
+ ))}
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/design-system/apps/design-lab/src/styles.css b/design-system/apps/design-lab/src/styles.css
index b6816b90e8..f4cd165144 100644
--- a/design-system/apps/design-lab/src/styles.css
+++ b/design-system/apps/design-lab/src/styles.css
@@ -801,6 +801,34 @@ body,
background: transparent;
}
+.component-library-section-heading {
+ display: grid;
+ grid-template-columns: minmax(240px, 0.75fr) minmax(0, 1.25fr);
+ align-items: end;
+ gap: 48px;
+ margin: 34px 0 18px;
+}
+
+.component-library-section-heading > div {
+ display: grid;
+ gap: 7px;
+}
+
+.component-library-section-heading h2 {
+ margin: 0;
+ color: var(--lab-text);
+ font-size: 22px;
+ letter-spacing: -0.025em;
+}
+
+.component-library-section-heading p {
+ max-width: 720px;
+ margin: 0;
+ color: var(--lab-text-secondary);
+ font-size: var(--bf-font-size-small);
+ line-height: var(--bf-line-height-body);
+}
+
.component-card {
display: grid;
grid-template-rows: auto 160px minmax(0, 1fr) auto;
@@ -3639,6 +3667,53 @@ input.lab-force-focus {
min-inline-size: 240px;
}
+.flow-chat-state-list {
+ display: grid;
+ width: 100%;
+ min-width: 0;
+ padding: 8px 24px 24px;
+}
+
+.flow-chat-state-list__item {
+ display: grid;
+ gap: 16px;
+ min-width: 0;
+ padding: 20px 0 24px;
+ border-bottom: var(--bf-border-width-default) solid var(--bf-color-border-subtle);
+}
+
+.flow-chat-state-list__item:last-child {
+ border-bottom: 0;
+}
+
+.flow-chat-state-list__heading {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ gap: var(--bf-space-4);
+}
+
+.flow-chat-state-list__heading strong {
+ color: var(--bf-color-content-primary);
+ font-size: var(--bf-font-size-small);
+}
+
+.flow-chat-state-list__heading code {
+ color: var(--bf-color-content-muted);
+ font-family: var(--bf-font-family-mono);
+ font-size: var(--bf-font-size-caption);
+}
+
+.flow-chat-state-list__preview {
+ width: min(100%, 920px);
+ min-width: 0;
+}
+
+.flow-chat-state-list__preview[data-component-name="AskUser"] {
+ width: min(100%, 680px);
+ margin-inline: auto;
+}
+
.component-preview-matrix[data-component="switch"] {
grid-template-columns: 88px repeat(4, minmax(112px, 1fr));
grid-template-rows: 50px minmax(250px, 1fr);
diff --git a/design-system/apps/design-lab/vite.config.ts b/design-system/apps/design-lab/vite.config.ts
index 2035f19e6e..9e03e8f75d 100644
--- a/design-system/apps/design-lab/vite.config.ts
+++ b/design-system/apps/design-lab/vite.config.ts
@@ -15,6 +15,10 @@ export default defineConfig(({ command }) => ({
alias:
command === "serve"
? [
+ {
+ find: /^@bitfun\/ui\/flow-chat$/,
+ replacement: path.join(uiSourceDirectory, "flow-chat.ts"),
+ },
{
find: /^@bitfun\/ui\/registry$/,
replacement: path.join(uiSourceDirectory, "registry.ts"),
diff --git a/design-system/apps/design-lab/vite/component-detail-contract.test.mjs b/design-system/apps/design-lab/vite/component-detail-contract.test.mjs
index d83d03809f..b94d625f2c 100644
--- a/design-system/apps/design-lab/vite/component-detail-contract.test.mjs
+++ b/design-system/apps/design-lab/vite/component-detail-contract.test.mjs
@@ -4,6 +4,9 @@ import test from "node:test";
const detailSource = new URL("../src/pages/ComponentDetailPage.tsx", import.meta.url);
const catalogSource = new URL("../src/pages/ComponentsPage.tsx", import.meta.url);
+const appSource = new URL("../src/App.tsx", import.meta.url);
+const flowChatPreviewSource = new URL("../src/preview/FlowChatPreviewRegistry.tsx", import.meta.url);
+const flowChatGallerySource = new URL("../src/preview/FlowChatToolGallery.tsx", import.meta.url);
const stylesSource = new URL("../src/styles.css", import.meta.url);
test("every preview matrix declares its state-column count", async () => {
@@ -34,6 +37,148 @@ test("preview matrices define horizontal columns for every registered state coun
source,
/\.component-preview-matrix\[data-state-count="5"\]\s*\{[^}]*grid-template-columns:\s*96px\s+repeat\(5, minmax\(144px, 1fr\)\)/s,
);
+ assert.match(
+ source,
+ /\.component-preview-matrix\[data-state-count="6"\]\s*\{[^}]*grid-template-columns:\s*96px\s+repeat\(6, minmax\(144px, 1fr\)\)/s,
+ );
+});
+
+test("FlowChat component details are registry-driven and stack every state vertically", async () => {
+ const [detail, catalog, preview, styles] = await Promise.all([
+ readFile(detailSource, "utf8"),
+ readFile(catalogSource, "utf8"),
+ readFile(flowChatPreviewSource, "utf8"),
+ readFile(stylesSource, "utf8"),
+ ]);
+
+ assert.match(detail, /if \(flowChatPreview\) \{\s*return component\.states;/);
+ assert.match(detail, /className="flow-chat-state-list"/);
+ assert.match(detail, /className="flow-chat-state-list__item"/);
+ assert.match(detail, /data-component-name=\{component\.name\}/);
+ assert.doesNotMatch(detail, /component-preview-matrix[^]*data-component="flow-chat-tool-card"/);
+ assert.match(catalog, /definition\.section === "framework"/);
+ assert.match(preview, /flowChatPreviewDefinitions/);
+ assert.match(preview, /AmbientToolCard/);
+ assert.match(preview, /ProminentToolCard/);
+ assert.match(preview, /CommandToolCard/);
+ assert.match(preview, /ContextCompressionToolCard/);
+ assert.match(preview, /FileOperationToolCard/);
+ assert.match(preview, /ReadFileToolCard/);
+ assert.match(preview, /ToolCardHeaderActions/);
+ assert.match(styles, /\.flow-chat-state-list\s*\{[^}]*display:\s*grid/s);
+ assert.match(styles, /\.flow-chat-state-list__item\s*\{[^}]*border-bottom:/s);
+ assert.match(
+ styles,
+ /\.flow-chat-state-list__preview\[data-component-name="AskUser"\]\s*\{[^}]*width:\s*min\(100%,\s*680px\)/s,
+ );
+ assert.doesNotMatch(styles, /\.component-preview-matrix\[data-component="flow-chat-tool-card"\]/);
+});
+
+test("FlowChat gallery renders only the real migrated tool-card components", async () => {
+ const [app, catalog, gallery, preview] = await Promise.all([
+ readFile(appSource, "utf8"),
+ readFile(catalogSource, "utf8"),
+ readFile(flowChatGallerySource, "utf8"),
+ readFile(flowChatPreviewSource, "utf8"),
+ ]);
+ const specimenNames = [...preview.matchAll(/\{ tool: "([^"]+)" \}/g)]
+ .map((match) => match[1]);
+ const productOwnedDeclaration = /const productOwnedToolExamples = \[([^\]]+)\] as const;/.exec(gallery);
+ const productOwnedNames = productOwnedDeclaration
+ ? [...productOwnedDeclaration[1].matchAll(/"([^"]+)"/g)].map((match) => match[1])
+ : [];
+ const migratedToolNames = [
+ "AgentSpawn",
+ "AgentSendInput",
+ "AgentWait",
+ "AskUserQuestion",
+ "Bash",
+ "ExecCommand",
+ "WriteStdin",
+ "ExecControl",
+ "ContextCompression",
+ "ControlHub",
+ "FinalizeMiniApp",
+ "PublishMiniApp",
+ "PublishAppearance",
+ "UnregisteredTool",
+ "LS",
+ "GetFileDiff",
+ "Write",
+ "Edit",
+ "Delete",
+ "GetToolSpec",
+ "Git",
+ "Glob",
+ "Grep",
+ "PageDeploy",
+ "PagePublish",
+ "Read",
+ "ReviewSessionSummary",
+ "RunCode",
+ "SessionControl",
+ "SessionMessage",
+ "Skill",
+ "TerminalControl",
+ "TodoWrite",
+ "view_image",
+ "WebFetch",
+ "WebSearch",
+ ];
+ const productOwnedToolNames = [
+ "CreatePlan",
+ "Task",
+ "LaunchReviewAgent",
+ "submit_code_review",
+ "MCP",
+ "InitMiniApp",
+ "GenerativeUI",
+ "ComputerUse",
+ "CreateCanvas",
+ "ReadCanvas",
+ "UpdateCanvas",
+ "PatchCanvas",
+ ];
+
+ assert.match(app, /const flowChatComponents = componentRegistry\.filter/);
+ assert.match(app, /route === "flow-chat"/);
+ assert.match(app, //);
+ assert.match(gallery, / productOwnedNames.includes(toolName)),
+ false,
+ );
+ assert.match(gallery, /const productOwnedToolExamples/);
+});
+
+test("CommandToolCard previews follow the runtime command-state presentation", async () => {
+ const preview = await readFile(flowChatPreviewSource, "utf8");
+ const commandPreview = /function CommandPreview[\s\S]*?\n}\n\nfunction resolveFileOperation/.exec(preview)?.[0];
+
+ assert.ok(commandPreview);
+ assert.match(
+ commandPreview,
+ /state === "expanded" \|\| state === "loading"/,
+ );
+ assert.match(commandPreview, /outputDensity=\{loading \? "compact" : "expanded"}/);
+ assert.match(
+ commandPreview,
+ /statusLabel=\{state === "error"[\s\S]*?: undefined}/,
+ );
+ assert.match(commandPreview, /action=\{t\(sample\.actionKey\)\}/);
+ assert.match(commandPreview, //);
+ assert.doesNotMatch(
+ commandPreview,
+ /statusLabel=\{[\s\S]*?components\.preview\.flowChat\.completed/,
+ );
+ assert.match(commandPreview, /const footerItems = completed \?/);
});
test("Button preview exposes the public presentation variants", async () => {
diff --git a/design-system/packages/design-tokens/src/system.tokens.json b/design-system/packages/design-tokens/src/system.tokens.json
index 10a37f0bea..ea7a32b1eb 100644
--- a/design-system/packages/design-tokens/src/system.tokens.json
+++ b/design-system/packages/design-tokens/src/system.tokens.json
@@ -26,7 +26,7 @@
},
"control": {
"$type": "fontFamily",
- "$value": "'SF Pro Text', -apple-system, BlinkMacSystemFont, 'Noto Sans SC', 'PingFang SC', 'Hiragino Sans GB', 'Segoe UI', 'Microsoft YaHei UI', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif"
+ "$value": "'SF Pro Text', -apple-system, BlinkMacSystemFont, 'Segoe UI Variable Text', 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Noto Sans SC', 'Microsoft YaHei UI', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif"
},
"mono": {
"$type": "fontFamily",
@@ -36,6 +36,7 @@
"size": {
"$type": "dimension",
"micro": { "$value": "10px" },
+ "meta": { "$value": "11px" },
"xs": { "$value": "12px" },
"sm": { "$value": "13px" },
"base": { "$value": "14px" },
@@ -67,6 +68,34 @@
"relaxed": { "$value": 1.6 }
},
"control": {
+ "askUser": {
+ "$type": "dimension",
+ "bodyGap": { "$value": "{space.3}" },
+ "bodyPadding": { "$value": "{space.4}" },
+ "descriptionLineHeight": { "$value": "20px" },
+ "descriptionMaxWidth": { "$value": "500px" },
+ "headerHeight": { "$value": "30px" },
+ "iconSize": { "$value": "14px" },
+ "optionContentGap": { "$value": "{space.2}" },
+ "optionGap": { "$value": "{space.1}" },
+ "optionPaddingBlock": { "$value": "7px" },
+ "optionPaddingInline": { "$value": "{space.2}" },
+ "questionOptionsGap": { "$value": "{space.3}" },
+ "questionPaddingInline": { "$value": "{space.1}" },
+ "summaryActionSize": { "$value": "22px" },
+ "summaryPaddingBlock": { "$value": "{space.1}" },
+ "summaryPaddingInlineEnd": { "$value": "{space.1}" },
+ "summaryPaddingInlineStart": { "$value": "{space.2}" }
+ },
+ "chatComposer": {
+ "$type": "dimension",
+ "compactGap": { "$value": "9px" },
+ "compactHeight": { "$value": "45px" },
+ "compactPaddingBlock": { "$value": "9px" },
+ "compactPaddingInline": { "$value": "9px" },
+ "compactTrackHeight": { "$value": "25px" },
+ "controlHeight": { "$value": "25px" }
+ },
"icon": {
"$type": "dimension",
"size2xs": { "$value": "8px" },
diff --git a/design-system/packages/design-tokens/tests/contract.test.mjs b/design-system/packages/design-tokens/tests/contract.test.mjs
index 6f23ff4d4e..200a3f29e9 100644
--- a/design-system/packages/design-tokens/tests/contract.test.mjs
+++ b/design-system/packages/design-tokens/tests/contract.test.mjs
@@ -137,6 +137,34 @@ test("ActionCard geometry preserves compact and descriptive entry compositions",
assert.equal(systemDocument.control.actionCard.radius.$value, "{radius.base}");
});
+test("AskUser geometry preserves the answered question reference contract", () => {
+ assert.equal(tokens["control.askUser.bodyGap"], "12px");
+ assert.equal(tokens["control.askUser.bodyPadding"], "16px");
+ assert.equal(tokens["control.askUser.descriptionLineHeight"], "20px");
+ assert.equal(tokens["control.askUser.descriptionMaxWidth"], "500px");
+ assert.equal(tokens["control.askUser.headerHeight"], "30px");
+ assert.equal(tokens["control.askUser.iconSize"], "14px");
+ assert.equal(tokens["control.askUser.optionContentGap"], "8px");
+ assert.equal(tokens["control.askUser.optionGap"], "4px");
+ assert.equal(tokens["control.askUser.optionPaddingBlock"], "7px");
+ assert.equal(tokens["control.askUser.optionPaddingInline"], "8px");
+ assert.equal(tokens["control.askUser.questionOptionsGap"], "12px");
+ assert.equal(tokens["control.askUser.questionPaddingInline"], "4px");
+ assert.equal(tokens["control.askUser.summaryActionSize"], "22px");
+ assert.equal(tokens["control.askUser.summaryPaddingBlock"], "4px");
+ assert.equal(tokens["control.askUser.summaryPaddingInlineEnd"], "4px");
+ assert.equal(tokens["control.askUser.summaryPaddingInlineStart"], "8px");
+});
+
+test("ChatComposer geometry preserves the scaled compact capsule contract", () => {
+ assert.equal(tokens["control.chatComposer.compactGap"], "9px");
+ assert.equal(tokens["control.chatComposer.compactHeight"], "45px");
+ assert.equal(tokens["control.chatComposer.compactPaddingBlock"], "9px");
+ assert.equal(tokens["control.chatComposer.compactPaddingInline"], "9px");
+ assert.equal(tokens["control.chatComposer.compactTrackHeight"], "25px");
+ assert.equal(tokens["control.chatComposer.controlHeight"], "25px");
+});
+
test("split-view content panels preserve the elevated shell curvature contract", async () => {
const systemDocument = await readSource("system.tokens.json");
@@ -329,11 +357,18 @@ test("shared system scales preserve the migrated Web UI foundation contract", ()
"space.16": "64px",
},
);
- assert.equal(tokens["font.family.control"].startsWith("'SF Pro Text'"), true);
- assert.equal(tokens["font.family.control"].includes("'Noto Sans SC'"), true);
+ const controlFontFamily = tokens["font.family.control"];
+ assert.equal(controlFontFamily.startsWith("'SF Pro Text'"), true);
+ assert.equal(controlFontFamily.includes("'Segoe UI Variable Text'"), true);
+ assert.equal(controlFontFamily.includes("'Noto Sans SC'"), true);
+ assert.ok(
+ controlFontFamily.indexOf("'Segoe UI Variable Text'")
+ < controlFontFamily.indexOf("'Noto Sans SC'"),
+ );
assert.equal(tokens["font.family.sans"].startsWith("'Noto Sans SC'"), true);
assert.equal(tokens["font.family.mono"].startsWith("'JetBrains Mono'"), true);
assert.equal(tokens["font.size.micro"], "10px");
+ assert.equal(tokens["font.size.meta"], "11px");
assert.equal(tokens["font.size.xs"], "12px");
assert.equal(tokens["font.size.small"], "13px");
assert.equal(tokens["font.size.4xl"], "26px");
diff --git a/design-system/packages/ui/README.md b/design-system/packages/ui/README.md
index 6a875c1ee8..b653c88110 100644
--- a/design-system/packages/ui/README.md
+++ b/design-system/packages/ui/README.md
@@ -17,3 +17,64 @@ export function Example() {
```
The package owns component anatomy, behavior, accessibility, and stable variants. It does not own theme selection persistence, product state, routes, locale resources, or platform APIs.
+
+## FlowChat tool cards
+
+FlowChat frameworks use an attention model rather than a size or border model:
+
+- `AmbientToolCard` keeps routine tool traces lightweight and glanceable.
+- `ProminentToolCard` gives attention-worthy results a framed summary, a stable
+ left content region, hover/focus-revealed right actions, and controlled detail
+ disclosure.
+
+Import these components from the dedicated product-surface entry:
+
+```tsx
+import {
+ AmbientToolCard,
+ AmbientToolCardHeader,
+ AskUser,
+ ChatComposer,
+ ChatComposerContent,
+ ChatComposerEndActions,
+ ChatComposerStartActions,
+ CommandToolCard,
+ ContextCompressionToolCard,
+ FileOperationToolCard,
+ ProminentToolCard,
+ ProminentToolCardHeader,
+ ReadFileToolCard,
+ ToolCardChangeSummary,
+} from "@bitfun/ui/flow-chat";
+```
+
+`ChatComposer` owns the reusable 32px context band and the compact/expanded
+40px/120px input-surface anatomy. Product consumers keep their editor, menus,
+model selection, voice input, sending, stores, and localized copy, and supply
+them through `contextBar`, `startActions`, `endActions`, or the equivalent
+compound slot components. The compound form is useful when a complex consumer
+needs to keep those sections adjacent in source while the package still owns
+the final DOM layout.
+
+`AskUser` is the controlled question-and-answer interaction for FlowChat. It
+owns native single- and multi-selection semantics, responsive option anatomy,
+custom text input, submission feedback, and the answered disclosure summary.
+Consumers keep question parsing, localized copy, draft persistence, and answer
+submission outside the package and provide them through typed props.
+
+Prominent headers keep information roles stable: `action` is the static primary
+label, `content` is the secondary flexible subject, `extra` is right-aligned
+dynamic metadata, and `actions` contains controls revealed on hover or keyboard
+focus. Use `ToolCardChangeSummary` for added/removed counts; domain icons and
+interaction affordances belong in `actions`, not in the summary.
+
+Concrete tool-card views compose those frameworks without importing product
+state. The published families cover file and command execution, search and web
+results, agent and session activity, Git and review summaries, page lifecycle,
+code execution, todos, images, and other routine tool traces. Each view owns its
+card anatomy, status presentation, disclosure behavior, and action placement.
+
+Tool-specific data shaping, localization, host actions, stores, and heavy
+renderers remain in the consuming product and enter through semantic props,
+callbacks, and slots. Bespoke product workflows remain product-owned rather
+than being forced into a standard package view.
diff --git a/design-system/packages/ui/package.json b/design-system/packages/ui/package.json
index 173dbb02be..da57bdf194 100644
--- a/design-system/packages/ui/package.json
+++ b/design-system/packages/ui/package.json
@@ -17,6 +17,10 @@
"types": "./dist/types/registry.d.ts",
"import": "./dist/registry.js"
},
+ "./flow-chat": {
+ "types": "./dist/types/flow-chat.d.ts",
+ "import": "./dist/flow-chat.js"
+ },
"./styles.css": "./dist/styles.css",
"./package.json": "./package.json"
},
diff --git a/design-system/packages/ui/src/flow-chat.ts b/design-system/packages/ui/src/flow-chat.ts
new file mode 100644
index 0000000000..fd818b9301
--- /dev/null
+++ b/design-system/packages/ui/src/flow-chat.ts
@@ -0,0 +1,3 @@
+import "./styles/layers.css";
+
+export * from "./flow-chat/index";
diff --git a/design-system/packages/ui/src/flow-chat/ask-user/AskUser.meta.ts b/design-system/packages/ui/src/flow-chat/ask-user/AskUser.meta.ts
new file mode 100644
index 0000000000..29b04fa062
--- /dev/null
+++ b/design-system/packages/ui/src/flow-chat/ask-user/AskUser.meta.ts
@@ -0,0 +1,77 @@
+import type { ComponentMeta } from "../../registry.types";
+
+export const askUserMeta = {
+ category: "flow-chat",
+ description: "A controlled, accessible question card with responsive options, custom answers, submission feedback, and an answered disclosure summary.",
+ maturity: "stable",
+ name: "AskUser",
+ props: [
+ { name: "questions", type: "readonly AskUserQuestion[]" },
+ { defaultValue: "{}", name: "answers", type: "AskUserAnswers" },
+ { defaultValue: "asking", name: "state", type: "AskUserState" },
+ { name: "summaryLabel", type: "ReactNode" },
+ { name: "summaryDetail", type: "ReactNode" },
+ { defaultValue: "false", name: "expanded", type: "boolean" },
+ { name: "onAnswersChange", type: "(questionId, values) => void" },
+ { name: "onSubmit", type: "() => void" },
+ ],
+ states: [
+ "asking",
+ "selected",
+ "submitting",
+ "completed",
+ "expanded",
+ "loading",
+ "error",
+ "disabled",
+ ],
+ tokens: [
+ "color.action.neutral.content",
+ "color.action.neutral.surfaceHover",
+ "color.border.default",
+ "color.border.subtle",
+ "color.content.disabled",
+ "color.content.inverse",
+ "color.content.muted",
+ "color.content.primary",
+ "color.content.secondary",
+ "color.focus.ring",
+ "color.status.danger.border",
+ "color.status.danger.content",
+ "color.status.danger.surface",
+ "color.status.success.content",
+ "color.status.warning.border",
+ "color.status.warning.content",
+ "color.status.warning.surface",
+ "color.surface.panel",
+ "color.surface.subtle",
+ "control.askUser.bodyGap",
+ "control.askUser.bodyPadding",
+ "control.askUser.descriptionLineHeight",
+ "control.askUser.descriptionMaxWidth",
+ "control.askUser.headerHeight",
+ "control.askUser.iconSize",
+ "control.askUser.optionContentGap",
+ "control.askUser.optionGap",
+ "control.askUser.optionPaddingBlock",
+ "control.askUser.optionPaddingInline",
+ "control.askUser.questionOptionsGap",
+ "control.askUser.questionPaddingInline",
+ "control.askUser.summaryActionSize",
+ "control.askUser.summaryPaddingBlock",
+ "control.askUser.summaryPaddingInlineEnd",
+ "control.askUser.summaryPaddingInlineStart",
+ "control.hitTarget",
+ "font.family.control",
+ "font.size.small",
+ "font.size.xs",
+ "font.weight.medium",
+ "font.weight.regular",
+ "font.weight.semibold",
+ "lineHeight.base",
+ "lineHeight.tight",
+ "radius.base",
+ "radius.lg",
+ "radius.xs",
+ ],
+} as const satisfies ComponentMeta;
diff --git a/design-system/packages/ui/src/flow-chat/ask-user/AskUser.module.css b/design-system/packages/ui/src/flow-chat/ask-user/AskUser.module.css
new file mode 100644
index 0000000000..e9c5439e39
--- /dev/null
+++ b/design-system/packages/ui/src/flow-chat/ask-user/AskUser.module.css
@@ -0,0 +1,454 @@
+@layer bf.components {
+ .root {
+ inline-size: 100%;
+ min-inline-size: 0;
+ overflow: hidden;
+ border: 0;
+ border-radius: var(--bf-radius-lg);
+ outline: var(--bf-border-width-default) solid var(--bf-color-border-subtle);
+ outline-offset: calc(-1 * var(--bf-border-width-default));
+ background: var(--bf-color-surface-panel);
+ box-shadow: none;
+ color: var(--bf-color-content-primary);
+ font-family: var(--bf-font-family-control);
+ line-height: var(--bf-line-height-tight);
+ }
+
+ .header,
+ .summaryButton {
+ min-block-size: var(--bf-control-ask-user-header-height);
+ border: 0;
+ color: var(--bf-color-action-neutral-content);
+ background: transparent;
+ font: inherit;
+ }
+
+ .header {
+ display: flex;
+ align-items: center;
+ padding-block: var(--bf-control-ask-user-summary-padding-block);
+ padding-inline: var(--bf-control-ask-user-summary-padding-inline-start);
+ border-block-end: var(--bf-border-width-default) solid var(--bf-color-border-subtle);
+ font-size: var(--bf-font-size-small);
+ font-weight: var(--bf-font-weight-medium);
+ }
+
+ .summaryButton {
+ display: flex;
+ inline-size: 100%;
+ align-items: center;
+ justify-content: space-between;
+ gap: var(--bf-control-ask-user-option-content-gap);
+ padding-block: var(--bf-control-ask-user-summary-padding-block);
+ padding-inline-start: var(--bf-control-ask-user-summary-padding-inline-start);
+ padding-inline-end: var(--bf-control-ask-user-summary-padding-inline-end);
+ text-align: start;
+ cursor: pointer;
+ }
+
+ .root[data-bf-expanded="true"] .summaryButton {
+ border-block-end: var(--bf-border-width-default) solid var(--bf-color-border-subtle);
+ }
+
+ .summaryButton:focus-visible {
+ outline: var(--bf-focus-width) solid var(--bf-color-focus-ring);
+ outline-offset: calc(-1 * var(--bf-focus-width));
+ }
+
+ .summaryLeading,
+ .summaryCopy {
+ display: flex;
+ min-inline-size: 0;
+ align-items: center;
+ }
+
+ .summaryLeading {
+ flex: 1 1 auto;
+ gap: var(--bf-control-ask-user-option-content-gap);
+ }
+
+ .summaryCopy {
+ flex: 1 1 auto;
+ gap: var(--bf-space-1);
+ font-size: var(--bf-font-size-small);
+ font-weight: var(--bf-font-weight-medium);
+ line-height: var(--bf-line-height-tight);
+ }
+
+ .summaryIcon,
+ .controlIcon,
+ .statusIcon {
+ display: inline-flex;
+ flex: 0 0 auto;
+ inline-size: var(--bf-control-ask-user-icon-size);
+ block-size: var(--bf-control-ask-user-icon-size);
+ align-items: center;
+ justify-content: center;
+ }
+
+ .summaryIcon {
+ color: var(--bf-color-status-success-content);
+ }
+
+ .summaryIcon > svg {
+ fill: currentColor;
+ stroke: var(--bf-color-content-inverse);
+ }
+
+ .summaryIcon > svg,
+ .controlIcon > svg,
+ .statusIcon > svg,
+ .summaryAction > svg {
+ display: block;
+ inline-size: 100%;
+ block-size: 100%;
+ }
+
+ .selectedSingleControl > circle:nth-of-type(2) {
+ fill: currentColor;
+ }
+
+ .summaryLabel,
+ .summaryArrow {
+ flex: 0 0 auto;
+ }
+
+ .summaryDetail {
+ overflow: hidden;
+ flex: 1 1 auto;
+ min-inline-size: 0;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .summaryAction {
+ display: inline-flex;
+ flex: 0 0 auto;
+ inline-size: var(--bf-control-ask-user-summary-action-size);
+ block-size: var(--bf-control-ask-user-summary-action-size);
+ align-items: center;
+ justify-content: center;
+ padding: var(--bf-space-1);
+ border-radius: var(--bf-radius-xs);
+ color: var(--bf-color-content-muted);
+ }
+
+ .details {
+ display: grid;
+ grid-template-rows: 1fr;
+ transition: grid-template-rows var(--bf-motion-duration-base) var(--bf-motion-easing-standard);
+ }
+
+ .root[data-bf-has-summary="true"][data-bf-expanded="false"] .details {
+ grid-template-rows: 0fr;
+ }
+
+ .detailsInner {
+ min-block-size: 0;
+ overflow: hidden;
+ }
+
+ .body {
+ display: flex;
+ flex-direction: column;
+ gap: var(--bf-control-ask-user-body-gap);
+ padding: var(--bf-control-ask-user-body-padding);
+ }
+
+ .question {
+ display: flex;
+ min-inline-size: 0;
+ flex-direction: column;
+ gap: 0;
+ margin: 0;
+ padding: 0;
+ border: 0;
+ }
+
+ .question + .question {
+ padding-block-start: var(--bf-control-ask-user-body-gap);
+ border-block-start: var(--bf-border-width-default) solid var(--bf-color-border-subtle);
+ }
+
+ .prompt {
+ display: block;
+ inline-size: 100%;
+ padding-inline: var(--bf-control-ask-user-question-padding-inline);
+ color: var(--bf-color-action-neutral-content);
+ font-size: var(--bf-font-size-small);
+ font-weight: var(--bf-font-weight-semibold);
+ line-height: var(--bf-line-height-tight);
+ }
+
+ .options {
+ display: flex;
+ min-inline-size: 0;
+ flex-direction: column;
+ gap: var(--bf-control-ask-user-option-gap);
+ margin-block-start: var(--bf-control-ask-user-question-options-gap);
+ }
+
+ .option {
+ display: flex;
+ min-inline-size: 0;
+ align-items: center;
+ gap: var(--bf-control-ask-user-option-content-gap);
+ padding-block: var(--bf-control-ask-user-option-padding-block);
+ padding-inline: var(--bf-control-ask-user-option-padding-inline);
+ border-radius: var(--bf-radius-base);
+ background: var(--bf-color-surface-subtle);
+ transition:
+ background-color var(--bf-motion-duration-fast) var(--bf-motion-easing-standard),
+ box-shadow var(--bf-motion-duration-fast) var(--bf-motion-easing-standard);
+ }
+
+ .option:hover:not(:has(.nativeControl:disabled)),
+ .option:has(.nativeControl[data-bf-preview-state="hover"]) {
+ background: var(--bf-color-action-neutral-surface-hover);
+ }
+
+ .option:has(.nativeControl:focus-visible) {
+ box-shadow: 0 0 0 var(--bf-focus-width) var(--bf-color-focus-ring);
+ }
+
+ .nativeControl {
+ position: absolute;
+ inline-size: 1px;
+ block-size: 1px;
+ overflow: hidden;
+ opacity: 0;
+ pointer-events: none;
+ }
+
+ .optionSelector {
+ display: flex;
+ flex: 1 1 auto;
+ min-inline-size: 0;
+ align-items: center;
+ gap: var(--bf-control-ask-user-option-content-gap);
+ cursor: pointer;
+ }
+
+ .nativeControl:disabled + .optionSelector {
+ cursor: default;
+ }
+
+ .controlIcon {
+ color: var(--bf-color-border-default);
+ }
+
+ .option[data-selected="true"] .controlIcon {
+ color: var(--bf-color-action-neutral-content);
+ }
+
+ .optionContent {
+ display: flex;
+ flex: 1 1 auto;
+ min-inline-size: 0;
+ align-items: center;
+ justify-content: space-between;
+ gap: var(--bf-space-4);
+ }
+
+ .optionLabel,
+ .optionDescription {
+ font-size: var(--bf-font-size-small);
+ }
+
+ .optionLabel {
+ flex: 0 1 auto;
+ min-inline-size: 0;
+ color: var(--bf-color-action-neutral-content);
+ font-weight: var(--bf-font-weight-medium);
+ line-height: var(--bf-line-height-tight);
+ }
+
+ .optionDescription {
+ overflow: hidden;
+ flex: 1 1 auto;
+ max-inline-size: var(--bf-control-ask-user-description-max-width);
+ min-inline-size: 0;
+ color: var(--bf-color-content-muted);
+ font-weight: var(--bf-font-weight-regular);
+ line-height: var(--bf-control-ask-user-description-line-height);
+ text-align: end;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .customInput {
+ flex: 1 1 var(--bf-control-ask-user-description-max-width);
+ max-inline-size: var(--bf-control-ask-user-description-max-width);
+ min-inline-size: 180px;
+ }
+
+ .customInput > :global([data-bf-component="input"]) {
+ inline-size: 100%;
+ }
+
+ .root[data-disabled="true"] .option {
+ color: var(--bf-color-content-disabled);
+ }
+
+ .root[data-disabled="true"] .optionLabel,
+ .root[data-disabled="true"] .optionDescription,
+ .root[data-disabled="true"] .controlIcon {
+ color: var(--bf-color-content-disabled);
+ }
+
+ .footer {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: var(--bf-space-2);
+ padding-block: var(--bf-space-2);
+ padding-inline: var(--bf-space-3);
+ border-block-start: var(--bf-border-width-default) solid var(--bf-color-border-subtle);
+ }
+
+ .submit,
+ .status,
+ .statusOnly,
+ .feedback {
+ display: flex;
+ align-items: center;
+ }
+
+ .status {
+ gap: var(--bf-space-1);
+ padding-inline-start: var(--bf-space-2);
+ border-inline-start: var(--bf-border-width-default) solid var(--bf-color-border-subtle);
+ color: var(--bf-color-content-muted);
+ }
+
+ .statusText {
+ font-size: var(--bf-font-size-xs);
+ font-weight: var(--bf-font-weight-medium);
+ line-height: var(--bf-line-height-base);
+ }
+
+ .root[data-bf-state="asking"] .statusIcon,
+ .root[data-bf-state="timeout"] .statusIcon {
+ color: var(--bf-color-status-warning-content);
+ }
+
+ .root[data-bf-state="submitted"] .statusIcon,
+ .root[data-bf-state="completed"] .statusIcon {
+ color: var(--bf-color-status-success-content);
+ }
+
+ .root[data-bf-state="error"] .statusIcon {
+ color: var(--bf-color-status-danger-content);
+ }
+
+ .root[data-bf-state="loading"] .statusIcon,
+ .root[data-bf-state="submitting"] .statusIcon {
+ color: var(--bf-color-content-secondary);
+ }
+
+ .root[data-bf-state="loading"] .statusIcon > svg,
+ .root[data-bf-state="submitting"] .statusIcon > svg {
+ animation: bf-ask-user-spin var(--bf-motion-duration-loop) linear infinite;
+ }
+
+ .statusOnly {
+ min-block-size: var(--bf-control-ask-user-header-height);
+ gap: var(--bf-space-2);
+ padding-block: var(--bf-space-3);
+ padding-inline: var(--bf-space-4);
+ }
+
+ .feedback {
+ gap: var(--bf-space-2);
+ margin-block-end: var(--bf-control-ask-user-body-padding);
+ margin-inline: var(--bf-control-ask-user-body-padding);
+ padding-block: var(--bf-space-2);
+ padding-inline: var(--bf-space-3);
+ border: var(--bf-border-width-default) solid var(--bf-color-status-warning-border);
+ border-radius: var(--bf-radius-base);
+ color: var(--bf-color-status-warning-content);
+ background: var(--bf-color-status-warning-surface);
+ }
+
+ .root[data-bf-state="error"] .feedback {
+ border-color: var(--bf-color-status-danger-border);
+ color: var(--bf-color-status-danger-content);
+ background: var(--bf-color-status-danger-surface);
+ }
+
+ :global([data-density="touch"]) .summaryButton,
+ :global([data-density="touch"]) .option {
+ min-block-size: var(--bf-control-hit-target);
+ }
+
+ @media (max-width: 640px) {
+ .option,
+ .optionSelector {
+ align-items: flex-start;
+ }
+
+ .option[data-custom="true"][data-selected="true"] {
+ flex-wrap: wrap;
+ }
+
+ .optionContent {
+ display: block;
+ }
+
+ .optionDescription {
+ display: block;
+ max-inline-size: none;
+ margin-block-start: var(--bf-space-1);
+ text-align: start;
+ white-space: normal;
+ }
+
+ .customInput {
+ flex-basis: 100%;
+ max-inline-size: none;
+ margin-inline-start: calc(var(--bf-control-ask-user-icon-size) + var(--bf-control-ask-user-option-content-gap));
+ }
+ }
+
+ @media (prefers-reduced-motion: reduce) {
+ .details,
+ .option {
+ transition: none;
+ }
+
+ .root[data-bf-state="loading"] .statusIcon > svg,
+ .root[data-bf-state="submitting"] .statusIcon > svg {
+ animation: none;
+ }
+ }
+
+ @media (forced-colors: active) {
+ .root,
+ .option,
+ .feedback {
+ border: var(--bf-border-width-default) solid CanvasText;
+ outline: 0;
+ box-shadow: none;
+ }
+
+ .option {
+ background: Canvas;
+ }
+
+ .option:has(.nativeControl:focus-visible),
+ .summaryButton:focus-visible {
+ outline-color: Highlight;
+ }
+
+ .summaryIcon,
+ .option[data-selected="true"] .controlIcon {
+ color: Highlight;
+ }
+ }
+}
+
+@keyframes bf-ask-user-spin {
+ to {
+ transform: rotate(1turn);
+ }
+}
diff --git a/design-system/packages/ui/src/flow-chat/ask-user/AskUser.tsx b/design-system/packages/ui/src/flow-chat/ask-user/AskUser.tsx
new file mode 100644
index 0000000000..58685306c0
--- /dev/null
+++ b/design-system/packages/ui/src/flow-chat/ask-user/AskUser.tsx
@@ -0,0 +1,477 @@
+import {
+ forwardRef,
+ useId,
+ useRef,
+ useState,
+ type HTMLAttributes,
+ type ReactNode,
+} from "react";
+import {
+ ArrowUp,
+ ChevronDown,
+ ChevronUp,
+ Circle,
+ CircleAlert,
+ CircleCheck,
+ Disc2,
+ LoaderCircle,
+ Square,
+ SquareCheckBig,
+} from "lucide-react";
+import { Button } from "../../components/Button";
+import { Input } from "../../components/Input";
+import { classNames } from "../../internal/classNames";
+import styles from "./AskUser.module.css";
+
+export type AskUserState =
+ | "asking"
+ | "submitting"
+ | "submitted"
+ | "completed"
+ | "loading"
+ | "error"
+ | "timeout";
+
+export interface AskUserOption {
+ description?: ReactNode;
+ label: ReactNode;
+ value: string;
+}
+
+export interface AskUserCustomOption extends AskUserOption {
+ inputLabel?: string;
+ placeholder?: string;
+}
+
+export interface AskUserQuestion {
+ customOption?: AskUserCustomOption;
+ id: string;
+ options: readonly AskUserOption[];
+ prompt: ReactNode;
+ selectionMode?: "multiple" | "single";
+}
+
+export type AskUserAnswers = Readonly<
+ Record
+>;
+
+export interface AskUserCustomAnswerChangeMeta {
+ isComposing: boolean;
+}
+
+export interface AskUserProps
+ extends Omit, "children" | "onChange"> {
+ answers?: AskUserAnswers;
+ customAnswers?: Readonly>;
+ defaultExpanded?: boolean;
+ disabled?: boolean;
+ expanded?: boolean;
+ header?: ReactNode;
+ onAnswersChange?: (questionId: string, values: readonly string[]) => void;
+ onCustomAnswerChange?: (
+ questionId: string,
+ value: string,
+ meta: AskUserCustomAnswerChangeMeta,
+ ) => void;
+ onExpandedChange?: (expanded: boolean) => void;
+ onSubmit?: () => void;
+ questions: readonly AskUserQuestion[];
+ state?: AskUserState;
+ statusLabel?: ReactNode;
+ submitDisabled?: boolean;
+ submitLabel?: ReactNode;
+ submitTitle?: string;
+ submittingLabel?: ReactNode;
+ summaryDetail?: ReactNode;
+ summaryLabel?: ReactNode;
+}
+
+function OptionControl({
+ checked,
+ multiple,
+}: {
+ checked: boolean;
+ multiple: boolean;
+}) {
+ const Icon = multiple
+ ? checked ? SquareCheckBig : Square
+ : checked ? Disc2 : Circle;
+
+ return (
+
+ );
+}
+
+function StatusIcon({ state }: { state: AskUserState }) {
+ if (state === "loading" || state === "submitting") {
+ return ;
+ }
+ if (state === "completed" || state === "submitted") {
+ return ;
+ }
+ return ;
+}
+
+function descriptionTitle(description: ReactNode): string | undefined {
+ return typeof description === "string" ? description : undefined;
+}
+
+export const AskUser = forwardRef(function AskUser({
+ answers = {},
+ className,
+ customAnswers = {},
+ defaultExpanded = false,
+ disabled = false,
+ expanded,
+ header,
+ onAnswersChange,
+ onCustomAnswerChange,
+ onExpandedChange,
+ onSubmit,
+ questions,
+ state = "asking",
+ statusLabel,
+ submitDisabled = false,
+ submitLabel,
+ submitTitle,
+ submittingLabel,
+ summaryDetail,
+ summaryLabel,
+ ...props
+}, ref) {
+ const instanceId = useId();
+ const detailsId = `${instanceId}-details`;
+ const [internalExpanded, setInternalExpanded] = useState(defaultExpanded);
+ const composingQuestionsRef = useRef(new Set());
+ const hasSummary = summaryLabel !== undefined && summaryLabel !== null;
+ const resolvedExpanded = hasSummary
+ ? expanded ?? internalExpanded
+ : true;
+ const interactionDisabled = disabled || state !== "asking";
+ const showStatusOnly = state === "loading"
+ || state === "error" && questions.length === 0;
+
+ function setExpanded(nextExpanded: boolean) {
+ if (expanded === undefined) {
+ setInternalExpanded(nextExpanded);
+ }
+ onExpandedChange?.(nextExpanded);
+ }
+
+ function updateAnswer(
+ question: AskUserQuestion,
+ value: string,
+ checked: boolean,
+ ) {
+ if (interactionDisabled) return;
+ const currentValues = answers[question.id] ?? [];
+ const nextValues = question.selectionMode === "multiple"
+ ? checked
+ ? currentValues.includes(value)
+ ? currentValues
+ : [...currentValues, value]
+ : currentValues.filter((candidate) => candidate !== value)
+ : checked ? [value] : [];
+ onAnswersChange?.(question.id, nextValues);
+ }
+
+ if (showStatusOnly) {
+ return (
+
+
+
+
+
+ {statusLabel}
+
+
+ );
+ }
+
+ const showFeedback = (state === "error" || state === "timeout")
+ && statusLabel !== undefined
+ && statusLabel !== null;
+ const showFooter = !hasSummary && (
+ submitLabel !== undefined && submitLabel !== null
+ || statusLabel !== undefined && statusLabel !== null
+ );
+
+ return (
+
+ {hasSummary ? (
+
+ ) : header !== undefined && header !== null ? (
+
{header}
+ ) : null}
+
+
+
+
+ {questions.map((question, questionIndex) => {
+ const multiple = question.selectionMode === "multiple";
+ const selectedValues = answers[question.id] ?? [];
+ const customOption = question.customOption;
+ const customSelected = customOption
+ ? selectedValues.includes(customOption.value)
+ : false;
+
+ return (
+
+ );
+ })}
+
+
+ {showFeedback && (
+
+
+
+
+
+ {statusLabel}
+
+
+ )}
+
+
+
+ {showFooter && (
+
+ {submitLabel !== undefined && submitLabel !== null && (
+
+ }
+ loading={state === "submitting"}
+ onClick={onSubmit}
+ size="sm"
+ title={submitTitle}
+ variant="fill"
+ >
+ {state === "submitting" && submittingLabel !== undefined
+ ? submittingLabel
+ : submitLabel}
+
+
+ )}
+ {statusLabel !== undefined && statusLabel !== null && (
+
+
+
+
+
+ {statusLabel}
+
+
+ )}
+
+ )}
+
+ );
+});
diff --git a/design-system/packages/ui/src/flow-chat/ask-user/index.ts b/design-system/packages/ui/src/flow-chat/ask-user/index.ts
new file mode 100644
index 0000000000..1f6239d9a1
--- /dev/null
+++ b/design-system/packages/ui/src/flow-chat/ask-user/index.ts
@@ -0,0 +1,10 @@
+export {
+ AskUser,
+ type AskUserAnswers,
+ type AskUserCustomAnswerChangeMeta,
+ type AskUserCustomOption,
+ type AskUserOption,
+ type AskUserProps,
+ type AskUserQuestion,
+ type AskUserState,
+} from "./AskUser";
diff --git a/design-system/packages/ui/src/flow-chat/composer/ChatComposer.meta.ts b/design-system/packages/ui/src/flow-chat/composer/ChatComposer.meta.ts
new file mode 100644
index 0000000000..0f18b8714f
--- /dev/null
+++ b/design-system/packages/ui/src/flow-chat/composer/ChatComposer.meta.ts
@@ -0,0 +1,46 @@
+import type { ComponentMeta } from "../../registry.types";
+
+export const chatComposerMeta = {
+ category: "flow-chat",
+ description: "A slot-based FlowChat composer with a context bar and compact or expanded input anatomy.",
+ maturity: "stable",
+ name: "ChatComposer",
+ props: [
+ { defaultValue: "compact", name: "layout", type: "compact | expanded" },
+ { name: "contextBar", type: "ReactNode" },
+ { name: "children", type: "ReactNode" },
+ { name: "startActions", type: "ReactNode" },
+ { name: "endActions", type: "ReactNode" },
+ { defaultValue: "false", name: "busy", type: "boolean" },
+ { defaultValue: "false", name: "disabled", type: "boolean" },
+ { name: "surfaceClassName", type: "string" },
+ ],
+ states: ["default", "expanded", "focus-visible", "busy", "disabled"],
+ tokens: [
+ "border.width.default",
+ "color.action.neutral.border",
+ "color.border.default",
+ "color.content.primary",
+ "color.surface.panel",
+ "color.surface.subtle",
+ "control.chatComposer.compactGap",
+ "control.chatComposer.compactHeight",
+ "control.chatComposer.compactPaddingBlock",
+ "control.chatComposer.compactPaddingInline",
+ "control.chatComposer.compactTrackHeight",
+ "control.chatComposer.controlHeight",
+ "control.height.md",
+ "font.family.control",
+ "font.size.small",
+ "font.weight.regular",
+ "radius.2xl",
+ "radius.pill",
+ "shadow.base",
+ "space.1",
+ "space.2",
+ "space.3",
+ "space.6",
+ "space.8",
+ "space.12",
+ ],
+} as const satisfies ComponentMeta;
diff --git a/design-system/packages/ui/src/flow-chat/composer/ChatComposer.module.css b/design-system/packages/ui/src/flow-chat/composer/ChatComposer.module.css
new file mode 100644
index 0000000000..fd822ce62e
--- /dev/null
+++ b/design-system/packages/ui/src/flow-chat/composer/ChatComposer.module.css
@@ -0,0 +1,145 @@
+@layer bf.components {
+ .root {
+ display: flex;
+ min-inline-size: 0;
+ inline-size: 100%;
+ flex-direction: column;
+ border-radius: var(--bf-radius-2xl);
+ color: var(--bf-color-content-primary);
+ background: transparent;
+ font-family: var(--bf-font-family-control);
+ }
+
+ .root[data-has-context="true"] {
+ background: var(--bf-color-surface-subtle);
+ }
+
+ .context {
+ display: flex;
+ min-inline-size: 0;
+ min-block-size: var(--bf-space-8);
+ align-items: center;
+ padding-inline: var(--bf-space-2);
+ }
+
+ .surface {
+ display: grid;
+ min-inline-size: 0;
+ inline-size: 100%;
+ min-block-size: var(--bf-control-height-md);
+ box-sizing: border-box;
+ align-items: center;
+ gap: var(--bf-space-3);
+ padding: var(--bf-space-2);
+ border: var(--bf-border-width-default) solid var(--bf-color-action-neutral-border);
+ border-radius: var(--bf-radius-2xl);
+ color: var(--bf-color-content-primary);
+ background: var(--bf-color-surface-panel);
+ box-shadow: var(--bf-shadow-base);
+ transition:
+ border-color var(--bf-motion-duration-fast) var(--bf-motion-easing-standard),
+ box-shadow var(--bf-motion-duration-fast) var(--bf-motion-easing-standard);
+ }
+
+ .surface[data-bf-layout="compact"] {
+ block-size: var(--bf-control-chat-composer-compact-height);
+ gap: var(--bf-control-chat-composer-compact-gap);
+ grid-template-areas: "start content end";
+ grid-template-columns: auto minmax(0, 1fr) auto;
+ grid-template-rows: var(--bf-control-chat-composer-compact-track-height);
+ min-block-size: var(--bf-control-chat-composer-compact-height);
+ align-content: center;
+ align-items: center;
+ padding-block: var(--bf-control-chat-composer-compact-padding-block);
+ padding-inline: var(--bf-control-chat-composer-compact-padding-inline);
+ border-radius: var(--bf-radius-pill);
+ }
+
+ .surface[data-bf-layout="expanded"] {
+ grid-template-areas:
+ "content content"
+ "start end";
+ grid-template-columns: minmax(0, 1fr) auto;
+ grid-template-rows: minmax(var(--bf-space-12), 1fr) auto;
+ min-block-size: calc(
+ var(--bf-control-height-md) +
+ var(--bf-control-height-md) +
+ var(--bf-control-height-md)
+ );
+ align-items: end;
+ }
+
+ .surface:focus-within {
+ border-color: var(--bf-color-border-default);
+ }
+
+ .content {
+ grid-area: content;
+ box-sizing: border-box;
+ display: flex;
+ min-inline-size: 0;
+ align-self: stretch;
+ align-items: center;
+ padding: var(--bf-space-1);
+ font-size: var(--bf-font-size-small);
+ font-weight: var(--bf-font-weight-regular);
+ line-height: var(--bf-line-height-base);
+ }
+
+ .surface[data-bf-layout="expanded"] .content {
+ align-items: flex-start;
+ }
+
+ .surface[data-bf-layout="compact"] .content {
+ block-size: var(--bf-control-chat-composer-compact-track-height);
+ align-self: center;
+ padding: 0;
+ }
+
+ .startActions,
+ .endActions {
+ box-sizing: border-box;
+ display: inline-flex;
+ min-inline-size: 0;
+ min-block-size: var(--bf-control-chat-composer-control-height);
+ align-items: center;
+ }
+
+ .startActions {
+ grid-area: start;
+ justify-self: start;
+ gap: var(--bf-space-2);
+ }
+
+ .endActions {
+ grid-area: end;
+ justify-self: end;
+ gap: var(--bf-space-1);
+ }
+
+ .surface[data-bf-layout="compact"] .startActions,
+ .surface[data-bf-layout="compact"] .endActions {
+ block-size: var(--bf-control-chat-composer-compact-track-height);
+ min-block-size: 0;
+ align-self: center;
+ }
+
+ .root[data-bf-state~="disabled"] .surface {
+ cursor: not-allowed;
+ opacity: var(--bf-opacity-disabled);
+ }
+
+ @media (prefers-reduced-motion: reduce) {
+ .surface {
+ transition: none;
+ }
+ }
+
+ @media (forced-colors: active) {
+ .surface,
+ .surface:focus-within {
+ border-color: CanvasText;
+ box-shadow: none;
+ }
+ }
+}
diff --git a/design-system/packages/ui/src/flow-chat/composer/ChatComposer.tsx b/design-system/packages/ui/src/flow-chat/composer/ChatComposer.tsx
new file mode 100644
index 0000000000..cf4c38eb28
--- /dev/null
+++ b/design-system/packages/ui/src/flow-chat/composer/ChatComposer.tsx
@@ -0,0 +1,150 @@
+import {
+ Children,
+ forwardRef,
+ isValidElement,
+ type HTMLAttributes,
+ type ReactNode,
+} from "react";
+import { classNames } from "../../internal/classNames";
+import styles from "./ChatComposer.module.css";
+
+export type ChatComposerLayout = "compact" | "expanded";
+
+export interface ChatComposerProps
+ extends Omit, "children"> {
+ busy?: boolean;
+ children: ReactNode;
+ contextBar?: ReactNode;
+ disabled?: boolean;
+ endActions?: ReactNode;
+ layout?: ChatComposerLayout;
+ startActions?: ReactNode;
+ surfaceClassName?: string;
+}
+
+export interface ChatComposerSlotProps {
+ children: ReactNode;
+}
+
+/** Compound slot for complex consumers that keep content and actions adjacent. */
+export function ChatComposerContent({ children }: ChatComposerSlotProps) {
+ return <>{children}>;
+}
+
+/** Compound slot for controls at the leading edge of the composer. */
+export function ChatComposerStartActions({ children }: ChatComposerSlotProps) {
+ return <>{children}>;
+}
+
+/** Compound slot for controls at the trailing edge of the composer. */
+export function ChatComposerEndActions({ children }: ChatComposerSlotProps) {
+ return <>{children}>;
+}
+
+function hasSlot(value: ReactNode): boolean {
+ if (Array.isArray(value)) return value.some(hasSlot);
+ return value !== undefined && value !== null && value !== false;
+}
+
+function resolveCompoundSlots(children: ReactNode) {
+ const content: ReactNode[] = [];
+ const startActions: ReactNode[] = [];
+ const endActions: ReactNode[] = [];
+
+ Children.toArray(children).forEach((child) => {
+ if (isValidElement(child)) {
+ if (child.type === ChatComposerContent) {
+ content.push(...Children.toArray(child.props.children));
+ return;
+ }
+ if (child.type === ChatComposerStartActions) {
+ startActions.push(...Children.toArray(child.props.children));
+ return;
+ }
+ if (child.type === ChatComposerEndActions) {
+ endActions.push(...Children.toArray(child.props.children));
+ return;
+ }
+ }
+ content.push(child);
+ });
+
+ return {
+ content,
+ endActions,
+ startActions,
+ };
+}
+
+/**
+ * Theme-independent FlowChat composer anatomy.
+ *
+ * Product state, localization, rich-text behavior, menus, and host actions are
+ * supplied through slots. The package owns the stable context-bar/surface
+ * geometry and the responsive compact/expanded layout.
+ */
+export const ChatComposer = forwardRef(
+ function ChatComposer({
+ "aria-busy": ariaBusy,
+ busy = false,
+ children,
+ className,
+ contextBar,
+ disabled = false,
+ endActions,
+ layout = "compact",
+ startActions,
+ surfaceClassName,
+ ...props
+ }, ref) {
+ const contextVisible = hasSlot(contextBar);
+ const compoundSlots = resolveCompoundSlots(children);
+ const resolvedStartActions = hasSlot(startActions)
+ ? startActions
+ : compoundSlots.startActions;
+ const resolvedEndActions = hasSlot(endActions)
+ ? endActions
+ : compoundSlots.endActions;
+ const state = [busy && "busy", disabled && "disabled"]
+ .filter(Boolean)
+ .join(" ") || undefined;
+
+ return (
+
+ {contextVisible && (
+
+ {contextBar}
+
+ )}
+
+ {hasSlot(resolvedStartActions) && (
+
+ {resolvedStartActions}
+
+ )}
+
+ {compoundSlots.content}
+
+ {hasSlot(resolvedEndActions) && (
+
+ {resolvedEndActions}
+
+ )}
+
+
+ );
+ },
+);
diff --git a/design-system/packages/ui/src/flow-chat/composer/index.ts b/design-system/packages/ui/src/flow-chat/composer/index.ts
new file mode 100644
index 0000000000..cdc480470c
--- /dev/null
+++ b/design-system/packages/ui/src/flow-chat/composer/index.ts
@@ -0,0 +1,9 @@
+export {
+ ChatComposer,
+ ChatComposerContent,
+ ChatComposerEndActions,
+ ChatComposerStartActions,
+ type ChatComposerLayout,
+ type ChatComposerProps,
+ type ChatComposerSlotProps,
+} from "./ChatComposer";
diff --git a/design-system/packages/ui/src/flow-chat/index.ts b/design-system/packages/ui/src/flow-chat/index.ts
new file mode 100644
index 0000000000..ae72e11017
--- /dev/null
+++ b/design-system/packages/ui/src/flow-chat/index.ts
@@ -0,0 +1,3 @@
+export * from "./ask-user";
+export * from "./composer";
+export * from "./tool-cards";
diff --git a/design-system/packages/ui/src/flow-chat/tool-cards/ActivityToolCards.module.css b/design-system/packages/ui/src/flow-chat/tool-cards/ActivityToolCards.module.css
new file mode 100644
index 0000000000..9536716928
--- /dev/null
+++ b/design-system/packages/ui/src/flow-chat/tool-cards/ActivityToolCards.module.css
@@ -0,0 +1,14 @@
+@layer bf.components {
+ .summary {
+ display: block;
+ min-inline-size: 0;
+ overflow: hidden;
+ color: var(--bf-color-content-muted);
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .summary[data-tone="danger"] {
+ color: var(--bf-color-status-danger-content);
+ }
+}
diff --git a/design-system/packages/ui/src/flow-chat/tool-cards/ActivityToolCards.tsx b/design-system/packages/ui/src/flow-chat/tool-cards/ActivityToolCards.tsx
new file mode 100644
index 0000000000..77bc59ac52
--- /dev/null
+++ b/design-system/packages/ui/src/flow-chat/tool-cards/ActivityToolCards.tsx
@@ -0,0 +1,109 @@
+import type { HTMLAttributes, ReactNode } from "react";
+import { Hourglass, Info, Terminal, Zap } from "lucide-react";
+import {
+ AmbientToolCard,
+ AmbientToolCardHeader,
+ type FlowChatToolStatus,
+} from "./FlowChatToolCard";
+import {
+ ToolCardStatusSlot,
+ type ToolCardStatusSlotProps,
+} from "./ToolCardStatusSlot";
+import styles from "./ActivityToolCards.module.css";
+
+interface ActivityToolCardBaseProps
+ extends Omit, "children" | "onClick"> {
+ action?: ReactNode;
+ defaultIcon?: ToolCardStatusSlotProps["defaultIcon"];
+ icon: ReactNode;
+ status: FlowChatToolStatus;
+ summary?: ReactNode;
+ summaryTitle?: string;
+ toolCard: string;
+}
+
+function ActivityToolCardBase({
+ action,
+ defaultIcon,
+ icon,
+ status,
+ summary,
+ summaryTitle,
+ toolCard,
+ ...props
+}: ActivityToolCardBaseProps) {
+ return (
+
+ {summary}
+
+ ) : undefined}
+ icon={(
+
+ )}
+ />
+ )}
+ status={status}
+ />
+ );
+}
+
+export type ActivityToolCardProps = Omit<
+ ActivityToolCardBaseProps,
+ "icon" | "toolCard"
+>;
+
+export function AgentWaitToolCard(props: ActivityToolCardProps) {
+ return (
+ }
+ toolCard="agent-wait"
+ />
+ );
+}
+
+export function GetToolSpecToolCard(props: ActivityToolCardProps) {
+ return (
+ }
+ toolCard="get-tool-spec"
+ />
+ );
+}
+
+export function SkillToolCard(props: ActivityToolCardProps) {
+ return (
+ }
+ toolCard="skill"
+ />
+ );
+}
+
+export function TerminalControlToolCard(props: ActivityToolCardProps) {
+ return (
+ }
+ toolCard="terminal-control"
+ />
+ );
+}
diff --git a/design-system/packages/ui/src/flow-chat/tool-cards/AmbientToolCard.meta.ts b/design-system/packages/ui/src/flow-chat/tool-cards/AmbientToolCard.meta.ts
new file mode 100644
index 0000000000..989fe45842
--- /dev/null
+++ b/design-system/packages/ui/src/flow-chat/tool-cards/AmbientToolCard.meta.ts
@@ -0,0 +1,34 @@
+import type { ComponentMeta } from "../../registry.types";
+
+export const ambientToolCardMeta = {
+ category: "flow-chat",
+ description: "A low-attention tool trace that stays glanceable in the conversation and can reveal supporting detail.",
+ maturity: "stable",
+ name: "AmbientToolCard",
+ props: [
+ { name: "status", type: "FlowChatToolStatus" },
+ { name: "header", type: "ReactNode" },
+ { defaultValue: "false", name: "isExpanded", type: "boolean" },
+ { name: "expandedContent", type: "ReactNode" },
+ { name: "onClick", type: "() => void" },
+ ],
+ states: ["default", "hover", "loading", "expanded", "error"],
+ tokens: [
+ "color.content.primary",
+ "color.content.secondary",
+ "color.content.muted",
+ "color.surface.panel",
+ "color.border.default",
+ "color.status.danger.content",
+ "control.height.sm",
+ "control.height.md",
+ "font.family.control",
+ "font.size.small",
+ "font.size.xl",
+ "font.weight.medium",
+ "font.weight.regular",
+ "radius.md",
+ "radius.sm",
+ "shadow.xs",
+ ],
+} as const satisfies ComponentMeta;
diff --git a/design-system/packages/ui/src/flow-chat/tool-cards/CommandToolCard.meta.ts b/design-system/packages/ui/src/flow-chat/tool-cards/CommandToolCard.meta.ts
new file mode 100644
index 0000000000..235d5d7da4
--- /dev/null
+++ b/design-system/packages/ui/src/flow-chat/tool-cards/CommandToolCard.meta.ts
@@ -0,0 +1,34 @@
+import type { ComponentMeta } from "../../registry.types";
+
+export const commandToolCardMeta = {
+ category: "flow-chat",
+ description: "A concrete prominent FlowChat command card with controlled disclosure, output, metadata, and capability-driven actions.",
+ maturity: "stable",
+ name: "CommandToolCard",
+ props: [
+ { name: "status", type: "FlowChatToolStatus" },
+ { name: "action", type: "ReactNode" },
+ { name: "command", type: "string | null" },
+ { name: "isExpanded", type: "boolean" },
+ { name: "output", type: "ReactNode" },
+ { defaultValue: "false", name: "reserveOutput", type: "boolean" },
+ { defaultValue: "false", name: "reserveFooter", type: "boolean" },
+ { name: "footerItems", type: "readonly CommandToolCardFooterItem[]" },
+ { name: "copyAction", type: "CommandToolCardCopyAction" },
+ ],
+ states: ["default", "hover", "loading", "expanded", "error", "confirmation"],
+ tokens: [
+ "color.action.neutral.surface",
+ "color.content.primary",
+ "color.content.secondary",
+ "color.content.muted",
+ "color.status.danger.content",
+ "color.status.success.content",
+ "color.status.warning.content",
+ "color.surface.canvas",
+ "color.surface.raised",
+ "font.family.mono",
+ "font.size.small",
+ "radius.sm",
+ ],
+} as const satisfies ComponentMeta;
diff --git a/design-system/packages/ui/src/flow-chat/tool-cards/CommandToolCard.module.css b/design-system/packages/ui/src/flow-chat/tool-cards/CommandToolCard.module.css
new file mode 100644
index 0000000000..5d98accc17
--- /dev/null
+++ b/design-system/packages/ui/src/flow-chat/tool-cards/CommandToolCard.module.css
@@ -0,0 +1,196 @@
+@layer bf.components {
+ .root {
+ min-inline-size: 0;
+ }
+
+ .command {
+ display: block;
+ overflow: hidden;
+ min-inline-size: 0;
+ margin: 0;
+ padding: 0;
+ border: 0;
+ border-radius: 0;
+ color: var(--bf-color-content-secondary);
+ background: transparent;
+ font-family: var(--bf-font-family-mono);
+ font-size: var(--bf-font-size-small);
+ font-weight: var(--bf-font-weight-medium);
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .command[data-empty="true"] {
+ color: var(--bf-color-content-muted);
+ font-style: italic;
+ font-weight: var(--bf-font-weight-regular);
+ }
+
+ .statusSummary {
+ display: inline-flex;
+ min-inline-size: 0;
+ align-items: center;
+ gap: var(--bf-space-1);
+ color: var(--bf-color-content-muted);
+ font-size: var(--bf-font-size-small);
+ }
+
+ .statusLabel {
+ display: inline-flex;
+ align-items: center;
+ padding-block: var(--bf-space-1);
+ padding-inline: var(--bf-space-2);
+ border-radius: var(--bf-radius-sm);
+ background: var(--bf-color-action-neutral-surface);
+ font-size: var(--bf-font-size-small);
+ font-weight: var(--bf-font-weight-semibold);
+ white-space: nowrap;
+ }
+
+ .statusLabel[data-tone="warning"] {
+ color: var(--bf-color-status-warning-content);
+ background: var(--bf-color-status-warning-surface);
+ }
+
+ .statusLabel[data-tone="danger"] {
+ color: var(--bf-color-status-danger-content);
+ background: var(--bf-color-status-danger-surface);
+ }
+
+ .criticalAction {
+ color: var(--bf-color-status-warning-content);
+ }
+
+ .details {
+ display: flex;
+ min-inline-size: 0;
+ flex-direction: column;
+ gap: var(--bf-space-2);
+ }
+
+ .outputFrame {
+ position: relative;
+ overflow: hidden;
+ min-block-size: 0;
+ border-radius: var(--bf-radius-sm);
+ background: var(--bf-color-surface-canvas);
+ }
+
+ .outputFrame[data-density="compact"] {
+ block-size: 5.375rem;
+ }
+
+ .outputFrame[data-density="expanded"] {
+ block-size: 20rem;
+ }
+
+ .output,
+ .waiting {
+ box-sizing: border-box;
+ inline-size: 100%;
+ block-size: 100%;
+ min-block-size: 0;
+ }
+
+ .output > * {
+ inline-size: 100%;
+ block-size: 100%;
+ max-block-size: 100%;
+ }
+
+ .waiting {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: var(--bf-space-3);
+ color: var(--bf-color-content-muted);
+ font-size: var(--bf-font-size-small);
+ font-style: italic;
+ text-align: center;
+ }
+
+ .outputActions {
+ position: absolute;
+ z-index: 1;
+ inset-block-start: var(--bf-space-1);
+ inset-inline-end: var(--bf-space-1);
+ display: inline-flex;
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity var(--bf-motion-duration-fast) var(--bf-motion-easing-standard);
+ }
+
+ .outputFrame:is(:hover, :focus-within) .outputActions,
+ .outputFrame[data-bf-preview-state="hover"] .outputActions {
+ opacity: 1;
+ pointer-events: auto;
+ }
+
+ .footer {
+ display: flex;
+ min-inline-size: 0;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: var(--bf-space-2);
+ padding-block: var(--bf-space-2);
+ padding-inline: var(--bf-space-3);
+ border-radius: var(--bf-radius-sm);
+ color: var(--bf-color-content-muted);
+ background: var(--bf-color-surface-raised);
+ font-size: var(--bf-font-size-small);
+ }
+
+ .footerItem {
+ display: inline-flex;
+ min-inline-size: 0;
+ align-items: center;
+ gap: var(--bf-space-1);
+ white-space: nowrap;
+ }
+
+ .footerItem[data-grow="true"] {
+ flex: 1 1 12rem;
+ }
+
+ .footerLabel {
+ color: var(--bf-color-content-muted);
+ font-size: var(--bf-font-size-small);
+ font-weight: var(--bf-font-weight-semibold);
+ }
+
+ .footerValue {
+ overflow: hidden;
+ color: var(--bf-color-content-secondary);
+ text-overflow: ellipsis;
+ }
+
+ .footerItem[data-monospace="true"] .footerValue {
+ font-family: var(--bf-font-family-mono);
+ }
+
+ .footerItem[data-tone="success"] .footerValue {
+ color: var(--bf-color-status-success-content);
+ }
+
+ .footerItem[data-tone="warning"] .footerValue {
+ color: var(--bf-color-status-warning-content);
+ }
+
+ .footerItem[data-tone="danger"] .footerValue {
+ color: var(--bf-color-status-danger-content);
+ }
+
+ .error {
+ color: var(--bf-color-status-danger-content);
+ font-size: var(--bf-font-size-small);
+ line-height: var(--bf-line-height-body);
+ white-space: pre-wrap;
+ overflow-wrap: anywhere;
+ }
+
+ @media (prefers-reduced-motion: reduce) {
+ .outputActions {
+ transition: none;
+ }
+ }
+}
diff --git a/design-system/packages/ui/src/flow-chat/tool-cards/CommandToolCard.tsx b/design-system/packages/ui/src/flow-chat/tool-cards/CommandToolCard.tsx
new file mode 100644
index 0000000000..ac61ec6e6b
--- /dev/null
+++ b/design-system/packages/ui/src/flow-chat/tool-cards/CommandToolCard.tsx
@@ -0,0 +1,233 @@
+import type {
+ HTMLAttributes,
+ MouseEvent as ReactMouseEvent,
+ ReactNode,
+} from "react";
+import {
+ Check,
+ Copy,
+ ExternalLink,
+ Square,
+ Terminal,
+} from "lucide-react";
+import { IconButton } from "../../components/IconButton/IconButton";
+import { classNames } from "../../internal/classNames";
+import {
+ ProminentToolCard,
+ ProminentToolCardHeader,
+ ToolCardHeaderActions,
+ type FlowChatToolStatus,
+} from "./FlowChatToolCard";
+import { ToolProcessingDots } from "./ToolProcessingDots";
+import styles from "./CommandToolCard.module.css";
+
+export interface CommandToolCardAction {
+ disabled?: boolean;
+ label: string;
+ onPress: (event: ReactMouseEvent) => void;
+ testId?: string;
+}
+
+export interface CommandToolCardCopyAction extends CommandToolCardAction {
+ copied?: boolean;
+ copiedLabel?: string;
+}
+
+export interface CommandToolCardFooterItem {
+ grow?: boolean;
+ label?: ReactNode;
+ monospace?: boolean;
+ tone?: "danger" | "neutral" | "success" | "warning";
+ value: ReactNode;
+}
+
+export interface CommandToolCardProps
+ extends Omit, "children" | "onClick"> {
+ action: ReactNode;
+ command?: string | null;
+ commandTestId?: string;
+ copyAction?: CommandToolCardCopyAction;
+ emptyCommand: ReactNode;
+ error?: ReactNode;
+ footerItems?: readonly CommandToolCardFooterItem[];
+ interruptAction?: CommandToolCardAction;
+ isExpanded: boolean;
+ onToggle?: () => void;
+ openAction?: CommandToolCardAction;
+ output?: ReactNode;
+ outputAction?: ReactNode;
+ outputDensity?: "compact" | "expanded";
+ reserveFooter?: boolean;
+ reserveOutput?: boolean;
+ requiresConfirmation?: boolean;
+ status: FlowChatToolStatus;
+ statusLabel?: ReactNode;
+ statusSummary?: ReactNode;
+ statusTone?: "danger" | "neutral" | "warning";
+ toggleTestId?: string;
+ waitingContent?: ReactNode;
+}
+
+const ACTIVE_STATUSES = new Set([
+ "preparing",
+ "receiving",
+ "running",
+ "streaming",
+]);
+
+export function CommandToolCard({
+ action,
+ className,
+ command,
+ commandTestId,
+ copyAction,
+ emptyCommand,
+ error,
+ footerItems = [],
+ interruptAction,
+ isExpanded,
+ onToggle,
+ openAction,
+ output,
+ outputAction,
+ outputDensity = "expanded",
+ reserveFooter = false,
+ reserveOutput = false,
+ requiresConfirmation = false,
+ status,
+ statusLabel,
+ statusSummary,
+ statusTone = "neutral",
+ toggleTestId,
+ waitingContent,
+ ...props
+}: CommandToolCardProps) {
+ const loading = ACTIVE_STATUSES.has(status);
+ const failed = status === "error";
+ const hasOutputFrame = Boolean(output || waitingContent || reserveOutput);
+ const hasFooter = reserveFooter || footerItems.length > 0;
+ const hasDetails = hasOutputFrame || hasFooter;
+ const resolvedCommand = command?.trim() ? command : null;
+ const actionItems = Boolean(copyAction || openAction || interruptAction);
+
+ const renderAction = (
+ kind: "copy" | "interrupt" | "open",
+ item: CommandToolCardAction | CommandToolCardCopyAction | undefined,
+ ) => {
+ if (!item) return null;
+ const copied = kind === "copy" && "copied" in item && item.copied;
+ const label = copied && "copiedLabel" in item && item.copiedLabel
+ ? item.copiedLabel
+ : item.label;
+ const icon = kind === "copy"
+ ? copied ? :
+ : kind === "open"
+ ?
+ : ;
+
+ return (
+
+ );
+ };
+
+ const details = hasDetails ? (
+
+ {hasOutputFrame && (
+
+ {outputAction &&
{outputAction}}
+ {output
+ ?
{output}
+ :
{waitingContent}
}
+
+ )}
+ {hasFooter && (
+
+ {footerItems.map((item, index) => (
+
+ {item.label !== undefined && item.label !== null && (
+ {item.label}
+ )}
+ {item.value}
+
+ ))}
+
+ )}
+
+ ) : undefined;
+
+ return (
+ : undefined}
+ expandedContent={details}
+ header={(
+
+ {renderAction("interrupt", interruptAction)}
+ {renderAction("copy", copyAction)}
+ {renderAction("open", openAction)}
+
+ ) : undefined}
+ content={(
+
+ {resolvedCommand ?? emptyCommand}
+
+ )}
+ extra={(statusSummary || statusLabel) ? (
+
+ {statusSummary}
+ {statusLabel && (
+ {statusLabel}
+ )}
+
+ ) : undefined}
+ icon={}
+ statusIcon={loading ? : undefined}
+ />
+ )}
+ headerExpandAffordance={hasDetails}
+ isExpanded={isExpanded}
+ isFailed={failed}
+ onClick={hasDetails && onToggle ? () => onToggle() : undefined}
+ requiresConfirmation={requiresConfirmation}
+ status={status}
+ toggleTestId={toggleTestId}
+ />
+
+ );
+}
diff --git a/design-system/packages/ui/src/flow-chat/tool-cards/ContextCompressionToolCard.meta.ts b/design-system/packages/ui/src/flow-chat/tool-cards/ContextCompressionToolCard.meta.ts
new file mode 100644
index 0000000000..51192af56d
--- /dev/null
+++ b/design-system/packages/ui/src/flow-chat/tool-cards/ContextCompressionToolCard.meta.ts
@@ -0,0 +1,23 @@
+import type { ComponentMeta } from "../../registry.types";
+
+export const contextCompressionToolCardMeta = {
+ category: "flow-chat",
+ description: "A concrete prominent FlowChat card with a compact context-length and compression-ratio result.",
+ maturity: "stable",
+ name: "ContextCompressionToolCard",
+ props: [
+ { name: "status", type: "FlowChatToolStatus" },
+ { name: "title", type: "ReactNode" },
+ { name: "summary", type: "ReactNode" },
+ { name: "processingText", type: "ReactNode" },
+ { name: "error", type: "ReactNode" },
+ ],
+ states: ["default", "loading", "error"],
+ tokens: [
+ "color.content.secondary",
+ "color.content.muted",
+ "color.status.danger.content",
+ "font.family.mono",
+ "font.size.small",
+ ],
+} as const satisfies ComponentMeta;
diff --git a/design-system/packages/ui/src/flow-chat/tool-cards/ContextCompressionToolCard.module.css b/design-system/packages/ui/src/flow-chat/tool-cards/ContextCompressionToolCard.module.css
new file mode 100644
index 0000000000..9aa4871dcb
--- /dev/null
+++ b/design-system/packages/ui/src/flow-chat/tool-cards/ContextCompressionToolCard.module.css
@@ -0,0 +1,34 @@
+@layer bf.components {
+ .root {
+ min-inline-size: 0;
+ }
+
+ .summary,
+ .processing {
+ display: block;
+ overflow: hidden;
+ min-inline-size: 0;
+ color: var(--bf-color-content-secondary);
+ font-size: var(--bf-font-size-small);
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .summary {
+ font-family: var(--bf-font-family-mono);
+ font-variant-numeric: tabular-nums;
+ }
+
+ .processing {
+ color: var(--bf-color-content-muted);
+ font-style: italic;
+ }
+
+ .error {
+ font-size: var(--bf-font-size-small);
+ line-height: var(--bf-line-height-body);
+ white-space: pre-wrap;
+ overflow-wrap: anywhere;
+ color: var(--bf-color-status-danger-content);
+ }
+}
diff --git a/design-system/packages/ui/src/flow-chat/tool-cards/ContextCompressionToolCard.tsx b/design-system/packages/ui/src/flow-chat/tool-cards/ContextCompressionToolCard.tsx
new file mode 100644
index 0000000000..b5c528f4b9
--- /dev/null
+++ b/design-system/packages/ui/src/flow-chat/tool-cards/ContextCompressionToolCard.tsx
@@ -0,0 +1,72 @@
+import type { HTMLAttributes, ReactNode } from "react";
+import { Archive } from "lucide-react";
+import { classNames } from "../../internal/classNames";
+import {
+ ProminentToolCard,
+ ProminentToolCardHeader,
+ type FlowChatToolStatus,
+} from "./FlowChatToolCard";
+import { ToolProcessingDots } from "./ToolProcessingDots";
+import styles from "./ContextCompressionToolCard.module.css";
+
+export interface ContextCompressionToolCardProps
+ extends Omit, "children" | "title"> {
+ error?: ReactNode;
+ processingText?: ReactNode;
+ status: FlowChatToolStatus;
+ summary?: ReactNode;
+ title: ReactNode;
+}
+
+const ACTIVE_STATUSES = new Set([
+ "preparing",
+ "receiving",
+ "running",
+ "streaming",
+]);
+
+export function ContextCompressionToolCard({
+ className,
+ error,
+ processingText,
+ status,
+ summary,
+ title,
+ ...props
+}: ContextCompressionToolCardProps) {
+ const loading = ACTIVE_STATUSES.has(status);
+ const failed = status === "error";
+ const content = summary ?? processingText;
+
+ return (
+ : undefined}
+ header={(
+
+ {content}
+
+ ) : undefined}
+ icon={}
+ statusIcon={loading ? : undefined}
+ />
+ )}
+ isFailed={failed}
+ status={status}
+ />
+
+ );
+}
diff --git a/design-system/packages/ui/src/flow-chat/tool-cards/FileOperationToolCard.meta.ts b/design-system/packages/ui/src/flow-chat/tool-cards/FileOperationToolCard.meta.ts
new file mode 100644
index 0000000000..a672c94089
--- /dev/null
+++ b/design-system/packages/ui/src/flow-chat/tool-cards/FileOperationToolCard.meta.ts
@@ -0,0 +1,31 @@
+import type { ComponentMeta } from "../../registry.types";
+
+export const fileOperationToolCardMeta = {
+ category: "flow-chat",
+ description: "A concrete FlowChat file-operation card with ambient delete and prominent write or edit presentation.",
+ maturity: "stable",
+ name: "FileOperationToolCard",
+ props: [
+ { name: "operation", type: "'write' | 'edit' | 'delete'" },
+ { name: "status", type: "FlowChatToolStatus" },
+ { name: "path", type: "string" },
+ { name: "pathLabel", type: "ReactNode" },
+ { name: "actionTestId", type: "string" },
+ { name: "pathTestId", type: "string" },
+ { name: "preview", type: "ReactNode" },
+ { name: "isExpanded", type: "boolean" },
+ { name: "changeSummary", type: "FileOperationToolCardProps['changeSummary']" },
+ ],
+ states: ["default", "hover", "loading", "expanded", "error", "confirmation"],
+ tokens: [
+ "color.action.neutral.surface",
+ "color.content.primary",
+ "color.content.secondary",
+ "color.content.muted",
+ "color.status.danger.content",
+ "font.family.mono",
+ "font.size.small",
+ "font.weight.semibold",
+ "radius.sm",
+ ],
+} as const satisfies ComponentMeta;
diff --git a/design-system/packages/ui/src/flow-chat/tool-cards/FileOperationToolCard.module.css b/design-system/packages/ui/src/flow-chat/tool-cards/FileOperationToolCard.module.css
new file mode 100644
index 0000000000..4bd8bb3bac
--- /dev/null
+++ b/design-system/packages/ui/src/flow-chat/tool-cards/FileOperationToolCard.module.css
@@ -0,0 +1,79 @@
+@layer bf.components {
+ .root {
+ min-inline-size: 0;
+ }
+
+ .path {
+ display: block;
+ overflow: hidden;
+ min-inline-size: 0;
+ color: var(--bf-color-content-secondary);
+ font-family: var(--bf-font-family-mono);
+ font-size: var(--bf-font-size-small);
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .path[data-operation="delete"] {
+ color: var(--bf-color-content-muted);
+ text-decoration: line-through;
+ }
+
+ .inlineMessage {
+ overflow: hidden;
+ min-inline-size: 0;
+ color: var(--bf-color-content-muted);
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .statusDetail {
+ display: inline-flex;
+ flex: 0 0 auto;
+ align-items: center;
+ padding-block: var(--bf-space-1);
+ padding-inline: var(--bf-space-2);
+ border-radius: var(--bf-radius-sm);
+ color: var(--bf-color-content-muted);
+ background: var(--bf-color-action-neutral-surface);
+ font-size: var(--bf-font-size-small);
+ white-space: nowrap;
+ }
+
+ .preview {
+ overflow: hidden;
+ inline-size: 100%;
+ min-inline-size: 0;
+ border-radius: var(--bf-radius-sm);
+ }
+
+ .preview > * {
+ min-inline-size: 0;
+ }
+
+ .error {
+ display: flex;
+ flex-direction: column;
+ gap: var(--bf-space-2);
+ color: var(--bf-color-status-danger-content);
+ font-size: var(--bf-font-size-small);
+ line-height: var(--bf-line-height-body);
+ }
+
+ .error[data-guidance="true"] {
+ color: var(--bf-color-content-secondary);
+ }
+
+ .errorTitle {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--bf-space-2);
+ font-weight: var(--bf-font-weight-semibold);
+ }
+
+ .errorMessage {
+ color: var(--bf-color-content-muted);
+ white-space: pre-wrap;
+ overflow-wrap: anywhere;
+ }
+}
diff --git a/design-system/packages/ui/src/flow-chat/tool-cards/FileOperationToolCard.tsx b/design-system/packages/ui/src/flow-chat/tool-cards/FileOperationToolCard.tsx
new file mode 100644
index 0000000000..eb4e3d2ac5
--- /dev/null
+++ b/design-system/packages/ui/src/flow-chat/tool-cards/FileOperationToolCard.tsx
@@ -0,0 +1,223 @@
+import type {
+ HTMLAttributes,
+ MouseEvent as ReactMouseEvent,
+ ReactNode,
+} from "react";
+import {
+ ArrowUpRight,
+ FileEdit,
+ FilePlus,
+ FileX2,
+ Info,
+ XCircle,
+} from "lucide-react";
+import { IconButton } from "../../components/IconButton/IconButton";
+import { classNames } from "../../internal/classNames";
+import {
+ AmbientToolCard,
+ AmbientToolCardHeader,
+ ProminentToolCard,
+ ProminentToolCardHeader,
+ ToolCardChangeSummary,
+ ToolCardHeaderActions,
+ type FlowChatToolStatus,
+} from "./FlowChatToolCard";
+import { ToolCardStatusSlot } from "./ToolCardStatusSlot";
+import { ToolProcessingDots } from "./ToolProcessingDots";
+import styles from "./FileOperationToolCard.module.css";
+
+export type FileOperationKind = "delete" | "edit" | "write";
+
+export interface FileOperationToolCardAction {
+ label: string;
+ onPress: (event: ReactMouseEvent) => void;
+ testId?: string;
+}
+
+export interface FileOperationToolCardError {
+ guidance?: boolean;
+ message: ReactNode;
+ title: ReactNode;
+}
+
+export interface FileOperationToolCardProps
+ extends Omit, "children" | "onClick"> {
+ actionLabel: ReactNode;
+ actionTestId?: string;
+ changeSummary?: {
+ additions: number | string;
+ deletions: number | string;
+ label: string;
+ };
+ error?: FileOperationToolCardError;
+ inlineMessage?: ReactNode;
+ isExpanded?: boolean;
+ onOpenFile?: FileOperationToolCardAction;
+ onToggle?: () => void;
+ operation: FileOperationKind;
+ path: string;
+ pathLabel: ReactNode;
+ pathTestId?: string;
+ preview?: ReactNode;
+ requiresConfirmation?: boolean;
+ status: FlowChatToolStatus;
+ statusDetail?: ReactNode;
+}
+
+const OPERATION_ICONS = {
+ delete: FileX2,
+ edit: FileEdit,
+ write: FilePlus,
+} as const;
+
+const ACTIVE_STATUSES = new Set([
+ "preparing",
+ "receiving",
+ "running",
+ "streaming",
+]);
+
+export function FileOperationToolCard({
+ actionLabel,
+ actionTestId,
+ changeSummary,
+ className,
+ error,
+ inlineMessage,
+ isExpanded = false,
+ onOpenFile,
+ onToggle,
+ operation,
+ path,
+ pathLabel,
+ pathTestId,
+ preview,
+ requiresConfirmation = false,
+ status,
+ statusDetail,
+ ...props
+}: FileOperationToolCardProps) {
+ const Icon = OPERATION_ICONS[operation];
+ const failed = status === "error";
+ const loading = ACTIVE_STATUSES.has(status);
+
+ if (operation === "delete") {
+ return (
+
+
+ {actionLabel}{": "}
+
+ {pathLabel}
+
+ >
+ )}
+ icon={(
+ }
+ />
+ )}
+ />
+ )}
+ isExpanded={false}
+ status={status}
+ />
+
+ );
+ }
+
+ const hasPreview = Boolean(preview);
+
+ return (
+
+
+
+ {error.guidance ? : }
+ {error.title}
+
+ {error.message}
+
+ ) : undefined}
+ expandedContent={hasPreview ? {preview}
: undefined}
+ header={(
+
+ }
+ onClick={onOpenFile.onPress}
+ size="sm"
+ data-testid={onOpenFile.testId}
+ title={onOpenFile.label}
+ variant="quiet"
+ />
+
+ ) : undefined}
+ content={inlineMessage ? (
+ {inlineMessage}
+ ) : (
+
+ {pathLabel}
+
+ )}
+ extra={statusDetail ? (
+ {statusDetail}
+ ) : changeSummary ? (
+
+ ) : undefined}
+ icon={}
+ statusIcon={loading ? : undefined}
+ />
+ )}
+ headerExpandAffordance={hasPreview}
+ isExpanded={Boolean(isExpanded && hasPreview && !failed)}
+ isFailed={failed}
+ onClick={hasPreview && onToggle ? () => onToggle() : undefined}
+ requiresConfirmation={requiresConfirmation}
+ status={status}
+ />
+
+ );
+}
diff --git a/design-system/packages/ui/src/flow-chat/tool-cards/FlowChatToolCard.module.css b/design-system/packages/ui/src/flow-chat/tool-cards/FlowChatToolCard.module.css
new file mode 100644
index 0000000000..1e1a80c697
--- /dev/null
+++ b/design-system/packages/ui/src/flow-chat/tool-cards/FlowChatToolCard.module.css
@@ -0,0 +1,539 @@
+@layer bf.components {
+ .prominentRoot,
+ .ambientRoot {
+ --_tool-card-action-size: var(--bf-space-6);
+ --_icon-size: var(--bf-font-size-xl);
+ --_tool-card-transition: var(--bf-motion-duration-fast) var(--bf-motion-easing-standard);
+
+ box-sizing: border-box;
+ display: flex;
+ inline-size: 100%;
+ min-inline-size: 0;
+ flex-direction: column;
+ color: var(--bf-color-content-primary);
+ font-family: var(--bf-font-family-control);
+ }
+
+ .prominentRoot {
+ overflow: hidden;
+ margin-block: var(--bf-space-1);
+ border: var(--bf-border-width-default) solid var(--bf-color-border-default);
+ border-radius: var(--bf-radius-md);
+ background: var(--bf-color-surface-panel);
+ box-shadow: var(--bf-shadow-xs);
+ transition:
+ border-color var(--_tool-card-transition),
+ box-shadow var(--_tool-card-transition),
+ background-color var(--_tool-card-transition);
+ }
+
+ .prominentRoot:is(:hover, :focus-within),
+ .prominentRoot[data-bf-preview-state="hover"],
+ :global([data-bf-preview-state="hover"]) .prominentRoot {
+ border-color: var(--bf-color-border-strong);
+ box-shadow: var(--bf-shadow-sm);
+ }
+
+ .prominentRoot[data-bf-state~="confirmation"] {
+ border-color: var(--bf-color-status-warning-border);
+ background: var(--bf-color-status-warning-surface);
+ }
+
+ .prominentRoot[data-bf-status="cancelled"],
+ .prominentRoot[data-bf-status="rejected"] {
+ opacity: var(--bf-opacity-muted);
+ }
+
+ .surface {
+ box-sizing: border-box;
+ position: relative;
+ inline-size: 100%;
+ min-inline-size: 0;
+ }
+
+ .prominentSurface {
+ border: 0;
+ background: transparent;
+ color: inherit;
+ }
+
+ .header {
+ box-sizing: border-box;
+ display: flex;
+ inline-size: 100%;
+ min-inline-size: 0;
+ align-items: center;
+ }
+
+ .prominentHeader {
+ min-block-size: var(--bf-control-height-sm);
+ gap: var(--bf-space-2);
+ padding-block: var(--bf-space-1);
+ padding-inline: var(--bf-space-3) var(--bf-space-1);
+ line-height: var(--bf-line-height-tight);
+ }
+
+ .actionLabel,
+ .content,
+ .extra {
+ font-size: var(--bf-font-size-body);
+ }
+
+ .ambientAction,
+ .ambientContent,
+ .ambientExtra {
+ min-inline-size: 0;
+ font-size: var(--bf-font-size-small);
+ }
+
+ .actionLabel {
+ flex: 0 0 auto;
+ color: var(--bf-color-content-primary);
+ font-weight: var(--bf-font-weight-semibold);
+ white-space: nowrap;
+ }
+
+ .content {
+ display: flex;
+ overflow: hidden;
+ flex: 1 1 auto;
+ align-items: center;
+ gap: var(--bf-space-2);
+ color: var(--bf-color-content-secondary);
+ font-weight: var(--bf-font-weight-regular);
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .content > * {
+ min-inline-size: 0;
+ }
+
+ .extra,
+ .statusIcon,
+ .headerActions,
+ .actionRegion {
+ display: inline-flex;
+ flex: 0 0 auto;
+ align-items: center;
+ }
+
+ .extra {
+ min-inline-size: 0;
+ gap: var(--bf-space-1);
+ margin-inline-start: auto;
+ color: var(--bf-color-content-muted);
+ font-size: var(--bf-font-size-small);
+ }
+
+ .changeSummary {
+ display: inline-flex;
+ flex: 0 0 auto;
+ align-items: center;
+ gap: var(--bf-space-1);
+ color: var(--bf-color-content-muted);
+ font-family: var(--bf-font-family-mono);
+ font-size: var(--bf-font-size-small);
+ font-variant-numeric: tabular-nums;
+ font-weight: var(--bf-font-weight-semibold);
+ white-space: nowrap;
+ }
+
+ .changeSummary [data-bf-change="added"] {
+ color: var(--bf-color-status-success-content);
+ }
+
+ .changeSummary [data-bf-change="removed"] {
+ color: var(--bf-color-status-danger-content);
+ }
+
+ .actionRegion {
+ overflow: hidden;
+ max-inline-size: 0;
+ gap: var(--bf-space-1);
+ margin-inline-start: 0;
+ padding-inline-start: 0;
+ border-inline-start: var(--bf-border-width-default) solid transparent;
+ opacity: 0;
+ pointer-events: none;
+ transform: translateX(var(--bf-space-1));
+ transition:
+ max-inline-size var(--_tool-card-transition),
+ margin-inline-start var(--_tool-card-transition),
+ padding-inline-start var(--_tool-card-transition),
+ border-color var(--_tool-card-transition),
+ opacity var(--_tool-card-transition),
+ transform var(--_tool-card-transition);
+ }
+
+ .prominentRoot:is(:hover, :focus-within) .actionRegion,
+ .prominentRoot[data-bf-preview-state="hover"] .actionRegion,
+ :global([data-bf-preview-state="hover"]) .prominentRoot .actionRegion,
+ .prominentRoot[data-actions-visible="true"] .actionRegion {
+ max-inline-size: 24rem;
+ margin-inline-start: var(--bf-space-1);
+ padding-inline-start: var(--bf-space-2);
+ border-inline-start-color: var(--bf-color-border-subtle);
+ opacity: 1;
+ pointer-events: auto;
+ transform: translateX(0);
+ }
+
+ .affordanceButton {
+ display: inline-flex;
+ inline-size: var(--bf-space-6);
+ block-size: var(--bf-space-6);
+ flex: 0 0 auto;
+ align-items: center;
+ justify-content: center;
+ margin: 0;
+ padding: 0;
+ border: 0;
+ border-radius: var(--bf-radius-sm);
+ color: var(--bf-color-content-muted);
+ background: transparent;
+ cursor: pointer;
+ transition:
+ background-color var(--_tool-card-transition),
+ color var(--_tool-card-transition);
+ }
+
+ .affordanceButton:is(:hover, [data-bf-preview-state="hover"]) {
+ color: var(--bf-color-content-primary);
+ background: var(--bf-color-action-neutral-surface);
+ }
+
+ .affordanceButton:focus-visible {
+ outline: var(--bf-focus-width) solid var(--bf-color-focus-ring);
+ outline-offset: var(--bf-focus-offset);
+ }
+
+ .affordanceButton > :where(svg, img) {
+ display: block;
+ inline-size: var(--bf-font-size-base);
+ block-size: var(--bf-font-size-base);
+ }
+
+ .affordanceButton[aria-expanded="true"] :global(svg) {
+ transform: rotate(180deg);
+ }
+
+ .affordanceButton :global(svg) {
+ transition: transform var(--_tool-card-transition);
+ }
+
+ .headerActions {
+ gap: var(--bf-space-1);
+ }
+
+ .headerActions:empty {
+ display: none;
+ }
+
+ .trailingActions {
+ display: inline-flex;
+ flex: 0 0 auto;
+ align-items: center;
+ }
+
+ .trailingActions[data-divider="true"] {
+ margin-inline-start: var(--bf-space-1);
+ padding-inline-start: var(--bf-space-2);
+ border-inline-start: var(--bf-border-width-default) solid var(--bf-color-border-subtle);
+ }
+
+ .iconSlot {
+ position: relative;
+ display: inline-flex;
+ flex: 0 0 auto;
+ inline-size: var(--_tool-card-action-size);
+ block-size: var(--_tool-card-action-size);
+ align-items: center;
+ justify-content: center;
+ color: var(--bf-color-content-secondary);
+ }
+
+ .iconSlot[data-divider="true"] {
+ margin-inline-end: var(--bf-space-1);
+ border-inline-end: var(--bf-border-width-default) solid var(--bf-color-border-subtle);
+ }
+
+ .iconMarks {
+ position: relative;
+ display: inline-flex;
+ inline-size: var(--_icon-size);
+ block-size: var(--_icon-size);
+ align-items: center;
+ justify-content: center;
+ }
+
+ .mainIcon,
+ .inlineAffordance {
+ position: absolute;
+ inset: 0;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ transition:
+ opacity var(--_tool-card-transition),
+ color var(--_tool-card-transition),
+ transform var(--_tool-card-transition);
+ }
+
+ .mainIcon > :where(svg, img),
+ .inlineAffordance > :where(svg, img),
+ .statusIcon > :where(svg, img) {
+ display: block;
+ inline-size: 100%;
+ block-size: 100%;
+ }
+
+ .inlineAffordance {
+ color: var(--bf-color-content-secondary);
+ opacity: 0;
+ }
+
+ .inlineAffordance[data-expanded="true"] > :where(svg, img) {
+ transform: rotate(180deg);
+ }
+
+ .ambientRoot:is(:hover, :focus-within) .iconSlot[data-bf-expandable="true"] .mainIcon,
+ .ambientRoot[data-bf-preview-state="hover"] .iconSlot[data-bf-expandable="true"] .mainIcon,
+ :global([data-bf-preview-state="hover"]) .ambientRoot .iconSlot[data-bf-expandable="true"] .mainIcon {
+ opacity: 0;
+ }
+
+ .ambientRoot:is(:hover, :focus-within) .iconSlot[data-bf-expandable="true"] .inlineAffordance,
+ .ambientRoot[data-bf-preview-state="hover"] .iconSlot[data-bf-expandable="true"] .inlineAffordance,
+ :global([data-bf-preview-state="hover"]) .ambientRoot .iconSlot[data-bf-expandable="true"] .inlineAffordance {
+ opacity: 1;
+ }
+
+ .iconAffordanceHit {
+ position: absolute;
+ z-index: 1;
+ inset: 0;
+ margin: 0;
+ padding: 0;
+ border: 0;
+ border-radius: var(--bf-radius-sm);
+ outline: 0;
+ background: transparent;
+ cursor: pointer;
+ }
+
+ .iconAffordanceHit:focus-visible {
+ outline: var(--bf-focus-width) solid var(--bf-color-focus-ring);
+ outline-offset: var(--bf-focus-offset);
+ }
+
+ .statusIcon {
+ inline-size: var(--bf-font-size-lg);
+ block-size: var(--bf-font-size-lg);
+ justify-content: center;
+ color: var(--bf-color-content-muted);
+ }
+
+ .statusIcon[data-divider="true"] {
+ box-sizing: content-box;
+ margin-inline-start: var(--bf-space-1);
+ padding-inline-start: var(--bf-space-2);
+ border-inline-start: var(--bf-border-width-default) solid var(--bf-color-border-subtle);
+ }
+
+ .prominentRoot[data-bf-status="completed"] .statusIcon {
+ color: var(--bf-color-status-success-content);
+ }
+
+ .prominentRoot[data-bf-status="error"] .statusIcon {
+ color: var(--bf-color-status-danger-content);
+ }
+
+ .collapse {
+ display: grid;
+ grid-template-rows: 0fr;
+ opacity: 0;
+ visibility: hidden;
+ transition:
+ grid-template-rows var(--_tool-card-collapse-duration) var(--bf-motion-easing-standard),
+ opacity var(--bf-motion-duration-fast) var(--bf-motion-easing-standard),
+ visibility 0s linear var(--_tool-card-collapse-duration);
+ }
+
+ .collapse[data-open="true"] {
+ grid-template-rows: 1fr;
+ opacity: 1;
+ visibility: visible;
+ transition-delay: 0s;
+ }
+
+ .collapse[data-animate="false"] {
+ transition: none;
+ }
+
+ .collapseInner {
+ overflow: hidden;
+ min-block-size: 0;
+ }
+
+ .expanded,
+ .error {
+ box-sizing: border-box;
+ padding: var(--bf-space-3);
+ border-block-start: var(--bf-border-width-default) solid var(--bf-color-border-subtle);
+ color: var(--bf-color-content-secondary);
+ font-size: var(--bf-font-size-small);
+ line-height: var(--bf-line-height-base);
+ }
+
+ .error {
+ color: var(--bf-color-status-danger-content);
+ }
+
+ .ambientRoot {
+ gap: 0;
+ color: var(--bf-color-content-muted);
+ line-height: var(--bf-line-height-base);
+ }
+
+ .ambientRoot[data-bf-direct-action="true"] {
+ border-radius: var(--bf-radius-sm);
+ cursor: pointer;
+ }
+
+ .ambientRoot[data-bf-direct-action="true"]:focus-visible {
+ outline: var(--bf-focus-width) solid var(--bf-color-focus-ring);
+ outline-offset: var(--bf-focus-offset);
+ }
+
+ .ambientSurface {
+ display: flex;
+ min-block-size: var(--bf-control-height-sm);
+ align-items: center;
+ gap: var(--bf-space-1);
+ padding-inline: var(--bf-space-1) var(--bf-space-2);
+ border: 0;
+ background: transparent;
+ box-shadow: none;
+ cursor: default;
+ transition:
+ color var(--_tool-card-transition),
+ opacity var(--_tool-card-transition);
+ }
+
+ .ambientSurface[data-bf-interactive="true"] {
+ cursor: pointer;
+ }
+
+ .ambientSurface:is(:hover, :focus-within),
+ .ambientRoot[data-bf-preview-state="hover"] .ambientSurface,
+ :global([data-bf-preview-state="hover"]) .ambientRoot .ambientSurface {
+ color: var(--bf-color-content-primary);
+ }
+
+ .ambientSurface .iconSlot {
+ --_tool-card-action-size: var(--bf-space-5);
+
+ margin-inline-end: 1px;
+ }
+
+ .ambientAction {
+ flex: 0 0 auto;
+ color: inherit;
+ font-weight: var(--bf-font-weight-medium);
+ white-space: nowrap;
+ }
+
+ .ambientContent {
+ overflow: hidden;
+ flex: 1 1 auto;
+ color: inherit;
+ font-weight: var(--bf-font-weight-regular);
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .ambientExtra {
+ display: inline-flex;
+ flex: 0 0 auto;
+ align-items: center;
+ color: inherit;
+ }
+
+ .ambientExpandedShell {
+ overflow: hidden;
+ margin-block: var(--bf-space-1);
+ border: var(--bf-border-width-default) solid var(--bf-color-border-default);
+ border-radius: var(--bf-radius-md);
+ background: var(--bf-color-surface-panel);
+ box-shadow: var(--bf-shadow-xs);
+ }
+
+ .ambientExpandedShell .ambientSurface {
+ min-block-size: var(--bf-control-height-md);
+ padding-inline: var(--bf-space-2);
+ color: var(--bf-color-content-primary);
+ }
+
+ .ambientRoot[data-bf-state~="loading"] .ambientAction,
+ .ambientRoot[data-bf-state~="loading"] .ambientContent,
+ .prominentRoot[data-bf-state~="loading"] .actionLabel,
+ .prominentRoot[data-bf-state~="loading"] .content {
+ animation: bf-flow-chat-tool-loading var(--bf-motion-duration-loop) var(--bf-motion-easing-standard) infinite alternate;
+ }
+
+ @keyframes bf-flow-chat-tool-loading {
+ from {
+ opacity: 1;
+ }
+
+ to {
+ opacity: var(--bf-opacity-hover);
+ }
+ }
+
+ .ambientRoot[data-bf-status="error"] .ambientAction,
+ .ambientRoot[data-bf-status="error"] .ambientContent,
+ .ambientRoot[data-bf-status="error"] .ambientExtra {
+ color: var(--bf-color-status-danger-content);
+ }
+
+ @media (prefers-reduced-motion: reduce) {
+ .prominentRoot,
+ .ambientRoot,
+ .actionRegion,
+ .mainIcon,
+ .inlineAffordance,
+ .collapse,
+ .affordanceButton,
+ .affordanceButton :global(svg) {
+ transition: none;
+ }
+
+ .ambientRoot[data-bf-state~="loading"] .ambientAction,
+ .ambientRoot[data-bf-state~="loading"] .ambientContent,
+ .prominentRoot[data-bf-state~="loading"] .actionLabel,
+ .prominentRoot[data-bf-state~="loading"] .content {
+ animation: none;
+ }
+ }
+
+ @media (forced-colors: active) {
+ .prominentRoot,
+ .ambientExpandedShell {
+ border-color: CanvasText;
+ }
+
+ .iconAffordanceHit:focus-visible {
+ outline-color: Highlight;
+ }
+
+ .ambientRoot[data-bf-direct-action="true"]:focus-visible {
+ outline-color: Highlight;
+ }
+
+ .affordanceButton:focus-visible {
+ outline-color: Highlight;
+ }
+ }
+}
diff --git a/design-system/packages/ui/src/flow-chat/tool-cards/FlowChatToolCard.tsx b/design-system/packages/ui/src/flow-chat/tool-cards/FlowChatToolCard.tsx
new file mode 100644
index 0000000000..064fdb91f9
--- /dev/null
+++ b/design-system/packages/ui/src/flow-chat/tool-cards/FlowChatToolCard.tsx
@@ -0,0 +1,871 @@
+import {
+ createContext,
+ useContext,
+ useEffect,
+ useRef,
+ useState,
+ type CSSProperties,
+ type HTMLAttributes,
+ type KeyboardEvent as ReactKeyboardEvent,
+ type MouseEvent as ReactMouseEvent,
+ type ReactNode,
+} from "react";
+import { ArrowUpRight, ChevronDown } from "lucide-react";
+import { classNames } from "../../internal/classNames";
+import styles from "./FlowChatToolCard.module.css";
+
+export type FlowChatToolStatus =
+ | "pending"
+ | "queued"
+ | "waiting"
+ | "preparing"
+ | "streaming"
+ | "receiving"
+ | "running"
+ | "completed"
+ | "error"
+ | "cancelled"
+ | "rejected"
+ | "analyzing"
+ | "pending_confirmation"
+ | "confirmed";
+
+export type ToolCardHeaderAffordanceKind = "expand" | "open-panel-right";
+
+const LOADING_STATUSES = new Set([
+ "queued",
+ "waiting",
+ "preparing",
+ "streaming",
+ "receiving",
+ "running",
+ "analyzing",
+]);
+
+const TOOL_CARD_COLLAPSE_DURATION_MS = 300;
+
+interface HeaderLayoutContextValue {
+ affordanceKind: ToolCardHeaderAffordanceKind;
+ attention: "ambient" | "prominent";
+ expandable: boolean;
+ isExpanded: boolean;
+ onAffordanceClick?: (event: ReactMouseEvent) => void;
+}
+
+const HeaderLayoutContext = createContext({
+ affordanceKind: "expand",
+ attention: "ambient",
+ expandable: false,
+ isExpanded: false,
+});
+
+function getAppearanceState({
+ isExpanded,
+ isFailed,
+ isLoading,
+ requiresConfirmation,
+}: {
+ isExpanded: boolean;
+ isFailed: boolean;
+ isLoading: boolean;
+ requiresConfirmation: boolean;
+}): string | undefined {
+ const states = [
+ isExpanded && "expanded",
+ isFailed && "failed",
+ isLoading && "loading",
+ requiresConfirmation && "confirmation",
+ ].filter(Boolean);
+
+ return states.length > 0 ? states.join(" ") : undefined;
+}
+
+function shouldIgnoreToggleClick(
+ event: ReactMouseEvent,
+ root: HTMLElement,
+): boolean {
+ if (event.defaultPrevented || event.button !== 0) {
+ return true;
+ }
+
+ const target = event.target as { closest?: (selectors: string) => Element | null } | null;
+ if (
+ typeof target?.closest === "function" &&
+ target.closest("button,a,input,textarea,select,[contenteditable='true'],[data-flow-card-ignore-toggle]")
+ ) {
+ return true;
+ }
+
+ const selection = root.ownerDocument.defaultView?.getSelection?.();
+ if (!selection || selection.isCollapsed || !selection.toString().trim()) {
+ return false;
+ }
+
+ const anchorInside = selection.anchorNode ? root.contains(selection.anchorNode) : false;
+ const focusInside = selection.focusNode ? root.contains(selection.focusNode) : false;
+ return anchorInside || focusInside;
+}
+
+interface CollapsibleRegionProps {
+ children?: ReactNode;
+ className?: string;
+ disableAnimation?: boolean;
+ isOpen: boolean;
+ part: "error" | "expanded";
+ status: FlowChatToolStatus;
+}
+
+type CollapsePhase = "closed" | "closing" | "open" | "opening";
+
+function CollapsibleRegion({
+ children,
+ className,
+ disableAnimation = false,
+ isOpen,
+ part,
+ status,
+}: CollapsibleRegionProps) {
+ const hasContent = children !== undefined && children !== null && children !== false;
+ const open = Boolean(isOpen && hasContent);
+ const hasMountedRef = useRef(false);
+ const [phase, setPhase] = useState(() => (open ? "open" : "closed"));
+ const [visuallyOpen, setVisuallyOpen] = useState(open);
+
+ useEffect(() => {
+ if (!hasMountedRef.current) {
+ hasMountedRef.current = true;
+ return;
+ }
+
+ const prefersReducedMotion =
+ typeof window !== "undefined" &&
+ (window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false);
+ const shouldAnimate = !disableAnimation && !prefersReducedMotion;
+
+ if (!shouldAnimate) {
+ setPhase(open ? "open" : "closed");
+ setVisuallyOpen(open);
+ return;
+ }
+
+ let frameId: number | undefined;
+ let timeoutId: ReturnType | undefined;
+
+ if (open) {
+ setPhase((currentPhase) => currentPhase === "open" ? currentPhase : "opening");
+ setVisuallyOpen(false);
+
+ if (typeof window !== "undefined" && typeof window.requestAnimationFrame === "function") {
+ frameId = window.requestAnimationFrame(() => setVisuallyOpen(true));
+ } else {
+ setVisuallyOpen(true);
+ }
+
+ timeoutId = setTimeout(() => setPhase("open"), TOOL_CARD_COLLAPSE_DURATION_MS);
+ } else {
+ setVisuallyOpen(false);
+ setPhase((currentPhase) => currentPhase === "closed" ? currentPhase : "closing");
+ timeoutId = setTimeout(() => setPhase("closed"), TOOL_CARD_COLLAPSE_DURATION_MS);
+ }
+
+ return () => {
+ if (frameId !== undefined && typeof window !== "undefined") {
+ window.cancelAnimationFrame(frameId);
+ }
+ if (timeoutId !== undefined) {
+ clearTimeout(timeoutId);
+ }
+ };
+ }, [disableAnimation, open]);
+
+ if (!hasContent) {
+ return null;
+ }
+
+ const shouldRender = open || phase !== "closed";
+
+ return (
+
+ {shouldRender && (
+
+ )}
+
+ );
+}
+
+export interface ProminentToolCardProps
+ extends Omit, "children" | "onClick"> {
+ className?: string;
+ disableExpandAnimation?: boolean;
+ errorContent?: ReactNode;
+ expandedContent?: ReactNode;
+ header: ReactNode;
+ headerAffordanceKind?: ToolCardHeaderAffordanceKind;
+ headerExpandAffordance?: boolean;
+ isExpanded?: boolean;
+ isFailed?: boolean;
+ onClick?: (event: ReactMouseEvent) => void;
+ requiresConfirmation?: boolean;
+ status: FlowChatToolStatus;
+ toggleTestId?: string;
+}
+
+export function ProminentToolCard({
+ className,
+ disableExpandAnimation = false,
+ errorContent,
+ expandedContent,
+ header,
+ headerAffordanceKind = "expand",
+ headerExpandAffordance,
+ isExpanded = false,
+ isFailed = false,
+ onClick,
+ requiresConfirmation = false,
+ status,
+ toggleTestId,
+ ...props
+}: ProminentToolCardProps) {
+ const failed = isFailed || status === "error";
+ const expandable = headerExpandAffordance ?? Boolean(onClick && expandedContent && !failed);
+ const loading = LOADING_STATUSES.has(status);
+ const confirmation = requiresConfirmation && ![
+ "completed",
+ "confirmed",
+ "cancelled",
+ "rejected",
+ "error",
+ ].includes(status);
+ const appearanceState = getAppearanceState({
+ isExpanded,
+ isFailed: failed,
+ isLoading: loading,
+ requiresConfirmation: confirmation,
+ });
+
+ const handleSurfaceClick = (event: ReactMouseEvent) => {
+ if (!onClick || shouldIgnoreToggleClick(event, event.currentTarget)) {
+ return;
+ }
+ onClick(event);
+ };
+
+ return (
+
+
+
+ {header}
+
+
+
+
+ {expandedContent}
+
+
+
+ {errorContent}
+
+
+ );
+}
+
+export interface AmbientToolCardProps
+ extends Omit, "children" | "onClick"> {
+ className?: string;
+ expandedContent?: ReactNode;
+ header: ReactNode;
+ isExpanded?: boolean;
+ onClick?: (event: ReactMouseEvent) => void;
+ status: FlowChatToolStatus;
+ toggleTestId?: string;
+}
+
+export function AmbientToolCard({
+ className,
+ expandedContent,
+ header,
+ isExpanded = false,
+ onClick,
+ onKeyDown: onRootKeyDown,
+ role,
+ status,
+ tabIndex,
+ toggleTestId,
+ ...props
+}: AmbientToolCardProps) {
+ const hasExpandedContent = Boolean(expandedContent);
+ const collapseTimerRef = useRef | null>(null);
+ const [keepExpandedShell, setKeepExpandedShell] = useState(
+ Boolean(isExpanded && hasExpandedContent),
+ );
+
+ useEffect(() => {
+ if (collapseTimerRef.current !== null) {
+ clearTimeout(collapseTimerRef.current);
+ collapseTimerRef.current = null;
+ }
+
+ if (isExpanded && hasExpandedContent) {
+ setKeepExpandedShell(true);
+ return;
+ }
+
+ if (!hasExpandedContent) {
+ setKeepExpandedShell(false);
+ return;
+ }
+
+ if (keepExpandedShell) {
+ collapseTimerRef.current = setTimeout(() => {
+ collapseTimerRef.current = null;
+ setKeepExpandedShell(false);
+ }, TOOL_CARD_COLLAPSE_DURATION_MS);
+ }
+ }, [hasExpandedContent, isExpanded, keepExpandedShell]);
+
+ useEffect(() => () => {
+ if (collapseTimerRef.current !== null) {
+ clearTimeout(collapseTimerRef.current);
+ }
+ }, []);
+
+ const loading = LOADING_STATUSES.has(status);
+ const expandedShell = keepExpandedShell || Boolean(isExpanded && hasExpandedContent);
+ const interactive = Boolean(onClick);
+ const directAction = interactive && !hasExpandedContent;
+ const appearanceState = getAppearanceState({
+ isExpanded,
+ isFailed: status === "error",
+ isLoading: loading,
+ requiresConfirmation: false,
+ });
+ const expandable = interactive && hasExpandedContent;
+
+ const handleSurfaceClick = (event: ReactMouseEvent) => {
+ if (!onClick || shouldIgnoreToggleClick(event, event.currentTarget)) {
+ return;
+ }
+ onClick(event);
+ };
+
+ const handleDirectActionKeyDown = (event: ReactKeyboardEvent) => {
+ onRootKeyDown?.(event);
+ if (!directAction || event.defaultPrevented || (event.key !== "Enter" && event.key !== " ")) {
+ return;
+ }
+
+ event.preventDefault();
+ const surface = event.currentTarget.querySelector(
+ '[data-bf-component="flow-chat-tool-card"][data-bf-part="surface"]',
+ );
+ surface?.click();
+ };
+
+ return (
+
+
+
+ {header}
+
+
+
+
+ {expandedContent}
+
+
+ );
+}
+
+export interface ToolCardIconSlotProps {
+ affordanceKind?: ToolCardHeaderAffordanceKind;
+ className?: string;
+ expandable?: boolean;
+ icon: ReactNode;
+ iconClassName?: string;
+ isExpanded?: boolean;
+ onAffordanceClick?: (event: ReactMouseEvent) => void;
+ showDivider?: boolean;
+}
+
+export function ToolCardIconSlot({
+ affordanceKind,
+ className,
+ expandable,
+ icon,
+ iconClassName,
+ isExpanded,
+ onAffordanceClick,
+ showDivider = false,
+}: ToolCardIconSlotProps) {
+ const layout = useContext(HeaderLayoutContext);
+ const resolvedExpandable = expandable ?? layout.expandable;
+ const resolvedKind = affordanceKind ?? layout.affordanceKind;
+ const resolvedExpanded = isExpanded ?? layout.isExpanded;
+ const handleAffordance = onAffordanceClick ?? layout.onAffordanceClick;
+ const showInlineAffordance = layout.attention === "ambient" && resolvedExpandable;
+ const isPanelAffordance = resolvedKind === "open-panel-right";
+
+ return (
+
+
+
+ {icon}
+
+ {showInlineAffordance && (
+
+ {isPanelAffordance
+ ?
+ : }
+
+ )}
+
+ {showInlineAffordance && handleAffordance && (
+
+ );
+}
+
+export interface ToolCardStatusIconProps {
+ className?: string;
+ icon: ReactNode;
+ withDivider?: boolean;
+}
+
+export function ToolCardStatusIcon({
+ className,
+ icon,
+ withDivider = false,
+}: ToolCardStatusIconProps) {
+ return (
+
+ {icon}
+
+ );
+}
+
+export interface ToolCardHeaderActionsProps {
+ children: ReactNode;
+ className?: string;
+}
+
+export function ToolCardHeaderActions({ children, className }: ToolCardHeaderActionsProps) {
+ return (
+ event.stopPropagation()}
+ >
+ {children}
+
+ );
+}
+
+export interface ToolCardChangeSummaryProps
+ extends Omit, "children"> {
+ additions?: number | string;
+ deletions?: number | string;
+}
+
+export function ToolCardChangeSummary({
+ additions,
+ className,
+ deletions,
+ ...props
+}: ToolCardChangeSummaryProps) {
+ const hasAdditions = additions !== undefined && additions !== null && additions !== "";
+ const hasDeletions = deletions !== undefined && deletions !== null && deletions !== "";
+
+ if (!hasAdditions && !hasDeletions) {
+ return null;
+ }
+
+ return (
+
+ {hasAdditions && (
+ +{additions}
+ )}
+ {hasDeletions && (
+ -{deletions}
+ )}
+
+ );
+}
+
+export interface ProminentToolCardHeaderProps {
+ action?: ReactNode;
+ actionDataAttributes?: Record<`data-${string}`, boolean | number | string | undefined>;
+ actionTestId?: string;
+ actions?: ReactNode;
+ affordanceKind?: ToolCardHeaderAffordanceKind;
+ content?: ReactNode;
+ expandAffordance?: boolean;
+ extra?: ReactNode;
+ headerExpanded?: boolean;
+ icon?: ReactNode;
+ iconClassName?: string;
+ onAffordanceClick?: (event: ReactMouseEvent) => void;
+ statusIcon?: ReactNode;
+ trailingActions?: ReactNode;
+}
+
+export function ProminentToolCardHeader({
+ action,
+ actionDataAttributes,
+ actionTestId,
+ actions,
+ affordanceKind,
+ content,
+ expandAffordance,
+ extra,
+ headerExpanded,
+ icon,
+ iconClassName,
+ onAffordanceClick,
+ statusIcon,
+ trailingActions,
+}: ProminentToolCardHeaderProps) {
+ const layout = useContext(HeaderLayoutContext);
+ const expandable = expandAffordance ?? layout.expandable;
+ const resolvedKind = affordanceKind ?? layout.affordanceKind;
+ const expanded = headerExpanded ?? layout.isExpanded;
+ const handleAffordance = onAffordanceClick ?? layout.onAffordanceClick;
+ const affordanceAction = expandable ? handleAffordance : undefined;
+ const hasActionRegion = Boolean(actions || affordanceAction || trailingActions);
+
+ return (
+
+ {icon !== undefined && icon !== null && icon !== false && icon !== "" && (
+
+ )}
+ {action !== undefined && action !== null && action !== false && action !== "" && (
+
+ {action}
+
+ )}
+ {content !== undefined && content !== null && content !== false && (
+
+ {content}
+
+ )}
+ {extra !== undefined && extra !== null && extra !== false && (
+
+ {extra}
+
+ )}
+ {statusIcon !== undefined && statusIcon !== null && statusIcon !== false && (
+
+ )}
+ {hasActionRegion && (
+
+ {actions}
+ {affordanceAction && (
+
+ )}
+ {trailingActions !== undefined && trailingActions !== null && trailingActions !== false && (
+
+ {trailingActions}
+
+ )}
+
+ )}
+
+ );
+}
+
+export interface AmbientToolCardHeaderProps {
+ action?: ReactNode;
+ affordanceKind?: ToolCardHeaderAffordanceKind;
+ content?: ReactNode;
+ expandable?: boolean;
+ extra?: ReactNode;
+ icon?: ReactNode;
+ iconClassName?: string;
+ isExpanded?: boolean;
+ onAffordanceClick?: (event: ReactMouseEvent) => void;
+ rightStatusIcon?: ReactNode;
+ rightStatusIconWithDivider?: boolean;
+ showDivider?: boolean;
+}
+
+export function AmbientToolCardHeader({
+ action,
+ affordanceKind = "expand",
+ content,
+ expandable,
+ extra,
+ icon,
+ iconClassName,
+ isExpanded,
+ onAffordanceClick,
+ rightStatusIcon,
+ rightStatusIconWithDivider = false,
+ showDivider = false,
+}: AmbientToolCardHeaderProps) {
+ const layout = useContext(HeaderLayoutContext);
+
+ return (
+ <>
+ {icon !== undefined && icon !== null && icon !== false && icon !== "" && (
+
+ )}
+ {action !== undefined && action !== null && action !== false && action !== "" && (
+
+ {action}
+
+ )}
+ {content !== undefined && content !== null && content !== false && (
+
+ {content}
+
+ )}
+ {extra !== undefined && extra !== null && extra !== false && (
+
+ {extra}
+
+ )}
+ {rightStatusIcon !== undefined && rightStatusIcon !== null && rightStatusIcon !== false && (
+
+ )}
+ >
+ );
+}
diff --git a/design-system/packages/ui/src/flow-chat/tool-cards/ProminentToolCard.meta.ts b/design-system/packages/ui/src/flow-chat/tool-cards/ProminentToolCard.meta.ts
new file mode 100644
index 0000000000..57d1bbd4dd
--- /dev/null
+++ b/design-system/packages/ui/src/flow-chat/tool-cards/ProminentToolCard.meta.ts
@@ -0,0 +1,45 @@
+import type { ComponentMeta } from "../../registry.types";
+
+export const prominentToolCardMeta = {
+ category: "flow-chat",
+ description: "An attention-worthy tool result with a stable summary region, hover-revealed actions, and controlled detail disclosure.",
+ maturity: "stable",
+ name: "ProminentToolCard",
+ props: [
+ { name: "status", type: "FlowChatToolStatus" },
+ { name: "header", type: "ReactNode" },
+ { defaultValue: "false", name: "isExpanded", type: "boolean" },
+ { name: "expandedContent", type: "ReactNode" },
+ { name: "errorContent", type: "ReactNode" },
+ { defaultValue: "false", name: "requiresConfirmation", type: "boolean" },
+ ],
+ states: ["default", "hover", "loading", "expanded", "error", "confirmation"],
+ tokens: [
+ "color.action.neutral.surface",
+ "color.content.primary",
+ "color.content.secondary",
+ "color.content.muted",
+ "color.surface.panel",
+ "color.border.default",
+ "color.border.strong",
+ "color.border.subtle",
+ "color.focus.ring",
+ "color.status.danger.content",
+ "color.status.success.content",
+ "color.status.warning.border",
+ "color.status.warning.surface",
+ "control.height.sm",
+ "font.family.control",
+ "font.size.base",
+ "font.size.body",
+ "font.size.lg",
+ "font.size.small",
+ "font.size.xl",
+ "font.weight.regular",
+ "font.weight.semibold",
+ "radius.md",
+ "radius.sm",
+ "shadow.xs",
+ "shadow.sm",
+ ],
+} as const satisfies ComponentMeta;
diff --git a/design-system/packages/ui/src/flow-chat/tool-cards/ProminentToolCards.meta.ts b/design-system/packages/ui/src/flow-chat/tool-cards/ProminentToolCards.meta.ts
new file mode 100644
index 0000000000..8afced3630
--- /dev/null
+++ b/design-system/packages/ui/src/flow-chat/tool-cards/ProminentToolCards.meta.ts
@@ -0,0 +1,92 @@
+import type { ComponentMeta } from "../../registry.types";
+
+const prominentTokens = [
+ "color.content.primary",
+ "color.content.secondary",
+ "color.content.muted",
+ "color.surface.canvas",
+ "color.surface.raised",
+ "color.border.default",
+ "color.status.danger.content",
+ "color.status.success.content",
+ "color.status.warning.content",
+ "font.family.control",
+ "font.family.mono",
+ "font.size.small",
+ "font.weight.medium",
+ "font.weight.regular",
+ "radius.sm",
+ "shadow.xs",
+] as const;
+
+const expandableProps = [
+ { name: "status", type: "FlowChatToolStatus" },
+ { defaultValue: "false", name: "isExpanded", type: "boolean" },
+ { name: "onToggle", type: "() => void" },
+] as const;
+
+export const agentControlToolCardMeta = {
+ category: "flow-chat",
+ description: "A prominent subagent card with an openable identity pill, lifecycle state, and prompt detail.",
+ maturity: "stable",
+ name: "AgentControlToolCard",
+ props: expandableProps,
+ states: ["default", "hover", "loading", "expanded", "error"],
+ tokens: prominentTokens,
+} as const satisfies ComponentMeta;
+
+export const fileDiffToolCardMeta = {
+ category: "flow-chat",
+ description: "A prominent file-diff card with a compact file path, change summary, message, and renderer slot.",
+ maturity: "stable",
+ name: "FileDiffToolCard",
+ props: [
+ ...expandableProps,
+ { name: "path", type: "string" },
+ { name: "pathLabel", type: "ReactNode" },
+ { name: "changeSummary", type: "FileDiffToolCardProps['changeSummary']" },
+ { name: "preview", type: "ReactNode" },
+ ],
+ states: ["default", "hover", "loading", "expanded", "error"],
+ tokens: prominentTokens,
+} as const satisfies ComponentMeta;
+
+export const gitToolCardMeta = {
+ category: "flow-chat",
+ description: "A prominent Git command card with output, warning, failure, and execution metadata.",
+ maturity: "stable",
+ name: "GitToolCard",
+ props: expandableProps,
+ states: ["default", "hover", "loading", "expanded", "error"],
+ tokens: prominentTokens,
+} as const satisfies ComponentMeta;
+
+export const pageDeployToolCardMeta = {
+ category: "flow-chat",
+ description: "A prominent page-deploy card with version metadata and host-provided actions.",
+ maturity: "stable",
+ name: "PageDeployToolCard",
+ props: expandableProps,
+ states: ["default", "hover", "loading", "expanded", "error"],
+ tokens: prominentTokens,
+} as const satisfies ComponentMeta;
+
+export const pagePublishToolCardMeta = {
+ category: "flow-chat",
+ description: "A prominent page-publish card with production and preview metadata and actions.",
+ maturity: "stable",
+ name: "PagePublishToolCard",
+ props: expandableProps,
+ states: ["default", "hover", "loading", "expanded", "error"],
+ tokens: prominentTokens,
+} as const satisfies ComponentMeta;
+
+export const reviewSummaryToolCardMeta = {
+ category: "flow-chat",
+ description: "A prominent review summary with changed files, status, and a host navigation action.",
+ maturity: "stable",
+ name: "ReviewSummaryToolCard",
+ props: expandableProps,
+ states: ["default", "hover", "loading", "expanded", "error"],
+ tokens: prominentTokens,
+} as const satisfies ComponentMeta;
diff --git a/design-system/packages/ui/src/flow-chat/tool-cards/ProminentToolCards.module.css b/design-system/packages/ui/src/flow-chat/tool-cards/ProminentToolCards.module.css
new file mode 100644
index 0000000000..1208a483dc
--- /dev/null
+++ b/design-system/packages/ui/src/flow-chat/tool-cards/ProminentToolCards.module.css
@@ -0,0 +1,335 @@
+@layer bf.components {
+ .command {
+ display: block;
+ min-inline-size: 0;
+ overflow: hidden;
+ color: var(--bf-color-content-secondary);
+ font-family: var(--bf-font-family-mono);
+ font-size: var(--bf-font-size-small);
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .summary,
+ .fileCount {
+ display: inline-flex;
+ min-inline-size: 0;
+ align-items: center;
+ gap: var(--bf-space-1);
+ color: var(--bf-color-content-muted);
+ font-size: var(--bf-font-size-small);
+ }
+
+ .summary {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .summary[data-tone="warning"] {
+ color: var(--bf-color-status-warning-content);
+ }
+
+ .summary[data-tone="danger"] {
+ color: var(--bf-color-status-danger-content);
+ }
+
+ .output {
+ display: flex;
+ max-block-size: 20rem;
+ min-inline-size: 0;
+ flex-direction: column;
+ gap: var(--bf-space-3);
+ overflow: auto;
+ }
+
+ .outputBlock,
+ .textPreview {
+ margin: 0;
+ padding: var(--bf-space-3);
+ border-radius: var(--bf-radius-sm);
+ background: var(--bf-color-surface-canvas);
+ color: var(--bf-color-content-secondary);
+ font-family: var(--bf-font-family-mono);
+ font-size: var(--bf-font-size-small);
+ line-height: var(--bf-line-height-body);
+ white-space: pre-wrap;
+ overflow-wrap: anywhere;
+ }
+
+ .outputGroup {
+ display: flex;
+ flex-direction: column;
+ gap: var(--bf-space-2);
+ }
+
+ .outputLabel,
+ .sectionLabel {
+ color: var(--bf-color-content-muted);
+ font-size: var(--bf-font-size-small);
+ font-weight: var(--bf-font-weight-semibold);
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ }
+
+ .outputGroup[data-tone="warning"] .outputBlock {
+ color: var(--bf-color-status-warning-content);
+ background: color-mix(in srgb, var(--bf-color-status-warning-content) 7%, transparent);
+ }
+
+ .outputGroup[data-tone="danger"] .outputBlock {
+ color: var(--bf-color-status-danger-content);
+ background: color-mix(in srgb, var(--bf-color-status-danger-content) 7%, transparent);
+ }
+
+ .footer {
+ display: flex;
+ min-inline-size: 0;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: var(--bf-space-2);
+ padding: var(--bf-space-2) var(--bf-space-3);
+ border-radius: var(--bf-radius-sm);
+ background: var(--bf-color-surface-raised);
+ color: var(--bf-color-content-muted);
+ font-size: var(--bf-font-size-small);
+ }
+
+ .footerItem {
+ display: inline-flex;
+ min-inline-size: 0;
+ align-items: baseline;
+ gap: var(--bf-space-1);
+ }
+
+ .footerItem[data-grow="true"] {
+ flex: 1 1 12rem;
+ }
+
+ .footerLabel {
+ font-weight: var(--bf-font-weight-semibold);
+ }
+
+ .footerValue {
+ min-inline-size: 0;
+ overflow: hidden;
+ color: var(--bf-color-content-secondary);
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .footerItem[data-monospace="true"] .footerValue {
+ font-family: var(--bf-font-family-mono);
+ }
+
+ .footerItem[data-tone="success"] .footerValue {
+ color: var(--bf-color-status-success-content);
+ }
+
+ .footerItem[data-tone="danger"] .footerValue {
+ color: var(--bf-color-status-danger-content);
+ }
+
+ .error {
+ color: var(--bf-color-status-danger-content);
+ font-size: var(--bf-font-size-small);
+ line-height: var(--bf-line-height-body);
+ white-space: pre-wrap;
+ overflow-wrap: anywhere;
+ }
+
+ .errorMeta {
+ margin-block-start: var(--bf-space-2);
+ color: var(--bf-color-content-muted);
+ font-family: var(--bf-font-family-mono);
+ font-size: var(--bf-font-size-small);
+ }
+
+ .diffPath {
+ display: block;
+ overflow: hidden;
+ min-inline-size: 0;
+ color: var(--bf-color-content-secondary);
+ font-family: var(--bf-font-family-mono);
+ font-size: var(--bf-font-size-small);
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .diffBody {
+ display: flex;
+ min-inline-size: 0;
+ flex-direction: column;
+ gap: var(--bf-space-3);
+ }
+
+ .message {
+ padding-block-end: var(--bf-space-2);
+ border-block-end: 1px solid var(--bf-color-border-subtle);
+ color: var(--bf-color-content-muted);
+ font-size: var(--bf-font-size-small);
+ }
+
+ .reviewDetails,
+ .lifecycleDetails {
+ display: flex;
+ min-inline-size: 0;
+ flex-direction: column;
+ gap: var(--bf-space-3);
+ }
+
+ .reviewSummary {
+ margin: 0;
+ color: var(--bf-color-content-secondary);
+ font-size: var(--bf-font-size-small);
+ line-height: var(--bf-line-height-body);
+ }
+
+ .fileList {
+ display: grid;
+ gap: var(--bf-space-1);
+ margin: 0;
+ padding-inline-start: var(--bf-space-4);
+ color: var(--bf-color-content-secondary);
+ font-family: var(--bf-font-family-mono);
+ font-size: var(--bf-font-size-small);
+ line-height: var(--bf-line-height-body);
+ }
+
+ .actions {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: var(--bf-space-2);
+ }
+
+ .field {
+ display: grid;
+ min-inline-size: 0;
+ grid-template-columns: minmax(6rem, auto) minmax(0, 1fr);
+ gap: var(--bf-space-2);
+ color: var(--bf-color-content-secondary);
+ font-size: var(--bf-font-size-small);
+ }
+
+ .fieldLabel {
+ color: var(--bf-color-content-muted);
+ font-weight: var(--bf-font-weight-semibold);
+ }
+
+ .fieldValue {
+ min-inline-size: 0;
+ overflow-wrap: anywhere;
+ }
+
+ .field[data-monospace="true"] .fieldValue {
+ font-family: var(--bf-font-family-mono);
+ }
+
+ .agentHeader {
+ display: flex;
+ inline-size: 100%;
+ min-inline-size: 0;
+ align-items: center;
+ gap: var(--bf-space-2);
+ }
+
+ .agentPill {
+ display: inline-flex;
+ min-inline-size: 0;
+ max-inline-size: min(52%, 15rem);
+ align-items: center;
+ gap: var(--bf-space-2);
+ padding: var(--bf-space-1) var(--bf-space-2) var(--bf-space-1) var(--bf-space-1);
+ border: 1px solid var(--bf-color-border-default);
+ border-radius: var(--bf-radius-pill);
+ background: var(--bf-color-surface-raised);
+ color: var(--bf-color-content-primary);
+ font: inherit;
+ }
+
+ button.agentPill {
+ cursor: pointer;
+ }
+
+ button.agentPill:is(:hover, :focus-visible) {
+ border-color: var(--bf-color-border-strong);
+ outline: none;
+ box-shadow: 0 0 0 2px var(--bf-color-focus-ring);
+ }
+
+ .agentAvatar {
+ display: inline-flex;
+ inline-size: 1.375rem;
+ block-size: 1.375rem;
+ flex: 0 0 1.375rem;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .agentName,
+ .agentStatus {
+ min-inline-size: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .agentName {
+ font-size: var(--bf-font-size-small);
+ font-weight: var(--bf-font-weight-medium);
+ }
+
+ .agentStatus {
+ color: var(--bf-color-content-muted);
+ font-size: var(--bf-font-size-small);
+ }
+
+ .agentStatus[data-tone="success"] {
+ color: var(--bf-color-status-success-content);
+ }
+
+ .agentStatus[data-tone="warning"] {
+ color: var(--bf-color-status-warning-content);
+ }
+
+ .agentStatus[data-tone="danger"] {
+ color: var(--bf-color-status-danger-content);
+ }
+
+ .agentIndicator {
+ display: inline-flex;
+ inline-size: 1.125rem;
+ block-size: 1.125rem;
+ margin-inline-start: auto;
+ flex: 0 0 1.125rem;
+ align-items: center;
+ justify-content: center;
+ color: var(--bf-color-content-muted);
+ opacity: 0;
+ transition: opacity var(--bf-motion-duration-fast) var(--bf-motion-easing-standard);
+ }
+
+ .agentRoot:is(:hover, :focus-within) .agentIndicator,
+ .agentRoot[data-bf-preview-state="hover"] .agentIndicator {
+ opacity: 1;
+ }
+
+ .agentPrompt {
+ max-block-size: 15rem;
+ overflow: auto;
+ padding: var(--bf-space-3);
+ border: 1px solid var(--bf-color-border-default);
+ border-radius: var(--bf-radius-sm);
+ background: var(--bf-color-surface-canvas);
+ color: var(--bf-color-content-secondary);
+ font-size: var(--bf-font-size-small);
+ line-height: var(--bf-line-height-body);
+ }
+
+ @media (prefers-reduced-motion: reduce) {
+ .agentIndicator {
+ transition: none;
+ }
+ }
+}
diff --git a/design-system/packages/ui/src/flow-chat/tool-cards/ProminentToolCards.tsx b/design-system/packages/ui/src/flow-chat/tool-cards/ProminentToolCards.tsx
new file mode 100644
index 0000000000..8ce8b5b9eb
--- /dev/null
+++ b/design-system/packages/ui/src/flow-chat/tool-cards/ProminentToolCards.tsx
@@ -0,0 +1,427 @@
+import type {
+ HTMLAttributes,
+ MouseEvent as ReactMouseEvent,
+ ReactNode,
+} from "react";
+import {
+ Bot,
+ ChevronDown,
+ ChevronRight,
+ FileText,
+ GitBranch,
+ GitCompare,
+ Rocket,
+ SearchCheck,
+ Sparkles,
+} from "lucide-react";
+import { classNames } from "../../internal/classNames";
+import {
+ ProminentToolCard,
+ ProminentToolCardHeader,
+ ToolCardChangeSummary,
+ ToolCardHeaderActions,
+ type FlowChatToolStatus,
+} from "./FlowChatToolCard";
+import { ToolProcessingDots } from "./ToolProcessingDots";
+import styles from "./ProminentToolCards.module.css";
+
+interface ProminentCardProps
+ extends Omit, "children" | "onClick" | "title"> {
+ isExpanded?: boolean;
+ onToggle?: () => void;
+ status: FlowChatToolStatus;
+}
+
+export interface GitToolCardFooterItem {
+ grow?: boolean;
+ label?: ReactNode;
+ monospace?: boolean;
+ tone?: "danger" | "neutral" | "success";
+ value: ReactNode;
+}
+
+export interface GitToolCardProps extends ProminentCardProps {
+ action: ReactNode;
+ command: ReactNode;
+ error?: ReactNode;
+ errorMeta?: ReactNode;
+ footerItems?: readonly GitToolCardFooterItem[];
+ headerActions?: ReactNode;
+ loading?: boolean;
+ statusSummary?: ReactNode;
+ statusTone?: "danger" | "neutral" | "warning";
+ stderr?: ReactNode;
+ stderrLabel?: ReactNode;
+ stderrTone?: "danger" | "warning";
+ stdout?: ReactNode;
+}
+
+export function GitToolCard({
+ action,
+ command,
+ error,
+ errorMeta,
+ footerItems = [],
+ headerActions,
+ isExpanded = false,
+ loading = false,
+ onToggle,
+ status,
+ statusSummary,
+ statusTone = "neutral",
+ stderr,
+ stderrLabel,
+ stderrTone = "danger",
+ stdout,
+ ...props
+}: GitToolCardProps) {
+ const hasDetails = Boolean(stdout || stderr || error || footerItems.length > 0);
+ const body = hasDetails ? (
+
+ {stdout &&
{stdout}}
+ {stderr && (
+
+ {stderrLabel &&
{stderrLabel}}
+
{stderr}
+
+ )}
+ {error && (
+
+ {error}
+ {errorMeta &&
{errorMeta}
}
+
+ )}
+ {footerItems.length > 0 && (
+
+ {footerItems.map((item, index) => (
+
+ {item.label && {item.label}}
+ {item.value}
+
+ ))}
+
+ )}
+
+ ) : undefined;
+
+ return (
+ {headerActions} : undefined}
+ content={{command}}
+ extra={statusSummary ? (
+ {statusSummary}
+ ) : undefined}
+ icon={}
+ statusIcon={loading ? : undefined}
+ />
+ )}
+ headerExpandAffordance={hasDetails}
+ isExpanded={Boolean(isExpanded && hasDetails && status !== "error")}
+ onClick={hasDetails && onToggle ? onToggle : undefined}
+ status={status}
+ />
+ );
+}
+
+export interface FileDiffToolCardProps extends ProminentCardProps {
+ action: ReactNode;
+ changeSummary?: {
+ additions: number | string;
+ deletions: number | string;
+ label: string;
+ };
+ error?: ReactNode;
+ loading?: boolean;
+ message?: ReactNode;
+ path: string;
+ pathLabel: ReactNode;
+ preview?: ReactNode;
+ textPreview?: ReactNode;
+}
+
+export function FileDiffToolCard({
+ action,
+ changeSummary,
+ error,
+ isExpanded = false,
+ loading = false,
+ message,
+ onToggle,
+ path,
+ pathLabel,
+ preview,
+ status,
+ textPreview,
+ ...props
+}: FileDiffToolCardProps) {
+ const body = preview || textPreview || message ? (
+
+ {message &&
{message}
}
+ {preview}
+ {textPreview &&
{textPreview}}
+
+ ) : undefined;
+ return (
+ {error} : undefined}
+ expandedContent={body}
+ header={(
+
+ {pathLabel}
+
+ )}
+ extra={changeSummary ? (
+
+ ) : undefined}
+ icon={}
+ statusIcon={loading ? : undefined}
+ />
+ )}
+ headerExpandAffordance={Boolean(body)}
+ isExpanded={Boolean(isExpanded && body && status !== "error")}
+ onClick={body && onToggle ? onToggle : undefined}
+ status={status}
+ />
+ );
+}
+
+export interface ReviewSummaryToolCardProps extends ProminentCardProps {
+ action?: ReactNode;
+ changedFiles?: readonly string[];
+ fileCountLabel?: ReactNode;
+ filesLabel?: ReactNode;
+ kind?: "deep-review" | "review";
+ loading?: boolean;
+ summary: ReactNode;
+ title: ReactNode;
+}
+
+export function ReviewSummaryToolCard({
+ action,
+ changedFiles = [],
+ fileCountLabel,
+ filesLabel,
+ isExpanded = false,
+ kind = "review",
+ loading = false,
+ onToggle,
+ status,
+ summary,
+ title,
+ ...props
+}: ReviewSummaryToolCardProps) {
+ const Icon = kind === "deep-review" ? Sparkles : SearchCheck;
+ return (
+
+ {summary}
+ {changedFiles.length > 0 && (
+
+ {filesLabel &&
{filesLabel}
}
+
+ {changedFiles.map((file) => - {file}
)}
+
+
+ )}
+ {action && {action}
}
+
+ )}
+ header={(
+ 0 && fileCountLabel ? (
+ {fileCountLabel}
+ ) : undefined}
+ icon={}
+ statusIcon={loading ? : undefined}
+ />
+ )}
+ isExpanded={isExpanded}
+ onClick={onToggle}
+ status={status}
+ />
+ );
+}
+
+export interface PageLifecycleToolCardField {
+ label: ReactNode;
+ monospace?: boolean;
+ value: ReactNode;
+}
+
+interface PageLifecycleToolCardBaseProps extends ProminentCardProps {
+ action: ReactNode;
+ actions?: ReactNode;
+ error?: ReactNode;
+ fields?: readonly PageLifecycleToolCardField[];
+ loading?: boolean;
+ subject: ReactNode;
+ toolCard: "page-deploy" | "page-publish";
+ version?: ReactNode;
+}
+
+function PageLifecycleToolCardBase({
+ action,
+ actions,
+ error,
+ fields = [],
+ isExpanded = false,
+ loading = false,
+ onToggle,
+ status,
+ subject,
+ toolCard,
+ version,
+ ...props
+}: PageLifecycleToolCardBaseProps) {
+ const hasDetails = fields.length > 0 || Boolean(actions || error);
+ const body = hasDetails ? (
+
+ {fields.map((field, index) => (
+
+ {field.label}
+ {field.value}
+
+ ))}
+ {error &&
{error}
}
+ {actions &&
{actions}
}
+
+ ) : undefined;
+ return (
+ {subject}{version ? ` @ ${version}` : ""}}
+ icon={}
+ statusIcon={loading ? : undefined}
+ />
+ )}
+ headerExpandAffordance={hasDetails}
+ isExpanded={Boolean(isExpanded && hasDetails && status !== "error")}
+ onClick={hasDetails && onToggle ? onToggle : undefined}
+ status={status}
+ />
+ );
+}
+
+export type PageDeployToolCardProps = Omit;
+export function PageDeployToolCard(props: PageDeployToolCardProps) {
+ return ;
+}
+
+export type PagePublishToolCardProps = Omit;
+export function PagePublishToolCard(props: PagePublishToolCardProps) {
+ return ;
+}
+
+export interface AgentControlToolCardProps extends ProminentCardProps {
+ agentName: ReactNode;
+ avatar?: ReactNode;
+ onOpenAgent?: (event: ReactMouseEvent) => void;
+ openAgentLabel?: string;
+ prompt?: ReactNode;
+ statusLabel: ReactNode;
+ statusTone?: "danger" | "neutral" | "success" | "warning";
+}
+
+export function AgentControlToolCard({
+ agentName,
+ avatar,
+ className,
+ isExpanded = false,
+ onOpenAgent,
+ onToggle,
+ openAgentLabel,
+ prompt,
+ status,
+ statusLabel,
+ statusTone = "neutral",
+ ...props
+}: AgentControlToolCardProps) {
+ const expandable = Boolean(prompt && onToggle);
+ const agentPillContent = (
+ <>
+
+ {avatar ?? }
+
+ {agentName}
+ >
+ );
+ const header = (
+
+ {onOpenAgent ? (
+
+ ) : {agentPillContent}}
+
+ {statusLabel}
+
+ {expandable && (
+
+ {isExpanded ? : }
+
+ )}
+
+ );
+
+ return (
+ {prompt} : undefined}
+ header={header}
+ headerExpandAffordance={false}
+ isExpanded={Boolean(isExpanded && prompt)}
+ onClick={expandable ? onToggle : undefined}
+ status={status}
+ />
+ );
+}
diff --git a/design-system/packages/ui/src/flow-chat/tool-cards/ReadFileToolCard.meta.ts b/design-system/packages/ui/src/flow-chat/tool-cards/ReadFileToolCard.meta.ts
new file mode 100644
index 0000000000..dd7e9a58a5
--- /dev/null
+++ b/design-system/packages/ui/src/flow-chat/tool-cards/ReadFileToolCard.meta.ts
@@ -0,0 +1,24 @@
+import type { ComponentMeta } from "../../registry.types";
+
+export const readFileToolCardMeta = {
+ category: "flow-chat",
+ description: "A concrete ambient FlowChat card for read-file progress, permission, and completion summaries.",
+ maturity: "stable",
+ name: "ReadFileToolCard",
+ props: [
+ { name: "status", type: "FlowChatToolStatus" },
+ { name: "summary", type: "ReactNode" },
+ { defaultValue: "false", name: "interactive", type: "boolean" },
+ { name: "onOpen", type: "() => void" },
+ ],
+ states: ["default", "hover", "loading", "error"],
+ tokens: [
+ "color.content.primary",
+ "color.content.secondary",
+ "color.content.muted",
+ "color.status.danger.content",
+ "color.status.success.content",
+ "font.family.control",
+ "font.size.small",
+ ],
+} as const satisfies ComponentMeta;
diff --git a/design-system/packages/ui/src/flow-chat/tool-cards/ReadFileToolCard.tsx b/design-system/packages/ui/src/flow-chat/tool-cards/ReadFileToolCard.tsx
new file mode 100644
index 0000000000..1117d280db
--- /dev/null
+++ b/design-system/packages/ui/src/flow-chat/tool-cards/ReadFileToolCard.tsx
@@ -0,0 +1,52 @@
+import type { HTMLAttributes, ReactNode } from "react";
+import { FileText } from "lucide-react";
+import {
+ AmbientToolCard,
+ AmbientToolCardHeader,
+ type FlowChatToolStatus,
+} from "./FlowChatToolCard";
+import { ToolCardStatusSlot } from "./ToolCardStatusSlot";
+
+export interface ReadFileToolCardProps
+ extends Omit, "children" | "onClick"> {
+ accessibleLabel?: string;
+ interactive?: boolean;
+ onOpen?: () => void;
+ status: FlowChatToolStatus;
+ summary: ReactNode;
+}
+
+export function ReadFileToolCard({
+ accessibleLabel,
+ className,
+ interactive = false,
+ onOpen,
+ status,
+ summary,
+ ...props
+}: ReadFileToolCardProps) {
+ const canOpen = interactive && Boolean(onOpen);
+
+ return (
+ }
+ />
+ )}
+ />
+ )}
+ isExpanded={false}
+ onClick={canOpen ? () => onOpen?.() : undefined}
+ status={status}
+ />
+ );
+}
diff --git a/design-system/packages/ui/src/flow-chat/tool-cards/SearchResultsToolCards.module.css b/design-system/packages/ui/src/flow-chat/tool-cards/SearchResultsToolCards.module.css
new file mode 100644
index 0000000000..b945c5451a
--- /dev/null
+++ b/design-system/packages/ui/src/flow-chat/tool-cards/SearchResultsToolCards.module.css
@@ -0,0 +1,137 @@
+@layer bf.components {
+ .details {
+ display: flex;
+ min-inline-size: 0;
+ flex-wrap: wrap;
+ align-items: baseline;
+ gap: var(--bf-space-1) var(--bf-space-3);
+ padding-block-end: var(--bf-space-2);
+ border-block-end: 1px solid var(--bf-color-border-subtle);
+ }
+
+ .detail {
+ display: inline-flex;
+ min-inline-size: 0;
+ align-items: baseline;
+ gap: var(--bf-space-1);
+ color: var(--bf-color-content-muted);
+ font-size: var(--bf-font-size-small);
+ }
+
+ .detailLabel {
+ flex: 0 0 auto;
+ color: var(--bf-color-content-secondary);
+ font-weight: var(--bf-font-weight-semibold);
+ }
+
+ .detailValue {
+ min-inline-size: 0;
+ overflow-wrap: anywhere;
+ }
+
+ .detail[data-monospace="true"] .detailValue {
+ font-family: var(--bf-font-family-mono);
+ }
+
+ .results {
+ display: flex;
+ max-block-size: 25rem;
+ min-inline-size: 0;
+ flex-direction: column;
+ overflow: auto;
+ }
+
+ .result {
+ display: grid;
+ min-inline-size: 0;
+ grid-template-columns: auto minmax(0, 1fr) auto;
+ align-items: start;
+ gap: var(--bf-space-2);
+ padding-block: var(--bf-space-2);
+ color: var(--bf-color-content-secondary);
+ font-size: var(--bf-font-size-small);
+ }
+
+ .result + .result {
+ border-block-start: 1px solid var(--bf-color-border-subtle);
+ }
+
+ .resultIcon {
+ display: inline-flex;
+ align-items: center;
+ color: var(--bf-color-content-muted);
+ }
+
+ .resultIcon > svg {
+ inline-size: 0.875rem;
+ block-size: 0.875rem;
+ }
+
+ .resultBody {
+ display: flex;
+ min-inline-size: 0;
+ flex-direction: column;
+ gap: var(--bf-space-1);
+ }
+
+ .resultTitle,
+ .resultButton {
+ min-inline-size: 0;
+ color: var(--bf-color-content-primary);
+ font: inherit;
+ font-weight: var(--bf-font-weight-medium);
+ text-align: start;
+ overflow-wrap: anywhere;
+ }
+
+ .resultButton {
+ appearance: none;
+ padding: 0;
+ border: 0;
+ background: transparent;
+ cursor: pointer;
+ }
+
+ .resultButton:is(:hover, :focus-visible) {
+ color: var(--bf-color-action-primary-content);
+ text-decoration: underline;
+ text-underline-offset: 0.16em;
+ }
+
+ .resultDescription,
+ .resultUrl,
+ .resultMeta {
+ color: var(--bf-color-content-muted);
+ line-height: var(--bf-line-height-body);
+ overflow-wrap: anywhere;
+ }
+
+ .resultUrl {
+ font-family: var(--bf-font-family-mono);
+ font-size: var(--bf-font-size-xs);
+ }
+
+ .resultMeta {
+ white-space: nowrap;
+ }
+
+ .resultText {
+ max-block-size: 25rem;
+ margin: 0;
+ overflow: auto;
+ color: var(--bf-color-content-secondary);
+ font-family: var(--bf-font-family-mono);
+ font-size: var(--bf-font-size-small);
+ line-height: var(--bf-line-height-body);
+ white-space: pre-wrap;
+ overflow-wrap: anywhere;
+ }
+
+ .overflowLabel {
+ padding-block: var(--bf-space-2);
+ color: var(--bf-color-content-muted);
+ font-size: var(--bf-font-size-small);
+ font-style: italic;
+ text-align: center;
+ }
+}
diff --git a/design-system/packages/ui/src/flow-chat/tool-cards/SearchResultsToolCards.tsx b/design-system/packages/ui/src/flow-chat/tool-cards/SearchResultsToolCards.tsx
new file mode 100644
index 0000000000..a28d5fe1eb
--- /dev/null
+++ b/design-system/packages/ui/src/flow-chat/tool-cards/SearchResultsToolCards.tsx
@@ -0,0 +1,169 @@
+import type { HTMLAttributes, ReactNode } from "react";
+import {
+ File,
+ Folder,
+ FolderOpen,
+ FolderSearch,
+ Globe,
+ Link,
+ Search,
+} from "lucide-react";
+import {
+ AmbientToolCard,
+ AmbientToolCardHeader,
+ type FlowChatToolStatus,
+} from "./FlowChatToolCard";
+import { ToolCardStatusSlot } from "./ToolCardStatusSlot";
+import styles from "./SearchResultsToolCards.module.css";
+
+export interface SearchToolCardDetail {
+ label: ReactNode;
+ monospace?: boolean;
+ value: ReactNode;
+}
+
+export interface SearchToolCardResult {
+ description?: ReactNode;
+ icon?: "directory" | "file" | "link";
+ key: string;
+ meta?: ReactNode;
+ onOpen?: () => void;
+ title: ReactNode;
+ url?: string;
+}
+
+interface SearchResultsToolCardBaseProps
+ extends Omit, "children" | "onClick" | "results"> {
+ details?: readonly SearchToolCardDetail[];
+ icon: ReactNode;
+ isExpanded?: boolean;
+ moreResultsLabel?: ReactNode;
+ onToggle?: () => void;
+ resultText?: string;
+ results?: readonly SearchToolCardResult[];
+ status: FlowChatToolStatus;
+ summary: ReactNode;
+ toolCard: string;
+}
+
+function ResultIcon({ kind }: { kind?: SearchToolCardResult["icon"] }) {
+ if (kind === "directory") return ;
+ if (kind === "link") return ;
+ return ;
+}
+
+function SearchResultsToolCardBase({
+ details = [],
+ icon,
+ isExpanded = false,
+ moreResultsLabel,
+ onToggle,
+ resultText,
+ results = [],
+ status,
+ summary,
+ toolCard,
+ ...props
+}: SearchResultsToolCardBaseProps) {
+ const hasDetails = details.length > 0 || results.length > 0 || Boolean(resultText);
+ const expandedContent = hasDetails ? (
+
+ {details.length > 0 && (
+
+ {details.map((detail, index) => (
+
+ {detail.label}
+ {detail.value}
+
+ ))}
+
+ )}
+
+ {resultText && (
+
{resultText}
+ )}
+
+ {results.length > 0 && (
+
+ {results.map((result) => (
+
+
+
+ {result.onOpen ? (
+
+ ) : (
+ {result.title}
+ )}
+ {result.description && (
+ {result.description}
+ )}
+ {result.url && {result.url}}
+
+ {result.meta && {result.meta}}
+
+ ))}
+ {moreResultsLabel && (
+
+ {moreResultsLabel}
+
+ )}
+
+ )}
+
+ ) : undefined;
+
+ return (
+
+ )}
+ />
+ )}
+ isExpanded={Boolean(isExpanded && hasDetails)}
+ onClick={hasDetails && onToggle ? onToggle : undefined}
+ status={status}
+ />
+ );
+}
+
+export type SearchResultsToolCardProps = Omit<
+ SearchResultsToolCardBaseProps,
+ "icon" | "toolCard"
+>;
+
+export function GrepSearchToolCard(props: SearchResultsToolCardProps) {
+ return } toolCard="grep-search" />;
+}
+
+export function GlobSearchToolCard(props: SearchResultsToolCardProps) {
+ return } toolCard="glob-search" />;
+}
+
+export function DirectoryListToolCard(props: SearchResultsToolCardProps) {
+ return } toolCard="directory-list" />;
+}
+
+export function WebSearchToolCard(props: SearchResultsToolCardProps) {
+ return } toolCard="web-search" />;
+}
diff --git a/design-system/packages/ui/src/flow-chat/tool-cards/SessionToolCards.module.css b/design-system/packages/ui/src/flow-chat/tool-cards/SessionToolCards.module.css
new file mode 100644
index 0000000000..af9901bb6c
--- /dev/null
+++ b/design-system/packages/ui/src/flow-chat/tool-cards/SessionToolCards.module.css
@@ -0,0 +1,105 @@
+@layer bf.components {
+ .details {
+ display: flex;
+ min-inline-size: 0;
+ flex-direction: column;
+ gap: var(--bf-space-3);
+ }
+
+ .field {
+ display: grid;
+ min-inline-size: 0;
+ grid-template-columns: minmax(7rem, auto) minmax(0, 1fr);
+ gap: var(--bf-space-2);
+ color: var(--bf-color-content-secondary);
+ font-size: var(--bf-font-size-small);
+ }
+
+ .fieldLabel,
+ .sectionLabel {
+ color: var(--bf-color-content-muted);
+ font-weight: var(--bf-font-weight-semibold);
+ }
+
+ .fieldValue {
+ min-inline-size: 0;
+ overflow-wrap: anywhere;
+ }
+
+ .field[data-monospace="true"] .fieldValue {
+ font-family: var(--bf-font-family-mono);
+ }
+
+ .sessionList {
+ display: flex;
+ flex-direction: column;
+ gap: var(--bf-space-2);
+ }
+
+ .session {
+ display: grid;
+ min-inline-size: 0;
+ grid-template-columns: minmax(0, 1fr) auto;
+ gap: var(--bf-space-1) var(--bf-space-3);
+ padding: var(--bf-space-3);
+ border-radius: var(--bf-radius-sm);
+ background: var(--bf-color-surface-raised);
+ color: var(--bf-color-content-secondary);
+ font-size: var(--bf-font-size-small);
+ }
+
+ .sessionId {
+ overflow-wrap: anywhere;
+ color: var(--bf-color-content-primary);
+ font-family: var(--bf-font-family-mono);
+ }
+
+ .sessionName {
+ min-inline-size: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .sessionAgent {
+ grid-column: 2;
+ grid-row: 1 / span 2;
+ align-self: center;
+ color: var(--bf-color-content-muted);
+ white-space: nowrap;
+ }
+
+ .messageSection {
+ display: flex;
+ min-inline-size: 0;
+ flex-direction: column;
+ gap: var(--bf-space-2);
+ }
+
+ .message {
+ max-block-size: 20rem;
+ margin: 0;
+ padding: var(--bf-space-3);
+ overflow: auto;
+ border-radius: var(--bf-radius-sm);
+ background: var(--bf-color-surface-raised);
+ color: var(--bf-color-content-secondary);
+ font-family: var(--bf-font-family-mono);
+ font-size: var(--bf-font-size-small);
+ line-height: var(--bf-line-height-body);
+ white-space: pre-wrap;
+ overflow-wrap: anywhere;
+ }
+
+ .empty {
+ color: var(--bf-color-content-muted);
+ font-size: var(--bf-font-size-small);
+ }
+
+ .error {
+ color: var(--bf-color-status-danger-content);
+ font-size: var(--bf-font-size-small);
+ white-space: pre-wrap;
+ overflow-wrap: anywhere;
+ }
+}
diff --git a/design-system/packages/ui/src/flow-chat/tool-cards/SessionToolCards.tsx b/design-system/packages/ui/src/flow-chat/tool-cards/SessionToolCards.tsx
new file mode 100644
index 0000000000..bb66f69b78
--- /dev/null
+++ b/design-system/packages/ui/src/flow-chat/tool-cards/SessionToolCards.tsx
@@ -0,0 +1,127 @@
+import type { HTMLAttributes, ReactNode } from "react";
+import { Layers, MessageSquare } from "lucide-react";
+import {
+ AmbientToolCard,
+ AmbientToolCardHeader,
+ type FlowChatToolStatus,
+} from "./FlowChatToolCard";
+import { ToolCardStatusSlot } from "./ToolCardStatusSlot";
+import styles from "./SessionToolCards.module.css";
+
+export interface SessionToolCardField {
+ label: ReactNode;
+ monospace?: boolean;
+ value: ReactNode;
+}
+
+export interface SessionToolCardSession {
+ agentType?: ReactNode;
+ id: ReactNode;
+ key: string;
+ name?: ReactNode;
+}
+
+interface SessionToolCardBaseProps
+ extends Omit, "children" | "onClick"> {
+ action: ReactNode;
+ emptyState?: ReactNode;
+ error?: ReactNode;
+ fields?: readonly SessionToolCardField[];
+ icon: ReactNode;
+ isExpanded?: boolean;
+ message?: ReactNode;
+ messageLabel?: ReactNode;
+ onToggle?: () => void;
+ sessions?: readonly SessionToolCardSession[];
+ status: FlowChatToolStatus;
+ summary: ReactNode;
+ toolCard: string;
+}
+
+function SessionToolCardBase({
+ action,
+ emptyState,
+ error,
+ fields = [],
+ icon,
+ isExpanded = false,
+ message,
+ messageLabel,
+ onToggle,
+ sessions = [],
+ status,
+ summary,
+ toolCard,
+ ...props
+}: SessionToolCardBaseProps) {
+ const hasDetails = fields.length > 0 || sessions.length > 0 || Boolean(emptyState || message || error);
+ const expandedContent = hasDetails ? (
+
+ {fields.map((field, index) => (
+
+ {field.label}
+ {field.value}
+
+ ))}
+
+ {sessions.length > 0 && (
+
+ {sessions.map((session) => (
+
+ {session.id}
+ {session.name && {session.name}}
+ {session.agentType && {session.agentType}}
+
+ ))}
+
+ )}
+
+ {message && (
+
+ {messageLabel &&
{messageLabel}}
+
{message}
+
+ )}
+
+ {emptyState &&
{emptyState}
}
+ {error &&
{error}
}
+
+ ) : undefined;
+
+ return (
+ }
+ />
+ )}
+ isExpanded={Boolean(isExpanded && hasDetails)}
+ onClick={hasDetails && onToggle ? onToggle : undefined}
+ status={status}
+ />
+ );
+}
+
+export interface SessionControlToolCardProps
+ extends Omit {}
+
+export function SessionControlToolCard(props: SessionControlToolCardProps) {
+ return } toolCard="session-control" />;
+}
+
+export interface SessionMessageToolCardProps
+ extends Omit {}
+
+export function SessionMessageToolCard(props: SessionMessageToolCardProps) {
+ return } toolCard="session-message" />;
+}
diff --git a/design-system/packages/ui/src/flow-chat/tool-cards/StandardAmbientToolCards.module.css b/design-system/packages/ui/src/flow-chat/tool-cards/StandardAmbientToolCards.module.css
new file mode 100644
index 0000000000..7b656d72b8
--- /dev/null
+++ b/design-system/packages/ui/src/flow-chat/tool-cards/StandardAmbientToolCards.module.css
@@ -0,0 +1,311 @@
+@layer bf.components {
+ .sections {
+ display: flex;
+ min-inline-size: 0;
+ flex-direction: column;
+ gap: var(--bf-space-3);
+ }
+
+ .section {
+ display: flex;
+ min-inline-size: 0;
+ flex-direction: column;
+ gap: var(--bf-space-2);
+ }
+
+ .sectionLabel {
+ color: var(--bf-color-content-muted);
+ font-size: var(--bf-font-size-small);
+ font-weight: var(--bf-font-weight-semibold);
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+ }
+
+ .error {
+ color: var(--bf-color-status-danger-content);
+ font-size: var(--bf-font-size-small);
+ line-height: var(--bf-line-height-body);
+ white-space: pre-wrap;
+ overflow-wrap: anywhere;
+ }
+
+ .codeBlock,
+ .resultBlock,
+ .messageBlock {
+ max-block-size: 20rem;
+ margin: 0;
+ padding: var(--bf-space-3);
+ overflow: auto;
+ border: 1px solid var(--bf-color-border-subtle);
+ border-radius: var(--bf-radius-sm);
+ background: var(--bf-color-surface-canvas);
+ color: var(--bf-color-content-secondary);
+ font-family: var(--bf-font-family-mono);
+ font-size: var(--bf-font-size-small);
+ line-height: var(--bf-line-height-body);
+ white-space: pre-wrap;
+ overflow-wrap: anywhere;
+ }
+
+ .meta {
+ display: flex;
+ min-inline-size: 0;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: var(--bf-space-2);
+ padding-block-end: var(--bf-space-2);
+ border-block-end: 1px solid var(--bf-color-border-subtle);
+ }
+
+ .metaLabel,
+ .pill {
+ display: inline-flex;
+ align-items: center;
+ padding-block: var(--bf-space-1);
+ padding-inline: var(--bf-space-2);
+ border: 1px solid var(--bf-color-border-subtle);
+ border-radius: var(--bf-radius-pill);
+ background: var(--bf-color-surface-raised);
+ color: var(--bf-color-content-secondary);
+ font-size: var(--bf-font-size-small);
+ }
+
+ .metaLabel {
+ font-family: var(--bf-font-family-mono);
+ font-weight: var(--bf-font-weight-semibold);
+ }
+
+ .metaDescription {
+ min-inline-size: 0;
+ color: var(--bf-color-content-muted);
+ font-size: var(--bf-font-size-small);
+ }
+
+ .fetchTitle {
+ min-inline-size: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .fetchMeta {
+ display: flex;
+ min-inline-size: 0;
+ flex-direction: column;
+ gap: var(--bf-space-3);
+ }
+
+ .openLink {
+ display: inline-flex;
+ min-inline-size: 0;
+ align-items: center;
+ align-self: flex-start;
+ gap: var(--bf-space-2);
+ appearance: none;
+ padding: 0;
+ border: 0;
+ color: var(--bf-color-action-primary-content);
+ background: transparent;
+ font: inherit;
+ font-size: var(--bf-font-size-small);
+ text-align: start;
+ cursor: pointer;
+ }
+
+ .openLink > svg {
+ inline-size: 0.875rem;
+ block-size: 0.875rem;
+ flex: 0 0 auto;
+ }
+
+ .openLinkText {
+ min-inline-size: 0;
+ overflow-wrap: anywhere;
+ }
+
+ .openLink:is(:hover, :focus-visible) .openLinkText {
+ text-decoration: underline;
+ text-underline-offset: 0.16em;
+ }
+
+ .detailsRow {
+ display: flex;
+ min-inline-size: 0;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: space-between;
+ gap: var(--bf-space-2);
+ }
+
+ .pills {
+ display: flex;
+ min-inline-size: 0;
+ flex-wrap: wrap;
+ gap: var(--bf-space-2);
+ }
+
+ .imageContent {
+ display: flex;
+ min-inline-size: 0;
+ justify-content: flex-start;
+ }
+
+ .imageButton {
+ display: inline-flex;
+ inline-size: 10rem;
+ max-inline-size: calc(100vi - 4.75rem);
+ aspect-ratio: 1;
+ align-items: center;
+ justify-content: center;
+ padding: 0;
+ overflow: hidden;
+ appearance: none;
+ border: 1px solid var(--bf-color-border-default);
+ border-radius: var(--bf-radius-sm);
+ background: var(--bf-color-surface-canvas);
+ cursor: zoom-in;
+ }
+
+ .imageButton:focus-visible {
+ outline: 2px solid var(--bf-color-focus-ring);
+ outline-offset: 2px;
+ }
+
+ .imageButton img {
+ display: block;
+ max-inline-size: 100%;
+ max-block-size: 100%;
+ object-fit: contain;
+ }
+
+ .imageError {
+ inline-size: 100%;
+ padding: var(--bf-space-3);
+ color: var(--bf-color-status-danger-content);
+ font-size: var(--bf-font-size-small);
+ text-align: center;
+ }
+
+ .lightbox {
+ display: flex;
+ min-block-size: 15rem;
+ max-block-size: 80vb;
+ align-items: center;
+ justify-content: center;
+ overflow: auto;
+ }
+
+ .lightbox img {
+ display: block;
+ max-inline-size: 100%;
+ max-block-size: 76vb;
+ object-fit: contain;
+ }
+
+ [data-bf-tool-card="default"][data-bf-confirmation="true"]
+ > [data-bf-component="flow-chat-tool-card"][data-bf-part="surface"] {
+ border-radius: var(--bf-radius-sm);
+ background: color-mix(in srgb, var(--bf-color-status-warning-content) 6%, transparent);
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--bf-color-status-warning-content) 24%, transparent);
+ }
+
+ .todoCompact {
+ display: flex;
+ min-inline-size: 0;
+ align-items: center;
+ gap: var(--bf-space-2);
+ color: var(--bf-color-content-muted);
+ font-size: var(--bf-font-size-small);
+ }
+
+ .todoCompactIcon {
+ display: inline-flex;
+ flex: 0 0 auto;
+ align-items: center;
+ }
+
+ .todoCompactIcon > svg {
+ inline-size: 0.875rem;
+ block-size: 0.875rem;
+ }
+
+ .todoCompactMeta {
+ font-family: var(--bf-font-family-mono);
+ white-space: nowrap;
+ }
+
+ .todoSummary {
+ display: inline-flex;
+ min-inline-size: 0;
+ align-items: center;
+ gap: var(--bf-space-2);
+ overflow: hidden;
+ color: var(--bf-color-content-muted);
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .todoStats {
+ flex: 0 0 auto;
+ font-variant-numeric: tabular-nums;
+ }
+
+ .todoList {
+ display: flex;
+ max-block-size: 17.5rem;
+ flex-direction: column;
+ overflow: auto;
+ }
+
+ .todoItem {
+ display: flex;
+ min-inline-size: 0;
+ align-items: center;
+ gap: var(--bf-space-2);
+ padding-block: var(--bf-space-2);
+ color: var(--bf-color-content-muted);
+ font-size: var(--bf-font-size-small);
+ }
+
+ .todoItem + .todoItem {
+ border-block-start: 1px solid var(--bf-color-border-subtle);
+ }
+
+ .todoItem[data-status="in_progress"] {
+ color: var(--bf-color-content-secondary);
+ }
+
+ .todoItem[data-status="completed"],
+ .todoItem[data-status="cancelled"] {
+ opacity: 0.72;
+ text-decoration: line-through;
+ }
+
+ .todoIcon {
+ display: inline-flex;
+ flex: 0 0 auto;
+ align-items: center;
+ }
+
+ .todoIcon > svg {
+ inline-size: 0.75rem;
+ block-size: 0.75rem;
+ }
+
+ .todoContent {
+ min-inline-size: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ @media (max-width: 40rem) {
+ .imageButton {
+ inline-size: 9rem;
+ }
+
+ .lightbox {
+ min-block-size: 10rem;
+ }
+ }
+}
diff --git a/design-system/packages/ui/src/flow-chat/tool-cards/StandardAmbientToolCards.tsx b/design-system/packages/ui/src/flow-chat/tool-cards/StandardAmbientToolCards.tsx
new file mode 100644
index 0000000000..5562fd8ef0
--- /dev/null
+++ b/design-system/packages/ui/src/flow-chat/tool-cards/StandardAmbientToolCards.tsx
@@ -0,0 +1,456 @@
+import type {
+ HTMLAttributes,
+ MouseEvent as ReactMouseEvent,
+ ReactNode,
+} from "react";
+import {
+ CheckCircle2,
+ Circle,
+ Code2,
+ Globe,
+ Image as ImageIcon,
+ Link,
+ ListTodo,
+ XCircle,
+} from "lucide-react";
+import { Modal } from "../../components/Modal/Modal";
+import {
+ AmbientToolCard,
+ AmbientToolCardHeader,
+ ToolCardHeaderActions,
+ type FlowChatToolStatus,
+} from "./FlowChatToolCard";
+import { ToolCardStatusSlot } from "./ToolCardStatusSlot";
+import { ToolProcessingDots } from "./ToolProcessingDots";
+import styles from "./StandardAmbientToolCards.module.css";
+
+interface AmbientCardProps
+ extends Omit, "children" | "content" | "onClick" | "title"> {
+ isExpanded?: boolean;
+ onToggle?: () => void;
+ status: FlowChatToolStatus;
+}
+
+function Section({ children, label }: { children: ReactNode; label?: ReactNode }) {
+ return (
+
+ {label && {label}
}
+ {children}
+
+ );
+}
+
+export interface RunCodeToolCardProps extends AmbientCardProps {
+ action: ReactNode;
+ error?: ReactNode;
+ headerActions?: ReactNode;
+ output?: ReactNode;
+ outputLabel?: ReactNode;
+ program?: ReactNode;
+ programLabel?: ReactNode;
+ summary: ReactNode;
+}
+
+export function RunCodeToolCard({
+ action,
+ error,
+ headerActions,
+ isExpanded = false,
+ onToggle,
+ output,
+ outputLabel,
+ program,
+ programLabel,
+ status,
+ summary,
+ ...props
+}: RunCodeToolCardProps) {
+ const hasDetails = Boolean(program || output || error);
+ return (
+
+ {program && }
+ {(output || error) && (
+
+ {error
+ ? {error}
+ : {output}
}
+
+ )}
+
+ ) : undefined}
+ header={(
+ {headerActions} : undefined}
+ icon={} />}
+ />
+ )}
+ isExpanded={Boolean(isExpanded && hasDetails)}
+ onClick={hasDetails && onToggle ? onToggle : undefined}
+ status={status}
+ />
+ );
+}
+
+export interface WebFetchToolCardProps extends AmbientCardProps {
+ action?: ReactNode;
+ content?: ReactNode;
+ copyAction?: ReactNode;
+ details?: readonly ReactNode[];
+ emptyContent?: ReactNode;
+ error?: ReactNode;
+ onOpenUrl?: (event: ReactMouseEvent) => void;
+ openUrlLabel?: string;
+ title: ReactNode;
+ url?: string;
+}
+
+export function WebFetchToolCard({
+ action,
+ content,
+ copyAction,
+ details = [],
+ emptyContent,
+ error,
+ isExpanded = false,
+ onOpenUrl,
+ onToggle,
+ openUrlLabel,
+ status,
+ title,
+ url,
+ ...props
+}: WebFetchToolCardProps) {
+ const hasDetails = Boolean(url || content || emptyContent || error);
+ return (
+
+ {url && (
+ onOpenUrl ? (
+
+ ) : {url}
+ )}
+ {(details.length > 0 || copyAction) && (
+
+
+ {details.map((detail, index) => (
+ {detail}
+ ))}
+
+ {copyAction && {copyAction}}
+
+ )}
+ {error ? (
+ {error}
+ ) : (
+ {content || emptyContent}
+ )}
+
+ ) : undefined}
+ header={(
+ {title}}
+ icon={(
+ }
+ />
+ )}
+ />
+ )}
+ isExpanded={Boolean(isExpanded && hasDetails)}
+ onClick={hasDetails && onToggle ? onToggle : undefined}
+ status={status}
+ />
+ );
+}
+
+export interface DefaultToolCardProps extends AmbientCardProps {
+ description?: ReactNode;
+ displayName: ReactNode;
+ error?: ReactNode;
+ hasDetails?: boolean;
+ icon?: ReactNode;
+ inputLabel?: ReactNode;
+ inputPreview?: ReactNode;
+ requiresConfirmation?: boolean;
+ resultLabel?: ReactNode;
+ resultPreview?: ReactNode;
+ summary: ReactNode;
+ toolName: ReactNode;
+}
+
+export function DefaultToolCard({
+ description,
+ displayName,
+ error,
+ hasDetails,
+ icon,
+ inputLabel,
+ inputPreview,
+ isExpanded = false,
+ onToggle,
+ requiresConfirmation = false,
+ resultLabel,
+ resultPreview,
+ status,
+ summary,
+ toolName,
+ ...props
+}: DefaultToolCardProps) {
+ const detailsAvailable = hasDetails ?? Boolean(inputPreview || resultPreview || error);
+ return (
+
+
+ {toolName}
+ {description && {description}}
+
+ {inputPreview && (
+
+ )}
+ {(resultPreview || error) && (
+
+ {error
+ ? {error}
+ : {resultPreview}}
+
+ )}
+
+ ) : undefined}
+ header={(
+ }
+ />
+ )}
+ isExpanded={Boolean(isExpanded && detailsAvailable)}
+ onClick={detailsAvailable && onToggle ? onToggle : undefined}
+ status={status}
+ />
+ );
+}
+
+export interface ViewImageToolCardProps extends AmbientCardProps {
+ alt: string;
+ errorText?: ReactNode;
+ height?: number;
+ imageFailed?: boolean;
+ lightboxOpen?: boolean;
+ lightboxTitle?: ReactNode;
+ onImageError?: () => void;
+ onLightboxClose?: () => void;
+ onOpenPreview?: (event: ReactMouseEvent) => void;
+ previewLabel: string;
+ source?: string;
+ statusText: ReactNode;
+ width?: number;
+}
+
+export function ViewImageToolCard({
+ alt,
+ errorText,
+ height,
+ imageFailed = false,
+ isExpanded = false,
+ lightboxOpen = false,
+ lightboxTitle,
+ onImageError,
+ onLightboxClose,
+ onOpenPreview,
+ onToggle,
+ previewLabel,
+ source,
+ status,
+ statusText,
+ width,
+ ...props
+}: ViewImageToolCardProps) {
+ return (
+ <>
+
+ {imageFailed ? (
+ {errorText}
+ ) : (
+
+ )}
+
+ ) : undefined}
+ header={(
+ } />}
+ />
+ )}
+ isExpanded={Boolean(source && isExpanded)}
+ onClick={source && onToggle ? onToggle : undefined}
+ status={status}
+ />
+ undefined)}
+ size="large"
+ title={lightboxTitle}
+ >
+
+

+
+
+ >
+ );
+}
+
+export type TodoToolCardItemStatus = "cancelled" | "completed" | "in_progress" | "pending";
+
+export interface TodoToolCardItem {
+ content: ReactNode;
+ key: string;
+ status: TodoToolCardItemStatus;
+}
+
+export interface TodoToolCardProps extends AmbientCardProps {
+ allCompleted?: boolean;
+ compactCountLabel?: ReactNode;
+ compactProgressLabel?: ReactNode;
+ completedCount: number;
+ items: readonly TodoToolCardItem[];
+ loading?: boolean;
+ mode?: "compact" | "standard";
+ summary?: ReactNode;
+ title: ReactNode;
+ totalCount: number;
+}
+
+function TodoStatusIcon({ status }: { status: TodoToolCardItemStatus }) {
+ if (status === "completed") return ;
+ if (status === "cancelled") return ;
+ if (status === "in_progress") return ;
+ return ;
+}
+
+export function TodoToolCard({
+ allCompleted = false,
+ compactCountLabel,
+ compactProgressLabel,
+ completedCount,
+ isExpanded = false,
+ items,
+ loading = false,
+ mode = "standard",
+ onToggle,
+ status,
+ summary,
+ title,
+ totalCount,
+ ...props
+}: TodoToolCardProps) {
+ if (mode === "compact") {
+ return (
+
+
+ {loading ? : }
+
+ {compactCountLabel && {compactCountLabel}}
+ {compactProgressLabel && {compactProgressLabel}}
+
+ );
+ }
+
+ const hasItems = items.length > 0;
+ const headerSummary = (
+
+ {summary}
+ {hasItems && ({completedCount}/{totalCount})}
+
+ );
+
+ return (
+
+ {items.map((item) => (
+
+
+ {item.content}
+
+ ))}
+
+ ) : undefined}
+ header={(
+ }
+ />
+ )}
+ />
+ )}
+ isExpanded={Boolean(isExpanded && hasItems)}
+ onClick={hasItems && onToggle ? onToggle : undefined}
+ status={status}
+ toggleTestId="todo-tool-card-toggle"
+ />
+ );
+}
diff --git a/design-system/packages/ui/src/flow-chat/tool-cards/StandardToolCards.meta.ts b/design-system/packages/ui/src/flow-chat/tool-cards/StandardToolCards.meta.ts
new file mode 100644
index 0000000000..6fe0632b47
--- /dev/null
+++ b/design-system/packages/ui/src/flow-chat/tool-cards/StandardToolCards.meta.ts
@@ -0,0 +1,181 @@
+import type { ComponentMeta } from "../../registry.types";
+
+const ambientTokens = [
+ "color.content.primary",
+ "color.content.secondary",
+ "color.content.muted",
+ "color.surface.canvas",
+ "color.surface.raised",
+ "color.border.default",
+ "color.status.danger.content",
+ "color.status.success.content",
+ "font.family.control",
+ "font.family.mono",
+ "font.size.small",
+ "font.weight.medium",
+ "font.weight.regular",
+ "radius.sm",
+] as const;
+
+const activityProps = [
+ { name: "status", type: "FlowChatToolStatus" },
+ { name: "action", type: "ReactNode" },
+ { name: "summary", type: "ReactNode" },
+] as const;
+
+const expandableProps = [
+ { name: "status", type: "FlowChatToolStatus" },
+ { name: "summary", type: "ReactNode" },
+ { defaultValue: "false", name: "isExpanded", type: "boolean" },
+ { name: "onToggle", type: "() => void" },
+] as const;
+
+export const agentWaitToolCardMeta = {
+ category: "flow-chat",
+ description: "A concrete ambient card for background-agent wait progress and completion summaries.",
+ maturity: "stable",
+ name: "AgentWaitToolCard",
+ props: activityProps,
+ states: ["default", "loading", "error"],
+ tokens: ambientTokens,
+} as const satisfies ComponentMeta;
+
+export const defaultToolCardMeta = {
+ category: "flow-chat",
+ description: "The fallback ambient card for a tool summary, sanitized input, result, and confirmation state.",
+ maturity: "stable",
+ name: "DefaultToolCard",
+ props: expandableProps,
+ states: ["default", "hover", "loading", "expanded", "confirmation", "error"],
+ tokens: ambientTokens,
+} as const satisfies ComponentMeta;
+
+export const directoryListToolCardMeta = {
+ category: "flow-chat",
+ description: "An ambient directory-list card with structured metadata and file results.",
+ maturity: "stable",
+ name: "DirectoryListToolCard",
+ props: expandableProps,
+ states: ["default", "hover", "loading", "expanded", "error"],
+ tokens: ambientTokens,
+} as const satisfies ComponentMeta;
+
+export const getToolSpecToolCardMeta = {
+ category: "flow-chat",
+ description: "A concrete ambient card for loading a tool specification into runtime context.",
+ maturity: "stable",
+ name: "GetToolSpecToolCard",
+ props: activityProps,
+ states: ["default", "loading", "error"],
+ tokens: ambientTokens,
+} as const satisfies ComponentMeta;
+
+export const globSearchToolCardMeta = {
+ category: "flow-chat",
+ description: "An ambient file-pattern search card with reusable result-list anatomy.",
+ maturity: "stable",
+ name: "GlobSearchToolCard",
+ props: expandableProps,
+ states: ["default", "hover", "loading", "expanded", "error"],
+ tokens: ambientTokens,
+} as const satisfies ComponentMeta;
+
+export const grepSearchToolCardMeta = {
+ category: "flow-chat",
+ description: "An ambient text-search card with query metadata and a scrollable result preview.",
+ maturity: "stable",
+ name: "GrepSearchToolCard",
+ props: expandableProps,
+ states: ["default", "hover", "loading", "expanded", "error"],
+ tokens: ambientTokens,
+} as const satisfies ComponentMeta;
+
+export const runCodeToolCardMeta = {
+ category: "flow-chat",
+ description: "An ambient code-execution card with host-rendered program and output slots.",
+ maturity: "stable",
+ name: "RunCodeToolCard",
+ props: expandableProps,
+ states: ["default", "hover", "loading", "expanded", "error"],
+ tokens: ambientTokens,
+} as const satisfies ComponentMeta;
+
+export const sessionControlToolCardMeta = {
+ category: "flow-chat",
+ description: "An ambient session-lifecycle card with semantic detail fields and session rows.",
+ maturity: "stable",
+ name: "SessionControlToolCard",
+ props: expandableProps,
+ states: ["default", "hover", "loading", "expanded", "error"],
+ tokens: ambientTokens,
+} as const satisfies ComponentMeta;
+
+export const sessionMessageToolCardMeta = {
+ category: "flow-chat",
+ description: "An ambient cross-session message card with target details and message content.",
+ maturity: "stable",
+ name: "SessionMessageToolCard",
+ props: expandableProps,
+ states: ["default", "hover", "loading", "expanded", "error"],
+ tokens: ambientTokens,
+} as const satisfies ComponentMeta;
+
+export const skillToolCardMeta = {
+ category: "flow-chat",
+ description: "A concrete ambient card for skill loading progress and outcomes.",
+ maturity: "stable",
+ name: "SkillToolCard",
+ props: activityProps,
+ states: ["default", "loading", "error"],
+ tokens: ambientTokens,
+} as const satisfies ComponentMeta;
+
+export const terminalControlToolCardMeta = {
+ category: "flow-chat",
+ description: "A concrete ambient card for interrupting or terminating a terminal session.",
+ maturity: "stable",
+ name: "TerminalControlToolCard",
+ props: activityProps,
+ states: ["default", "loading", "error"],
+ tokens: ambientTokens,
+} as const satisfies ComponentMeta;
+
+export const todoToolCardMeta = {
+ category: "flow-chat",
+ description: "An ambient task-progress card with compact and expandable list presentations.",
+ maturity: "stable",
+ name: "TodoToolCard",
+ props: expandableProps,
+ states: ["default", "hover", "loading", "expanded", "error"],
+ tokens: ambientTokens,
+} as const satisfies ComponentMeta;
+
+export const viewImageToolCardMeta = {
+ category: "flow-chat",
+ description: "An ambient image-result card with an expandable preview and accessible lightbox.",
+ maturity: "stable",
+ name: "ViewImageToolCard",
+ props: expandableProps,
+ states: ["default", "hover", "loading", "expanded", "error"],
+ tokens: ambientTokens,
+} as const satisfies ComponentMeta;
+
+export const webFetchToolCardMeta = {
+ category: "flow-chat",
+ description: "An ambient web-fetch card with source metadata, result actions, and content preview.",
+ maturity: "stable",
+ name: "WebFetchToolCard",
+ props: expandableProps,
+ states: ["default", "hover", "loading", "expanded", "error"],
+ tokens: ambientTokens,
+} as const satisfies ComponentMeta;
+
+export const webSearchToolCardMeta = {
+ category: "flow-chat",
+ description: "An ambient web-search card with link, snippet, URL, and summary result presentations.",
+ maturity: "stable",
+ name: "WebSearchToolCard",
+ props: expandableProps,
+ states: ["default", "hover", "loading", "expanded", "error"],
+ tokens: ambientTokens,
+} as const satisfies ComponentMeta;
diff --git a/design-system/packages/ui/src/flow-chat/tool-cards/ToolCardStatusSlot.module.css b/design-system/packages/ui/src/flow-chat/tool-cards/ToolCardStatusSlot.module.css
new file mode 100644
index 0000000000..e5fb1a81dd
--- /dev/null
+++ b/design-system/packages/ui/src/flow-chat/tool-cards/ToolCardStatusSlot.module.css
@@ -0,0 +1,58 @@
+@layer bf.components {
+ .root {
+ position: relative;
+ display: inline-flex;
+ inline-size: 100%;
+ block-size: 100%;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .statusLayer,
+ .iconLayer {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ transition: opacity var(--bf-motion-duration-fast) var(--bf-motion-easing-standard);
+ }
+
+ .statusLayer > :where(svg, img),
+ .iconLayer > :where(svg, img) {
+ display: block;
+ inline-size: 100%;
+ block-size: 100%;
+ }
+
+ .iconLayer {
+ position: absolute;
+ inset: 0;
+ opacity: 0;
+ }
+
+ .root[data-default-icon="tool"] .statusLayer {
+ opacity: 0;
+ }
+
+ .root[data-default-icon="tool"] .iconLayer {
+ opacity: 1;
+ }
+
+ .success {
+ color: var(--bf-color-status-success-content);
+ }
+
+ .danger {
+ color: var(--bf-color-status-danger-content);
+ }
+
+ .muted {
+ color: var(--bf-color-content-muted);
+ }
+
+ @media (prefers-reduced-motion: reduce) {
+ .statusLayer,
+ .iconLayer {
+ transition: none;
+ }
+ }
+}
diff --git a/design-system/packages/ui/src/flow-chat/tool-cards/ToolCardStatusSlot.tsx b/design-system/packages/ui/src/flow-chat/tool-cards/ToolCardStatusSlot.tsx
new file mode 100644
index 0000000000..fbc939abb3
--- /dev/null
+++ b/design-system/packages/ui/src/flow-chat/tool-cards/ToolCardStatusSlot.tsx
@@ -0,0 +1,80 @@
+import { Check, Clock, X, type LucideProps } from "lucide-react";
+import { classNames } from "../../internal/classNames";
+import type { FlowChatToolStatus } from "./FlowChatToolCard";
+import {
+ ToolProcessingDots,
+ type ToolProcessingDotsSize,
+} from "./ToolProcessingDots";
+import styles from "./ToolCardStatusSlot.module.css";
+
+export interface ToolCardStatusSlotProps {
+ className?: string;
+ defaultIcon?: "status" | "tool";
+ size?: ToolProcessingDotsSize;
+ status: FlowChatToolStatus;
+ toolIcon?: React.ReactNode;
+}
+
+function StatusGlyph({
+ size,
+ status,
+}: {
+ size: ToolProcessingDotsSize;
+ status: FlowChatToolStatus;
+}) {
+ const iconProps: LucideProps = {
+ "aria-hidden": true,
+ size,
+ };
+
+ switch (status) {
+ case "completed":
+ case "confirmed":
+ return ;
+ case "error":
+ return ;
+ case "cancelled":
+ case "rejected":
+ return ;
+ case "queued":
+ case "waiting":
+ return ;
+ default:
+ return ;
+ }
+}
+
+export function ToolCardStatusSlot({
+ className,
+ defaultIcon = "status",
+ size = 16,
+ status,
+ toolIcon,
+}: ToolCardStatusSlotProps) {
+ return (
+
+
+
+
+ {toolIcon !== undefined && toolIcon !== null && toolIcon !== false && (
+
+ {toolIcon}
+
+ )}
+
+ );
+}
diff --git a/design-system/packages/ui/src/flow-chat/tool-cards/ToolProcessingDots.module.css b/design-system/packages/ui/src/flow-chat/tool-cards/ToolProcessingDots.module.css
new file mode 100644
index 0000000000..21ac400d0a
--- /dev/null
+++ b/design-system/packages/ui/src/flow-chat/tool-cards/ToolProcessingDots.module.css
@@ -0,0 +1,65 @@
+@layer bf.components {
+ .root {
+ --_dot-size: 2px;
+ --_dot-gap: 2px;
+
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: var(--_dot-gap);
+ color: inherit;
+ vertical-align: middle;
+ }
+
+ .root[data-size="10"] {
+ --_dot-gap: 1px;
+ }
+
+ .root[data-size="14"] {
+ --_dot-size: 2.5px;
+ }
+
+ .root[data-size="16"] {
+ --_dot-size: 3px;
+ --_dot-gap: 3px;
+ }
+
+ .dot {
+ inline-size: var(--_dot-size);
+ block-size: var(--_dot-size);
+ flex: 0 0 auto;
+ border-radius: var(--bf-radius-pill);
+ background: currentColor;
+ animation: bf-flow-chat-processing-dot var(--bf-motion-duration-loop) var(--bf-motion-easing-standard) infinite;
+ }
+
+ .dot:nth-child(2) {
+ animation-delay: var(--bf-motion-duration-fast);
+ }
+
+ .dot:nth-child(3) {
+ animation-delay: var(--bf-motion-duration-base);
+ }
+
+ @keyframes bf-flow-chat-processing-dot {
+ 0%,
+ 60%,
+ 100% {
+ opacity: var(--bf-opacity-disabled);
+ transform: translateY(0);
+ }
+
+ 30% {
+ opacity: 1;
+ transform: translateY(-2px);
+ }
+ }
+
+ @media (prefers-reduced-motion: reduce) {
+ .dot {
+ animation: none;
+ opacity: var(--bf-opacity-hover);
+ transform: none;
+ }
+ }
+}
diff --git a/design-system/packages/ui/src/flow-chat/tool-cards/ToolProcessingDots.tsx b/design-system/packages/ui/src/flow-chat/tool-cards/ToolProcessingDots.tsx
new file mode 100644
index 0000000000..434fafbe75
--- /dev/null
+++ b/design-system/packages/ui/src/flow-chat/tool-cards/ToolProcessingDots.tsx
@@ -0,0 +1,29 @@
+import { classNames } from "../../internal/classNames";
+import styles from "./ToolProcessingDots.module.css";
+
+export type ToolProcessingDotsSize = 10 | 12 | 14 | 16;
+
+export interface ToolProcessingDotsProps {
+ className?: string;
+ size?: ToolProcessingDotsSize;
+}
+
+export function ToolProcessingDots({
+ className,
+ size = 14,
+}: ToolProcessingDotsProps) {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/design-system/packages/ui/src/flow-chat/tool-cards/index.ts b/design-system/packages/ui/src/flow-chat/tool-cards/index.ts
new file mode 100644
index 0000000000..0d07e21f17
--- /dev/null
+++ b/design-system/packages/ui/src/flow-chat/tool-cards/index.ts
@@ -0,0 +1,105 @@
+export {
+ AmbientToolCard,
+ AmbientToolCardHeader,
+ ProminentToolCard,
+ ProminentToolCardHeader,
+ ToolCardChangeSummary,
+ ToolCardHeaderActions,
+ ToolCardIconSlot,
+ ToolCardStatusIcon,
+ type AmbientToolCardHeaderProps,
+ type AmbientToolCardProps,
+ type FlowChatToolStatus,
+ type ProminentToolCardHeaderProps,
+ type ProminentToolCardProps,
+ type ToolCardChangeSummaryProps,
+ type ToolCardHeaderActionsProps,
+ type ToolCardHeaderAffordanceKind,
+ type ToolCardIconSlotProps,
+ type ToolCardStatusIconProps,
+} from "./FlowChatToolCard";
+export {
+ CommandToolCard,
+ type CommandToolCardAction,
+ type CommandToolCardCopyAction,
+ type CommandToolCardFooterItem,
+ type CommandToolCardProps,
+} from "./CommandToolCard";
+export {
+ ContextCompressionToolCard,
+ type ContextCompressionToolCardProps,
+} from "./ContextCompressionToolCard";
+export {
+ FileOperationToolCard,
+ type FileOperationKind,
+ type FileOperationToolCardAction,
+ type FileOperationToolCardError,
+ type FileOperationToolCardProps,
+} from "./FileOperationToolCard";
+export {
+ ReadFileToolCard,
+ type ReadFileToolCardProps,
+} from "./ReadFileToolCard";
+export {
+ AgentWaitToolCard,
+ GetToolSpecToolCard,
+ SkillToolCard,
+ TerminalControlToolCard,
+ type ActivityToolCardProps,
+} from "./ActivityToolCards";
+export {
+ DirectoryListToolCard,
+ GlobSearchToolCard,
+ GrepSearchToolCard,
+ WebSearchToolCard,
+ type SearchResultsToolCardProps,
+ type SearchToolCardDetail,
+ type SearchToolCardResult,
+} from "./SearchResultsToolCards";
+export {
+ SessionControlToolCard,
+ SessionMessageToolCard,
+ type SessionControlToolCardProps,
+ type SessionMessageToolCardProps,
+ type SessionToolCardField,
+ type SessionToolCardSession,
+} from "./SessionToolCards";
+export {
+ DefaultToolCard,
+ RunCodeToolCard,
+ TodoToolCard,
+ ViewImageToolCard,
+ WebFetchToolCard,
+ type DefaultToolCardProps,
+ type RunCodeToolCardProps,
+ type TodoToolCardItem,
+ type TodoToolCardItemStatus,
+ type TodoToolCardProps,
+ type ViewImageToolCardProps,
+ type WebFetchToolCardProps,
+} from "./StandardAmbientToolCards";
+export {
+ AgentControlToolCard,
+ FileDiffToolCard,
+ GitToolCard,
+ PageDeployToolCard,
+ PagePublishToolCard,
+ ReviewSummaryToolCard,
+ type AgentControlToolCardProps,
+ type FileDiffToolCardProps,
+ type GitToolCardFooterItem,
+ type GitToolCardProps,
+ type PageDeployToolCardProps,
+ type PageLifecycleToolCardField,
+ type PagePublishToolCardProps,
+ type ReviewSummaryToolCardProps,
+} from "./ProminentToolCards";
+export {
+ ToolCardStatusSlot,
+ type ToolCardStatusSlotProps,
+} from "./ToolCardStatusSlot";
+export {
+ ToolProcessingDots,
+ type ToolProcessingDotsProps,
+ type ToolProcessingDotsSize,
+} from "./ToolProcessingDots";
diff --git a/design-system/packages/ui/src/registry.ts b/design-system/packages/ui/src/registry.ts
index 9edde12537..873c660fb1 100644
--- a/design-system/packages/ui/src/registry.ts
+++ b/design-system/packages/ui/src/registry.ts
@@ -23,6 +23,39 @@ import { statusPillMeta } from "./components/StatusPill/StatusPill.meta";
import { switchMeta } from "./components/Switch/Switch.meta";
import { tabGroupMeta } from "./components/TabGroup/TabGroup.meta";
import { toolbarMeta } from "./components/Toolbar/Toolbar.meta";
+import { askUserMeta } from "./flow-chat/ask-user/AskUser.meta";
+import { chatComposerMeta } from "./flow-chat/composer/ChatComposer.meta";
+import { ambientToolCardMeta } from "./flow-chat/tool-cards/AmbientToolCard.meta";
+import { commandToolCardMeta } from "./flow-chat/tool-cards/CommandToolCard.meta";
+import { contextCompressionToolCardMeta } from "./flow-chat/tool-cards/ContextCompressionToolCard.meta";
+import { fileOperationToolCardMeta } from "./flow-chat/tool-cards/FileOperationToolCard.meta";
+import { prominentToolCardMeta } from "./flow-chat/tool-cards/ProminentToolCard.meta";
+import {
+ agentControlToolCardMeta,
+ fileDiffToolCardMeta,
+ gitToolCardMeta,
+ pageDeployToolCardMeta,
+ pagePublishToolCardMeta,
+ reviewSummaryToolCardMeta,
+} from "./flow-chat/tool-cards/ProminentToolCards.meta";
+import { readFileToolCardMeta } from "./flow-chat/tool-cards/ReadFileToolCard.meta";
+import {
+ agentWaitToolCardMeta,
+ defaultToolCardMeta,
+ directoryListToolCardMeta,
+ getToolSpecToolCardMeta,
+ globSearchToolCardMeta,
+ grepSearchToolCardMeta,
+ runCodeToolCardMeta,
+ sessionControlToolCardMeta,
+ sessionMessageToolCardMeta,
+ skillToolCardMeta,
+ terminalControlToolCardMeta,
+ todoToolCardMeta,
+ viewImageToolCardMeta,
+ webFetchToolCardMeta,
+ webSearchToolCardMeta,
+} from "./flow-chat/tool-cards/StandardToolCards.meta";
import type { ComponentMeta } from "./registry.types";
export type {
@@ -35,12 +68,27 @@ export const componentRegistry = [
actionCardMeta,
actionItemMeta,
activityItemMeta,
+ agentControlToolCardMeta,
+ agentWaitToolCardMeta,
+ ambientToolCardMeta,
+ askUserMeta,
buttonMeta,
cardMeta,
+ chatComposerMeta,
+ commandToolCardMeta,
composerMeta,
confirmDialogMeta,
+ contextCompressionToolCardMeta,
+ defaultToolCardMeta,
+ directoryListToolCardMeta,
fieldMeta,
fieldGroupMeta,
+ fileDiffToolCardMeta,
+ fileOperationToolCardMeta,
+ getToolSpecToolCardMeta,
+ gitToolCardMeta,
+ globSearchToolCardMeta,
+ grepSearchToolCardMeta,
iconMeta,
iconButtonMeta,
inputMeta,
@@ -48,13 +96,27 @@ export const componentRegistry = [
menuMeta,
modalMeta,
navigationPanelMeta,
+ pageDeployToolCardMeta,
pageHeaderMeta,
+ pagePublishToolCardMeta,
+ prominentToolCardMeta,
+ readFileToolCardMeta,
+ reviewSummaryToolCardMeta,
+ runCodeToolCardMeta,
scrollAreaMeta,
searchFieldMeta,
segmentedControlMeta,
selectMeta,
+ sessionControlToolCardMeta,
+ sessionMessageToolCardMeta,
+ skillToolCardMeta,
statusPillMeta,
switchMeta,
tabGroupMeta,
+ terminalControlToolCardMeta,
+ todoToolCardMeta,
toolbarMeta,
+ viewImageToolCardMeta,
+ webFetchToolCardMeta,
+ webSearchToolCardMeta,
] as const satisfies readonly ComponentMeta[];
diff --git a/design-system/packages/ui/src/registry.types.ts b/design-system/packages/ui/src/registry.types.ts
index bd9ffc87e0..148f743aad 100644
--- a/design-system/packages/ui/src/registry.types.ts
+++ b/design-system/packages/ui/src/registry.types.ts
@@ -7,7 +7,7 @@ export interface ComponentPropMeta {
}
export interface ComponentMeta {
- category: "primitive" | "action" | "form" | "feedback" | "navigation";
+ category: "primitive" | "action" | "form" | "feedback" | "navigation" | "flow-chat";
description: string;
maturity: ComponentMaturity;
name: string;
diff --git a/design-system/packages/ui/tests/ask-user.test.mjs b/design-system/packages/ui/tests/ask-user.test.mjs
new file mode 100644
index 0000000000..d86c250b06
--- /dev/null
+++ b/design-system/packages/ui/tests/ask-user.test.mjs
@@ -0,0 +1,92 @@
+import assert from "node:assert/strict";
+import { readFile } from "node:fs/promises";
+import test from "node:test";
+import { createElement } from "react";
+import { renderToStaticMarkup } from "react-dom/server";
+import { AskUser } from "../dist/flow-chat.js";
+
+const questions = [
+ {
+ customOption: {
+ description: "Provide custom text input",
+ inputLabel: "Custom version",
+ label: "Other",
+ placeholder: "Enter a version",
+ value: "other",
+ },
+ id: "version",
+ options: [
+ {
+ description: "Latest Beta pre-release — newer and has passed basic testing",
+ label: "v0.2.19-beta.1 (Recommended)",
+ value: "beta",
+ },
+ {
+ description: "The stable release marked as Latest on GitHub — the most stable one",
+ label: "v0.2.18",
+ value: "stable",
+ },
+ ],
+ prompt: "BitFun has three versions — which would you like me to pull?",
+ selectionMode: "single",
+ },
+];
+
+test("AskUser renders the answered disclosure and native selection semantics", () => {
+ const markup = renderToStaticMarkup(createElement(AskUser, {
+ answers: { version: ["beta"] },
+ expanded: true,
+ questions,
+ state: "completed",
+ summaryDetail: "Choose version: v0.2.19-beta.1 (Recommended)",
+ summaryLabel: "1 question answered",
+ }));
+
+ assert.match(markup, /data-bf-component="ask-user"/);
+ assert.match(markup, /data-bf-state="completed"/);
+ assert.match(markup, /data-bf-expanded="true"/);
+ assert.match(markup, /