From 7a5a6ef99b0dc004b3d80c8761a8b9848b460cd1 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Wed, 15 Jul 2026 14:08:35 -0400 Subject: [PATCH] fix(desktop): keep generated images out of tool overflow Generalize the clarify opt-out into an UNBOUNDABLE_TOOLS set so a run containing image_generate also stays a plain, fully-visible stack instead of collapsing into the bounded window, where the max-height + gradient mask clipped the image the same way it clipped clarify forms. --- .../components/assistant-ui/tool/fallback.test.ts | 15 +++++++++++++-- .../src/components/assistant-ui/tool/fallback.tsx | 15 ++++++++++----- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/apps/desktop/src/components/assistant-ui/tool/fallback.test.ts b/apps/desktop/src/components/assistant-ui/tool/fallback.test.ts index 008e701b23cf..53d082241933 100644 --- a/apps/desktop/src/components/assistant-ui/tool/fallback.test.ts +++ b/apps/desktop/src/components/assistant-ui/tool/fallback.test.ts @@ -1,13 +1,24 @@ import { describe, expect, it } from 'vitest' -import { shouldBoundToolGroup } from './fallback' +import { shouldBoundToolGroup, UNBOUNDABLE_TOOLS } from './fallback' describe('shouldBoundToolGroup', () => { it('bounds long runs of ordinary tool calls', () => { expect(shouldBoundToolGroup(3, false)).toBe(true) }) - it('never bounds a run containing clarify', () => { + it('leaves short runs unbounded', () => { + expect(shouldBoundToolGroup(2, false)).toBe(false) + }) + + it('never bounds a run holding an unboundable tool', () => { expect(shouldBoundToolGroup(3, true)).toBe(false) }) }) + +describe('UNBOUNDABLE_TOOLS', () => { + it('exempts clarify forms and generated images from the window', () => { + expect(UNBOUNDABLE_TOOLS.has('clarify')).toBe(true) + expect(UNBOUNDABLE_TOOLS.has('image_generate')).toBe(true) + }) +}) diff --git a/apps/desktop/src/components/assistant-ui/tool/fallback.tsx b/apps/desktop/src/components/assistant-ui/tool/fallback.tsx index f40ff7693f50..705c900e3b21 100644 --- a/apps/desktop/src/components/assistant-ui/tool/fallback.tsx +++ b/apps/desktop/src/components/assistant-ui/tool/fallback.tsx @@ -611,8 +611,13 @@ function ToolEntry({ part }: ToolEntryProps) { // auto-scrolling window; fewer than this stays a plain inline stack. const TOOL_GROUP_SCROLL_THRESHOLD = 3 -export function shouldBoundToolGroup(childCount: number, containsClarify: boolean) { - return childCount >= TOOL_GROUP_SCROLL_THRESHOLD && !containsClarify +// Tools whose body (an interactive form, a full-size image) must never be +// trapped behind the window's max-height + fade mask. A run holding any of +// them stays a plain, fully-visible stack no matter how long it is. +export const UNBOUNDABLE_TOOLS = new Set(['clarify', 'image_generate']) + +export function shouldBoundToolGroup(childCount: number, hasUnboundable: boolean) { + return childCount >= TOOL_GROUP_SCROLL_THRESHOLD && !hasUnboundable } // Pin-to-bottom + top-fade for the bounded tool window. Pins the newest row on @@ -688,15 +693,15 @@ export const ToolGroupSlot: FC s.message.id) const messageRunning = useAuiState(selectMessageRunning) - const containsClarify = useAuiState(s => + const hasUnboundable = useAuiState(s => s.message.parts .slice(Math.max(0, startIndex), endIndex + 1) - .some(part => part.type === 'tool-call' && part.toolName === 'clarify') + .some(part => part.type === 'tool-call' && UNBOUNDABLE_TOOLS.has(part.toolName)) ) const enterRef = useEnterAnimation(messageRunning, `tool-group:${messageId}:${startIndex}`) - const bounded = shouldBoundToolGroup(Children.count(children), containsClarify) + const bounded = shouldBoundToolGroup(Children.count(children), hasUnboundable) const { contentRef, faded, onScroll, scrollRef } = useToolWindow(bounded) return (