From 4df4035e31bd2e1be61d549b6c2c6611d450070c Mon Sep 17 00:00:00 2001 From: Bob Lee Date: Sat, 12 Sep 2026 11:04:48 +0800 Subject: [PATCH 1/3] fix(pet): preserve macOS drag movement and direction --- .../AgentCompanionDesktopPet.scss | 3 + .../AgentCompanionDesktopPet.test.tsx | 55 +++++++++++++- .../AgentCompanionDesktopPet.tsx | 48 +++++++++++- .../AgentCompanionPointerDragService.test.ts | 65 ++++++++++++++++ .../AgentCompanionPointerDragService.ts | 75 +++++++++++++++++++ 5 files changed, 240 insertions(+), 6 deletions(-) create mode 100644 src/web-ui/src/infrastructure/config/services/AgentCompanionPointerDragService.test.ts create mode 100644 src/web-ui/src/infrastructure/config/services/AgentCompanionPointerDragService.ts diff --git a/src/web-ui/src/app/components/AgentCompanionDesktopPet/AgentCompanionDesktopPet.scss b/src/web-ui/src/app/components/AgentCompanionDesktopPet/AgentCompanionDesktopPet.scss index 5a7244de6e..e8622ff507 100644 --- a/src/web-ui/src/app/components/AgentCompanionDesktopPet/AgentCompanionDesktopPet.scss +++ b/src/web-ui/src/app/components/AgentCompanionDesktopPet/AgentCompanionDesktopPet.scss @@ -24,6 +24,7 @@ position: relative; background: transparent; user-select: none; + -webkit-user-select: none; /* Holds the dock anchored bottom-right, above the click-away backdrop. */ &__stack { @@ -172,6 +173,8 @@ place-items: center; cursor: grab; pointer-events: auto; + touch-action: none; + -webkit-user-drag: none; &:active { cursor: grabbing; diff --git a/src/web-ui/src/app/components/AgentCompanionDesktopPet/AgentCompanionDesktopPet.test.tsx b/src/web-ui/src/app/components/AgentCompanionDesktopPet/AgentCompanionDesktopPet.test.tsx index 553ce81cbe..12c39bbe8f 100644 --- a/src/web-ui/src/app/components/AgentCompanionDesktopPet/AgentCompanionDesktopPet.test.tsx +++ b/src/web-ui/src/app/components/AgentCompanionDesktopPet/AgentCompanionDesktopPet.test.tsx @@ -14,7 +14,9 @@ const invokeMock = vi.hoisted(() => vi.fn(() => Promise.resolve())); const cursorPositionMock = vi.hoisted(() => vi.fn(() => Promise.resolve({ x: 0, y: 0 }))); const startDraggingMock = vi.hoisted(() => vi.fn(() => Promise.resolve())); const controlledDragMock = vi.hoisted(() => vi.fn()); +const pointerDrag = vi.hoisted(() => ({ prepare: vi.fn(), move: vi.fn(), finish: vi.fn(), cancel: vi.fn() })); vi.mock('@/infrastructure/config/services/AgentCompanionDragService', () => ({ startAgentCompanionDrag: controlledDragMock })); +vi.mock('@/infrastructure/config/services/AgentCompanionPointerDragService', () => ({ prepareAgentCompanionPointerDrag: pointerDrag.prepare })); /** Backs the Tauri IPC bridge, so window resize requests are observable. */ const hostInvokeMock = vi.hoisted(() => vi.fn(() => Promise.resolve())); @@ -142,6 +144,8 @@ describe('AgentCompanionDesktopPet', () => { element.dispatchEvent(new window.MouseEvent(type, { bubbles: true, cancelable: true, + screenX: at?.clientX, + screenY: at?.clientY, ...at, })); }); @@ -156,6 +160,8 @@ describe('AgentCompanionDesktopPet', () => { startDraggingMock.mockReset(); startDraggingMock.mockResolvedValue(undefined); controlledDragMock.mockReset(); + pointerDrag.prepare.mockReset().mockReturnValue(pointerDrag); + pointerDrag.move.mockReset(); pointerDrag.finish.mockReset(); pointerDrag.cancel.mockReset(); listenMock.mockReset(); const activityListeners: Array<(event: { payload: AgentCompanionActivityPayload }) => void> = []; @@ -266,8 +272,8 @@ describe('AgentCompanionDesktopPet', () => { controlledDragMock.mockReturnValue(stop); try { vi.resetModules(); - const { AgentCompanionDesktopPet: WindowsPet } = await import('./AgentCompanionDesktopPet'); - await act(async () => root.render()); + const { AgentCompanionDesktopPet: PlatformPet } = await import('./AgentCompanionDesktopPet'); + await act(async () => root.render()); const hitbox = query('.openbitfun-agent-companion-window__pet-hitbox')!; dispatch(hitbox, 'pointerdown', { clientX: 100, clientY: 100 }); dispatch(hitbox, 'pointermove', { clientX: 120, clientY: 100 }); @@ -277,6 +283,8 @@ describe('AgentCompanionDesktopPet', () => { expect(query('[data-testid="pixel-pet"]')?.getAttribute('data-mood')).toBe('dragging'); act(() => controlledDragMock.mock.calls[0][0]('left')); expect(query('[data-testid="pixel-pet"]')?.getAttribute('data-direction')).toBe('left'); + act(() => controlledDragMock.mock.calls[0][0]('right')); + expect(query('[data-testid="pixel-pet"]')?.getAttribute('data-direction')).toBe('right'); dispatch(hitbox, 'pointerup', { clientX: 120, clientY: 100 }); expect(stop).toHaveBeenCalledTimes(1); expect(query('[data-testid="pixel-pet"]')?.getAttribute('data-action')).toBe('waving'); @@ -285,11 +293,54 @@ describe('AgentCompanionDesktopPet', () => { dispatch(hitbox, 'pointercancel'); expect(stop).toHaveBeenCalledTimes(2); expect(query('[data-testid="pixel-pet"]')?.getAttribute('data-action')).toBe('none'); + expect(query('[data-testid="pixel-pet"]')?.getAttribute('data-mood')).not.toBe('dragging'); + + dispatch(hitbox, 'pointerdown', { clientX: 100, clientY: 100 }); + dispatch(hitbox, 'pointermove', { clientX: 80, clientY: 100 }); + dispatch(hitbox, 'lostpointercapture'); + expect(stop).toHaveBeenCalledTimes(3); + expect(query('[data-testid="pixel-pet"]')?.getAttribute('data-mood')).not.toBe('dragging'); } finally { userAgent.mockRestore(); } }); + it('uses captured screen coordinates on macOS and prevents native text selection at pointer-down', async () => { + const userAgent = vi.spyOn(window.navigator, 'userAgent', 'get').mockReturnValue('Macintosh; Intel Mac OS X 10_15_7'); + try { + vi.resetModules(); + const { AgentCompanionDesktopPet: MacPet } = await import('./AgentCompanionDesktopPet'); + await act(async () => root.render()); + const hitbox = query('.openbitfun-agent-companion-window__pet-hitbox')!; + const down = new MouseEvent('pointerdown', { + bubbles: true, cancelable: true, clientX: 100, clientY: 100, screenX: 100, screenY: 100, + }); + act(() => hitbox.dispatchEvent(down)); + expect(down.defaultPrevented).toBe(true); + expect(pointerDrag.prepare).toHaveBeenCalledWith({ x: 100, y: 100 }, expect.any(Function), expect.any(Function)); + dispatch(hitbox, 'pointermove', { clientX: 120, clientY: 100 }); + await act(async () => Promise.resolve()); + expect(query('[data-testid="pixel-pet"]')?.getAttribute('data-mood')).toBe('dragging'); + expect(pointerDrag.move).toHaveBeenLastCalledWith({ x: 120, y: 100 }); + expect(startDraggingMock).not.toHaveBeenCalled(); + expect(controlledDragMock).not.toHaveBeenCalled(); + act(() => pointerDrag.prepare.mock.calls[0][1]('left')); + expect(query('[data-testid="pixel-pet"]')?.getAttribute('data-direction')).toBe('left'); + dispatch(hitbox, 'pointermove', { clientX: 80, clientY: 100 }); + expect(pointerDrag.move).toHaveBeenLastCalledWith({ x: 80, y: 100 }); + dispatch(hitbox, 'pointerup', { clientX: 70, clientY: 100 }); + expect(pointerDrag.move).toHaveBeenLastCalledWith({ x: 70, y: 100 }); + expect(pointerDrag.finish).toHaveBeenCalledTimes(1); + expect(pointerDrag.cancel).not.toHaveBeenCalled(); + expect(query('[data-testid="pixel-pet"]')?.getAttribute('data-action')).toBe('waving'); + dispatch(hitbox, 'pointerdown', { clientX: 100, clientY: 100 }); + dispatch(hitbox, 'pointermove', { clientX: 120, clientY: 100 }); + dispatch(hitbox, 'lostpointercapture'); + expect(pointerDrag.cancel).toHaveBeenCalledTimes(1); + expect(query('[data-testid="pixel-pet"]')?.getAttribute('data-mood')).not.toBe('dragging'); + } finally { userAgent.mockRestore(); } + }); + it('closes the desktop pet from the pet context menu', () => { const hitbox = query('.openbitfun-agent-companion-window__pet-hitbox'); expect(hitbox).not.toBeNull(); diff --git a/src/web-ui/src/app/components/AgentCompanionDesktopPet/AgentCompanionDesktopPet.tsx b/src/web-ui/src/app/components/AgentCompanionDesktopPet/AgentCompanionDesktopPet.tsx index 2f94d5bdaa..95fa70d791 100644 --- a/src/web-ui/src/app/components/AgentCompanionDesktopPet/AgentCompanionDesktopPet.tsx +++ b/src/web-ui/src/app/components/AgentCompanionDesktopPet/AgentCompanionDesktopPet.tsx @@ -19,6 +19,7 @@ import { isImeOwnedKeyboardEvent } from '@/shared/utils/ime'; import { isReducedMotionPreferred } from '@/shared/utils/motionPreference'; import { getPetLookDirection } from '@/infrastructure/config/services/agentCompanionPetSprite'; import { startAgentCompanionDrag } from '@/infrastructure/config/services/AgentCompanionDragService'; +import { prepareAgentCompanionPointerDrag, type CompanionPointerDrag } from '@/infrastructure/config/services/AgentCompanionPointerDragService'; import './AgentCompanionDesktopPet.scss'; const log = createLogger('AgentCompanionDesktopPet'); @@ -35,9 +36,13 @@ const BUBBLE_OUTPUT_TYPEWRITER_INTERVAL_MS = 28; const WINDOW_EDGE_BUFFER = 4; const POINTER_HOVER_POLL_INTERVAL_MS = 120; const PET_LOOK_HOLD_MS = 960; -/** Clicks shorter/smaller than this use `show_main_window`; beyond it we start a native drag. */ +/** Clicks shorter/smaller than this use `show_main_window`; beyond it we start dragging. */ const PET_DRAG_THRESHOLD_PX = 8; const IS_WINDOWS_WEBVIEW = /\bWindows\b/i.test(window.navigator.userAgent); +const IS_MACOS_WEBVIEW = /\bMacintosh\b/i.test(window.navigator.userAgent); +// AppKit's native drag returns immediately and may consume mouse-up. Keep pointer +// capture on macOS too, so running direction and drag lifetime follow the pointer. +const USE_CONTROLLED_PET_DRAG = IS_WINDOWS_WEBVIEW || IS_MACOS_WEBVIEW; const PET_COMMAND_EVENT = 'agent-companion://pet-command'; const MENU_EDGE_MARGIN = 4; @@ -156,7 +161,11 @@ export const AgentCompanionDesktopPet: React.FC = () => { const [isDraggingPet, setIsDraggingPet] = useState(false); const [dragDirection, setDragDirection] = useState<'left' | 'right'>('right'); const stopDragRef = useRef<(() => void) | null>(null); - useEffect(() => () => stopDragRef.current?.(), []); + const pointerDragRef = useRef(null); + useEffect(() => () => { + stopDragRef.current?.(); + pointerDragRef.current?.cancel(); + }, []); const [petFrameSize, setPetFrameSize] = useState<{ width: number; height: number } | null>(null); const [overlay, setOverlay] = useState(null); const [menuAnchor, setMenuAnchor] = useState(null); @@ -493,7 +502,7 @@ export const AgentCompanionDesktopPet: React.FC = () => { }); void tauriWindow.onMoved(event => { - if (!IS_WINDOWS_WEBVIEW && petPointerSessionRef.current?.dragStarted && windowPosition && event.payload.x !== windowPosition.x) { + if (!USE_CONTROLLED_PET_DRAG && petPointerSessionRef.current?.dragStarted && windowPosition && event.payload.x !== windowPosition.x) { setDragDirection(event.payload.x > windowPosition.x ? 'right' : 'left'); } windowPosition = event.payload; @@ -807,6 +816,8 @@ export const AgentCompanionDesktopPet: React.FC = () => { return; } petPointerSessionRef.current = null; + pointerDragRef.current?.cancel(); + pointerDragRef.current = null; stopDragRef.current?.(); stopDragRef.current = null; setIsDraggingPet(false); @@ -821,6 +832,9 @@ export const AgentCompanionDesktopPet: React.FC = () => { if (event.button !== 0) { return; } + // WebKit can start a native text/image selection before our drag threshold + // is reached. Cancel that default at pointer-down, while retaining capture. + event.preventDefault(); // The bubble composer stays interactive while open (it lives inside a // bubble), so touching the pet is what dismisses it. if (overlay?.kind === 'composer') { @@ -832,6 +846,19 @@ export const AgentCompanionDesktopPet: React.FC = () => { startY: event.clientY, dragStarted: false, }; + if (IS_MACOS_WEBVIEW) { + const target = event.currentTarget; + const session = petPointerSessionRef.current; + pointerDragRef.current?.cancel(); + pointerDragRef.current = prepareAgentCompanionPointerDrag( + { x: event.screenX, y: event.screenY }, + setDragDirection, + error => { + log.warn('Failed to move Agent companion window', error); + if (petPointerSessionRef.current === session) clearPetPointerSession(target, session.pointerId); + }, + ); + } try { event.currentTarget.setPointerCapture(event.pointerId); } catch { @@ -841,7 +868,11 @@ export const AgentCompanionDesktopPet: React.FC = () => { const onPetPointerMove = (event: React.PointerEvent) => { const session = petPointerSessionRef.current; - if (!session || event.pointerId !== session.pointerId || session.dragStarted) { + if (!session || event.pointerId !== session.pointerId) { + return; + } + if (session.dragStarted) { + pointerDragRef.current?.move({ x: event.screenX, y: event.screenY }); return; } const dx = event.clientX - session.startX; @@ -854,6 +885,10 @@ export const AgentCompanionDesktopPet: React.FC = () => { setDragDirection(dx < 0 ? 'left' : 'right'); setIsDraggingPet(true); setReaction(null); + if (IS_MACOS_WEBVIEW) { + pointerDragRef.current?.move({ x: event.screenX, y: event.screenY }); + return; + } if (IS_WINDOWS_WEBVIEW) { const target = event.currentTarget; stopDragRef.current = startAgentCompanionDrag(setDragDirection, error => { @@ -882,6 +917,11 @@ export const AgentCompanionDesktopPet: React.FC = () => { return; } const shouldShowMain = !session.dragStarted; + if (session.dragStarted && pointerDragRef.current) { + pointerDragRef.current.move({ x: event.screenX, y: event.screenY }); + pointerDragRef.current.finish(); + pointerDragRef.current = null; + } clearPetPointerSession(event.currentTarget, event.pointerId); if (session.dragStarted) setReaction({ action: 'waving' }); if (shouldShowMain) { diff --git a/src/web-ui/src/infrastructure/config/services/AgentCompanionPointerDragService.test.ts b/src/web-ui/src/infrastructure/config/services/AgentCompanionPointerDragService.test.ts new file mode 100644 index 0000000000..68a7a69331 --- /dev/null +++ b/src/web-ui/src/infrastructure/config/services/AgentCompanionPointerDragService.test.ts @@ -0,0 +1,65 @@ +import { beforeEach, expect, it, vi } from 'vitest'; +import { prepareAgentCompanionPointerDrag } from './AgentCompanionPointerDragService'; +const mocks = vi.hoisted(() => ({ position: vi.fn(), scale: vi.fn(), move: vi.fn() })); +vi.mock('@tauri-apps/api/window', () => ({ + LogicalPosition: class { constructor(public x: number, public y: number) {} }, + getCurrentWindow: () => ({ outerPosition: mocks.position, scaleFactor: mocks.scale, setPosition: mocks.move }), +})); +const settle = async () => { for (let i = 0; i < 8; i++) await Promise.resolve(); }; +beforeEach(() => { + mocks.position.mockReset().mockResolvedValue({ x: 7360, y: 1824 }); + mocks.scale.mockReset().mockResolvedValue(2); + mocks.move.mockReset().mockResolvedValue(undefined); +}); +it('uses Retina logical screen coordinates and follows both directions without a global cursor poll', async () => { + const direction = vi.fn(); + const drag = prepareAgentCompanionPointerDrag({ x: 3760, y: 974 }, direction, vi.fn()); + await settle(); + drag.move({ x: 3690, y: 974 }); await settle(); + expect(mocks.move).toHaveBeenLastCalledWith(expect.objectContaining({ x: 3610, y: 912 })); + expect(direction).toHaveBeenLastCalledWith('left'); + drag.move({ x: 3790, y: 964 }); await settle(); + expect(mocks.move).toHaveBeenLastCalledWith(expect.objectContaining({ x: 3710, y: 902 })); + expect(direction).toHaveBeenLastCalledWith('right'); + drag.cancel(); + drag.move({ x: 4000, y: 964 }); await settle(); + expect(mocks.move).toHaveBeenCalledTimes(2); +}); +it('applies the final move even when a quick release precedes origin acquisition', async () => { + let resolve!: (point: { x: number; y: number }) => void; + mocks.position.mockReturnValue(new Promise(done => { resolve = done; })); + const drag = prepareAgentCompanionPointerDrag({ x: 30, y: 40 }, vi.fn(), vi.fn()); + drag.move({ x: 10, y: 60 }); drag.finish(); + drag.move({ x: 500, y: 500 }); + resolve({ x: -200, y: -80 }); await settle(); + expect(mocks.move).toHaveBeenCalledOnce(); + expect(mocks.move).toHaveBeenCalledWith(expect.objectContaining({ x: -120, y: -20 })); +}); +it('coalesces pending movement and flushes the last position on release', async () => { + let resolve!: () => void; + mocks.move.mockReturnValueOnce(new Promise(done => { resolve = done; })); + const drag = prepareAgentCompanionPointerDrag({ x: 30, y: 40 }, vi.fn(), vi.fn()); + await settle(); drag.move({ x: 50, y: 40 }); + drag.move({ x: 60, y: 40 }); drag.move({ x: 70, y: 40 }); drag.finish(); + expect(mocks.move).toHaveBeenCalledTimes(1); + resolve(); await settle(); + expect(mocks.move).toHaveBeenCalledTimes(2); + expect(mocks.move).toHaveBeenLastCalledWith(expect.objectContaining({ x: 3720, y: 912 })); +}); +it('cancels pending movement on lost capture or unmount', async () => { + let resolve!: (point: { x: number; y: number }) => void; + mocks.position.mockReturnValue(new Promise(done => { resolve = done; })); + const drag = prepareAgentCompanionPointerDrag({ x: 30, y: 40 }, vi.fn(), vi.fn()); + drag.move({ x: 60, y: 40 }); drag.cancel(); + resolve({ x: 200, y: 300 }); await settle(); + expect(mocks.move).not.toHaveBeenCalled(); +}); +it('reports move failures and stops accepting subsequent movement', async () => { + mocks.move.mockRejectedValue(new Error('Window closed')); + const error = vi.fn(); + const drag = prepareAgentCompanionPointerDrag({ x: 30, y: 40 }, vi.fn(), error); + await settle(); drag.move({ x: 50, y: 40 }); await settle(); + drag.move({ x: 60, y: 40 }); await settle(); + expect(error).toHaveBeenCalledOnce(); + expect(mocks.move).toHaveBeenCalledOnce(); +}); diff --git a/src/web-ui/src/infrastructure/config/services/AgentCompanionPointerDragService.ts b/src/web-ui/src/infrastructure/config/services/AgentCompanionPointerDragService.ts new file mode 100644 index 0000000000..f4f4b822f4 --- /dev/null +++ b/src/web-ui/src/infrastructure/config/services/AgentCompanionPointerDragService.ts @@ -0,0 +1,75 @@ +import { getCurrentWindow, LogicalPosition } from '@tauri-apps/api/window'; + +export interface CompanionPointerPosition { x: number; y: number } +export interface CompanionPointerDrag { + move: (position: CompanionPointerPosition) => void; + finish: () => void; + cancel: () => void; +} + +/** + * WebKit screen coordinates and macOS window positions share logical screen + * space. Capture the window origin at pointer-down, then coalesce pointer moves + * while an IPC is in flight. No global cursor polling or native drag handoff. + */ +export function prepareAgentCompanionPointerDrag( + grab: CompanionPointerPosition, + onDirection: (direction: 'left' | 'right') => void, + onError: (error: unknown) => void, +): CompanionPointerDrag { + const petWindow = getCurrentWindow(); + let origin: CompanionPointerPosition | null = null; + let latest: CompanionPointerPosition | null = null; + let previous = grab; + let cancelled = false; + let finished = false; + let moving = false; + + const cancel = () => { cancelled = true; latest = null; }; + const fail = (error: unknown) => { + if (cancelled) return; + cancel(); + onError(error); + }; + const flush = async () => { + if (cancelled || moving || !origin || !latest) return; + moving = true; + try { + while (!cancelled && latest) { + const pointer = latest; + latest = null; + await petWindow.setPosition(new LogicalPosition( + origin.x + pointer.x - grab.x, + origin.y + pointer.y - grab.y, + )); + } + } catch (error) { + fail(error); + } finally { + moving = false; + } + }; + + void Promise.all([petWindow.outerPosition(), petWindow.scaleFactor()]) + .then(([position, scale]) => { + if (cancelled) return; + if (!Number.isFinite(scale) || scale <= 0) throw new Error('Invalid companion window scale factor'); + origin = { x: position.x / scale, y: position.y / scale }; + void flush(); + }).catch(fail); + + return { + move: pointer => { + if (cancelled || finished) return; + if (pointer.x === previous.x && pointer.y === previous.y) return; + if (pointer.x !== previous.x) onDirection(pointer.x > previous.x ? 'right' : 'left'); + previous = pointer; + latest = pointer; + void flush(); + }, + // A quick release may precede origin acquisition or the last IPC. Keep the + // final requested position, but accept no further pointer events. + finish: () => { finished = true; void flush(); }, + cancel, + }; +} From a2c4a34b5df5ae7080bea0aef4d8a8a0f6e9fe58 Mon Sep 17 00:00:00 2001 From: Bob Lee Date: Sat, 12 Sep 2026 11:09:16 +0800 Subject: [PATCH 2/3] fix(ui): align tool card headers and contain overflow --- .../tool-cards/FlowChatToolCard.module.css | 21 ++++++++++++++++--- .../flow-chat/tool-cards/FlowChatToolCard.tsx | 8 +++++-- .../StandardAmbientToolCards.module.css | 2 +- .../tool-cards/StandardAmbientToolCards.tsx | 2 +- .../ui/tests/flow-chat-tool-card.test.mjs | 21 ++++++++++++++++--- 5 files changed, 44 insertions(+), 10 deletions(-) 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 index 92e3407025..9787d1c458 100644 --- 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 @@ -110,7 +110,9 @@ } .actionLabel { - flex: 0 0 auto; + min-inline-size: 0; + overflow: hidden; + flex: 0 1 auto; color: var(--openbitfun-color-content-primary); font-weight: var(--_tool-card-title-font-weight); white-space: nowrap; @@ -481,7 +483,8 @@ } .ambientAction { - flex: 0 0 auto; + overflow: hidden; + flex: 0 1 auto; color: inherit; font-weight: var(--_tool-card-title-font-weight); white-space: nowrap; @@ -489,7 +492,7 @@ .ambientContent { overflow: hidden; - flex: 1 1 auto; + flex: 1 1 0; color: inherit; font-weight: var(--_tool-card-content-font-weight); white-space: nowrap; @@ -513,9 +516,21 @@ } .ambientExpandedShell .ambientSurface { + /* Once enclosed, use the same header geometry as prominent cards. + Collapsed traces retain their compact, flush transcript alignment. */ + min-block-size: var(--openbitfun-control-height-sm); + gap: var(--openbitfun-space-2); + padding-block: var(--openbitfun-space-1); + padding-inline: var(--openbitfun-space-3) var(--openbitfun-space-1); + line-height: var(--openbitfun-type-heading-page-line-height); color: var(--openbitfun-color-content-primary); } + .ambientExpandedShell .ambientSurface .iconSlot { + --_tool-card-action-size: var(--openbitfun-space-6); + --_flow-chat-tool-card-icon-size: var(--openbitfun-font-size-xl); + } + .ambientRoot[data-openbitfun-state~="loading"] .ambientAction, .ambientRoot[data-openbitfun-state~="loading"] .ambientContent, .prominentRoot[data-openbitfun-state~="loading"] .actionLabel, 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 index 0db70d155d..4e3fbce22e 100644 --- a/design-system/packages/ui/src/flow-chat/tool-cards/FlowChatToolCard.tsx +++ b/design-system/packages/ui/src/flow-chat/tool-cards/FlowChatToolCard.tsx @@ -699,7 +699,9 @@ export function ProminentToolCardSummary({ data-openbitfun-part="action" data-testid={actionTestId} > - {action} + {typeof action === "string" || typeof action === "number" + ? {action} + : action} )} {content !== undefined && content !== null && content !== false && ( @@ -821,7 +823,9 @@ export function AmbientToolCardHeader({ data-openbitfun-component="flow-chat-tool-card" data-openbitfun-part="action" > - {action} + {typeof action === "string" || typeof action === "number" + ? {action} + : action} )} {content !== undefined && content !== null && content !== false && ( 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 index 663d561a85..d6832f96c8 100644 --- 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 @@ -155,7 +155,7 @@ .imageButton { display: inline-flex; inline-size: 10rem; - max-inline-size: calc(100vi - 4.75rem); + max-inline-size: 100%; aspect-ratio: 1; align-items: center; justify-content: center; 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 index 46adf81397..36a228fd45 100644 --- a/design-system/packages/ui/src/flow-chat/tool-cards/StandardAmbientToolCards.tsx +++ b/design-system/packages/ui/src/flow-chat/tool-cards/StandardAmbientToolCards.tsx @@ -336,7 +336,7 @@ export function ViewImageToolCard({ ) : undefined} header={(