Skip to content

feat(ui): add new icons and enhance FadeDiv, Modal, Tabs, ExpandableTextDisplay#7563

Merged
Subash-Mohan merged 4 commits intomainfrom
agent-message-foundation
Jan 26, 2026
Merged

feat(ui): add new icons and enhance FadeDiv, Modal, Tabs, ExpandableTextDisplay#7563
Subash-Mohan merged 4 commits intomainfrom
agent-message-foundation

Conversation

@Subash-Mohan
Copy link
Copy Markdown
Contributor

@Subash-Mohan Subash-Mohan commented Jan 20, 2026

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.

Screenshot 2026-01-20 at 5 31 32 PM

How Has This Been Tested?

From UI

Additional Options

  • [Optional] Override Linear Check

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

    • Agent timeline: step-by-step tool view with streaming headers, parallel tabs, and compact mode; renderers for Search, Fetch, Python, Reasoning, Deep Research, and Research Agent.
    • SourceTag: source chips with a details card for queries, URLs, and results.
    • Tabs/Modal/Text: Tabs add contained/pill variants and a sliding indicator; Modal gains md-sm width; ExpandableTextDisplay adds copy/download.
    • FadingEdgeContainer and icons/utilities: gradient edge container; new Branch, Circle, Download, Terminal icons; mergeRefs helper.
  • Refactors

    • ToolsList now uses FadingEdgeContainer and removes FadeDiv; BedrockForm ensures full-width content in Tabs.
    • Chat/infra: packetCount added for memo perf; stop reason parsing made robust; minor prose spacing fix.

Written for commit cc7069b. Summary will update on new commits.

@Subash-Mohan Subash-Mohan requested a review from a team as a code owner January 20, 2026 11:53
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 13 files

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Jan 20, 2026

Greptile Summary

This 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

  • New Icons: Added Branch, Circle, Download, and Terminal icons to the icon library
  • Tabs Component: Added pill variant with sliding indicator animation, context-based variant propagation, isLoading prop support, and optimized context value with useMemo
  • ExpandableTextDisplay: New component supporting collapsible text preview with expand-to-modal functionality, copy/download actions, and strict type-safe maxLines prop (1-6)
  • FadingEdgeContainer: Replaced limited FadeDiv with flexible container supporting both top and bottom fade directions
  • Modal: Added md-sm width option (40rem) between existing md and sm sizes
  • Utilities: Added mergeRefs helper for combining multiple React refs

All changes follow the project's frontend standards (absolute imports, cn utility usage, custom color classes) and maintain consistency with existing patterns.

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk
  • All changes are well-structured UI component enhancements with proper TypeScript typing, follow established patterns, and include comprehensive documentation. The Tabs refactor uses proper React patterns (useMemo for context optimization, MutationObserver for DOM tracking). The FadeDiv migration is complete with no remaining usages. Code quality is high with clear comments and type safety improvements.
  • No files require special attention

Important Files Changed

Filename Overview
web/src/lib/utils.ts Added mergeRefs utility for combining multiple React refs
web/src/refresh-components/FadingEdgeContainer.tsx New flexible fade gradient container with configurable top/bottom direction
web/src/refresh-components/Tabs.tsx Major refactor: added pill variant, context propagation, sliding indicator, isLoading prop, optimized context memoization
web/src/refresh-components/texts/ExpandableTextDisplay.tsx New component for collapsible text with modal expansion, copy/download functionality, improved maxLines type safety
web/src/sections/actions/ToolsList.tsx Replaced FadeDiv with FadingEdgeContainer, simplified footer layout

Sequence Diagram

sequenceDiagram
    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
Loading


<Tabs.Content value={AUTH_METHOD_ACCESS_KEY}>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-4 w-full">
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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">
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Section here too?

const widthClasses = {
lg: "w-[80dvw]",
md: "w-[60rem]",
"md-sm": "w-[40rem]",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I think we should come up with a better name haha. Maybe renaming lg -> xl, md -> lg, and md-sm -> md.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Subash-Mohan and others added 2 commits January 26, 2026 11:10
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
return code;
return code
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
Fix with Cubic

@@ -0,0 +1,112 @@
import React from "react";
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with Cubic

}

// Display content only (no timeline steps)
if (hasDisplayContent && !hasPackets) {
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with Cubic

// ============================================================================

function processPacket(state: ProcessorState, packet: Packet): void {
if (!packet) return;
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.)

View Feedback

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>
Fix with Cubic

const icon = FiTool;

if (renderType === RenderType.HIGHLIGHT) {
if (renderType === RenderType.COMPACT) {
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
if (renderType === RenderType.COMPACT) {
if (renderType === RenderType.COMPACT || renderType === RenderType.HIGHLIGHT) {
Fix with Cubic

toSourceInfo={(doc: OnyxDocument) => resultToSourceInfo(doc)}
onClick={(doc: OnyxDocument) => {
if (doc.link) {
window.open(doc.link, "_blank");
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with Cubic


const seenDocIds = new Set<string>();
const results = documentDeltas
.flatMap((delta) => delta?.documents || [])
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.)

View Feedback

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>
Fix with Cubic

// 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 || [])
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.)

View Feedback

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>
Fix with Cubic

getKey={(doc: OnyxDocument) => doc.document_id}
toSourceInfo={(doc: OnyxDocument) => documentToSourceInfo(doc)}
onClick={(doc: OnyxDocument) => {
if (doc.link) window.open(doc.link, "_blank");
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with Cubic

next: TimelineRendererComponentProps
): boolean {
return (
prev.packets.length === next.packets.length &&
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with Cubic

@Subash-Mohan Subash-Mohan added this pull request to the merge queue Jan 26, 2026
Merged via the queue into main with commit a557d76 Jan 26, 2026
77 checks passed
@Subash-Mohan Subash-Mohan deleted the agent-message-foundation branch January 26, 2026 10:30
@wenxi-onyx wenxi-onyx mentioned this pull request Jan 26, 2026
1 task
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants