Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
f18f37d
feat(chat-tools): add unified tool-response design system + global er…
claude Jun 21, 2026
1b55abe
feat(chat-tools): migrate tool-response cards to unified design system
claude Jun 21, 2026
07f0c00
fix(chat-tools): remove unused ToolCardBody import
claude Jun 21, 2026
7214fe2
docs(tool-ui-audit): complete spreadsheet with artist/search/media to…
claude Jun 21, 2026
bcad407
refactor(chat-tools): address review — DRY humanize util, barrel expo…
claude Jun 21, 2026
fa41b5a
fix(chat-tools): gate tool-error retry to the latest message
claude Jun 21, 2026
5ed1193
docs(chat-tools): add light/dark gallery screenshots of redesigned to…
claude Jun 21, 2026
1c06ca4
fix(chat-tools): address review findings + correct dark-mode screenshots
claude Jun 21, 2026
e7f3ca0
fix(chat-tools): refine review follow-ups (cubic)
claude Jun 21, 2026
44de97f
refactor(chat-tools): resolve remaining review threads
claude Jun 21, 2026
1144eb4
test(chat-tools): add Storybook for verifying tool-response UI states
claude Jun 22, 2026
fb85f64
refactor(chat-tools): registry for tool skeletons + hostname-safe soc…
claude Jun 22, 2026
cbf8c1f
test(storybook): isolate QueryClient per story to prevent cache leakage
claude Jun 22, 2026
547454f
refactor(chat-tools): address review nits
claude Jun 22, 2026
eed770d
feat(chat-tools): plain-English generic tool card for unknown/MCP tools
claude Jun 22, 2026
70989de
wip(chat-tools): design-pass motion/polish (Spotify + in-progress bat…
claude Jun 22, 2026
91f56cc
feat(chat-tools): design-pass — motion, data-viz, clarity across all …
claude Jun 22, 2026
6525c21
docs(tool-ui-audit): after-state screenshots (all states) + before/af…
claude Jun 22, 2026
50cb587
fix(chat-tools): address design-pass review (a11y, security, correctn…
claude Jun 22, 2026
e5dafbe
fix(chat-tools): reject protocol-relative video URLs; key-based avata…
claude Jun 22, 2026
6ec2cbf
refactor(chat-tools): extract ArtistAvatar into its own file
claude Jun 22, 2026
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
13 changes: 13 additions & 0 deletions .storybook/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { StorybookConfig } from "@storybook/nextjs-vite";

const config: StorybookConfig = {
stories: ["../components/**/*.stories.@(ts|tsx)"],
addons: [],
framework: {
name: "@storybook/nextjs-vite",
options: {},
},
staticDirs: ["../public"],
};

export default config;
66 changes: 66 additions & 0 deletions .storybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React, { useEffect, useState } from "react";
import type { Preview } from "@storybook/nextjs-vite";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { StoryBoundary } from "../components/VercelChat/tools/__stories__/StoryBoundary";
import "../app/globals.css";

/**
* Per-story wrapper: a fresh QueryClient (so cache never leaks between stories)
* plus the app's real `.dark` class toggle driven by the toolbar.
*/
function StoryWrapper({
theme,
children,
}: {
theme: string;
children: React.ReactNode;
}) {
// useState initializer runs once per story mount → isolated cache per story.
const [queryClient] = useState(
() => new QueryClient({ defaultOptions: { queries: { retry: false } } }),
);
useEffect(() => {
const root = document.documentElement;
root.classList.toggle("dark", theme === "dark");
root.style.colorScheme = theme;
}, [theme]);
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
}

const preview: Preview = {
parameters: {
layout: "padded",
controls: { expanded: true },
backgrounds: { disable: true },
},
globalTypes: {
theme: {
description: "Color theme",
defaultValue: "light",
toolbar: {
title: "Theme",
icon: "circlehollow",
items: [
{ value: "light", title: "Light", icon: "sun" },
{ value: "dark", title: "Dark", icon: "moon" },
],
dynamicTitle: true,
},
},
},
decorators: [
(Story, context) => (
<StoryWrapper theme={context.globals.theme}>
<div className="bg-background p-6 text-foreground">
<StoryBoundary label={context.title || "Story"}>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Global StoryBoundary in preview.tsx masks React render errors across all stories, weakening regression detection in visual tests and manual review.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .storybook/preview.tsx, line 57:

<comment>Global StoryBoundary in preview.tsx masks React render errors across all stories, weakening regression detection in visual tests and manual review.</comment>

<file context>
@@ -53,7 +54,9 @@ const preview: Preview = {
       <StoryWrapper theme={context.globals.theme}>
         <div className="bg-background p-6 text-foreground">
-          <Story />
+          <StoryBoundary label={context.title || "Story"}>
+            <Story />
+          </StoryBoundary>
</file context>

<Story />
</StoryBoundary>
</div>
</StoryWrapper>
),
],
};

export default preview;
19 changes: 16 additions & 3 deletions components/VercelChat/MessageParts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import { Dispatch, SetStateAction } from "react";
import { cn } from "@/lib/utils";
import ViewingMessage from "./ViewingMessage";
import EditingMessage from "./EditingMessage";
import { getToolCallComponent, getToolResultComponent } from "./ToolComponents";
import { getToolCallComponent } from "./getToolCallComponent";
import { getToolResultComponent } from "./getToolResultComponent";
import { getToolErrorComponent } from "./getToolErrorComponent";
import MessageFileViewer from "./message-file-viewer";
import { EnhancedReasoning } from "@/components/reasoning/EnhancedReasoning";
import { Actions, Action } from "@/components/actions";
Expand All @@ -25,7 +27,11 @@ interface MessagePartsProps {
}

export function MessageParts({ message, mode, setMode }: MessagePartsProps) {
const { status, reload } = useVercelChatContext();
const { status, reload, messages } = useVercelChatContext();
// `reload()` regenerates the latest assistant turn, so a tool-error retry
// only makes sense on the most recent message — older historical tool
// failures would otherwise regenerate the wrong turn.
const isLatestMessage = messages[messages.length - 1]?.id === message.id;
return (
<div className={cn("flex flex-col gap-4 w-full group")}>
{message.parts?.map(
Expand Down Expand Up @@ -107,7 +113,14 @@ export function MessageParts({ message, mode, setMode }: MessagePartsProps) {

if (isToolOrDynamicToolUIPart(part)) {
const { state } = part as ToolUIPart;
if (state !== "output-available") {
if (state === "output-error") {
return getToolErrorComponent(
part as ToolUIPart,
isLatestMessage && status !== "streaming"
? () => reload()
: undefined,
);
} else if (state !== "output-available") {
return getToolCallComponent(part as ToolUIPart);
} else {
return getToolResultComponent(part as ToolUIPart);
Expand Down
25 changes: 25 additions & 0 deletions components/VercelChat/getToolCallComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { ToolUIPart, getToolOrDynamicToolName, DynamicToolUIPart } from "ai";
import GenericToolCard from "./tools/GenericToolCard";
import { TOOL_CALL_SKELETONS } from "./toolCallSkeletons";

/**
* Renders the loading state for an in-flight tool call: a bespoke skeleton when
* the tool has one (see TOOL_CALL_SKELETONS), otherwise the generic tool card,
* which explains the step in plain English and echoes its input so repeated
* calls read as distinct, intentional steps (not a frozen loop).
*/
export function getToolCallComponent(part: ToolUIPart | DynamicToolUIPart) {
const { toolCallId } = part;
const toolName = getToolOrDynamicToolName(part);
const skeleton = TOOL_CALL_SKELETONS[toolName];
if (skeleton) return <div key={toolCallId}>{skeleton}</div>;
return (
<div key={toolCallId}>
<GenericToolCard
name={toolName}
input={(part as { input?: unknown }).input}
state="loading"
/>
</div>
);
}
22 changes: 22 additions & 0 deletions components/VercelChat/getToolErrorComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ToolUIPart, getToolOrDynamicToolName, DynamicToolUIPart } from "ai";
import { ToolError } from "./tools/shared/ToolError";

/**
* Unified error surface for any tool that resolves to `output-error`.
* Without this, failed tools fell through to the loading skeleton and
* appeared to hang forever.
*/
export function getToolErrorComponent(
part: ToolUIPart | DynamicToolUIPart,
onRetry?: () => void,
) {
const { toolCallId } = part;
const toolName = getToolOrDynamicToolName(part);
// `errorText` is a runtime-populated field not yet in ToolUIPart's public type.
const errorText = (part as { errorText?: string }).errorText;
return (
<div key={toolCallId}>
<ToolError title={toolName} message={errorText} onRetry={onRetry} />
</div>
);
}
Loading
Loading