feat(ui): add new icons and enhance FadeDiv, Modal, Tabs, ExpandableTextDisplay#7563
feat(ui): add new icons and enhance FadeDiv, Modal, Tabs, ExpandableTextDisplay#7563Subash-Mohan merged 4 commits intomainfrom
Conversation
Greptile SummaryThis PR enhances UI components with new icons and improves several core components. The changes add 4 new icons (Branch, Circle, Download, Terminal) and introduce major improvements to the Tabs component (pill variant, animated indicator, context propagation), a new ExpandableTextDisplay component for collapsible text with modal expansion, and a more flexible FadingEdgeContainer to replace the old FadeDiv. Key Changes
All changes follow the project's frontend standards (absolute imports, Confidence Score: 5/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant User
participant TabsList
participant TabsContext
participant TabsTrigger
participant MutationObserver
participant PillIndicator
Note over TabsList: Tabs Component Initialization (Pill Variant)
User->>TabsList: Render with variant="pill"
TabsList->>TabsContext: Create context value with useMemo
TabsList->>MutationObserver: Setup observer on listRef
TabsList->>PillIndicator: Initial render (opacity: 0)
Note over TabsTrigger: Tab Trigger Rendering
TabsList->>TabsTrigger: Render child triggers
TabsTrigger->>TabsContext: useTabsContext() to get variant
TabsTrigger->>TabsTrigger: Apply variant-specific styles
Note over User: User Clicks Tab
User->>TabsTrigger: Click inactive tab
TabsTrigger->>TabsTrigger: Radix updates data-state="active"
MutationObserver->>MutationObserver: Detect data-state change
MutationObserver->>PillIndicator: Calculate position (left, width)
PillIndicator->>PillIndicator: Animate to new position
Note over User: ExpandableTextDisplay Interaction
User->>ExpandableTextDisplay: Click expand button
ExpandableTextDisplay->>Modal: Open with content
Modal->>Modal: Render with width="md-sm" (40rem)
User->>CopyIconButton: Click copy
CopyIconButton->>Clipboard: Copy full content
User->>IconButton: Click download
IconButton->>Browser: Trigger .txt download
|
|
|
||
| <Tabs.Content value={AUTH_METHOD_ACCESS_KEY}> | ||
| <div className="flex flex-col gap-4"> | ||
| <div className="flex flex-col gap-4 w-full"> |
There was a problem hiding this comment.
Could we use Section from general-layouts.tsx here instead?
|
|
||
| <Tabs.Content value={AUTH_METHOD_LONG_TERM_API_KEY}> | ||
| <div className="flex flex-col gap-4"> | ||
| <div className="flex flex-col gap-4 w-full"> |
| const widthClasses = { | ||
| lg: "w-[80dvw]", | ||
| md: "w-[60rem]", | ||
| "md-sm": "w-[40rem]", |
There was a problem hiding this comment.
Hmm I think we should come up with a better name haha. Maybe renaming lg -> xl, md -> lg, and md-sm -> md.
There was a problem hiding this comment.
Could you give me a quick summary of what these changes are? Is the screenshot on this PR related to the changes in this file?
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
11 issues found across 42 files (changes from recent commits).
Prompt for AI agents (all issues)
Check if these issues are valid — if so, understand the root cause of each and fix them.
<file name="web/src/app/chat/message/messageComponents/timeline/AgentTimeline.tsx">
<violation number="1" location="web/src/app/chat/message/messageComponents/timeline/AgentTimeline.tsx:296">
P2: `children` is never rendered, so the final message/toolbar passed in is dropped in both the display-only branch and the main render path.</violation>
</file>
<file name="web/src/app/chat/message/messageComponents/timeline/hooks/packetProcessor.ts">
<violation number="1" location="web/src/app/chat/message/messageComponents/timeline/hooks/packetProcessor.ts:331">
P2: Avoid silently ignoring malformed packets. Fail fast when a packet or its obj/type is missing so upstream can surface the issue.
(Based on your team's feedback about fail-fast behavior for malformed backend packets in stream parsing.) [FEEDBACK_USED]</violation>
</file>
<file name="web/src/app/chat/message/messageComponents/renderers/CustomToolRenderer.tsx">
<violation number="1" location="web/src/app/chat/message/messageComponents/renderers/CustomToolRenderer.tsx:71">
P2: `RenderType.HIGHLIGHT` no longer triggers the compact rendering path, so the short renderer will always use the full layout. Include HIGHLIGHT in this condition (or revert) so the short renderer stays compact.</violation>
</file>
<file name="web/src/app/chat/message/messageComponents/timeline/renderers/code/PythonToolRenderer.tsx">
<violation number="1" location="web/src/app/chat/message/messageComponents/timeline/renderers/code/PythonToolRenderer.tsx:28">
P1: Fallback returns raw code into `dangerouslySetInnerHTML`, which can inject unescaped HTML when highlighting fails. Escape the code in the catch block to prevent XSS.</violation>
</file>
<file name="web/src/app/chat/message/messageComponents/timeline/renderers/search/SearchChipList.tsx">
<violation number="1" location="web/src/app/chat/message/messageComponents/timeline/renderers/search/SearchChipList.tsx:1">
P2: Add the "use client" directive because this component uses React hooks in the app/ directory; without it, Next.js will treat it as a Server Component and throw an error at build/runtime.</violation>
</file>
<file name="web/src/app/chat/message/messageComponents/timeline/renderers/search/SearchToolRenderer.tsx">
<violation number="1" location="web/src/app/chat/message/messageComponents/timeline/renderers/search/SearchToolRenderer.tsx:1">
P1: This component uses the browser `window` API, so the file must be a Client Component. Add a `"use client"` directive at the top to avoid Server Component runtime errors.</violation>
<violation number="2" location="web/src/app/chat/message/messageComponents/timeline/renderers/search/SearchToolRenderer.tsx:102">
P2: Opening `_blank` without `noopener` leaves `window.opener` exposed and enables reverse-tabnabbing. Add `noopener,noreferrer` to the window features.</violation>
</file>
<file name="web/src/app/chat/message/messageComponents/timeline/renderers/search/searchStateUtils.ts">
<violation number="1" location="web/src/app/chat/message/messageComponents/timeline/renderers/search/searchStateUtils.ts:67">
P2: Avoid defensive fallbacks in stream parsing; this silently skips malformed query packets instead of failing fast.
(Based on your team's feedback about failing fast on malformed backend packets.) [FEEDBACK_USED]</violation>
<violation number="2" location="web/src/app/chat/message/messageComponents/timeline/renderers/search/searchStateUtils.ts:76">
P2: Avoid defensive fallbacks in stream parsing; this hides malformed document packets instead of failing loudly.
(Based on your team's feedback about failing fast on malformed backend packets.) [FEEDBACK_USED]</violation>
</file>
<file name="web/src/app/chat/message/messageComponents/timeline/renderers/fetch/FetchToolRenderer.tsx">
<violation number="1" location="web/src/app/chat/message/messageComponents/timeline/renderers/fetch/FetchToolRenderer.tsx:78">
P2: Use `noopener,noreferrer` when opening external links in a new tab to prevent reverse tabnabbing via window.opener.</violation>
</file>
<file name="web/src/app/chat/message/messageComponents/timeline/TimelineRendererComponent.tsx">
<violation number="1" location="web/src/app/chat/message/messageComponents/timeline/TimelineRendererComponent.tsx:48">
P2: Memoization only compares packets by length, which can skip updates when packet content changes but the length stays the same. This can render stale data or the wrong renderer.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| try { | ||
| return hljs.highlight(code, { language: "python" }).value; | ||
| } catch { | ||
| return code; |
There was a problem hiding this comment.
P1: Fallback returns raw code into dangerouslySetInnerHTML, which can inject unescaped HTML when highlighting fails. Escape the code in the catch block to prevent XSS.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At web/src/app/chat/message/messageComponents/timeline/renderers/code/PythonToolRenderer.tsx, line 28:
<comment>Fallback returns raw code into `dangerouslySetInnerHTML`, which can inject unescaped HTML when highlighting fails. Escape the code in the catch block to prevent XSS.</comment>
<file context>
@@ -0,0 +1,199 @@
+ try {
+ return hljs.highlight(code, { language: "python" }).value;
+ } catch {
+ return code;
+ }
+ }, [code]);
</file context>
| return code; | |
| return code | |
| .replace(/&/g, "&") | |
| .replace(/</g, "<") | |
| .replace(/>/g, ">") | |
| .replace(/"/g, """) | |
| .replace(/'/g, "'"); |
| @@ -0,0 +1,112 @@ | |||
| import React from "react"; | |||
There was a problem hiding this comment.
P1: This component uses the browser window API, so the file must be a Client Component. Add a "use client" directive at the top to avoid Server Component runtime errors.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At web/src/app/chat/message/messageComponents/timeline/renderers/search/SearchToolRenderer.tsx, line 1:
<comment>This component uses the browser `window` API, so the file must be a Client Component. Add a `"use client"` directive at the top to avoid Server Component runtime errors.</comment>
<file context>
@@ -0,0 +1,112 @@
+import React from "react";
+import { SvgSearch, SvgGlobe, SvgSearchMenu } from "@opal/icons";
+import { SearchToolPacket } from "@/app/chat/services/streamingModels";
</file context>
| } | ||
|
|
||
| // Display content only (no timeline steps) | ||
| if (hasDisplayContent && !hasPackets) { |
There was a problem hiding this comment.
P2: children is never rendered, so the final message/toolbar passed in is dropped in both the display-only branch and the main render path.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At web/src/app/chat/message/messageComponents/timeline/AgentTimeline.tsx, line 296:
<comment>`children` is never rendered, so the final message/toolbar passed in is dropped in both the display-only branch and the main render path.</comment>
<file context>
@@ -0,0 +1,441 @@
+ }
+
+ // Display content only (no timeline steps)
+ if (hasDisplayContent && !hasPackets) {
+ return (
+ <div className={cn("flex flex-col", className)}>
</file context>
| // ============================================================================ | ||
|
|
||
| function processPacket(state: ProcessorState, packet: Packet): void { | ||
| if (!packet) return; |
There was a problem hiding this comment.
P2: Avoid silently ignoring malformed packets. Fail fast when a packet or its obj/type is missing so upstream can surface the issue.
(Based on your team's feedback about fail-fast behavior for malformed backend packets in stream parsing.)
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At web/src/app/chat/message/messageComponents/timeline/hooks/packetProcessor.ts, line 331:
<comment>Avoid silently ignoring malformed packets. Fail fast when a packet or its obj/type is missing so upstream can surface the issue.
(Based on your team's feedback about fail-fast behavior for malformed backend packets in stream parsing.) </comment>
<file context>
@@ -0,0 +1,439 @@
+// ============================================================================
+
+function processPacket(state: ProcessorState, packet: Packet): void {
+ if (!packet) return;
+
+ // Handle TopLevelBranching packets - these tell us how many parallel branches to expect
</file context>
| const icon = FiTool; | ||
|
|
||
| if (renderType === RenderType.HIGHLIGHT) { | ||
| if (renderType === RenderType.COMPACT) { |
There was a problem hiding this comment.
P2: RenderType.HIGHLIGHT no longer triggers the compact rendering path, so the short renderer will always use the full layout. Include HIGHLIGHT in this condition (or revert) so the short renderer stays compact.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At web/src/app/chat/message/messageComponents/renderers/CustomToolRenderer.tsx, line 71:
<comment>`RenderType.HIGHLIGHT` no longer triggers the compact rendering path, so the short renderer will always use the full layout. Include HIGHLIGHT in this condition (or revert) so the short renderer stays compact.</comment>
<file context>
@@ -68,10 +68,11 @@ export const CustomToolRenderer: MessageRenderer<CustomToolPacket, {}> = ({
const icon = FiTool;
- if (renderType === RenderType.HIGHLIGHT) {
+ if (renderType === RenderType.COMPACT) {
return children({
icon,
</file context>
| if (renderType === RenderType.COMPACT) { | |
| if (renderType === RenderType.COMPACT || renderType === RenderType.HIGHLIGHT) { |
| toSourceInfo={(doc: OnyxDocument) => resultToSourceInfo(doc)} | ||
| onClick={(doc: OnyxDocument) => { | ||
| if (doc.link) { | ||
| window.open(doc.link, "_blank"); |
There was a problem hiding this comment.
P2: Opening _blank without noopener leaves window.opener exposed and enables reverse-tabnabbing. Add noopener,noreferrer to the window features.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At web/src/app/chat/message/messageComponents/timeline/renderers/search/SearchToolRenderer.tsx, line 102:
<comment>Opening `_blank` without `noopener` leaves `window.opener` exposed and enables reverse-tabnabbing. Add `noopener,noreferrer` to the window features.</comment>
<file context>
@@ -0,0 +1,112 @@
+ toSourceInfo={(doc: OnyxDocument) => resultToSourceInfo(doc)}
+ onClick={(doc: OnyxDocument) => {
+ if (doc.link) {
+ window.open(doc.link, "_blank");
+ }
+ }}
</file context>
|
|
||
| const seenDocIds = new Set<string>(); | ||
| const results = documentDeltas | ||
| .flatMap((delta) => delta?.documents || []) |
There was a problem hiding this comment.
P2: Avoid defensive fallbacks in stream parsing; this hides malformed document packets instead of failing loudly.
(Based on your team's feedback about failing fast on malformed backend packets.)
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At web/src/app/chat/message/messageComponents/timeline/renderers/search/searchStateUtils.ts, line 76:
<comment>Avoid defensive fallbacks in stream parsing; this hides malformed document packets instead of failing loudly.
(Based on your team's feedback about failing fast on malformed backend packets.) </comment>
<file context>
@@ -0,0 +1,97 @@
+
+ const seenDocIds = new Set<string>();
+ const results = documentDeltas
+ .flatMap((delta) => delta?.documents || [])
+ .filter((doc) => {
+ if (!doc || !doc.document_id) return false;
</file context>
| // Deduplicate queries using Set for O(n) instead of indexOf which is O(n²) | ||
| const seenQueries = new Set<string>(); | ||
| const queries = queryDeltas | ||
| .flatMap((delta) => delta?.queries || []) |
There was a problem hiding this comment.
P2: Avoid defensive fallbacks in stream parsing; this silently skips malformed query packets instead of failing fast.
(Based on your team's feedback about failing fast on malformed backend packets.)
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At web/src/app/chat/message/messageComponents/timeline/renderers/search/searchStateUtils.ts, line 67:
<comment>Avoid defensive fallbacks in stream parsing; this silently skips malformed query packets instead of failing fast.
(Based on your team's feedback about failing fast on malformed backend packets.) </comment>
<file context>
@@ -0,0 +1,97 @@
+ // Deduplicate queries using Set for O(n) instead of indexOf which is O(n²)
+ const seenQueries = new Set<string>();
+ const queries = queryDeltas
+ .flatMap((delta) => delta?.queries || [])
+ .filter((query) => {
+ if (seenQueries.has(query)) return false;
</file context>
| getKey={(doc: OnyxDocument) => doc.document_id} | ||
| toSourceInfo={(doc: OnyxDocument) => documentToSourceInfo(doc)} | ||
| onClick={(doc: OnyxDocument) => { | ||
| if (doc.link) window.open(doc.link, "_blank"); |
There was a problem hiding this comment.
P2: Use noopener,noreferrer when opening external links in a new tab to prevent reverse tabnabbing via window.opener.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At web/src/app/chat/message/messageComponents/timeline/renderers/fetch/FetchToolRenderer.tsx, line 78:
<comment>Use `noopener,noreferrer` when opening external links in a new tab to prevent reverse tabnabbing via window.opener.</comment>
<file context>
@@ -0,0 +1,135 @@
+ getKey={(doc: OnyxDocument) => doc.document_id}
+ toSourceInfo={(doc: OnyxDocument) => documentToSourceInfo(doc)}
+ onClick={(doc: OnyxDocument) => {
+ if (doc.link) window.open(doc.link, "_blank");
+ }}
+ emptyState={<BlinkingDot />}
</file context>
| next: TimelineRendererComponentProps | ||
| ): boolean { | ||
| return ( | ||
| prev.packets.length === next.packets.length && |
There was a problem hiding this comment.
P2: Memoization only compares packets by length, which can skip updates when packet content changes but the length stays the same. This can render stale data or the wrong renderer.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At web/src/app/chat/message/messageComponents/timeline/TimelineRendererComponent.tsx, line 48:
<comment>Memoization only compares packets by length, which can skip updates when packet content changes but the length stays the same. This can render stale data or the wrong renderer.</comment>
<file context>
@@ -0,0 +1,116 @@
+ next: TimelineRendererComponentProps
+): boolean {
+ return (
+ prev.packets.length === next.packets.length &&
+ prev.stopPacketSeen === next.stopPacketSeen &&
+ prev.stopReason === next.stopReason &&
</file context>
Description
This pull request refactors and improves several UI components, primarily focusing on icon management, gradient fade containers, and text display. It introduces new reusable components and icons, replaces a custom fade div with a more flexible solution, and adds an expandable/collapsible text display modal. The changes also include minor styling and utility improvements.
How Has This Been Tested?
From UI
Additional Options
Summary by cubic
Builds a new agent timeline for assistant messages with step-by-step tool renders, parallel tabs, and compact/expand controls. Also upgrades core UI (Tabs, Modal, fades) and adds SourceTag chips and new icons for clearer, faster reviews.
New Features
Refactors
Written for commit cc7069b. Summary will update on new commits.