Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions apps/web/src/components/chat/ActivePlanCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// FILE: ActivePlanCard.tsx
// Purpose: Renders the skinny inline checklist for active turn plans above the composer.
// Layer: Chat UI component
// Depends on: session-logic active plan state and shared button/icon primitives

import { memo } from "react";

import type { ActivePlanState } from "../../session-logic";
import { BotIcon, CheckIcon, ChevronRightIcon, ListTodoIcon, LoaderIcon } from "lucide-react";
import { cn } from "~/lib/utils";
import { Button } from "../ui/button";

interface ActivePlanCardProps {
activePlan: ActivePlanState;
backgroundTaskCount?: number;
onOpenSidebar: () => void;
}

function stepStatusIcon(status: ActivePlanState["steps"][number]["status"]) {
if (status === "completed") {
return <CheckIcon className="size-3.5" />;
}
if (status === "inProgress") {
return <LoaderIcon className="size-3.5 animate-spin" />;
}
return <span className="block size-2 rounded-full border border-current/65" />;
}

export const ActivePlanCard = memo(function ActivePlanCard({
activePlan,
backgroundTaskCount = 0,
onOpenSidebar,
}: ActivePlanCardProps) {
const totalCount = activePlan.steps.length;
const completedCount = activePlan.steps.filter((step) => step.status === "completed").length;
const stepOccurrenceCount = new Map<string, number>();

return (
<div className="mx-auto mb-3 w-full max-w-3xl">
<div className="overflow-hidden rounded-[24px] border border-border/70 bg-card/80 shadow-sm">
<div className="flex items-center justify-between gap-3 px-4 py-3">
<div className="flex min-w-0 items-center gap-2 text-[13px] text-muted-foreground/80">
<ListTodoIcon className="size-4 shrink-0" />
<span className="truncate">
{completedCount} out of {totalCount} tasks completed
</span>
</div>
<Button
type="button"
variant="ghost"
size="icon-xs"
className="shrink-0 rounded-full text-muted-foreground/65 hover:text-foreground/80"
onClick={onOpenSidebar}
aria-label="Open plan sidebar"
title="Open plan sidebar"
>
<ChevronRightIcon className="size-3.5" />
</Button>
</div>

<ol className="space-y-0.5 px-4 pb-3">
{activePlan.steps.map((step, index) => {
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">
<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>
Comment on lines +68 to +90
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.

</li>
);
})}
</ol>

{backgroundTaskCount > 0 ? (
<div className="flex items-center justify-between gap-3 border-t border-border/60 px-4 py-2.5 text-[12px] text-muted-foreground/72">
<div className="flex min-w-0 items-center gap-2">
<BotIcon className="size-3.5 shrink-0" />
<span className="truncate">
{backgroundTaskCount} background agent{backgroundTaskCount === 1 ? "" : "s"}
</span>
</div>
<Button
type="button"
variant="ghost"
size="icon-xs"
className="shrink-0 rounded-full text-muted-foreground/65 hover:text-foreground/80"
onClick={onOpenSidebar}
aria-label="Open plan sidebar"
title="Open plan sidebar"
>
<ChevronRightIcon className="size-3.5" />
</Button>
</div>
) : null}
</div>
</div>
);
});

export type { ActivePlanCardProps };
25 changes: 25 additions & 0 deletions apps/web/src/components/chat/ChatEmptyStateHero.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { memo } from "react";

export const ChatEmptyStateHero = memo(function ChatEmptyStateHero({
projectName,
}: {
projectName: string | undefined;
}) {
return (
<div className="flex flex-col items-center gap-4 select-none">
<img
alt="DP Code logo"
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.

width={112}
/>

<div className="flex flex-col items-center gap-0.5">
<h1 className="text-2xl font-semibold tracking-tight text-foreground/90">Let's build</h1>
{projectName && <span className="text-lg text-muted-foreground/40">{projectName}</span>}
</div>
</div>
);
});
47 changes: 47 additions & 0 deletions apps/web/src/components/chat/ComposerImageAttachmentChip.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it } from "vitest";

import { ComposerImageAttachmentChip } from "./ComposerImageAttachmentChip";

describe("ComposerImageAttachmentChip", () => {
it("renders a filename-first pill instead of the old thumbnail tile", () => {
const markup = renderToStaticMarkup(
<ComposerImageAttachmentChip
image={{
id: "image-1",
type: "image",
name: "CleanShot 2026-04-11 at 20.00.33@2x.png",
mimeType: "image/png",
sizeBytes: 1024,
previewUrl: "blob:image-1",
file: new File(["image"], "CleanShot 2026-04-11 at 20.00.33@2x.png", {
type: "image/png",
}),
}}
images={[
{
id: "image-1",
type: "image",
name: "CleanShot 2026-04-11 at 20.00.33@2x.png",
mimeType: "image/png",
sizeBytes: 1024,
previewUrl: "blob:image-1",
file: new File(["image"], "CleanShot 2026-04-11 at 20.00.33@2x.png", {
type: "image/png",
}),
},
]}
nonPersisted={false}
onExpandImage={() => {}}
onRemoveImage={() => {}}
/>,
);

expect(markup).toContain("CleanShot 2026-04-11 at 20.00.33@2x.png");
expect(markup).toContain("rounded-full");
expect(markup).toContain("Preview CleanShot 2026-04-11 at 20.00.33@2x.png");
expect(markup).toContain("Remove CleanShot 2026-04-11 at 20.00.33@2x.png");
expect(markup).not.toContain("absolute right-1 top-1");
expect(markup).not.toContain("h-14 w-14");
});
});
87 changes: 87 additions & 0 deletions apps/web/src/components/chat/ComposerImageAttachmentChip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// FILE: ComposerImageAttachmentChip.tsx
// Purpose: Renders filename-first composer image attachments as compact pills with preview/remove actions.
// Layer: Chat composer presentation
// Depends on: composer draft image metadata, shared chip styles, and expanded image preview helpers.

import { memo } from "react";
import { type ComposerImageAttachment } from "../../composerDraftStore";
import { CircleAlertIcon, XIcon } from "lucide-react";
import { cn } from "~/lib/utils";
import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
import { COMPOSER_INLINE_CHIP_DISMISS_BUTTON_CLASS_NAME } from "../composerInlineChip";
import { buildExpandedImagePreview, type ExpandedImagePreview } from "./ExpandedImagePreview";

interface ComposerImageAttachmentChipProps {
image: ComposerImageAttachment;
images: readonly ComposerImageAttachment[];
nonPersisted: boolean;
onExpandImage: (preview: ExpandedImagePreview) => void;
onRemoveImage: (imageId: string) => void;
}

export const ComposerImageAttachmentChip = memo(function ComposerImageAttachmentChip({
image,
images,
nonPersisted,
onExpandImage,
onRemoveImage,
}: ComposerImageAttachmentChipProps) {
return (
<div className="inline-flex min-w-0 max-w-full items-center gap-0.5 rounded-full border border-border/60 bg-background/92 p-0.5 shadow-[0_1px_0_rgba(255,255,255,0.14)_inset]">
<button
type="button"
className="flex min-w-0 max-w-[232px] items-center gap-1.5 rounded-full py-0 pl-0 pr-0.5 text-left transition-colors hover:bg-accent/28 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
aria-label={`Preview ${image.name}`}
onClick={() => {
const preview = buildExpandedImagePreview(images, image.id);
if (!preview) return;
onExpandImage(preview);
}}
>
<span className="flex size-6 shrink-0 items-center justify-center overflow-hidden rounded-full border border-border/55 bg-muted/35">
{image.previewUrl ? (
<img src={image.previewUrl} alt={image.name} className="size-full object-cover" />
) : (
<span className="px-1 text-[9px] font-medium uppercase tracking-[0.08em] text-muted-foreground/70">
IMG
</span>
)}
</span>
<span className="min-w-0 truncate text-[12px] font-medium text-foreground/84">
{image.name}
</span>
</button>

{nonPersisted && (
<Tooltip>
<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>
}
/>
Comment on lines +57 to +67
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.

<TooltipPopup side="top" className="max-w-64 whitespace-normal leading-tight">
Draft attachment could not be saved locally and may be lost on navigation.
</TooltipPopup>
</Tooltip>
)}

<button
type="button"
className={cn(
COMPOSER_INLINE_CHIP_DISMISS_BUTTON_CLASS_NAME,
"size-5 rounded-full text-muted-foreground/62 hover:bg-accent/36 hover:text-foreground",
)}
onClick={() => onRemoveImage(image.id)}
aria-label={`Remove ${image.name}`}
>
<XIcon className="size-3" />
</button>
</div>
);
});
Loading
Loading