mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-07 18:27:45 +02:00
[eric] chat: collapsing a disclosure keeps the clicked row anchored, the page never jumps
This commit is contained in:
@@ -4,7 +4,7 @@ import Typography from '@mui/material/Typography';
|
||||
import Collapse from '@mui/material/Collapse';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import { COLLAPSE_MS, COLLAPSE_EASE, chevronSx, shimmerTextSx } from './toolRowMotion';
|
||||
import { COLLAPSE_MS, COLLAPSE_EASE, chevronSx, shimmerTextSx, keepRowAnchored } from './toolRowMotion';
|
||||
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
|
||||
import BlockIcon from '@mui/icons-material/Block';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
@@ -79,7 +79,7 @@ export const CompactMcpBubble: React.FC<CompactMcpBubbleProps> = ({
|
||||
return (
|
||||
<Box {...selectAttrs} sx={{ my: 0 }}>
|
||||
<Box
|
||||
onClick={canToggleDetails ? toggle : undefined}
|
||||
onClick={canToggleDetails ? (e: React.MouseEvent) => { keepRowAnchored(e.currentTarget as HTMLElement); toggle(); } : undefined}
|
||||
sx={{
|
||||
cursor: canToggleDetails ? 'pointer' : 'default',
|
||||
borderBottom: showBody && canToggleDetails ? `1px solid ${c.border.subtle}` : 'none',
|
||||
|
||||
@@ -11,7 +11,7 @@ import { AgentMessage } from '@/shared/state/agentsSlice';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import { ElapsedTimer, formatElapsed } from '../parsing/toolBubbleChrome';
|
||||
import { AgentResponseBody } from './AgentResponseBody';
|
||||
import { chevronSx, shimmerTextSx, railEnterSx, pressSx } from './toolRowMotion';
|
||||
import { chevronSx, shimmerTextSx, railEnterSx, pressSx, keepRowAnchored } from './toolRowMotion';
|
||||
|
||||
interface CreateAgentBubbleProps {
|
||||
call: AgentMessage;
|
||||
@@ -47,7 +47,7 @@ export const CreateAgentBubble: React.FC<CreateAgentBubbleProps> = ({
|
||||
{/* Flat row like every other tool disclosure: no capsule, accent + shimmer only while live. */}
|
||||
<Box
|
||||
className="osw-tool-row"
|
||||
onClick={toggle}
|
||||
onClick={(e: React.MouseEvent) => { keepRowAnchored(e.currentTarget as HTMLElement); toggle(); }}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
|
||||
@@ -24,7 +24,7 @@ import { domainFromUrl } from './SourceFavicons';
|
||||
import { DomainIcon } from './DomainIcon';
|
||||
import VendoredToolUi from '@toolui/VendoredToolUi';
|
||||
import WidgetCopyChip from '../tool-ui/WidgetCopyChip';
|
||||
import { COLLAPSE_MS, COLLAPSE_EASE, chevronSx, shimmerTextSx, railEnterSx } from './toolRowMotion';
|
||||
import { COLLAPSE_MS, COLLAPSE_EASE, chevronSx, shimmerTextSx, railEnterSx, keepRowAnchored } from './toolRowMotion';
|
||||
|
||||
interface DefaultToolBubbleProps {
|
||||
call: AgentMessage;
|
||||
@@ -87,7 +87,7 @@ export const DefaultToolBubble: React.FC<DefaultToolBubbleProps> = ({
|
||||
<Box sx={{ '--glow-rgb': accentRgb } as any}>
|
||||
<Box
|
||||
className="osw-tool-row"
|
||||
onClick={canToggleDetails ? toggle : undefined}
|
||||
onClick={canToggleDetails ? (e: React.MouseEvent) => { keepRowAnchored(e.currentTarget as HTMLElement); toggle(); } : undefined}
|
||||
sx={{
|
||||
// Rows are FLAT in every state (Claude/ChatGPT transition language): no capsule at rest,
|
||||
// no box on expand; the output hangs off the indent rail below.
|
||||
|
||||
@@ -8,7 +8,7 @@ import TerminalIcon from '@mui/icons-material/Terminal';
|
||||
import CheckIcon from '@mui/icons-material/Check';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import { summarizeToolGroup } from './summarizeToolGroup';
|
||||
import { COLLAPSE_MS, COLLAPSE_EASE, chevronSx, shimmerTextSx, railEnterSx, pressSx } from './toolRowMotion';
|
||||
import { COLLAPSE_MS, COLLAPSE_EASE, chevronSx, shimmerTextSx, railEnterSx, pressSx, keepRowAnchored } from './toolRowMotion';
|
||||
import { AgentMessage, ToolGroupMeta } from '@/shared/state/agentsSlice';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import { useMountReveal } from './useMountReveal';
|
||||
@@ -150,7 +150,7 @@ const ToolGroupBubble: React.FC<Props> = React.memo(({ group, isSessionRunning =
|
||||
box around the group, the detail hangs off a thin indent rail under the same quiet row. */}
|
||||
{!expanded ? (
|
||||
<Box
|
||||
onClick={() => { userToggledRef.current = true; setExpanded(true); }}
|
||||
onClick={(e: React.MouseEvent) => { keepRowAnchored(e.currentTarget as HTMLElement); userToggledRef.current = true; setExpanded(true); }}
|
||||
sx={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
@@ -186,7 +186,7 @@ const ToolGroupBubble: React.FC<Props> = React.memo(({ group, isSessionRunning =
|
||||
</Box>
|
||||
) : (
|
||||
<Box
|
||||
onClick={() => { userToggledRef.current = true; setExpanded(false); }}
|
||||
onClick={(e: React.MouseEvent) => { keepRowAnchored(e.currentTarget as HTMLElement); userToggledRef.current = true; setExpanded(false); }}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
|
||||
@@ -49,3 +49,25 @@ export const pressSx: Record<string, unknown> = {
|
||||
transition: 'transform 100ms ease',
|
||||
'&:active': { transform: 'scale(0.98)' },
|
||||
};
|
||||
|
||||
// assistant-ui's scroll lock, anchored: while a disclosure animates, the clicked row keeps its exact
|
||||
// viewport Y (the scroller compensates per frame), so collapsing a tall block never jumps the page.
|
||||
export function keepRowAnchored(rowEl: HTMLElement | null): void {
|
||||
if (!rowEl) return;
|
||||
let scroller: HTMLElement | null = rowEl.parentElement;
|
||||
while (scroller) {
|
||||
const style = getComputedStyle(scroller);
|
||||
if (/(auto|scroll)/.test(style.overflowY) && scroller.scrollHeight > scroller.clientHeight) break;
|
||||
scroller = scroller.parentElement;
|
||||
}
|
||||
if (!scroller) return;
|
||||
const anchorY = rowEl.getBoundingClientRect().top;
|
||||
let raf = 0;
|
||||
const step = (): void => {
|
||||
const dy = rowEl.getBoundingClientRect().top - anchorY;
|
||||
if (dy !== 0) scroller!.scrollTop += dy;
|
||||
raf = requestAnimationFrame(step);
|
||||
};
|
||||
raf = requestAnimationFrame(step);
|
||||
window.setTimeout(() => cancelAnimationFrame(raf), COLLAPSE_MS + 80);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user