Merge pull request #71678 from NousResearch/bb/code-block-overflow

fix(desktop): keep code and diffs out of the tool overflow window
This commit is contained in:
brooklyn! 2026-07-25 20:42:22 -05:00 committed by GitHub
commit a288fc341c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 54 additions and 9 deletions

View file

@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest'
import { shouldBoundToolGroup, technicalTrace, UNBOUNDABLE_TOOLS } from './fallback'
import { isUnboundableTool, shouldBoundToolGroup, technicalTrace } from './fallback'
import { isFileEditTool } from './fallback-model'
describe('shouldBoundToolGroup', () => {
it('bounds long runs of ordinary tool calls', () => {
@ -16,10 +17,34 @@ describe('shouldBoundToolGroup', () => {
})
})
describe('UNBOUNDABLE_TOOLS', () => {
describe('isUnboundableTool', () => {
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)
expect(isUnboundableTool('clarify')).toBe(true)
expect(isUnboundableTool('image_generate')).toBe(true)
})
it('exempts tools whose body is a code block the user reads', () => {
expect(isUnboundableTool('execute_code')).toBe(true)
expect(isUnboundableTool('read_file')).toBe(true)
})
// The window clips a diff to a ~2-row viewport, so every tool that renders
// one has to be exempt. Derived from isFileEditTool rather than re-listed,
// so a newly supported edit tool can't be exempt in one place and clipped
// in the other.
it('exempts every file-edit tool, so diffs are never clipped', () => {
for (const toolName of ['edit_file', 'patch', 'write_file']) {
expect(isFileEditTool(toolName)).toBe(true)
expect(isUnboundableTool(toolName)).toBe(true)
}
})
// Console output is a log tail: the last lines are the ones that matter,
// which is exactly what the bounded window pins. Keeping it boundable is
// what stops the exemption from swallowing the feature whole.
it('still bounds console output and other ordinary rows', () => {
expect(isUnboundableTool('terminal')).toBe(false)
expect(isUnboundableTool('web_search')).toBe(false)
})
})

View file

@ -673,10 +673,30 @@ function TerminalTranscript({ command, exitCode }: TerminalTranscriptProps) {
// auto-scrolling window; fewer than this stays a plain inline stack.
const TOOL_GROUP_SCROLL_THRESHOLD = 3
// 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'])
// Tools whose body (an interactive form, a full-size image, a syntax-
// highlighted code/diff block) 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.
//
// A row rendered by ToolEntry carries `data-tool-row`, so once the user
// expands it the `:has([data-tool-row][data-tool-open])` rule in styles.css
// lifts the cap on its own. That escape hatch is why most tools are safe to
// bound. These are the ones it cannot reach:
//
// - `clarify` / `image_generate` render their own components and never emit
// `data-tool-row`, so no amount of expanding frees them.
// - the code tools *do* emit it, but their body is a code block the user
// reads rather than a one-line status — peering at a diff through a
// ~2-row viewport until you think to expand it is the bug. Console output
// (`terminal`) stays boundable: it's a log tail, and the last lines are
// the ones that matter, which is exactly what the window pins.
const CODE_BODY_TOOLS = ['execute_code', 'read_file']
const UNBOUNDABLE_TOOLS = new Set(['clarify', 'image_generate', ...CODE_BODY_TOOLS])
export function isUnboundableTool(toolName: string): boolean {
return UNBOUNDABLE_TOOLS.has(toolName) || isFileEditTool(toolName)
}
export function shouldBoundToolGroup(childCount: number, hasUnboundable: boolean) {
return childCount >= TOOL_GROUP_SCROLL_THRESHOLD && !hasUnboundable
@ -761,7 +781,7 @@ export const ToolGroupSlot: FC<PropsWithChildren<{ endIndex: number; startIndex:
const hasUnboundable = useAuiState(s =>
s.message.parts
.slice(Math.max(0, startIndex), endIndex + 1)
.some(part => part.type === 'tool-call' && UNBOUNDABLE_TOOLS.has(part.toolName))
.some(part => part.type === 'tool-call' && isUnboundableTool(part.toolName))
)
const enterRef = useEnterAnimation(messageRunning, `tool-group:${messageId}:${startIndex}`)