Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/web-ui/src/flow_chat/components/modern/ExploreRegion.scss
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,9 @@

.explore-region__content {
position: relative;
// Align nested tool cards with the summary text, not the group chevron.
padding: 0 0 0 20px;
// Keep expanded rows on the same leading edge as the collapsed group.
// A nested inset makes auto-collapse read as a horizontal jump.
padding: 0;
box-sizing: border-box;
// Hide scrollbar track by default; only show when content actually overflows.
overflow: hidden;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ This document explains the scroll-stability mechanism used by `VirtualMessageLis

Every rule below is compensation for content that changes size on its own. The
cheapest way to keep the pane stable is to not generate the movement in the
first place. Four invariants hold across the message list, and breaking any of
first place. Five invariants hold across the message list, and breaking any of
them reintroduces the "the chat keeps refreshing itself" report:

1. **Keep a live action's projection identity stable.** A collapsible tool
Expand Down Expand Up @@ -37,8 +37,14 @@ them reintroduces the "the chat keeps refreshing itself" report:
share one duration (see `flowChatCollapseMotion.ts` /
`SmoothHeightCollapse`). Do not hard-swap `BaseToolCard` ↔ `CompactToolCard`
for expand/collapse — that remounts the body with no height transition.

A fifth, related rule lives in `useTypewriter`: `replayOnMount` defaults to
5. **Keep the leading edge stable across collapse states.** A revealed body
must not add `margin-inline-start`, `padding-inline-start`, or an equivalent
left offset relative to its collapsed header. Expanded thinking, explore
rows, tool details, image previews, and subagent projections all begin on
their owning message/card edge. Vertical and trailing-edge spacing may
remain, but a leading inset reads as a horizontal jump during collapse.

A sixth, related rule lives in `useTypewriter`: `replayOnMount` defaults to
false, so a still-streaming block that remounts continues from its current text
instead of resetting to an empty string and re-growing.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';

function readSource(relativePath: string): string {
return readFileSync(
fileURLToPath(new URL(relativePath, import.meta.url)),
'utf8',
).replace(/\r\n?/g, '\n');
}

function extractBlock(stylesheet: string, selector: string): string {
const escapedSelector = selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const match = stylesheet.match(
new RegExp(`${escapedSelector}\\s*\\{(?<body>[\\s\\S]*?)\\n\\s*\\}`),
);
return match?.groups?.body ?? '';
}

describe('FlowChat collapse leading-edge alignment', () => {
it('guards every shared collapse body against leading indentation', () => {
const stylesheet = readSource('./VirtualMessageList.scss');
const projectionRoots = [
'.explore-region__content',
'.thinking-content',
'.base-tool-card-expanded',
'.base-tool-card-error',
'.compact-tool-card-expanded',
'.view-image-tool-card__content',
'.subagent-items-container',
'.subagent-projection-container--expanded',
];

for (const selector of projectionRoots) {
expect(stylesheet).toContain(selector);
}
expect(stylesheet).toContain('margin-inline-start: 0;');
expect(stylesheet).toContain('padding-inline-start: 0;');
});

it('keeps custom explore and thinking collapse bodies on their header edge', () => {
const exploreStyles = readSource('./ExploreRegion.scss');
const thinkingStyles = readSource('../../tool-cards/ModelThinkingDisplay.scss');
const exploreContent = extractBlock(exploreStyles, '.explore-region__content');
const thinkingContent = extractBlock(thinkingStyles, '.thinking-content');

expect(exploreContent).toContain('padding: 0;');
expect(thinkingContent).toMatch(
/padding:\s*var\(--flowchat-card-expanded-pad-y\)\s*var\(--flowchat-card-expanded-pad-x\)\s*var\(--flowchat-card-expanded-pad-y\)\s*0;/,
);
});

it('keeps tool, image, and subagent detail roots free of leading offsets', () => {
const baseToolStyles = readSource('../../tool-cards/BaseToolCard.scss');
const compactToolStyles = readSource('../../tool-cards/CompactToolCard.scss');
const imageStyles = readSource('../../tool-cards/ViewImageToolCard.scss');
const subagentStyles = readSource('./SubagentItems.scss');
const taskStyles = readSource('../../tool-cards/TaskToolDisplay.scss');

expect(extractBlock(baseToolStyles, '.base-tool-card-expanded')).toMatch(
/padding:\s*var\(--flowchat-card-expanded-pad-y\)\s*var\(--tool-card-expanded-pad-x\)\s*var\(--flowchat-card-expanded-pad-y\)\s*0;/,
);
expect(extractBlock(baseToolStyles, '.base-tool-card-error')).toContain(
'margin-left: 0;',
);
expect(extractBlock(compactToolStyles, '.compact-tool-card-expanded')).toContain(
'margin-left: 0;',
);
expect(extractBlock(compactToolStyles, '.flow-tool-card-note')).toContain(
'margin-left: 0;',
);
expect(extractBlock(imageStyles, '.view-image-tool-card__content')).toContain(
'margin: 8px 0 0;',
);
expect(extractBlock(subagentStyles, '.subagent-items-container')).toContain(
'padding: 10px 10px 10px 0;',
);
expect(
extractBlock(taskStyles, '.task-expanded-content .task-prompt-content'),
).toContain('padding: 0 var(--flowchat-card-pad-x) 0 0;');
expect(taskStyles).toContain(
'.subagent-projection-container--expanded {\n' +
' padding:\n' +
' 8px\n' +
' calc(var(--flowchat-card-expanded-pad-x) + var(--flowchat-card-pad-x))\n' +
' 10px\n' +
' 0;',
);
});

it('does not pull expanded footer or list surfaces past the leading edge', () => {
const terminalStyles = readSource('../../tool-cards/TerminalToolCard.scss');
const gitStyles = readSource('../../tool-cards/GitToolDisplay.scss');
const miniAppStyles = readSource('../../tool-cards/MiniAppToolDisplay.scss');
const todoStyles = readSource('../../tool-cards/TodoWriteDisplay.scss');

expect(terminalStyles).toContain(
'.base-tool-card-wrapper.terminal-tool-card .terminal-result-footer {\n margin-left: 0;',
);
expect(gitStyles).not.toMatch(/git-result-footer[\s\S]{0,120}margin-left:\s*-\d/);
expect(miniAppStyles).toContain(
'.base-tool-card-wrapper.miniapp-tool-display .miniapp-result-footer {\n margin-left: 0;',
);
expect(extractBlock(todoStyles, '.todo-expanded-body')).toMatch(
/margin:\s*calc\(var\(--flowchat-card-expanded-pad-y\) \* -1\)\s*calc\(var\(--tool-card-expanded-pad-x\) \* -1\)\s*calc\(var\(--flowchat-card-expanded-pad-y\) \* -1\)\s*0;/,
);
});
});
6 changes: 3 additions & 3 deletions src/web-ui/src/flow_chat/components/modern/SubagentItems.scss
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@

// Subagent container for items under the same parent task.
.subagent-items-container {
// Match header right padding (10px). Left padding aligns subagent content
// with the header content column (past the icon rail).
padding: 10px 10px 10px var(--tool-card-header-icon-slot);
// Do not offset projected content past the task icon rail: expansion and
// collapse must keep the same leading edge.
padding: 10px 10px 10px 0;

// Continue the task card border on left/right/bottom; top border is hidden.
background: var(--color-bg-scene);
Expand Down
19 changes: 19 additions & 0 deletions src/web-ui/src/flow_chat/components/modern/VirtualMessageList.scss
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,25 @@
background: transparent; // Transparent background for glass effect.
overflow: hidden; /* Prevent overflow. */
overflow-x: hidden; /* Explicitly disable horizontal scrolling. */

/*
* FlowChat collapse contract: revealing a body may change height, never its
* leading edge. Owner styles also keep their offsets at zero; this selector
* is the surface-level guard for FlowChat's collapsible projection roots.
* It stays explicit so ordinary card-internal padding (for example question
* options) remains untouched.
*/
.explore-region__content,
.thinking-content,
.base-tool-card-expanded,
.base-tool-card-error,
.compact-tool-card-expanded,
.view-image-tool-card__content,
.subagent-items-container,
.subagent-projection-container--expanded {
margin-inline-start: 0;
padding-inline-start: 0;
}


// Ensure the Virtuoso container has correct sizing.
Expand Down
10 changes: 7 additions & 3 deletions src/web-ui/src/flow_chat/tool-cards/BaseToolCard.scss
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,11 @@
/* ========== Expanded content area ========== */
.base-tool-card-expanded {
/* Full-bleed dividers in children use these (header pad-x is 0; do not conflate). */
padding: var(--flowchat-card-expanded-pad-y) var(--tool-card-expanded-pad-x);
padding:
var(--flowchat-card-expanded-pad-y)
var(--tool-card-expanded-pad-x)
var(--flowchat-card-expanded-pad-y)
0;
background: transparent;
position: relative;
opacity: 1;
Expand All @@ -234,8 +238,8 @@
var(--flowchat-card-expanded-pad-y)
var(--flowchat-card-expanded-pad-x)
var(--flowchat-card-expanded-pad-y)
calc(var(--flowchat-card-expanded-pad-x) + var(--flowchat-card-expanded-pad-x));
margin-left: var(--flowchat-inline-gap);
0;
margin-left: 0;
background: none !important;
border: none !important;
box-shadow: none !important;
Expand Down
6 changes: 3 additions & 3 deletions src/web-ui/src/flow_chat/tool-cards/CompactToolCard.scss
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@
}

.flow-tool-card-note {
margin-left: 1.5rem;
margin-left: 0;
color: var(--color-text-secondary);
font-size: 0.82em;
line-height: var(--flowchat-support-line-height);
Expand Down Expand Up @@ -298,8 +298,8 @@
.compact-tool-card-expanded {
position: relative;
margin-top: 8px;
margin-left: 1rem;
padding: 12px;
margin-left: 0;
padding: 12px 12px 12px 0;
background: var(--color-bg-primary);
border: 1px solid var(--border-base);
border-radius: 6px;
Expand Down
4 changes: 2 additions & 2 deletions src/web-ui/src/flow_chat/tool-cards/GitToolDisplay.scss
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
/* Footer bar: full width + background to bottom inner edge of expanded content (matches .compact-tool-card-expanded padding). */
.base-tool-card-wrapper.git-tool-display .git-result-footer,
.compact-tool-card-wrapper.git-tool-display .git-result-footer {
margin-left: -8px;
margin-left: 0;
margin-right: -8px;
margin-bottom: -8px;
padding: 0.375rem 0.5rem;
Expand All @@ -65,7 +65,7 @@

/* Expanded + same shell as Terminal: match `terminal-result-footer` inset from `TerminalToolCard.scss`. */
.base-tool-card-wrapper.terminal-tool-card.git-tool-display .git-result-footer {
margin-left: -12px;
margin-left: 0;
margin-right: -12px;
margin-bottom: -12px;
padding: 0.375rem var(--flowchat-card-pad-x);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
}

.base-tool-card-wrapper.miniapp-tool-display .miniapp-result-footer {
margin-left: calc(-1 * var(--flowchat-card-expanded-pad-x));
margin-left: 0;
margin-right: calc(-1 * var(--flowchat-card-expanded-pad-x));
margin-bottom: calc(-1 * var(--flowchat-card-expanded-pad-y));
padding: var(--flowchat-card-expanded-pad-y) var(--flowchat-card-expanded-pad-x);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,12 @@
font-family: var(--font-family-sans);
word-break: break-word;
color: var(--color-text-muted);
padding: var(--flowchat-card-expanded-pad-y) var(--flowchat-card-expanded-pad-x);
// Preserve vertical breathing room without shifting expanded reasoning right.
padding:
var(--flowchat-card-expanded-pad-y)
var(--flowchat-card-expanded-pad-x)
var(--flowchat-card-expanded-pad-y)
0;
background: transparent;
border: none;
border-radius: 6px;
Expand Down
13 changes: 5 additions & 8 deletions src/web-ui/src/flow_chat/tool-cards/TaskToolDisplay.scss
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@
The generic thinking content has vertical padding; with a one-line max-height
that can leave only padding visible and push the prompt text out of view. */
.task-expanded-content .task-prompt-content {
padding: 0 var(--flowchat-card-pad-x);
padding: 0 var(--flowchat-card-pad-x) 0 0;
max-height: 3.2em;
overflow-y: auto;
}
Expand Down Expand Up @@ -370,7 +370,7 @@
.task-interruption-divider {
height: 1px;
background: var(--border-base);
margin: -2px calc(-1 * var(--tool-card-expanded-pad-x)) 0;
margin: -2px calc(-1 * var(--tool-card-expanded-pad-x)) 0 0;
}

.task-expanded-content .task-prompt-markdown {
Expand Down Expand Up @@ -610,16 +610,13 @@
border-top: 1px solid var(--border-base);
}

// Match `.task-expanded-content .task-prompt-content` horizontal inset.
// Keep projected subagent output on the task card's leading edge.
.subagent-projection-container--expanded {
--task-prompt-inline-pad: calc(
var(--flowchat-card-expanded-pad-x) + var(--flowchat-card-pad-x)
);
padding:
8px
var(--task-prompt-inline-pad)
calc(var(--flowchat-card-expanded-pad-x) + var(--flowchat-card-pad-x))
10px
var(--task-prompt-inline-pad);
0;
}

// Hide the header top accent line — the wrapper border already frames
Expand Down
2 changes: 1 addition & 1 deletion src/web-ui/src/flow_chat/tool-cards/TerminalToolCard.scss
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@

/* Footer bar: full width + background to bottom inner edge of expanded content (matches .base-tool-card-expanded padding). */
.base-tool-card-wrapper.terminal-tool-card .terminal-result-footer {
margin-left: calc(-1 * var(--flowchat-card-expanded-pad-x));
margin-left: 0;
margin-right: calc(-1 * var(--flowchat-card-expanded-pad-x));
margin-bottom: calc(-1 * var(--flowchat-card-expanded-pad-y));
padding: var(--flowchat-inline-gap) var(--flowchat-card-expanded-pad-x);
Expand Down
6 changes: 5 additions & 1 deletion src/web-ui/src/flow_chat/tool-cards/TodoWriteDisplay.scss
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,11 @@

/* Expanded body — align list with card edges */
.todo-expanded-body {
margin: calc(var(--flowchat-card-expanded-pad-y) * -1) calc(var(--tool-card-expanded-pad-x) * -1);
margin:
calc(var(--flowchat-card-expanded-pad-y) * -1)
calc(var(--tool-card-expanded-pad-x) * -1)
calc(var(--flowchat-card-expanded-pad-y) * -1)
0;
}
.todo-full-list {
background: transparent;
Expand Down
6 changes: 1 addition & 5 deletions src/web-ui/src/flow_chat/tool-cards/ViewImageToolCard.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
display: flex;
justify-content: flex-start;
min-width: 0;
margin: 8px 0 0 28px;
margin: 8px 0 0;
}

.view-image-tool-card__preview-button {
Expand Down Expand Up @@ -58,10 +58,6 @@
}

@media (max-width: 640px) {
.view-image-tool-card__content {
margin-left: 24px;
}

.view-image-tool-card__preview-button {
width: 144px;
}
Expand Down