Skip to content

Commit 54f5bde

Browse files
authored
chore(explorer): fix scrolling, more tool status indicators, and long tool msgs (#102789)
- fixes bug where autoscroll would fight the user's scrolling while a run was in progress - adds tool status indicator even if block has content on it - shows more of the tool msgs (see screenshot) <img width="797" height="205" alt="Screenshot 2025-11-05 at 8 45 33 AM" src="https://github.com/user-attachments/assets/cea88e2d-f1f7-481f-ad7c-e44eb7e9617d" />
1 parent 9d48157 commit 54f5bde

File tree

3 files changed

+117
-52
lines changed

3 files changed

+117
-52
lines changed

static/app/views/seerExplorer/blockComponents.tsx

Lines changed: 50 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -51,35 +51,42 @@ function getToolStatus(
5151
return 'loading';
5252
}
5353

54-
const hasContent = hasValidContent(block.message.content);
55-
if (hasContent) {
56-
return 'content';
57-
}
58-
5954
// Check tool_links for empty_results metadata
6055
const toolLinks = block.tool_links || [];
61-
if (toolLinks.length === 0) {
62-
// No metadata available, assume success
63-
return 'success';
64-
}
56+
const hasTools = (block.message.tool_calls?.length || 0) > 0;
6557

66-
let hasSuccess = false;
67-
let hasFailure = false;
68-
69-
toolLinks.forEach(link => {
70-
if (link?.params?.empty_results === true) {
71-
hasFailure = true;
72-
} else if (link !== null) {
73-
hasSuccess = true;
58+
if (hasTools) {
59+
if (toolLinks.length === 0) {
60+
// No metadata available, assume success
61+
return 'success';
7462
}
75-
});
7663

77-
if (hasFailure && hasSuccess) {
78-
return 'mixed';
64+
let hasSuccess = false;
65+
let hasFailure = false;
66+
67+
toolLinks.forEach(link => {
68+
if (link?.params?.empty_results === true) {
69+
hasFailure = true;
70+
} else if (link !== null) {
71+
hasSuccess = true;
72+
}
73+
});
74+
75+
if (hasFailure && hasSuccess) {
76+
return 'mixed';
77+
}
78+
if (hasFailure) {
79+
return 'failure';
80+
}
81+
return 'success';
7982
}
80-
if (hasFailure) {
81-
return 'failure';
83+
84+
// No tools, check if there's content
85+
const hasContent = hasValidContent(block.message.content);
86+
if (hasContent) {
87+
return 'content';
8288
}
89+
8390
return 'success';
8491
}
8592

@@ -256,21 +263,21 @@ function BlockComponent({
256263
<BlockContentWrapper hasOnlyTools={!hasContent && hasTools}>
257264
{hasContent && <BlockContent text={block.message.content} />}
258265
{hasTools && (
259-
<Stack gap="md">
266+
<ToolCallStack gap="md">
260267
{block.message.tool_calls?.map((toolCall, idx) => {
261268
const toolString = toolsUsed[idx];
262269
return (
263-
<Text
270+
<ToolCallText
264271
key={`${toolCall.function}-${idx}`}
265272
size="xs"
266273
variant="muted"
267274
monospace
268275
>
269276
{toolString}
270-
</Text>
277+
</ToolCallText>
271278
);
272279
})}
273-
</Stack>
280+
</ToolCallStack>
274281
)}
275282
</BlockContentWrapper>
276283
</BlockRow>
@@ -387,6 +394,9 @@ const ResponseDot = styled('div')<{
387394
const BlockContentWrapper = styled('div')<{hasOnlyTools?: boolean}>`
388395
padding: ${p =>
389396
p.hasOnlyTools ? `${p.theme.space.md} ${p.theme.space.xl}` : p.theme.space.xl};
397+
flex: 1;
398+
min-width: 0;
399+
overflow: hidden;
390400
`;
391401

392402
const BlockContent = styled(MarkedText)`
@@ -443,6 +453,20 @@ const FocusIndicator = styled('div')`
443453
background: ${p => p.theme.purple400};
444454
`;
445455

456+
const ToolCallStack = styled(Stack)`
457+
width: 100%;
458+
min-width: 0;
459+
padding-right: ${p => p.theme.space.lg};
460+
`;
461+
462+
const ToolCallText = styled(Text)`
463+
white-space: nowrap;
464+
overflow: hidden;
465+
text-overflow: ellipsis;
466+
width: 100%;
467+
max-width: 100%;
468+
`;
469+
446470
const ActionButtonBar = styled(ButtonBar)`
447471
position: absolute;
448472
bottom: ${p => p.theme.space['2xs']};

static/app/views/seerExplorer/explorerPanel.tsx

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ function ExplorerPanel({isVisible = false}: ExplorerPanelProps) {
2828
>(new Map());
2929
const panelRef = useRef<HTMLDivElement>(null);
3030
const hoveredBlockIndex = useRef<number>(-1);
31+
const userScrolledUpRef = useRef<boolean>(false);
3132

3233
// Custom hooks
3334
const {panelSize, handleMaxSize, handleMedSize} = usePanelSizing();
@@ -65,6 +66,7 @@ function ExplorerPanel({isVisible = false}: ExplorerPanelProps) {
6566
},
6667
onNavigate: () => {
6768
setIsMinimized(false);
69+
userScrolledUpRef.current = true;
6870
},
6971
});
7072

@@ -73,6 +75,7 @@ function ExplorerPanel({isVisible = false}: ExplorerPanelProps) {
7375
if (isVisible) {
7476
setFocusedBlockIndex(-1);
7577
setIsMinimized(false); // Expand when opening
78+
userScrolledUpRef.current = false;
7679
setTimeout(() => {
7780
// Scroll to bottom when panel opens
7881
if (scrollContainerRef.current) {
@@ -101,9 +104,42 @@ function ExplorerPanel({isVisible = false}: ExplorerPanelProps) {
101104
};
102105
}, [isVisible]);
103106

104-
// Auto-scroll to bottom when new blocks are added
107+
// Track scroll position to detect if user scrolled up
105108
useEffect(() => {
106-
if (scrollContainerRef.current) {
109+
if (!isVisible) {
110+
return undefined;
111+
}
112+
113+
const handleScroll = () => {
114+
const container = scrollContainerRef.current;
115+
if (!container) {
116+
return;
117+
}
118+
119+
const {scrollTop, scrollHeight, clientHeight} = container;
120+
const isAtBottom = scrollHeight - scrollTop - clientHeight < 50; // 50px threshold
121+
122+
// If user is at/near bottom, mark that they haven't scrolled up
123+
if (isAtBottom) {
124+
userScrolledUpRef.current = false;
125+
} else {
126+
userScrolledUpRef.current = true;
127+
}
128+
};
129+
130+
const container = scrollContainerRef.current;
131+
if (container) {
132+
container.addEventListener('scroll', handleScroll);
133+
return () => {
134+
container.removeEventListener('scroll', handleScroll);
135+
};
136+
}
137+
return undefined;
138+
}, [isVisible, focusedBlockIndex]);
139+
140+
// Auto-scroll to bottom when new blocks are added, but only if user hasn't scrolled up
141+
useEffect(() => {
142+
if (!userScrolledUpRef.current && scrollContainerRef.current) {
107143
scrollContainerRef.current.scrollTop = scrollContainerRef.current.scrollHeight;
108144
}
109145
}, [blocks]);
@@ -113,6 +149,23 @@ function ExplorerPanel({isVisible = false}: ExplorerPanelProps) {
113149
blockRefs.current = blockRefs.current.slice(0, blocks.length);
114150
}, [blocks]);
115151

152+
// Reset scroll state when navigating to input (which is at the bottom)
153+
useEffect(() => {
154+
if (focusedBlockIndex === -1 && scrollContainerRef.current) {
155+
// Small delay to let scrollIntoView complete
156+
setTimeout(() => {
157+
const container = scrollContainerRef.current;
158+
if (container) {
159+
const {scrollTop, scrollHeight, clientHeight} = container;
160+
const isAtBottom = scrollHeight - scrollTop - clientHeight < 50;
161+
if (isAtBottom) {
162+
userScrolledUpRef.current = false;
163+
}
164+
}
165+
}, 100);
166+
}
167+
}, [focusedBlockIndex]);
168+
116169
// Auto-focus input when user starts typing while a block is focused
117170
useEffect(() => {
118171
if (!isVisible) {
@@ -148,6 +201,8 @@ function ExplorerPanel({isVisible = false}: ExplorerPanelProps) {
148201
if (inputValue.trim() && !isPolling) {
149202
sendMessage(inputValue.trim());
150203
setInputValue('');
204+
// Reset scroll state so we auto-scroll to show the response
205+
userScrolledUpRef.current = false;
151206
// Reset textarea height
152207
if (textareaRef.current) {
153208
textareaRef.current.style.height = 'auto';

static/app/views/seerExplorer/utils.tsx

Lines changed: 10 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,7 @@ const TOOL_FORMATTERS: Record<string, ToolFormatter> = {
2121

2222
telemetry_index_dependencies: (args, isLoading) => {
2323
const title = args.title || 'item';
24-
const truncatedTitle = title.length > 75 ? title.slice(0, 75) + '...' : title;
25-
return isLoading
26-
? `Tracing the flow of ${truncatedTitle}...`
27-
: `Traced the flow of ${truncatedTitle}`;
24+
return isLoading ? `Tracing the flow of ${title}...` : `Traced the flow of ${title}`;
2825
},
2926

3027
google_search: (args, isLoading) => {
@@ -34,11 +31,7 @@ const TOOL_FORMATTERS: Record<string, ToolFormatter> = {
3431

3532
trace_explorer_query: (args, isLoading) => {
3633
const question = args.question || 'spans';
37-
const truncatedQuestion =
38-
question.length > 75 ? question.slice(0, 75) + '...' : question;
39-
return isLoading
40-
? `Querying spans: '${truncatedQuestion}'`
41-
: `Queried spans: '${truncatedQuestion}'`;
34+
return isLoading ? `Querying spans: '${question}'` : `Queried spans: '${question}'`;
4235
},
4336

4437
get_trace_waterfall: (args, isLoading) => {
@@ -74,34 +67,29 @@ const TOOL_FORMATTERS: Record<string, ToolFormatter> = {
7467
switch (mode) {
7568
case 'read_file':
7669
if (path) {
77-
const truncatedPath = path.length > 50 ? path.slice(0, 50) + '...' : path;
7870
return isLoading
79-
? `Reading ${truncatedPath} from ${repoName}...`
80-
: `Read ${truncatedPath} from ${repoName}`;
71+
? `Reading ${path} from ${repoName}...`
72+
: `Read ${path} from ${repoName}`;
8173
}
8274
return isLoading
8375
? `Reading file from ${repoName}...`
8476
: `Read file from ${repoName}`;
8577

8678
case 'find_files':
8779
if (pattern) {
88-
const truncatedPattern =
89-
pattern.length > 40 ? pattern.slice(0, 40) + '...' : pattern;
9080
return isLoading
91-
? `Finding files matching '${truncatedPattern}' in ${repoName}...`
92-
: `Found files matching '${truncatedPattern}' in ${repoName}`;
81+
? `Finding files matching '${pattern}' in ${repoName}...`
82+
: `Found files matching '${pattern}' in ${repoName}`;
9383
}
9484
return isLoading
9585
? `Finding files in ${repoName}...`
9686
: `Found files in ${repoName}`;
9787

9888
case 'search_content':
9989
if (pattern) {
100-
const truncatedPattern =
101-
pattern.length > 40 ? pattern.slice(0, 40) + '...' : pattern;
10290
return isLoading
103-
? `Searching for '${truncatedPattern}' in ${repoName}...`
104-
: `Searched for '${truncatedPattern}' in ${repoName}`;
91+
? `Searching for '${pattern}' in ${repoName}...`
92+
: `Searched for '${pattern}' in ${repoName}`;
10593
}
10694
return isLoading
10795
? `Searching code in ${repoName}...`
@@ -141,11 +129,9 @@ const TOOL_FORMATTERS: Record<string, ToolFormatter> = {
141129
}
142130

143131
if (filePath) {
144-
const truncatedPath =
145-
filePath.length > 40 ? filePath.slice(0, 40) + '...' : filePath;
146132
return isLoading
147-
? `Excavating commits affecting '${truncatedPath}'${dateRangeStr} in ${repoName}...`
148-
: `Excavated commits affecting '${truncatedPath}'${dateRangeStr} in ${repoName}`;
133+
? `Excavating commits affecting '${filePath}'${dateRangeStr} in ${repoName}...`
134+
: `Excavated commits affecting '${filePath}'${dateRangeStr} in ${repoName}`;
149135
}
150136

151137
return isLoading

0 commit comments

Comments
 (0)