From 5925563013ff5f0e12ffd44a5da3d81bd7d6de14 Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Fri, 24 Jul 2026 14:00:56 +0300 Subject: [PATCH 1/2] Refactor chat UI --- AGENTS.md | 4 +- ARCHITECTURE.md | 2 +- components/ai/action-bar.tsx | 57 +++++++++++ components/ai/composer.tsx | 113 +++++++++++++++++++++ components/ai/message.tsx | 40 ++++++++ components/ai/thread.tsx | 38 +++++++ components/composer/composer.tsx | 111 -------------------- components/session/conversation.tsx | 6 +- components/session/message.tsx | 95 ++++++----------- components/session/model-activity.tsx | 28 +---- components/session/page-header.tsx | 23 +++-- components/session/sandbox-control.tsx | 32 +++--- components/session/session-start.tsx | 66 ++++++++---- components/session/session-view.tsx | 108 ++++++++++++-------- components/session/sidebar-item.tsx | 4 +- components/session/tool-activity.tsx | 26 ++++- components/ui/copy-button.tsx | 40 -------- components/ui/message-scroller.tsx | 41 -------- components/workspace/workspace-browser.tsx | 4 +- components/workspace/workspace-panel.tsx | 80 +++++++++++---- 20 files changed, 517 insertions(+), 401 deletions(-) create mode 100644 components/ai/action-bar.tsx create mode 100644 components/ai/composer.tsx create mode 100644 components/ai/message.tsx create mode 100644 components/ai/thread.tsx delete mode 100644 components/composer/composer.tsx delete mode 100644 components/ui/copy-button.tsx delete mode 100644 components/ui/message-scroller.tsx diff --git a/AGENTS.md b/AGENTS.md index 0731b9a..c5455dc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,8 +26,8 @@ of the product. the behavior implied by its name; do not move unrelated actions into it to hide coordination. An optional feature must be removable by deleting its import and composition node without breaking sibling capabilities. -- Express UI variants with focused components and early returns. Do not accumulate - JSX in mutable variables or turn one component into a dispatcher for unrelated UI. +- Mutable JSX variables are an antipattern. Compose focused components with early + returns instead of assigning JSX to `let` variables. - Keep one source of truth and derive the rest. Model state with one explicit status, not overlapping booleans or synchronization effects. - Keep logic above JSX. Avoid ternaries and boolean chains in markup. Use diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 9f97e10..a4f9977 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -227,7 +227,7 @@ lib/ lowest-level reusable modules and extractable feature packages self-contained voice input package components/ ui/ generic visual primitives - composer/ message input, optional controls, and submit composition + ai/ reusable chat layout, actions, scrolling, and composition code/ Pierre-backed source and diff facades session/ conversation, activity, navigation, and preview control workspace/ file navigation, tree, queries, and panel diff --git a/components/ai/action-bar.tsx b/components/ai/action-bar.tsx new file mode 100644 index 0000000..0874d38 --- /dev/null +++ b/components/ai/action-bar.tsx @@ -0,0 +1,57 @@ +import { Check, Copy } from "lucide-react"; +import { type ComponentProps, useEffect, useState } from "react"; + +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; + +const timeFormatter = new Intl.DateTimeFormat(undefined, { + hour: "numeric", + minute: "2-digit", +}); + +type ActionBarProps = ComponentProps<"div"> & { + readonly createdAt?: number; + readonly text: string; +}; + +export function ActionBar({ className, createdAt, text, ...props }: ActionBarProps) { + const [isCopied, setCopied] = useState(false); + + useEffect(() => { + if (!isCopied) return; + const timeout = window.setTimeout(() => setCopied(false), 1_200); + return () => window.clearTimeout(timeout); + }, [isCopied]); + + async function copy(): Promise { + await navigator.clipboard.writeText(text); + setCopied(true); + } + + return ( +
+ {createdAt !== undefined && ( + + )} + +
+ ); +} diff --git a/components/ai/composer.tsx b/components/ai/composer.tsx new file mode 100644 index 0000000..af3031f --- /dev/null +++ b/components/ai/composer.tsx @@ -0,0 +1,113 @@ +import { ArrowUp, Square } from "lucide-react"; +import { type KeyboardEvent, type SubmitEvent, useEffect, useRef } from "react"; + +import { Button } from "@/components/ui/button"; +import ChatVoiceInput from "@/lib/chat-voice-input"; +import { useComposerStore } from "@/lib/composer-store"; + +type ComposerProps = { + readonly disabled: boolean; + readonly isGenerating: boolean; + readonly onSend: (message: string) => void; + readonly onStop?: () => void; +}; + +type TextInputProps = { + readonly disabled: boolean; + readonly onValueChange: (value: string) => void; + readonly value: string; +}; + +type SubmitButtonProps = Pick; + +function handleKeyDown(event: KeyboardEvent): void { + if (event.key !== "Enter" || event.shiftKey || event.nativeEvent.isComposing) return; + + event.preventDefault(); + event.currentTarget.form?.requestSubmit(); +} + +function TextInput({ disabled, onValueChange, value }: TextInputProps) { + const textareaRef = useRef(null); + + useEffect(() => { + if (!disabled) textareaRef.current?.focus(); + }, [disabled]); + + return ( + <> + +