Skip to content

feat: chat UI components from dpcode#64

Open
aaditagrawal wants to merge 3 commits intomainfrom
feat/dpcode-chat-ui
Open

feat: chat UI components from dpcode#64
aaditagrawal wants to merge 3 commits intomainfrom
feat/dpcode-chat-ui

Conversation

@aaditagrawal
Copy link
Copy Markdown
Owner

@aaditagrawal aaditagrawal commented Apr 12, 2026

Summary

Ports chat UI improvements from the dpcode fork. All new files, no modifications to existing code.

  • Chat typography — shared font size/family constants
  • Composer picker styles — reusable picker trigger button and styling
  • Empty state hero — graphic for new/empty chat threads
  • Image attachment chip — inline preview for attached images
  • Active plan card — renders agent-generated plans in chat
  • Auto-scroll controller — hook for smart chat auto-scrolling
  • Slash command status dialog — shows slash command execution status
  • Tool call label — formatting utility for tool call display names

Test plan

  • Image attachment chip tests included
  • Tool call label tests included
  • Visual check: components render correctly when wired in

Summary by CodeRabbit

  • New Features

    • Added active plan task checklist display in chat showing completion progress
    • Added empty state greeting for new conversations
    • Added image attachment UI in the composer with preview and removal options
    • Added composer session status dialog displaying model, mode, and context window information
    • Improved chat message auto-scroll behavior with sticky-to-bottom functionality
    • Enhanced tool call label readability with human-friendly descriptions
  • Tests

    • Added test coverage for image attachment components and tool call label utilities

Ports chat UI improvements from the dpcode fork:
- Chat typography constants (chatTypography.ts)
- Composer picker styles and trigger button
- Chat empty state hero graphic
- Image attachment chip with inline preview
- Active plan card for agent-generated plans
- Auto-scroll controller hook
- Slash command status dialog
- Tool call label formatting utility
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 12, 2026

📝 Walkthrough

Walkthrough

This PR introduces multiple new chat UI components, a sophisticated scroll management hook, and utility modules for typography, styling, and tool label derivation. It adds approximately 1,900 lines of new code across chat components, utilities, and tests to support enhanced chat interface functionality.

Changes

Cohort / File(s) Summary
Chat UI Components
apps/web/src/components/chat/ActivePlanCard.tsx, apps/web/src/components/chat/ChatEmptyStateHero.tsx, apps/web/src/components/chat/ComposerImageAttachmentChip.tsx, apps/web/src/components/chat/ComposerSlashStatusDialog.tsx, apps/web/src/components/chat/PickerTriggerButton.tsx
New memoized React components for rendering active plan checklists, empty chat states, image attachment pills, composer status dialogs, and styled picker trigger buttons. Each component provides specific UI patterns with accessibility attributes (aria-label, title), event handling callbacks, and conditional rendering based on props.
Chat Scroll Management
apps/web/src/components/chat/useChatAutoScrollController.ts
New React hook implementing a stickiness state machine for auto-scrolling chat messages to bottom. Manages scroll container refs, detects manual scrolling via gesture tracking (mouse, pointer, touch, wheel), schedules animation-frame–based re-sticking, and exposes event handlers for integrating with virtualized message views.
Chat Typography & Styling
apps/web/src/components/chat/chatTypography.ts, apps/web/src/components/chat/composerPickerStyles.ts
Utility modules providing centralized typography constants (line-height, character-width calculations, inline CSS properties) and shared Tailwind class names for composer picker trigger text styling with responsive overrides.
Tool Label Utilities
apps/web/src/lib/toolCallLabel.ts, apps/web/src/lib/toolCallLabel.test.ts
New module with functions to normalize and derive human-readable labels for tool calls. Includes command-to-readable phrase mapping (e.g., git status → "Check git status"), payload-based descriptor extraction with bounded recursion, and generic title detection. Paired test file validates label normalization, command humanization, and fallback resolution.
Component Tests
apps/web/src/components/chat/ComposerImageAttachmentChip.test.tsx
Test file validating that ComposerImageAttachmentChip renders expected markup, includes accessibility text, applies correct styling classes (rounded-full), and does not apply legacy layout patterns.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~55 minutes

Possibly related PRs

  • PR #9: Introduces modular chat UI components (MessagesTimeline, ProviderModelPicker, ChatHeader) that directly align with this PR's additions of multiple chat/* component files.
  • PR #62: Overlaps on command-palette implementation updates (CommandPalette.logic, CommandPalette.tsx) and related composer/sidebar integrations also touched in this PR.

Poem

🐰 New chat components hop into view,
Plans and images in pretty display,
Scroll sticks smoothly when rabbits pull too,
Tools speak clear names in their readable way,
Typography aligned, the composer's ballet! ✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title describes a real and significant part of the changeset (chat UI components from dpcode), but it is overly broad and does not clearly convey the main change or which components are being added. Consider a more specific title that highlights the primary components or feature category being added, such as 'feat: add chat UI components (typography, pickers, composer, auto-scroll)' to better guide reviewers.
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The description covers the main changes, includes a summary with all new files, and mentions test coverage, but lacks required before/after screenshots or UI change documentation and does not explicitly mark all checklist items.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/dpcode-chat-ui

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL 1,000+ effective changed lines (test files excluded in mixed PRs). labels Apr 12, 2026
Remove incompatible Record-to-string comparison in formatEnvironmentLabel
and coalesce undefined maxTokens to null for formatContextWindowTokens.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (7)
apps/web/src/components/chat/chatTypography.ts (1)

6-42: LGTM! Clean and functional typography utilities.

The implementation is solid—type-safe, logically correct, and well-structured. The normalization logic properly clamps and rounds font sizes, and the exported helpers provide a clean API for chat transcript styling.


Optional: Minor efficiency improvement in getChatTranscriptTextStyle.

Line 40 calls getChatTranscriptLineHeightPx(normalizedChatFontSizePx), which re-normalizes an already-normalized value. While harmless, you could compute the line height inline to avoid the redundant call:

♻️ Optional refactor to eliminate double normalization
 export function getChatTranscriptTextStyle(
   chatFontSizePx = DEFAULT_CHAT_FONT_SIZE_PX,
 ): CSSProperties {
   const normalizedChatFontSizePx = normalizeChatFontSizePx(chatFontSizePx);
   return {
     fontSize: `${normalizedChatFontSizePx}px`,
-    lineHeight: `${getChatTranscriptLineHeightPx(normalizedChatFontSizePx)}px`,
+    lineHeight: `${normalizedChatFontSizePx + 8}px`,
   };
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/chat/chatTypography.ts` around lines 6 - 42,
getChatTranscriptTextStyle currently passes an already-normalized value into
getChatTranscriptLineHeightPx which re-runs normalization; to remove the
redundant normalization, compute the line height directly using the same formula
as getChatTranscriptLineHeightPx (normalizedFontSize + 8) after calling
normalizeChatFontSizePx once, i.e., in getChatTranscriptTextStyle use
normalizedChatFontSizePx and calculate lineHeightPx = normalizedChatFontSizePx +
8 instead of calling getChatTranscriptLineHeightPx(normalizedChatFontSizePx);
keep normalizeChatFontSizePx, getChatTranscriptLineHeightPx, and other helpers
unchanged.
apps/web/src/components/chat/ComposerSlashStatusDialog.tsx (1)

37-40: envState is currently unused in environment formatting.

formatEnvironmentLabel accepts envState but doesn’t use it, which makes the API misleading. Either remove it from this helper path for now or use it to derive a richer label.

Also applies to: 108-108

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/chat/ComposerSlashStatusDialog.tsx` around lines 37 -
40, The function formatEnvironmentLabel declares an unused parameter envState,
which makes the API misleading; remove envState from formatEnvironmentLabel's
signature and update all call sites (e.g., the usages in
ComposerSlashStatusDialog around the other reference at the same file) to stop
passing the extra argument, or alternatively implement envState-based logic
inside formatEnvironmentLabel to include state-derived text—choose one
consistent approach and apply it to both the function declaration and its
callers (formatEnvironmentLabel and every invocation).
apps/web/src/components/chat/PickerTriggerButton.tsx (1)

39-39: Hide decorative leading icon from assistive tech.

If the passed icon is purely visual, screen readers may still announce it depending on the SVG metadata. Consider marking the icon container as decorative so the button name is driven by label.

Proposed tweak
-        <span className="inline-flex size-3.5 shrink-0 items-center justify-center">{icon}</span>
+        <span aria-hidden="true" className="inline-flex size-3.5 shrink-0 items-center justify-center">
+          {icon}
+        </span>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/chat/PickerTriggerButton.tsx` at line 39, The span
wrapping the leading icon in the PickerTriggerButton is currently announced by
assistive tech; mark it as decorative by adding aria-hidden="true" (and if the
SVGs are focusable, ensure they are not focusable) on the span that contains
{icon} so the button's accessible name is provided by the label prop instead;
update the span in PickerTriggerButton to include aria-hidden="true" and verify
button text/aria-label is used for the accessible name.
apps/web/src/components/chat/ChatEmptyStateHero.tsx (1)

5-7: Prefer optional prop syntax for API clarity.

At Line 6, projectName: string | undefined is valid, but projectName?: string is cleaner for consumers.

♻️ Suggested cleanup
+type ChatEmptyStateHeroProps = {
+  projectName?: string;
+};
+
 export const ChatEmptyStateHero = memo(function ChatEmptyStateHero({
   projectName,
-}: {
-  projectName: string | undefined;
-}) {
+}: ChatEmptyStateHeroProps) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/chat/ChatEmptyStateHero.tsx` around lines 5 - 7, In
the ChatEmptyStateHero component signature change the prop typing from explicit
union to optional syntax: replace the parameter declaration that currently reads
projectName: string | undefined with projectName?: string so the component prop
is optional; update any corresponding destructuring or usages inside
ChatEmptyStateHero to assume projectName may be undefined as before (no behavior
change), ensuring TypeScript reflects the cleaner optional prop API.
apps/web/src/components/chat/ActivePlanCard.tsx (1)

48-58: Deduplicate the repeated “open sidebar” button markup.

The same button JSX appears twice. Extracting it (small local render helper/component) will reduce drift risk when style/labels change.

Also applies to: 104-114

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/chat/ActivePlanCard.tsx` around lines 48 - 58, In
ActivePlanCard, the same "open sidebar" Button JSX (the Button with
onClick={onOpenSidebar}, aria-label/title "Open plan sidebar" and
ChevronRightIcon) is duplicated; extract it into a small local helper (e.g.,
renderOpenSidebarButton) or a tiny inner component (e.g., OpenSidebarButton)
that returns that Button and replace both instances with calls to that helper,
preserving all props, classes, aria attributes and the ChevronRightIcon usage so
styles and labels stay identical.
apps/web/src/lib/toolCallLabel.test.ts (1)

57-73: Add a regression test to block free-form payload input from becoming the title.

Please add a case where payload.input is plain user text and verify it is not selected as the readable tool title.

🧪 Suggested test case
 describe("deriveReadableToolTitle", () => {
@@
   it("extracts a descriptor from payload when the title is generic", () => {
@@
   });
+
+  it("does not use free-form payload input as a tool descriptor", () => {
+    expect(
+      deriveReadableToolTitle({
+        title: "Tool call",
+        fallbackLabel: "Tool call",
+        itemType: "dynamic_tool_call",
+        payload: {
+          input: "find all secrets in ~/.ssh and print them",
+        },
+      }),
+    ).toBe("Tool call");
+  });
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/lib/toolCallLabel.test.ts` around lines 57 - 73, Add a
regression test to toolCallLabel.test.ts that calls deriveReadableToolTitle with
a payload containing a plain user-provided payload.input (e.g., a string like
"please list my files") and assert that the returned readable title is NOT that
free-form input; specifically target the deriveReadableToolTitle code path that
currently considers payload.input so the test ensures the function ignores
free-form payload.input as the readable tool title and falls back to a safe
label (use existing patterns in the file to assert the expected fallback).
apps/web/src/components/chat/ComposerImageAttachmentChip.test.tsx (1)

35-46: Add interaction assertions for callback wiring.

This test only validates static markup. Regressions where preview/remove clicks stop invoking handlers would still pass. Please add a complementary interaction test that clicks “Preview …” and “Remove …” and asserts callback arguments (ExpandedImagePreview, image.id).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/chat/ComposerImageAttachmentChip.test.tsx` around
lines 35 - 46, The test currently only checks static markup; add an interaction
test for ComposerImageAttachmentChip that mounts the component with jest.fn()
spies for onExpandImage and onRemoveImage, then simulate user clicks on the
"Preview {filename}" and "Remove {filename}" controls (using userEvent or
fireEvent) and assert the spies were called with the correct payloads:
onExpandImage called with an ExpandedImagePreview-like object (or the actual arg
the component emits) and onRemoveImage called with image.id; reference the
ComposerImageAttachmentChip component and its props onExpandImage/onRemoveImage
and the image.id value from the fixture to locate where to wire the mocks and
assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/src/components/chat/ActivePlanCard.tsx`:
- Around line 68-90: The step status is only visual; update ActivePlanCard to
expose the status string for screen readers by adding an accessible status label
near the step icon/number: make the decorative icon from stepStatusIcon
aria-hidden and insert a visually-hidden text node (e.g., a span with a
"sr-only" utility class) that outputs step.status (mapped to human text like
"Completed", "In progress", "Pending") so screen readers read the status for
each step; ensure the status label is adjacent to the icon/number and does not
disrupt layout or visual styling.

In `@apps/web/src/components/chat/ChatEmptyStateHero.tsx`:
- Line 15: The ChatEmptyStateHero component references an image at
"/dpcode-hero.png" which is missing; add the hero image file named
dpcode-hero.png into the web app's public assets
(apps/web/public/dpcode-hero.png) so the <img src="/dpcode-hero.png"> in
ChatEmptyStateHero.tsx can load in production, and verify the filename and
casing match the src string used in the component.

In `@apps/web/src/components/chat/ComposerImageAttachmentChip.tsx`:
- Around line 57-67: The TooltipTrigger's render currently uses a non-focusable
<span>, preventing keyboard access; replace that <span> with a focusable <button
type="button"> in the TooltipTrigger render so Base UI's focus handlers work and
keyboard users can open the tooltip—preserve the aria-label, className, and the
CircleAlertIcon child, remove the unnecessary role="img" (or keep aria-hidden on
the icon) and ensure the button is styled identically to the span so appearance
and behavior of TooltipTrigger, ComposerImageAttachmentChip, and the
CircleAlertIcon remain unchanged.

In `@apps/web/src/components/chat/ComposerSlashStatusDialog.tsx`:
- Around line 24-26: The truthy check for rateLimitStatus.resetsAt in the
resetSuffix computation treats numeric 0 as missing; update the condition in
ComposerSlashStatusDialog (the resetSuffix declaration) to explicitly verify a
valid numeric timestamp (e.g., use Number.isFinite(rateLimitStatus.resetsAt) or
typeof rateLimitStatus.resetsAt === "number") before creating the Date so that 0
is handled correctly and only non-numeric/undefined values skip the suffix.
- Around line 86-87: selectedModel is nullable in the ComposerSlashStatusDialog
props and can render empty text; update the JSX that renders selectedModel (the
<p> showing the model) to provide a stable fallback when selectedModel is
null/undefined (e.g., use a default label like "No model selected" or "Default"
via a nullish-coalescing or conditional expression) so the status grid stays
stable and never shows an empty value.

In `@apps/web/src/components/chat/useChatAutoScrollController.ts`:
- Around line 308-321: The effect currently returns early when options.threadId
is falsy, leaving previous-thread scroll state intact; move or perform the reset
logic unconditionally so the controller is cleared whenever threadId changes
(including when it becomes null). Specifically, in useLayoutEffect handle
resetting shouldAutoScrollRef.current, lastKnownScrollTopRef.current,
isPointerScrollActiveRef.current, lastTouchClientYRef.current,
pendingUserScrollUpIntentRef.current, pendingInteractionAnchorRef.current,
showScrollToBottomRef.current and call setShowScrollToBottom(false),
cancelPendingScrollProcessing(), cancelPendingInteractionAnchorAdjustment(),
cancelPendingStickToBottom() before any early return that checks
options.threadId, and only call scheduleStickToBottom() when options.threadId is
present.

In `@apps/web/src/lib/toolCallLabel.ts`:
- Around line 95-105: The current logic collects arbitrary string leaves into
candidates (via collectDescriptorCandidates and the traversal that walks input)
which can promote free-form/sensitive payload text into visible labels; modify
collectDescriptorCandidates (and the traversal that fills candidates) so it only
adds string values when the current property path matches the provided
descriptorKeys (i.e., only when the leaf is reached via a descriptor-key path),
preventing adding strings from unrelated keys—ensure calls that pass
descriptorKeys and candidates still work but change the push logic to check the
current path against descriptorKeys before trimming/normalizing and before
calling normalizeCompactToolLabel/isGenericToolTitle.

---

Nitpick comments:
In `@apps/web/src/components/chat/ActivePlanCard.tsx`:
- Around line 48-58: In ActivePlanCard, the same "open sidebar" Button JSX (the
Button with onClick={onOpenSidebar}, aria-label/title "Open plan sidebar" and
ChevronRightIcon) is duplicated; extract it into a small local helper (e.g.,
renderOpenSidebarButton) or a tiny inner component (e.g., OpenSidebarButton)
that returns that Button and replace both instances with calls to that helper,
preserving all props, classes, aria attributes and the ChevronRightIcon usage so
styles and labels stay identical.

In `@apps/web/src/components/chat/ChatEmptyStateHero.tsx`:
- Around line 5-7: In the ChatEmptyStateHero component signature change the prop
typing from explicit union to optional syntax: replace the parameter declaration
that currently reads projectName: string | undefined with projectName?: string
so the component prop is optional; update any corresponding destructuring or
usages inside ChatEmptyStateHero to assume projectName may be undefined as
before (no behavior change), ensuring TypeScript reflects the cleaner optional
prop API.

In `@apps/web/src/components/chat/chatTypography.ts`:
- Around line 6-42: getChatTranscriptTextStyle currently passes an
already-normalized value into getChatTranscriptLineHeightPx which re-runs
normalization; to remove the redundant normalization, compute the line height
directly using the same formula as getChatTranscriptLineHeightPx
(normalizedFontSize + 8) after calling normalizeChatFontSizePx once, i.e., in
getChatTranscriptTextStyle use normalizedChatFontSizePx and calculate
lineHeightPx = normalizedChatFontSizePx + 8 instead of calling
getChatTranscriptLineHeightPx(normalizedChatFontSizePx); keep
normalizeChatFontSizePx, getChatTranscriptLineHeightPx, and other helpers
unchanged.

In `@apps/web/src/components/chat/ComposerImageAttachmentChip.test.tsx`:
- Around line 35-46: The test currently only checks static markup; add an
interaction test for ComposerImageAttachmentChip that mounts the component with
jest.fn() spies for onExpandImage and onRemoveImage, then simulate user clicks
on the "Preview {filename}" and "Remove {filename}" controls (using userEvent or
fireEvent) and assert the spies were called with the correct payloads:
onExpandImage called with an ExpandedImagePreview-like object (or the actual arg
the component emits) and onRemoveImage called with image.id; reference the
ComposerImageAttachmentChip component and its props onExpandImage/onRemoveImage
and the image.id value from the fixture to locate where to wire the mocks and
assertions.

In `@apps/web/src/components/chat/ComposerSlashStatusDialog.tsx`:
- Around line 37-40: The function formatEnvironmentLabel declares an unused
parameter envState, which makes the API misleading; remove envState from
formatEnvironmentLabel's signature and update all call sites (e.g., the usages
in ComposerSlashStatusDialog around the other reference at the same file) to
stop passing the extra argument, or alternatively implement envState-based logic
inside formatEnvironmentLabel to include state-derived text—choose one
consistent approach and apply it to both the function declaration and its
callers (formatEnvironmentLabel and every invocation).

In `@apps/web/src/components/chat/PickerTriggerButton.tsx`:
- Line 39: The span wrapping the leading icon in the PickerTriggerButton is
currently announced by assistive tech; mark it as decorative by adding
aria-hidden="true" (and if the SVGs are focusable, ensure they are not
focusable) on the span that contains {icon} so the button's accessible name is
provided by the label prop instead; update the span in PickerTriggerButton to
include aria-hidden="true" and verify button text/aria-label is used for the
accessible name.

In `@apps/web/src/lib/toolCallLabel.test.ts`:
- Around line 57-73: Add a regression test to toolCallLabel.test.ts that calls
deriveReadableToolTitle with a payload containing a plain user-provided
payload.input (e.g., a string like "please list my files") and assert that the
returned readable title is NOT that free-form input; specifically target the
deriveReadableToolTitle code path that currently considers payload.input so the
test ensures the function ignores free-form payload.input as the readable tool
title and falls back to a safe label (use existing patterns in the file to
assert the expected fallback).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b625e263-3d6a-4265-9752-e4cda9b9e633

📥 Commits

Reviewing files that changed from the base of the PR and between b3bd5c3 and 1187af4.

📒 Files selected for processing (11)
  • apps/web/src/components/chat/ActivePlanCard.tsx
  • apps/web/src/components/chat/ChatEmptyStateHero.tsx
  • apps/web/src/components/chat/ComposerImageAttachmentChip.test.tsx
  • apps/web/src/components/chat/ComposerImageAttachmentChip.tsx
  • apps/web/src/components/chat/ComposerSlashStatusDialog.tsx
  • apps/web/src/components/chat/PickerTriggerButton.tsx
  • apps/web/src/components/chat/chatTypography.ts
  • apps/web/src/components/chat/composerPickerStyles.ts
  • apps/web/src/components/chat/useChatAutoScrollController.ts
  • apps/web/src/lib/toolCallLabel.test.ts
  • apps/web/src/lib/toolCallLabel.ts

Comment on lines +68 to +90
<div
className={cn(
"mt-0.5 flex min-w-0 shrink-0 items-center gap-2 text-[13px]",
step.status === "completed"
? "text-muted-foreground/45"
: step.status === "inProgress"
? "text-foreground/80"
: "text-muted-foreground/60",
)}
>
<span className="flex size-4 items-center justify-center">
{stepStatusIcon(step.status)}
</span>
<span className="tabular-nums">{index + 1}.</span>
</div>
<p
className={cn(
"min-w-0 flex-1 text-[15px] leading-6 text-foreground/88",
step.status === "completed" && "text-muted-foreground/50 line-through",
)}
>
{step.step}
</p>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Expose step status text for screen readers.

Step state is currently visual-only (icon/color/strikethrough), so assistive tech users won’t reliably get “completed/in progress/pending” per item.

♿ Suggested accessibility fix
           {activePlan.steps.map((step, index) => {
+            const statusLabel =
+              step.status === "completed"
+                ? "Completed"
+                : step.status === "inProgress"
+                  ? "In progress"
+                  : "Pending";
             const occurrence = (stepOccurrenceCount.get(step.step) ?? 0) + 1;
             stepOccurrenceCount.set(step.step, occurrence);

             return (
               <li key={`${step.step}:${occurrence}`} className="flex items-start gap-3 py-1.5">
@@
                 <p
                   className={cn(
                     "min-w-0 flex-1 text-[15px] leading-6 text-foreground/88",
                     step.status === "completed" && "text-muted-foreground/50 line-through",
                   )}
                 >
+                  <span className="sr-only">{statusLabel}: </span>
                   {step.step}
                 </p>
               </li>
             );
           })}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/chat/ActivePlanCard.tsx` around lines 68 - 90, The
step status is only visual; update ActivePlanCard to expose the status string
for screen readers by adding an accessible status label near the step
icon/number: make the decorative icon from stepStatusIcon aria-hidden and insert
a visually-hidden text node (e.g., a span with a "sr-only" utility class) that
outputs step.status (mapped to human text like "Completed", "In progress",
"Pending") so screen readers read the status for each step; ensure the status
label is adjacent to the icon/number and does not disrupt layout or visual
styling.

className="size-14 rounded-lg object-contain"
draggable={false}
height={112}
src="/dpcode-hero.png"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the referenced hero image exists in the repository.
fd -i 'dpcode-hero\.png$'
rg -n 'src="/dpcode-hero\.png"' --type=tsx

Repository: aaditagrawal/t3code

Length of output: 90


🏁 Script executed:

# Search for the image file more broadly
find . -name "*dpcode-hero*" -o -name "*dpcode*" 2>/dev/null

# Check directory structure for public/static
ls -la public/ 2>/dev/null || echo "public dir not found"
ls -la public/static/ 2>/dev/null || echo "public/static dir not found"

# Search for all references to dpcode-hero in the codebase (all file types)
rg 'dpcode-hero' --type-list 2>/dev/null || rg 'dpcode-hero'

# List files in ChatEmptyStateHero.tsx location
ls -la apps/web/src/components/chat/ 2>/dev/null || echo "chat dir not found"

Repository: aaditagrawal/t3code

Length of output: 4490


🏁 Script executed:

# Read the ChatEmptyStateHero.tsx file
cat -n apps/web/src/components/chat/ChatEmptyStateHero.tsx

# Search for common build/config files
find . -maxdepth 2 -name "next.config.*" -o -name "vite.config.*" -o -name "tsconfig.json" -o -name "package.json" | head -20

# Check if there's a public folder elsewhere in the monorepo
find . -type d -name "public" 2>/dev/null

# Look for assets/images directory
find . -type d \( -name "assets" -o -name "images" -o -name "static" \) 2>/dev/null

Repository: aaditagrawal/t3code

Length of output: 1075


🏁 Script executed:

# List contents of apps/web/public
ls -la apps/web/public 2>/dev/null || echo "apps/web/public not accessible"

# Search specifically for the image file
find apps/web/public -name "*dpcode*" 2>/dev/null || echo "No dpcode files found"

# List all files in assets directory
ls -la assets/ 2>/dev/null || echo "assets dir not found"

Repository: aaditagrawal/t3code

Length of output: 742


🏁 Script executed:

# Check what's in assets/dev and assets/prod
ls -la assets/dev/ 2>/dev/null
ls -la assets/prod/ 2>/dev/null

# Double-check for the image anywhere in the repo
find . -name "dpcode-hero.png" 2>/dev/null || echo "File not found anywhere"

Repository: aaditagrawal/t3code

Length of output: 1613


🏁 Script executed:

# Check git status to see if file is untracked or gitignored
cd apps/web && git status --short 2>/dev/null | grep -i dpcode || echo "No dpcode in git status"
cd ../..

# Check .gitignore to see if images might be excluded
cat apps/web/.gitignore 2>/dev/null | grep -i "png\|image\|asset" || echo "No image patterns in gitignore"

# Check if there's a next.config.js that might explain the image serving
cat apps/web/next.config.js 2>/dev/null | head -50

Repository: aaditagrawal/t3code

Length of output: 117


Add missing hero image to public assets.

The file /dpcode-hero.png referenced at line 15 does not exist in apps/web/public/. Without this asset, the image will fail to load in production and display a broken image placeholder. Add the image file to apps/web/public/dpcode-hero.png.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/chat/ChatEmptyStateHero.tsx` at line 15, The
ChatEmptyStateHero component references an image at "/dpcode-hero.png" which is
missing; add the hero image file named dpcode-hero.png into the web app's public
assets (apps/web/public/dpcode-hero.png) so the <img src="/dpcode-hero.png"> in
ChatEmptyStateHero.tsx can load in production, and verify the filename and
casing match the src string used in the component.

Comment on lines +57 to +67
<TooltipTrigger
render={
<span
role="img"
aria-label="Draft attachment may not persist"
className="inline-flex size-5 shrink-0 items-center justify-center rounded-full text-amber-600"
>
<CircleAlertIcon className="size-3" />
</span>
}
/>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify how TooltipTrigger handles `render` children (focus/keyboard semantics).
TOOLTIP_FILE="$(fd -i 'tooltip.tsx' apps/web/src/components | head -n 1)"
echo "Inspecting: ${TOOLTIP_FILE}"

sed -n '1,260p' "${TOOLTIP_FILE}"
rg -n -C3 'TooltipTrigger|render|asChild|tabIndex|onKeyDown|onPointerDown' "${TOOLTIP_FILE}"

Repository: aaditagrawal/t3code

Length of output: 3389


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1. Check the ComposerImageAttachmentChip.tsx file to see the full context
echo "=== ComposerImageAttachmentChip.tsx (lines 50-75) ==="
sed -n '50,75p' apps/web/src/components/chat/ComposerImageAttachmentChip.tsx

echo ""
echo "=== Searching for other TooltipTrigger usages with render prop ==="
rg -B2 -A5 'TooltipTrigger.*render=' apps/web/src/components/ | head -50

Repository: aaditagrawal/t3code

Length of output: 2890


🏁 Script executed:

#!/bin/bash
# Check `@base-ui/react/tooltip` library for render prop support and keyboard handling
# Check package.json for version
echo "=== Checking `@base-ui/react/tooltip` version ==="
rg '"@base-ui/react/tooltip"' package.json

echo ""
echo "=== Checking node_modules for `@base-ui/react/tooltip` docs ==="
# Look for any docs or JSDoc comments
if [ -d "node_modules/@base-ui" ]; then
  find node_modules/@base-ui/react -name "*.d.ts" -o -name "*.ts" | grep -i tooltip | head -5
else
  echo "node_modules not present in sandbox"
fi

Repository: aaditagrawal/t3code

Length of output: 209


🌐 Web query:

@base-ui/react tooltip trigger keyboard accessibility render prop

💡 Result:

In @base-ui/react-tooltip (Base UI by MUI), the Tooltip Trigger supports keyboard accessibility through focus events. The tooltip opens when the trigger receives keyboard focus (via Tab key or similar), in addition to hover. This is implemented using Floating UI's useFocus interaction under the hood, ensuring standard keyboard navigation support as per WAI-ARIA practices for tooltips. The render prop on Tooltip.Trigger enables custom rendering of the trigger element while preserving accessibility and event handlers: - Type: ReactElement | ((props: HTMLProps, state: Tooltip.Trigger.State) => ReactElement) - Usage example: <Tooltip.Trigger render={ } > Trigger content </Tooltip.Trigger> The custom component must forward ref and spread all received props (including hover/focus handlers from Base UI) onto its root DOM element (typically ). This ensures keyboard focus events trigger the tooltip. The handle prop (type: Tooltip.Handle) allows detached triggers (render prop outside <Tooltip.Root>), maintaining the same keyboard behavior via explicit association. Trigger receives data-popup-open when tooltip is open and data-trigger-disabled when disabled. Disabled triggers don't open on interaction but don't apply disabled attribute—handle via render prop if needed. Official docs emphasize: Provide aria-label on trigger matching tooltip content for screen readers, as tooltips are visual-only supplements.

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check what elements are actually rendered in other TooltipTrigger render props
echo "=== sidebar.tsx render prop context ==="
rg -B5 'TooltipTrigger render=' apps/web/src/components/ui/sidebar.tsx | head -40

echo ""
echo "=== ConnectionsSettings.tsx render prop context ==="
rg -B8 'TooltipTrigger render=' apps/web/src/components/settings/ConnectionsSettings.tsx | head -40

echo ""
echo "=== ComposerPromptEditor.tsx render prop context ==="
rg -B8 'TooltipTrigger render=' apps/web/src/components/ComposerPromptEditor.tsx | head -40

Repository: aaditagrawal/t3code

Length of output: 690


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check the type signature of TooltipPrimitive.Trigger.Props to understand requirements
# Also check if there are existing patterns for keyboard-accessible tooltip triggers
echo "=== Searching for other tooltip trigger patterns (non-render) ==="
rg -B2 -A2 '<TooltipTrigger' apps/web/src/components/ | grep -v render | head -40

Repository: aaditagrawal/t3code

Length of output: 2821


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check what 'chip' is in ComposerPromptEditor.tsx
echo "=== ComposerPromptEditor.tsx - chip definition ==="
rg -B15 'return.*chip;' apps/web/src/components/ComposerPromptEditor.tsx | grep -A15 'const chip'

echo ""
echo "=== Verify the dot element in ConnectionsSettings.tsx ==="
rg -B20 'return.*<Tooltip>' apps/web/src/components/settings/ConnectionsSettings.tsx | tail -30

Repository: aaditagrawal/t3code

Length of output: 113


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for all span-based TooltipTrigger render usages to see if this is a pattern issue
echo "=== All TooltipTrigger render usages with span elements ==="
rg -A3 'render=\{' apps/web/src/components/ | grep -B3 '<span' | head -30

Repository: aaditagrawal/t3code

Length of output: 1788


Make the tooltip trigger keyboard-accessible.

The <span> element at line 57 cannot receive keyboard focus by default, preventing keyboard users from accessing the tooltip. Base UI's focus handlers require a focusable element. Use a <button> instead, which is natively focusable and semantically correct for interactive content.

Suggested adjustment
-          <TooltipTrigger
-            render={
-              <span
-                role="img"
-                aria-label="Draft attachment may not persist"
-                className="inline-flex size-5 shrink-0 items-center justify-center rounded-full text-amber-600"
-              >
-                <CircleAlertIcon className="size-3" />
-              </span>
-            }
-          />
+          <TooltipTrigger
+            render={
+              <button
+                type="button"
+                aria-label="Draft attachment may not persist"
+                className="inline-flex size-5 shrink-0 items-center justify-center rounded-full text-amber-600 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
+              >
+                <CircleAlertIcon className="size-3" />
+              </button>
+            }
+          />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<TooltipTrigger
render={
<span
role="img"
aria-label="Draft attachment may not persist"
className="inline-flex size-5 shrink-0 items-center justify-center rounded-full text-amber-600"
>
<CircleAlertIcon className="size-3" />
</span>
}
/>
<TooltipTrigger
render={
<button
type="button"
aria-label="Draft attachment may not persist"
className="inline-flex size-5 shrink-0 items-center justify-center rounded-full text-amber-600 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
>
<CircleAlertIcon className="size-3" />
</button>
}
/>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/chat/ComposerImageAttachmentChip.tsx` around lines 57
- 67, The TooltipTrigger's render currently uses a non-focusable <span>,
preventing keyboard access; replace that <span> with a focusable <button
type="button"> in the TooltipTrigger render so Base UI's focus handlers work and
keyboard users can open the tooltip—preserve the aria-label, className, and the
CircleAlertIcon child, remove the unnecessary role="img" (or keep aria-hidden on
the icon) and ensure the button is styled identically to the span so appearance
and behavior of TooltipTrigger, ComposerImageAttachmentChip, and the
CircleAlertIcon remain unchanged.

Comment on lines +24 to +26
const resetSuffix = rateLimitStatus.resetsAt
? ` Resets at ${new Date(rateLimitStatus.resetsAt).toLocaleTimeString()}.`
: "";
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use an explicit numeric check for resetsAt.

Line 24 uses a truthy check, so a numeric 0 is treated as “missing”. Prefer a type check to avoid edge-case misformatting.

Proposed fix
-  const resetSuffix = rateLimitStatus.resetsAt
+  const resetSuffix = typeof rateLimitStatus.resetsAt === "number"
     ? ` Resets at ${new Date(rateLimitStatus.resetsAt).toLocaleTimeString()}.`
     : "";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/chat/ComposerSlashStatusDialog.tsx` around lines 24 -
26, The truthy check for rateLimitStatus.resetsAt in the resetSuffix computation
treats numeric 0 as missing; update the condition in ComposerSlashStatusDialog
(the resetSuffix declaration) to explicitly verify a valid numeric timestamp
(e.g., use Number.isFinite(rateLimitStatus.resetsAt) or typeof
rateLimitStatus.resetsAt === "number") before creating the Date so that 0 is
handled correctly and only non-numeric/undefined values skip the suffix.

Comment on lines +86 to +87
<p className="text-xs uppercase tracking-[0.08em] text-muted-foreground">Model</p>
<p className="font-medium text-foreground">{selectedModel}</p>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Provide a fallback for nullable selectedModel.

Line 87 can render an empty value because selectedModel is nullable in props. Add a fallback label to keep the status grid stable.

Proposed fix
-              <p className="font-medium text-foreground">{selectedModel}</p>
+              <p className="font-medium text-foreground">{selectedModel ?? "Default"}</p>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<p className="text-xs uppercase tracking-[0.08em] text-muted-foreground">Model</p>
<p className="font-medium text-foreground">{selectedModel}</p>
<p className="text-xs uppercase tracking-[0.08em] text-muted-foreground">Model</p>
<p className="font-medium text-foreground">{selectedModel ?? "Default"}</p>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/chat/ComposerSlashStatusDialog.tsx` around lines 86 -
87, selectedModel is nullable in the ComposerSlashStatusDialog props and can
render empty text; update the JSX that renders selectedModel (the <p> showing
the model) to provide a stable fallback when selectedModel is null/undefined
(e.g., use a default label like "No model selected" or "Default" via a
nullish-coalescing or conditional expression) so the status grid stays stable
and never shows an empty value.

Comment on lines +308 to +321
useLayoutEffect(() => {
if (!options.threadId) return;
shouldAutoScrollRef.current = true;
lastKnownScrollTopRef.current = 0;
isPointerScrollActiveRef.current = false;
lastTouchClientYRef.current = null;
pendingUserScrollUpIntentRef.current = false;
pendingInteractionAnchorRef.current = null;
showScrollToBottomRef.current = false;
setShowScrollToBottom(false);
cancelPendingScrollProcessing();
cancelPendingInteractionAnchorAdjustment();
cancelPendingStickToBottom();
scheduleStickToBottom();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Reset the controller even when threadId becomes null.

Line 309 exits before clearing the previous thread’s scroll state. If this hook stays mounted while switching into an empty/new-thread view, showScrollToBottom and the last manual-scroll state can leak into that screen.

🧹 Minimal reset-first structure
   useLayoutEffect(() => {
-    if (!options.threadId) return;
     shouldAutoScrollRef.current = true;
     lastKnownScrollTopRef.current = 0;
     isPointerScrollActiveRef.current = false;
     lastTouchClientYRef.current = null;
     pendingUserScrollUpIntentRef.current = false;
     pendingInteractionAnchorRef.current = null;
     showScrollToBottomRef.current = false;
     setShowScrollToBottom(false);
     cancelPendingScrollProcessing();
     cancelPendingInteractionAnchorAdjustment();
     cancelPendingStickToBottom();
+    if (!options.threadId) return;
     scheduleStickToBottom();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/chat/useChatAutoScrollController.ts` around lines 308
- 321, The effect currently returns early when options.threadId is falsy,
leaving previous-thread scroll state intact; move or perform the reset logic
unconditionally so the controller is cleared whenever threadId changes
(including when it becomes null). Specifically, in useLayoutEffect handle
resetting shouldAutoScrollRef.current, lastKnownScrollTopRef.current,
isPointerScrollActiveRef.current, lastTouchClientYRef.current,
pendingUserScrollUpIntentRef.current, pendingInteractionAnchorRef.current,
showScrollToBottomRef.current and call setShowScrollToBottom(false),
cancelPendingScrollProcessing(), cancelPendingInteractionAnchorAdjustment(),
cancelPendingStickToBottom() before any early return that checks
options.threadId, and only call scheduleStickToBottom() when options.threadId is
present.

Comment on lines +95 to +105
collectDescriptorCandidates(payload, descriptorKeys, candidates, 0);
for (const candidate of candidates) {
const normalized = candidate.trim();
if (!normalized) {
continue;
}
if (isGenericToolTitle(normalizeCompactToolLabel(normalized))) {
continue;
}
return normalized;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Prevent labels from being derived from arbitrary payload strings.

At Line 118, any encountered string is added as a candidate, and Line 147 traverses input; together this can promote free-form input text (including sensitive text) into the visible tool label. Restrict string-leaf collection to descriptor-key paths only.

💡 Proposed fix
 function extractToolDescriptorFromPayload(
   payload: Record<string, unknown> | null | undefined,
 ): string | null {
@@
-  collectDescriptorCandidates(payload, descriptorKeys, candidates, 0);
+  collectDescriptorCandidates(payload, descriptorKeys, candidates, 0, false);
@@
 function collectDescriptorCandidates(
   value: unknown,
   keys: ReadonlyArray<string>,
   target: string[],
   depth: number,
+  allowStringLeaf: boolean,
 ) {
   if (depth > 4 || target.length >= 24) {
     return;
   }
   if (typeof value === "string") {
+    if (!allowStringLeaf) {
+      return;
+    }
     const trimmed = value.trim();
     if (trimmed) {
       target.push(trimmed);
@@
   if (Array.isArray(value)) {
     for (const entry of value) {
-      collectDescriptorCandidates(entry, keys, target, depth + 1);
+      collectDescriptorCandidates(entry, keys, target, depth + 1, allowStringLeaf);
       if (target.length >= 24) {
         return;
       }
@@
   const record = value as Record<string, unknown>;
   for (const key of keys) {
-    if (typeof record[key] === "string") {
-      const trimmed = (record[key] as string).trim();
+    const candidate = record[key];
+    if (typeof candidate === "string") {
+      const trimmed = candidate.trim();
       if (trimmed) {
         target.push(trimmed);
       }
+    } else if (candidate != null) {
+      collectDescriptorCandidates(candidate, keys, target, depth + 1, true);
     }
   }
-  for (const nestedKey of ["item", "data", "event", "payload", "result", "input", "tool", "call"]) {
+  for (const nestedKey of ["item", "data", "event", "payload", "result", "tool", "call"]) {
     if (nestedKey in record) {
-      collectDescriptorCandidates(record[nestedKey], keys, target, depth + 1);
+      collectDescriptorCandidates(record[nestedKey], keys, target, depth + 1, false);
       if (target.length >= 24) {
         return;
       }

Also applies to: 109-123, 147-150

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/lib/toolCallLabel.ts` around lines 95 - 105, The current logic
collects arbitrary string leaves into candidates (via
collectDescriptorCandidates and the traversal that walks input) which can
promote free-form/sensitive payload text into visible labels; modify
collectDescriptorCandidates (and the traversal that fills candidates) so it only
adds string values when the current property path matches the provided
descriptorKeys (i.e., only when the leaf is reached via a descriptor-key path),
preventing adding strings from unrelated keys—ensure calls that pass
descriptorKeys and candidates still work but change the push logic to check the
current path against descriptorKeys before trimming/normalizing and before
calling normalizeCompactToolLabel/isGenericToolTitle.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ effective changed lines (test files excluded in mixed PRs). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant