diff --git a/apps/web/app/components/chain-of-thought.tsx b/apps/web/app/components/chain-of-thought.tsx index f84d833b872..8604fae63ea 100644 --- a/apps/web/app/components/chain-of-thought.tsx +++ b/apps/web/app/components/chain-of-thought.tsx @@ -1259,7 +1259,10 @@ function ToolStep({ : "var(--color-text-secondary)", }} > - {label} + setShowOutput((v) => !v) : undefined} + >{label} {/* Exit code badge for exec tools */} {kind === "exec" && status === "done" && output?.exitCode !== undefined && ( - {status === "done" && ( - - )} {(showOutput || status === "running") && (
-					
+					{!richHtml && }
 					{(attachmentInfo.message || richHtml) && (
 						
void; + /** Hide the header action buttons (when they're rendered elsewhere, e.g. tab bar). */ + hideHeaderActions?: boolean; }; export const ChatPanel = forwardRef( @@ -834,6 +835,7 @@ export const ChatPanel = forwardRef( subagentTask, subagentLabel, onBack, + hideHeaderActions, }, ref, ) { @@ -853,6 +855,9 @@ export const ChatPanel = forwardRef( const [showFilePicker, setShowFilePicker] = useState(false); + const [mounted, setMounted] = useState(false); + useEffect(() => { setMounted(true); }, []); + // ── Reconnection state ── const [isReconnecting, setIsReconnecting] = useState(false); const reconnectAbortRef = useRef(null); @@ -881,10 +886,13 @@ export const ChatPanel = forwardRef( const [rawView, _setRawView] = useState(false); // ── Hero state (new chat screen) ── - const [greeting, setGreeting] = useState(""); - const [visiblePrompts, setVisiblePrompts] = useState([]); + const [greeting, setGreeting] = useState("How can I help?"); + const [visiblePrompts, setVisiblePrompts] = useState(PROMPT_SUGGESTIONS.slice(0, 7)); + const heroInitRef = useRef(false); useEffect(() => { + if (heroInitRef.current) return; + heroInitRef.current = true; const greetings = [ "Ready to build?", "Let's automate something?", @@ -901,9 +909,6 @@ export const ChatPanel = forwardRef( }; const allGreetings = [getTimeGreeting(), ...greetings]; setGreeting(allGreetings[Math.floor(Math.random() * allGreetings.length)]); - }, []); - - useEffect(() => { const shuffled = [...PROMPT_SUGGESTIONS].sort(() => 0.5 - Math.random()); setVisiblePrompts(shuffled.slice(0, 7)); }, []); @@ -1683,7 +1688,10 @@ export const ChatPanel = forwardRef( `/api/web-sessions/${sessionId}`, ); if (!response.ok) { - throw new Error("Failed to load session"); + console.warn(`Session ${sessionId} not found (${response.status}), starting fresh.`); + setMessages([]); + setLoadingSession(false); + return; } const data = await response.json(); @@ -2059,7 +2067,7 @@ export const ChatPanel = forwardRef( )}
- {isStreaming && ( + {isStreaming ? ( - )} - {isStreaming ? ( - ) : ( - )}
+ )} )} @@ -2337,6 +2320,7 @@ export const ChatPanel = forwardRef(
{/* Messages */}
(

+ ) : (showHeroState && !mounted) ? ( +
) : showHeroState ? (
{/* Hero greeting */} {greeting && ( - - {greeting.split(" ").map((word, i) => ( - - {word} - - ))} - + {greeting} + )} {/* Centered input bar */} - - - {inputBarContainer(handleInputDragOver, handleInputDragLeave, handleInputDrop)} - - +
+ {inputBarContainer(handleInputDragOver, handleInputDragLeave, handleInputDrop)} +
{/* Prompt suggestion pills */} - +
{visiblePrompts.slice(0, 3).map((template) => { const Icon = template.icon; @@ -2462,7 +2407,7 @@ export const ChatPanel = forwardRef( ); })}
- +
) : messages.length === 0 ? (
@@ -2560,13 +2505,7 @@ export const ChatPanel = forwardRef( style={{ background: "var(--color-bg-glass)" }} >
- - {inputBarContainer(handleInputDragOver, handleInputDragLeave, handleInputDrop)} - + {inputBarContainer(handleInputDragOver, handleInputDragLeave, handleInputDrop)}
)} @@ -2583,7 +2522,6 @@ export const ChatPanel = forwardRef( )}
- ); }, ); diff --git a/apps/web/app/components/tiptap/file-mention-extension.ts b/apps/web/app/components/tiptap/file-mention-extension.ts index 591e14fc931..99cae350025 100644 --- a/apps/web/app/components/tiptap/file-mention-extension.ts +++ b/apps/web/app/components/tiptap/file-mention-extension.ts @@ -83,7 +83,7 @@ export const FileMentionNode = Node.create({ }, HTMLAttributes, ), - `@${label}`, + label, ]; }, }); diff --git a/apps/web/app/components/ui/button.tsx b/apps/web/app/components/ui/button.tsx new file mode 100644 index 00000000000..3aec3a7d3c1 --- /dev/null +++ b/apps/web/app/components/ui/button.tsx @@ -0,0 +1,47 @@ +"use client"; + +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; +import { cn } from "@/lib/utils"; + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 outline-none ring-0 border-none", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground shadow hover:bg-primary/90", + destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", + outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", + secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline" + }, + size: { + default: "h-9 px-4 py-2", + sm: "h-8 rounded-md px-3 text-xs", + lg: "h-10 rounded-md px-8", + icon: "h-9 w-9" + } + }, + defaultVariants: { + variant: "default", + size: "default" + } + } +); + +export interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps { + asChild?: boolean; +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button"; + return ; + } +); + +Button.displayName = "Button"; + +export { Button, buttonVariants }; diff --git a/apps/web/app/components/ui/card.tsx b/apps/web/app/components/ui/card.tsx new file mode 100644 index 00000000000..89d4105d04f --- /dev/null +++ b/apps/web/app/components/ui/card.tsx @@ -0,0 +1,36 @@ +"use client"; + +import * as React from "react"; +import { cn } from "@/lib/utils"; + +const Card = React.forwardRef>(({ className, ...props }, ref) => ( +
+)); +Card.displayName = "Card"; + +const CardHeader = React.forwardRef>(({ className, ...props }, ref) => ( +
+)); +CardHeader.displayName = "CardHeader"; + +const CardTitle = React.forwardRef>( + ({ className, ...props }, ref) =>

+); +CardTitle.displayName = "CardTitle"; + +const CardDescription = React.forwardRef>( + ({ className, ...props }, ref) =>

+); +CardDescription.displayName = "CardDescription"; + +const CardContent = React.forwardRef>(({ className, ...props }, ref) => ( +

+)); +CardContent.displayName = "CardContent"; + +const CardFooter = React.forwardRef>(({ className, ...props }, ref) => ( +
+)); +CardFooter.displayName = "CardFooter"; + +export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }; diff --git a/apps/web/app/components/ui/dropdown-menu.tsx b/apps/web/app/components/ui/dropdown-menu.tsx index ff3f3d16af0..b7d93d5a8e0 100644 --- a/apps/web/app/components/ui/dropdown-menu.tsx +++ b/apps/web/app/components/ui/dropdown-menu.tsx @@ -49,7 +49,7 @@ function DropdownMenuContent({ return ( >(({ className, type, ...props }, ref) => { + return ( + + ); +}); +Input.displayName = "Input"; + +export { Input }; diff --git a/apps/web/app/components/ui/label.tsx b/apps/web/app/components/ui/label.tsx new file mode 100644 index 00000000000..f2383a12c82 --- /dev/null +++ b/apps/web/app/components/ui/label.tsx @@ -0,0 +1,18 @@ +"use client"; + +import * as React from "react"; +import * as LabelPrimitive from "@radix-ui/react-label"; +import { cva, type VariantProps } from "class-variance-authority"; +import { cn } from "@/lib/utils"; + +const labelVariants = cva("text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"); + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & VariantProps +>(({ className, ...props }, ref) => ( + +)); +Label.displayName = LabelPrimitive.Root.displayName; + +export { Label }; diff --git a/apps/web/app/components/ui/switch.tsx b/apps/web/app/components/ui/switch.tsx new file mode 100644 index 00000000000..7bf70bb283b --- /dev/null +++ b/apps/web/app/components/ui/switch.tsx @@ -0,0 +1,28 @@ +"use client"; + +import * as React from "react"; +import * as SwitchPrimitives from "@radix-ui/react-switch"; +import { cn } from "@/lib/utils"; + +const Switch = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +Switch.displayName = SwitchPrimitives.Root.displayName; + +export { Switch }; diff --git a/apps/web/app/components/workspace/chat-sessions-sidebar.tsx b/apps/web/app/components/workspace/chat-sessions-sidebar.tsx index b3e9e5b7bf4..9b63940d73e 100644 --- a/apps/web/app/components/workspace/chat-sessions-sidebar.tsx +++ b/apps/web/app/components/workspace/chat-sessions-sidebar.tsx @@ -9,7 +9,7 @@ import { DropdownMenuTrigger, } from "../ui/dropdown-menu"; -type WebSession = { +export type WebSession = { id: string; title: string; createdAt: number; @@ -55,6 +55,8 @@ type ChatSessionsSidebarProps = { onCollapse?: () => void; /** When true, show a loader instead of empty state (e.g. initial sessions fetch). */ loading?: boolean; + /** When true, renders just the content without the aside wrapper (for embedding in another sidebar). */ + embedded?: boolean; }; /** Format a timestamp into a human-readable relative time string. */ @@ -164,6 +166,7 @@ export function ChatSessionsSidebar({ onClose, width: widthProp, loading = false, + embedded = false, }: ChatSessionsSidebarProps) { const [hoveredId, setHoveredId] = useState(null); const [renamingId, setRenamingId] = useState(null); @@ -229,24 +232,13 @@ export function ChatSessionsSidebar({ const grouped = groupSessions(filteredSessions); const width = mobile ? "280px" : (widthProp ?? 260); - const headerHeight = 40; // px — match padding so list content clears the overlay - const sidebar = ( -