diff --git a/src/web-ui/src/flow_chat/components/CopyableTextPreview.scss b/src/web-ui/src/flow_chat/components/CopyableTextPreview.scss new file mode 100644 index 0000000000..f14a8a4543 --- /dev/null +++ b/src/web-ui/src/flow_chat/components/CopyableTextPreview.scss @@ -0,0 +1,68 @@ +.copyable-text-preview { + flex: 1; + min-width: 0; + font-family: var(--tool-card-font-mono); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.copyable-text-preview.copyable-text-preview--theme-font { + font-family: var(--font-family-sans); +} + +.copyable-text-preview--compact { + font-size: var(--flowchat-font-size-xs); + line-height: var(--flowchat-support-line-height); + font-weight: normal; + color: var(--color-text-muted); +} + +.copyable-text-preview__empty { + color: color-mix(in srgb, var(--color-error) 72%, var(--color-static-white)); + font-style: italic; +} + +.copyable-text-preview-tooltip-content { + max-width: 100%; + display: flex; + align-items: flex-start; + gap: var(--size-gap-2); + font-family: var(--font-family-sans); + font-size: var(--flowchat-font-size-sm); + line-height: var(--flowchat-support-line-height); +} + +.copyable-text-preview-tooltip-content__text { + min-width: 0; + flex: 1; + white-space: pre-wrap; + word-break: break-word; +} + +.copyable-text-preview-tooltip { + .bitfun-tooltip__content { + max-width: min(960px, calc(100vw - 32px)); + } +} + +.copyable-text-preview-tooltip__copy { + flex: none; + margin-top: 1px; + color: var(--color-text-muted); + + &.icon-btn--xs { + width: 18px; + height: 18px; + border-radius: 4px; + + svg { + width: 12px; + height: 12px; + } + } + + &.copied { + color: var(--color-success); + } +} diff --git a/src/web-ui/src/flow_chat/components/CopyableTextPreview.test.tsx b/src/web-ui/src/flow_chat/components/CopyableTextPreview.test.tsx new file mode 100644 index 0000000000..a4e29231a6 --- /dev/null +++ b/src/web-ui/src/flow_chat/components/CopyableTextPreview.test.tsx @@ -0,0 +1,82 @@ +// @vitest-environment jsdom + +import React, { act } from 'react'; +import { createRoot, type Root } from 'react-dom/client'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { CopyableTextPreview } from './CopyableTextPreview'; + +globalThis.IS_REACT_ACT_ENVIRONMENT = true; + +vi.mock('react-i18next', async () => { + const actual = await vi.importActual('react-i18next'); + return { + ...actual, + useTranslation: () => ({ + t: (key: string) => ({ + 'toolCards.common.copy': 'Copy', + 'toolCards.common.copied': 'Copied', + 'toolCards.common.copyFailed': 'Failed to copy', + })[key] ?? key, + }), + }; +}); + +vi.mock('../../component-library', () => ({ + Tooltip: ({ content, children }: { content: React.ReactNode; children: React.ReactElement }) => ( + <> + {children} + {content} + + ), + IconButton: ({ children, tooltip: _tooltip, ...props }: React.ButtonHTMLAttributes & { tooltip?: string }) => ( + + ), +})); + +describe('CopyableTextPreview', () => { + let container: HTMLDivElement; + let root: Root; + + beforeEach(() => { + container = document.createElement('div'); + document.body.appendChild(container); + root = createRoot(container); + }); + + afterEach(() => { + act(() => root.unmount()); + container.remove(); + vi.unstubAllGlobals(); + }); + + it('copies the complete interactive tooltip text', async () => { + const writeText = vi.fn(() => Promise.resolve()); + Object.defineProperty(navigator, 'clipboard', { + configurable: true, + value: { writeText }, + }); + const tooltipText = 'pnpm run type-check:web\npnpm run lint:web'; + + await act(async () => { + root.render( + , + ); + }); + + const copyButton = container.querySelector('[aria-label="Copy"]'); + expect(copyButton).not.toBeNull(); + expect(container.querySelector('.copyable-text-preview-tooltip-content__text')?.textContent) + .toBe(tooltipText); + + await act(async () => { + copyButton?.click(); + await Promise.resolve(); + }); + + expect(writeText).toHaveBeenCalledWith(tooltipText); + }); +}); diff --git a/src/web-ui/src/flow_chat/components/CopyableTextPreview.tsx b/src/web-ui/src/flow_chat/components/CopyableTextPreview.tsx new file mode 100644 index 0000000000..7cc5e2b22e --- /dev/null +++ b/src/web-ui/src/flow_chat/components/CopyableTextPreview.tsx @@ -0,0 +1,83 @@ +import React from 'react'; +import { Check, Copy } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; +import { IconButton, Tooltip } from '../../component-library'; +import { useCopyTextAction } from '../hooks/useCopyTextAction'; +import './CopyableTextPreview.scss'; + +interface CopyableTextPreviewProps extends React.HTMLAttributes { + text?: string | null; + emptyText: React.ReactNode; + as?: 'span' | 'code'; + className?: string; + tooltipContent?: React.ReactNode; + tooltipPlacement?: 'top' | 'bottom' | 'left' | 'right'; +} + +export const CopyableTextPreview = React.forwardRef(({ + text, + emptyText, + as = 'span', + className, + tooltipContent, + tooltipPlacement = 'bottom', + ...restProps +}, ref) => { + const { t } = useTranslation('flow-chat'); + const content = text?.trim() + ? text + : {emptyText}; + const resolvedClassName = `copyable-text-preview${className ? ` ${className}` : ''}`; + const copyText = typeof tooltipContent === 'string' && tooltipContent.trim() + ? tooltipContent + : undefined; + const { copied, copy } = useCopyTextAction({ + getText: () => copyText ?? '', + successMessage: t('toolCards.common.copied'), + failureMessage: t('toolCards.common.copyFailed'), + showSuccessNotification: false, + }); + const copyTooltip = copied ? t('toolCards.common.copied') : t('toolCards.common.copy'); + const node = as === 'code' ? ( + + {content} + + ) : ( + + {content} + + ); + + if (!tooltipContent) { + return node; + } + + return ( + + {tooltipContent} + {copyText && ( + + {copied ? : } + + )} + + } + placement={tooltipPlacement} + className="copyable-text-preview-tooltip" + interactive + > + {node} + + ); +}); + +CopyableTextPreview.displayName = 'CopyableTextPreview'; diff --git a/src/web-ui/src/flow_chat/components/modern/PermissionRequestPanel.scss b/src/web-ui/src/flow_chat/components/modern/PermissionRequestPanel.scss index cfe5329c27..31ff9e6a57 100644 --- a/src/web-ui/src/flow_chat/components/modern/PermissionRequestPanel.scss +++ b/src/web-ui/src/flow_chat/components/modern/PermissionRequestPanel.scss @@ -30,8 +30,7 @@ background: color-mix(in srgb, var(--color-bg-elevated) 96%, var(--color-warning)); box-shadow: 0 16px 40px var(--color-overlay-black-30), - 0 3px 10px var(--color-overlay-black-12), - inset 3px 0 0 color-mix(in srgb, var(--color-warning) 76%, transparent); + 0 3px 10px var(--color-overlay-black-12); color: var(--color-text-primary); transform-origin: center bottom; animation: permission-request-panel-in 180ms cubic-bezier(0.23, 1, 0.32, 1); @@ -97,6 +96,8 @@ height: 30px; padding: 0; border-radius: 5px; + border-color: transparent; + background: transparent; } .permission-request-panel__collapse:hover, @@ -104,6 +105,10 @@ background: color-mix(in srgb, var(--color-warning) 14%, var(--color-bg-primary)); } +.permission-request-panel__collapse:hover { + background: transparent; +} + .permission-request-panel__collapse:focus-visible, .permission-request-panel__collapsed-trigger:focus-visible { outline: 2px solid var(--color-warning); @@ -231,6 +236,7 @@ border: 1px solid var(--border-base); border-radius: 6px; padding: 7px 9px; + font-family: var(--font-family-sans); color: var(--color-text-primary); background: var(--color-bg-primary); } @@ -301,13 +307,15 @@ } .permission-request-panel__actions button:disabled { - cursor: wait; opacity: 0.6; } -.permission-request-panel__batch-actions button:disabled { +.permission-request-panel__actions button:disabled:not(.permission-request-panel__feedback-disabled) { cursor: wait; - opacity: 0.6; +} + +.permission-request-panel__actions button.permission-request-panel__feedback-disabled { + cursor: not-allowed; } .permission-request-panel .permission-request-panel__reject { diff --git a/src/web-ui/src/flow_chat/components/modern/PermissionRequestPanel.test.tsx b/src/web-ui/src/flow_chat/components/modern/PermissionRequestPanel.test.tsx index 4113c99068..935bb64a4a 100644 --- a/src/web-ui/src/flow_chat/components/modern/PermissionRequestPanel.test.tsx +++ b/src/web-ui/src/flow_chat/components/modern/PermissionRequestPanel.test.tsx @@ -26,32 +26,64 @@ const TRANSLATIONS: Record = { 'permission.actions.other': 'Other action', }; -vi.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string, values?: Record) => { - if (key === 'permission.subagentOwner') { - return `${values?.subagent} subagent`; - } - if (key === 'permission.allowAlwaysTooltip') { - return `Always allow saves matching access for ${values?.projectPath}`; - } - if (key === 'permission.risks.pageSave') { - return `Save ${values?.slug} as ${values?.visibility} without deploying.`; - } - if (key === 'permission.collapsePanel') { - return 'Collapse permission requests'; - } - if (key === 'permission.expandPanel') { - return `Expand ${values?.count} pending permission requests`; - } - return TRANSLATIONS[key] ?? key; - }, - }), -})); +vi.mock('react-i18next', async () => { + const actual = await vi.importActual('react-i18next'); + return { + ...actual, + useTranslation: () => ({ + t: (key: string, values?: Record) => { + if (key === 'permission.subagentOwner') { + return `${values?.subagent} subagent`; + } + if (key === 'permission.allowAlwaysTooltip') { + return `Always allow saves matching access for ${values?.projectPath}`; + } + if (key === 'permission.risks.pageSave') { + return `Save ${values?.slug} as ${values?.visibility} without deploying.`; + } + if (key === 'permission.collapsePanel') { + return 'Collapse permission requests'; + } + if (key === 'permission.expandPanel') { + return `Expand ${values?.count} pending permission requests`; + } + return TRANSLATIONS[key] ?? key; + }, + }), + }; +}); vi.mock('@/component-library', () => ({ - Tooltip: ({ content, children }: { content: string; children: React.ReactElement }) => ( - {children} + Tooltip: ({ + content, + children, + interactive, + }: { + content: React.ReactNode; + children: React.ReactElement; + interactive?: boolean; + }) => { + const textContent = (node: React.ReactNode): string => { + if (typeof node === 'string' || typeof node === 'number') return String(node); + if (Array.isArray(node)) return node.map(textContent).join(''); + if (React.isValidElement<{ children?: React.ReactNode }>(node)) { + return textContent(node.props.children); + } + return ''; + }; + const tooltipText = textContent(content); + return ( + + {children} + + ); + }, + IconButton: ({ + children, + tooltip: _tooltip, + ...props + }: React.ButtonHTMLAttributes & { tooltip?: string }) => ( + ), })); @@ -136,7 +168,7 @@ describe('PermissionRequestPanel', () => { expect(tooltips).not.toContain('project-1'); }); - it('keeps resources to one ellipsized summary with the complete value in a tooltip', () => { + it('keeps resources to one ellipsized summary with an interactive multiline tooltip', () => { const longResource = 'src/a-very-long-directory-name/another-long-directory/file-with-a-long-name.ts'; const bashRequest = { ...request(false), @@ -155,8 +187,10 @@ describe('PermissionRequestPanel', () => { const resourceSummary = container.querySelector('.permission-request-panel__resource-summary'); expect(resourceSummary?.textContent).toBe(`${longResource}, pnpm run type-check:web`); + expect(resourceSummary?.classList.contains('copyable-text-preview--theme-font')).toBe(true); expect(resourceSummary?.parentElement?.getAttribute('data-tooltip')) - .toBe(`${longResource}, pnpm run type-check:web`); + .toBe(`${longResource}\npnpm run type-check:web`); + expect(resourceSummary?.parentElement?.getAttribute('data-tooltip-interactive')).toBe('true'); expect(container.textContent).toContain('Run command'); }); @@ -253,6 +287,71 @@ describe('PermissionRequestPanel', () => { expect(container.querySelectorAll('[role="listitem"]')).toHaveLength(2); }); + it('shows batch controls for a single request and responds through the batch handler', async () => { + const first = request(false); + const onRespondBatch = vi.fn(() => Promise.resolve()); + await act(async () => { + root.render( + , + ); + }); + + const batchButton = [...container.querySelectorAll('button')].find( + (button) => button.textContent?.includes('permission.allowCurrentAndFollowing'), + ); + expect(batchButton).toBeDefined(); + expect([...container.querySelectorAll('button')] + .some((button) => button.textContent?.includes('permission.rejectCurrentAndFollowing'))).toBe(true); + + await act(async () => { + batchButton?.click(); + await Promise.resolve(); + }); + + expect(onRespondBatch).toHaveBeenCalledWith(first.requestId, 'once', undefined); + }); + + it('disables allow actions while rejection feedback is present', () => { + act(() => { + root.render( + , + ); + }); + + const feedbackInput = container.querySelector('textarea'); + const valueSetter = Object.getOwnPropertyDescriptor( + HTMLTextAreaElement.prototype, + 'value', + )?.set; + act(() => { + valueSetter?.call(feedbackInput, 'Use a safer command instead.'); + feedbackInput?.dispatchEvent(new Event('input', { bubbles: true })); + }); + + const buttonWithLabel = (label: string) => [...container.querySelectorAll('button')].find( + (button) => button.textContent?.includes(label), + ); + const allowOnce = buttonWithLabel('permission.allowOnce'); + const allowAlways = buttonWithLabel('permission.allowAlways'); + const allowAll = buttonWithLabel('permission.allowCurrentAndFollowing'); + expect(allowOnce?.disabled).toBe(true); + expect(allowAlways?.disabled).toBe(true); + expect(allowAll?.disabled).toBe(true); + expect(allowOnce?.classList.contains('permission-request-panel__feedback-disabled')).toBe(true); + expect(allowAlways?.classList.contains('permission-request-panel__feedback-disabled')).toBe(true); + expect(allowAll?.classList.contains('permission-request-panel__feedback-disabled')).toBe(true); + expect(buttonWithLabel('permission.reject')?.disabled).toBe(false); + expect(buttonWithLabel('permission.rejectCurrentAndFollowing')?.disabled).toBe(false); + }); + it('collapses to an anchored permission indicator and reopens it with the session pending count', () => { act(() => { root.render( diff --git a/src/web-ui/src/flow_chat/components/modern/PermissionRequestPanel.tsx b/src/web-ui/src/flow_chat/components/modern/PermissionRequestPanel.tsx index a952e4c3b0..99205fee5b 100644 --- a/src/web-ui/src/flow_chat/components/modern/PermissionRequestPanel.tsx +++ b/src/web-ui/src/flow_chat/components/modern/PermissionRequestPanel.tsx @@ -7,6 +7,7 @@ import type { PermissionRequest, } from '@/infrastructure/api/service-api/AgentAPI'; import { useChatInputState } from '../../store/chatInputStateStore'; +import { CopyableTextPreview } from '../CopyableTextPreview'; import { CHAT_INPUT_DROP_ZONE_BOTTOM_PX } from '../../utils/flowChatScrollLayout'; import './PermissionRequestPanel.scss'; @@ -97,6 +98,8 @@ export function PermissionRequestPanel({ const request = requests[0]; const risk = permissionRisk(request, t); const pendingCount = Math.max(totalPendingCount ?? requests.length, requests.length); + const hasRejectFeedback = feedback.trim().length > 0; + const allowActionsDisabledForFeedback = hasRejectFeedback && !responding; const alwaysAllowTooltip = request?.saveResources?.length ? request.projectPath?.trim() @@ -189,36 +192,44 @@ export function PermissionRequestPanel({
- {requests.map((item, index) => ( -
-
-
- {item.source.identity} - {item.delegation && ( - - {t('permission.subagentOwner', { subagent: item.delegation.subagentType })} - - )} + {requests.map((item, index) => { + const resourceSummary = item.resources.join(', '); + const resourceTooltip = item.resources.join('\n'); + + return ( +
+
+
+ {item.source.identity} + {item.delegation && ( + + {t('permission.subagentOwner', { subagent: item.delegation.subagentType })} + + )} +
+ {index === 0 ? t('permission.current') : t('permission.pending')} +
+
+ + {permissionActionLabel(item.action, t)} + + +
- {index === 0 ? t('permission.current') : t('permission.pending')} -
-
- - {permissionActionLabel(item.action, t)} - - - - - {item.resources.join(', ')} - -
-
- ))} + ); + })}
{risk &&

{risk}

} {error &&

{t('permission.responseFailed')}

} @@ -232,12 +243,22 @@ export function PermissionRequestPanel({ />
- {!!request.saveResources?.length && ( - @@ -251,21 +272,24 @@ export function PermissionRequestPanel({
- {requests.length > 1 && ( -
- - -
- )} +
+ + +
)} diff --git a/src/web-ui/src/flow_chat/tool-cards/useCopyTextAction.ts b/src/web-ui/src/flow_chat/hooks/useCopyTextAction.ts similarity index 100% rename from src/web-ui/src/flow_chat/tool-cards/useCopyTextAction.ts rename to src/web-ui/src/flow_chat/hooks/useCopyTextAction.ts diff --git a/src/web-ui/src/flow_chat/tool-cards/ExecProcessToolCardView.tsx b/src/web-ui/src/flow_chat/tool-cards/ExecProcessToolCardView.tsx index 14aa312de3..943dbc6bcd 100644 --- a/src/web-ui/src/flow_chat/tool-cards/ExecProcessToolCardView.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/ExecProcessToolCardView.tsx @@ -8,7 +8,7 @@ import { } from '@/tools/terminal/components/LazyTerminalOutputRenderer'; import { BaseToolCard, ToolCardHeader } from './BaseToolCard'; import { ToolCardCopyAction, ToolCardHeaderActions } from './ToolCardHeaderActions'; -import { ToolCommandPreview } from './ToolCommandPreview'; +import { CopyableTextPreview } from '../components/CopyableTextPreview'; import { ToolTimeoutIndicator } from './ToolTimeoutIndicator'; import { DotMatrixLoader } from '../../component-library'; import { useToolCardHeightContract, type ToolCardCollapseReason } from './useToolCardHeightContract'; @@ -304,15 +304,15 @@ export const ExecProcessToolCardView: React.FC = ( : undefined; const renderPrimaryText = (variant: 'default' | 'compact' = 'default') => ( - diff --git a/src/web-ui/src/flow_chat/tool-cards/GitToolDisplay.scss b/src/web-ui/src/flow_chat/tool-cards/GitToolDisplay.scss index b217956997..aff009dcac 100644 --- a/src/web-ui/src/flow_chat/tool-cards/GitToolDisplay.scss +++ b/src/web-ui/src/flow_chat/tool-cards/GitToolDisplay.scss @@ -26,7 +26,7 @@ /* Collapsed row: match thinking summary typography (`ModelThinkingDisplay.scss` `.thinking-label`). */ .compact-tool-card-wrapper.git-tool-display { .compact-card-action, - .compact-card-content .tool-command-preview.tool-command-preview--compact { + .compact-card-content .copyable-text-preview.copyable-text-preview--compact { font-family: var(--font-family-sans); font-size: var(--flowchat-font-size-sm); line-height: var(--flowchat-support-line-height); @@ -107,7 +107,7 @@ // Prevent command preview from consuming all flex space so the inline // summary can sit right next to the command text instead of being // pushed to the far right. - .tool-command-preview { + .copyable-text-preview { flex: 0 1 auto; max-width: 50%; } @@ -128,7 +128,7 @@ /* Collapsed row only — expanded uses `.terminal-command` from TerminalToolCard.scss. */ .compact-tool-card-wrapper.git-tool-display .git-tool-info .git-command-preview, - .compact-tool-card-wrapper.git-tool-display .git-command-preview.tool-command-preview--compact { + .compact-tool-card-wrapper.git-tool-display .git-command-preview.copyable-text-preview--compact { color: var(--color-text-muted); } diff --git a/src/web-ui/src/flow_chat/tool-cards/GitToolDisplay.tsx b/src/web-ui/src/flow_chat/tool-cards/GitToolDisplay.tsx index cd70022fdf..e62f7d48d3 100644 --- a/src/web-ui/src/flow_chat/tool-cards/GitToolDisplay.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/GitToolDisplay.tsx @@ -9,7 +9,7 @@ import { CubeLoading } from '../../component-library'; import type { ToolCardProps } from '../types/flow-chat'; import { BaseToolCard, ToolCardHeader } from './BaseToolCard'; import { ToolCardCopyAction, ToolCardHeaderActions } from './ToolCardHeaderActions'; -import { ToolCommandPreview } from './ToolCommandPreview'; +import { CopyableTextPreview } from '../components/CopyableTextPreview'; import { createLogger } from '@/shared/utils/logger'; import { useToolCardHeightContract } from './useToolCardHeightContract'; import './GitToolDisplay.scss'; @@ -152,13 +152,13 @@ export const GitToolDisplay: React.FC = ({ }; const renderCommandPreview = (variant: 'expanded' | 'compact') => ( - diff --git a/src/web-ui/src/flow_chat/tool-cards/TerminalToolCard.scss b/src/web-ui/src/flow_chat/tool-cards/TerminalToolCard.scss index 963c540e26..afcecc6e52 100644 --- a/src/web-ui/src/flow_chat/tool-cards/TerminalToolCard.scss +++ b/src/web-ui/src/flow_chat/tool-cards/TerminalToolCard.scss @@ -21,7 +21,7 @@ * sans + `sm` + line-height 1.5 + normal — not monospace (mono reads “thinner” vs “思考了 N 字”). */ .compact-card-action, - .compact-card-content .tool-command-preview.tool-command-preview--compact { + .compact-card-content .copyable-text-preview.copyable-text-preview--compact { font-family: var(--font-family-sans); font-size: var(--flowchat-font-size-sm); line-height: var(--flowchat-support-line-height); @@ -265,7 +265,7 @@ flex: 1; gap: 0; - .tool-command-preview { + .copyable-text-preview { flex: 0 1 auto; max-width: 60%; } @@ -615,7 +615,7 @@ } .compact-card-action, - .compact-card-content .tool-command-preview--compact { + .compact-card-content .copyable-text-preview--compact { font-size: var(--flowchat-font-size-xs); line-height: var(--flowchat-support-line-height); font-weight: normal; diff --git a/src/web-ui/src/flow_chat/tool-cards/TerminalToolCard.tsx b/src/web-ui/src/flow_chat/tool-cards/TerminalToolCard.tsx index 646e28ebc0..14b04df5a5 100644 --- a/src/web-ui/src/flow_chat/tool-cards/TerminalToolCard.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/TerminalToolCard.tsx @@ -27,7 +27,7 @@ import { useToolCardCompletionGracePeriod } from './useToolCardCompletionGracePe import { getTerminalViewState, type TerminalViewState } from './terminalToolCardState'; import { ToolTimeoutIndicator } from './ToolTimeoutIndicator'; import { ToolCardCopyAction, ToolCardHeaderActions } from './ToolCardHeaderActions'; -import { ToolCommandPreview } from './ToolCommandPreview'; +import { CopyableTextPreview } from '../components/CopyableTextPreview'; import { formatSessionViewPreviewText } from '../utils/sessionViewPreview'; import './TerminalToolCard.scss'; @@ -566,14 +566,14 @@ export const TerminalToolCard: React.FC = ({ const emptyText = t(showConfirmButtons ? 'toolCards.terminal.commandEmpty' : 'toolCards.terminal.noCommand'); return ( - { - command?: string | null; - emptyText: React.ReactNode; - as?: 'span' | 'code'; - className?: string; - tooltipContent?: React.ReactNode; - tooltipPlacement?: 'top' | 'bottom' | 'left' | 'right'; -} - -export const ToolCommandPreview = React.forwardRef(({ - command, - emptyText, - as = 'span', - className, - tooltipContent, - tooltipPlacement = 'bottom', - ...restProps -}, ref) => { - const content = command?.trim() - ? command - : {emptyText}; - const resolvedClassName = `tool-command-preview${className ? ` ${className}` : ''}`; - const node = as === 'code' ? ( - - {content} - - ) : ( - - {content} - - ); - - if (!tooltipContent) { - return node; - } - - return ( - {tooltipContent}
} - placement={tooltipPlacement} - className="tool-command-preview-tooltip" - interactive - > - {node} - - ); -}); - -ToolCommandPreview.displayName = 'ToolCommandPreview'; diff --git a/src/web-ui/src/locales/en-US/flow-chat.json b/src/web-ui/src/locales/en-US/flow-chat.json index 5f83e4b841..d1ff87149d 100644 --- a/src/web-ui/src/locales/en-US/flow-chat.json +++ b/src/web-ui/src/locales/en-US/flow-chat.json @@ -1660,6 +1660,9 @@ "success": "Success", "failed": "Failed", "error": "Error", + "copy": "Copy", + "copied": "Copied", + "copyFailed": "Failed to copy", "expand": "Expand", "collapse": "Collapse", "collapseContent": "Collapse content", diff --git a/src/web-ui/src/locales/zh-CN/flow-chat.json b/src/web-ui/src/locales/zh-CN/flow-chat.json index 7b9c820f11..96dc9f5960 100644 --- a/src/web-ui/src/locales/zh-CN/flow-chat.json +++ b/src/web-ui/src/locales/zh-CN/flow-chat.json @@ -1660,6 +1660,9 @@ "success": "成功", "failed": "失败", "error": "错误", + "copy": "复制", + "copied": "已复制", + "copyFailed": "复制失败", "expand": "展开", "collapse": "收起", "collapseContent": "收起内容", diff --git a/src/web-ui/src/locales/zh-TW/flow-chat.json b/src/web-ui/src/locales/zh-TW/flow-chat.json index 898ac75820..9966dde990 100644 --- a/src/web-ui/src/locales/zh-TW/flow-chat.json +++ b/src/web-ui/src/locales/zh-TW/flow-chat.json @@ -1660,6 +1660,9 @@ "success": "成功", "failed": "失敗", "error": "錯誤", + "copy": "複製", + "copied": "已複製", + "copyFailed": "複製失敗", "expand": "展開", "collapse": "收起", "collapseContent": "收起內容",