diff --git a/src/App.stories.tsx b/src/App.stories.tsx index 94ea4a81c1..8c7aff4fb5 100644 --- a/src/App.stories.tsx +++ b/src/App.stories.tsx @@ -584,7 +584,7 @@ export const ActiveWorkspaceWithChat: Story = { metadata: { historySequence: 2, timestamp: STABLE_TIMESTAMP - 290000, - model: "claude-sonnet-4-20250514", + model: "anthropic:claude-sonnet-4-5", usage: { inputTokens: 1250, outputTokens: 450, @@ -634,7 +634,7 @@ export const ActiveWorkspaceWithChat: Story = { metadata: { historySequence: 4, timestamp: STABLE_TIMESTAMP - 270000, - model: "claude-sonnet-4-20250514", + model: "anthropic:claude-sonnet-4-5", usage: { inputTokens: 2100, outputTokens: 680, @@ -657,7 +657,7 @@ export const ActiveWorkspaceWithChat: Story = { metadata: { historySequence: 5, timestamp: STABLE_TIMESTAMP - 260000, - model: "claude-sonnet-4-20250514", + model: "anthropic:claude-sonnet-4-5", usage: { inputTokens: 1800, outputTokens: 520, @@ -709,7 +709,7 @@ export const ActiveWorkspaceWithChat: Story = { metadata: { historySequence: 7, timestamp: STABLE_TIMESTAMP - 230000, - model: "claude-sonnet-4-20250514", + model: "anthropic:claude-sonnet-4-5", usage: { inputTokens: 2800, outputTokens: 420, @@ -769,7 +769,7 @@ export const ActiveWorkspaceWithChat: Story = { metadata: { historySequence: 9, timestamp: STABLE_TIMESTAMP - 170000, - model: "claude-sonnet-4-20250514", + model: "anthropic:claude-sonnet-4-5", usage: { inputTokens: 3500, outputTokens: 520, @@ -810,7 +810,7 @@ export const ActiveWorkspaceWithChat: Story = { metadata: { historySequence: 10, timestamp: STABLE_TIMESTAMP - 160000, - model: "claude-sonnet-4-20250514", + model: "anthropic:claude-sonnet-4-5", usage: { inputTokens: 800, outputTokens: 150, @@ -820,12 +820,60 @@ export const ActiveWorkspaceWithChat: Story = { }, }); + // User follow-up asking about documentation + callback({ + id: "msg-11", + role: "user", + parts: [ + { + type: "text", + text: "Should we add documentation for the authentication changes?", + }, + ], + metadata: { + historySequence: 11, + timestamp: STABLE_TIMESTAMP - 150000, + }, + }); + // Mark as caught up callback({ type: "caught-up" }); + + // Now start streaming assistant response with reasoning + callback({ + type: "stream-start", + workspaceId: workspaceId, + messageId: "msg-12", + model: "anthropic:claude-sonnet-4-5", + historySequence: 12, + }); + + // Send reasoning delta + callback({ + type: "reasoning-delta", + workspaceId: workspaceId, + messageId: "msg-12", + delta: + "The user is asking about documentation. This is important because the authentication changes introduce a breaking change for API clients. They'll need to know how to include JWT tokens in their requests. I should suggest adding both inline code comments and updating the API documentation to explain the new authentication requirements, including examples of how to obtain and use tokens.", + tokens: 65, + timestamp: STABLE_TIMESTAMP - 140000, + }); }, 100); + // Keep sending reasoning deltas to maintain streaming state + const intervalId = setInterval(() => { + callback({ + type: "reasoning-delta", + workspaceId: workspaceId, + messageId: "msg-12", + delta: ".", + tokens: 1, + timestamp: NOW, + }); + }, 2000); + return () => { - // Cleanup + clearInterval(intervalId); }; } else if (wsId === streamingWorkspaceId) { // Streaming workspace - show active work in progress @@ -860,7 +908,7 @@ export const ActiveWorkspaceWithChat: Story = { metadata: { historySequence: 0, timestamp: now - 5000, // 5 seconds ago - model: "claude-sonnet-4-20250514", + model: "anthropic:claude-sonnet-4-5", usage: { inputTokens: 200, outputTokens: 50, @@ -896,7 +944,7 @@ export const ActiveWorkspaceWithChat: Story = { type: "stream-start", workspaceId: streamingWorkspaceId, messageId: "stream-msg-2", - model: "claude-sonnet-4-20250514", + model: "anthropic:claude-sonnet-4-5", historySequence: 2, }); @@ -1221,7 +1269,7 @@ These tables should render cleanly without any disruptive copy or download actio metadata: { historySequence: 2, timestamp: STABLE_TIMESTAMP + 1000, - model: "claude-sonnet-4-20250514", + model: "anthropic:claude-sonnet-4-5", usage: { inputTokens: 100, outputTokens: 500, diff --git a/src/components/AIView.tsx b/src/components/AIView.tsx index 37870af8d1..f86a356a93 100644 --- a/src/components/AIView.tsx +++ b/src/components/AIView.tsx @@ -374,94 +374,96 @@ const AIViewInner: React.FC = ({ tabIndex={0} className="h-full overflow-y-auto p-[15px] leading-[1.5] break-words whitespace-pre-wrap" > - {mergedMessages.length === 0 ? ( -
-

No Messages Yet

-

Send a message below to begin

-

- đź’ˇ Tip: Add a{" "} - - .cmux/init - {" "} - hook to your project to run setup commands -
- (e.g., install dependencies, build) when creating new workspaces -

-
- ) : ( - <> - {mergedMessages.map((msg) => { - const isAtCutoff = - editCutoffHistoryId !== undefined && - msg.type !== "history-hidden" && - msg.type !== "workspace-init" && - msg.historyId === editCutoffHistoryId; - - return ( - -
- -
- {isAtCutoff && ( +
+ {mergedMessages.length === 0 ? ( +
+

No Messages Yet

+

Send a message below to begin

+

+ đź’ˇ Tip: Add a{" "} + + .cmux/init + {" "} + hook to your project to run setup commands +
+ (e.g., install dependencies, build) when creating new workspaces +

+
+ ) : ( + <> + {mergedMessages.map((msg) => { + const isAtCutoff = + editCutoffHistoryId !== undefined && + msg.type !== "history-hidden" && + msg.type !== "workspace-init" && + msg.historyId === editCutoffHistoryId; + + return ( +
- ⚠️ Messages below this line will be removed when you submit the edit +
- )} - {shouldShowInterruptedBarrier(msg) && } -
- ); - })} - {/* Show RetryBarrier after the last message if needed */} - {showRetryBarrier && } - - )} - - {canInterrupt && ( - - )} + {isAtCutoff && ( +
+ ⚠️ Messages below this line will be removed when you submit the edit +
+ )} + {shouldShowInterruptedBarrier(msg) && } + + ); + })} + {/* Show RetryBarrier after the last message if needed */} + {showRetryBarrier && } + + )} + + {canInterrupt && ( + + )} +
{!autoScroll && ( )} - = (props) => { className="bg-separator border-border-light relative flex flex-col gap-1 border-t px-[15px] pt-[5px] pb-[15px]" data-component="ChatInputSection" > - {/* Creation error toast */} - {variant === "creation" && creationState?.error && ( -
- {creationState.error} -
- )} - - {/* Workspace toast */} - {variant === "workspace" && } - - {/* Command suggestions - workspace only */} - {variant === "workspace" && ( - setShowCommandSuggestions(false)} - isVisible={showCommandSuggestions} - ariaLabel="Slash command suggestions" - listId={commandListId} - /> - )} - -
- 0 ? commandListId : undefined - } - aria-expanded={showCommandSuggestions && commandSuggestions.length > 0} - /> -
- - {/* Image attachments - workspace only */} - {variant === "workspace" && ( - - )} - -
- {/* Editing indicator - workspace only */} - {variant === "workspace" && editingMessage && ( -
- Editing message ({formatKeybind(KEYBINDS.CANCEL_EDIT)} to cancel) +
+ {/* Creation error toast */} + {variant === "creation" && creationState?.error && ( +
+ {creationState.error}
)} -
- {/* Model Selector - always visible */} -
- inputRef.current?.focus()} - /> - - ? - - Click to edit or use{" "} - {formatKeybind(KEYBINDS.OPEN_MODEL_SELECTOR)} -
-
- Abbreviations: -
• /model opus - Claude Opus 4.1 -
• /model sonnet - Claude Sonnet 4.5 -
-
- Full format: -
- /model provider:model-name -
- (e.g., /model anthropic:claude-sonnet-4-5) -
-
-
+ {/* Workspace toast */} + {variant === "workspace" && ( + + )} - {/* Thinking Slider - slider hidden on narrow containers, label always clickable */} -
- -
+ {/* Command suggestions - workspace only */} + {variant === "workspace" && ( + setShowCommandSuggestions(false)} + isVisible={showCommandSuggestions} + ariaLabel="Slash command suggestions" + listId={commandListId} + /> + )} - {/* Context 1M Checkbox - always visible */} -
- -
+
+ 0 ? commandListId : undefined + } + aria-expanded={showCommandSuggestions && commandSuggestions.length > 0} + /> +
- {preferredModel && ( -
- - Calculating tokens… -
- } - > - - + {/* Image attachments - workspace only */} + {variant === "workspace" && ( + + )} + +
+ {/* Editing indicator - workspace only */} + {variant === "workspace" && editingMessage && ( +
+ Editing message ({formatKeybind(KEYBINDS.CANCEL_EDIT)} to cancel)
)} - -
+
+ {/* Model Selector - always visible */} +
+ inputRef.current?.focus()} + /> + + ? + + Click to edit or use{" "} + {formatKeybind(KEYBINDS.OPEN_MODEL_SELECTOR)} +
+
+ Abbreviations: +
• /model opus - Claude Opus 4.1 +
• /model sonnet - Claude Sonnet 4.5 +
+
+ Full format: +
+ /model provider:model-name +
+ (e.g., /model anthropic:claude-sonnet-4-5) +
+
+
- {/* Creation controls - second row for creation variant */} - {variant === "creation" && ( - - )} + {/* Thinking Slider - slider hidden on narrow containers, label always clickable */} +
+ +
+ + {/* Context 1M Checkbox - always visible */} +
+ +
+ + {preferredModel && ( +
+ + Calculating tokens… +
+ } + > + + +
+ )} + + +
+ + {/* Creation controls - second row for creation variant */} + {variant === "creation" && ( + + )} +
diff --git a/src/components/Messages/AssistantMessage.tsx b/src/components/Messages/AssistantMessage.tsx index b60d1a137c..07ca5a42f1 100644 --- a/src/components/Messages/AssistantMessage.tsx +++ b/src/components/Messages/AssistantMessage.tsx @@ -1,17 +1,17 @@ -import React, { useState } from "react"; +import { COMPACTED_EMOJI } from "@/constants/ui"; +import { useCopyToClipboard } from "@/hooks/useCopyToClipboard"; +import { useStartHere } from "@/hooks/useStartHere"; import type { DisplayedMessage } from "@/types/message"; +import { copyToClipboard } from "@/utils/clipboard"; +import { Clipboard, ClipboardCheck, FileText, ListStart } from "lucide-react"; +import React, { useState } from "react"; +import { CompactingMessageContent } from "./CompactingMessageContent"; +import { CompactionBackground } from "./CompactionBackground"; import { MarkdownRenderer } from "./MarkdownRenderer"; -import { TypewriterMarkdown } from "./TypewriterMarkdown"; import type { ButtonConfig } from "./MessageWindow"; import { MessageWindow } from "./MessageWindow"; -import { useStartHere } from "@/hooks/useStartHere"; -import { useCopyToClipboard } from "@/hooks/useCopyToClipboard"; -import { COMPACTED_EMOJI } from "@/constants/ui"; import { ModelDisplay } from "./ModelDisplay"; -import { CompactingMessageContent } from "./CompactingMessageContent"; -import { CompactionBackground } from "./CompactionBackground"; -import type { KebabMenuItem } from "@/components/KebabMenu"; -import { copyToClipboard } from "@/utils/clipboard"; +import { TypewriterMarkdown } from "./TypewriterMarkdown"; interface AssistantMessageProps { message: DisplayedMessage & { type: "assistant" }; @@ -52,42 +52,33 @@ export const AssistantMessage: React.FC = ({ ? [] : [ { - label: copied ? "âś“ Copied" : "Copy", + label: copied ? "Copied" : "Copy", onClick: () => void copyToClipboard(content), + icon: copied ? : , }, ]; - // Kebab menu items (less frequently used actions) - const kebabMenuItems: KebabMenuItem[] = isStreaming - ? [] - : [ - // Add Start Here button if workspaceId is available and message is not already compacted - ...(workspaceId && !isCompacted - ? [ - { - label: buttonLabel, - onClick: openModal, - disabled: startHereDisabled, - tooltip: "Replace all chat history with this message", - }, - ] - : []), - { - label: showRaw ? "Show Markdown" : "Show Text", - onClick: () => setShowRaw(!showRaw), - active: showRaw, - }, - ]; + if (!isStreaming) { + buttons.push({ + label: buttonLabel, + onClick: openModal, + disabled: startHereDisabled, + tooltip: "Replace all chat history with this message", + icon: , + }); + buttons.push({ + label: showRaw ? "Show Markdown" : "Show Text", + onClick: () => setShowRaw(!showRaw), + active: showRaw, + icon: , + }); + } // Render appropriate content based on state const renderContent = () => { // Empty streaming state if (isStreaming && !content) { - return ( -
- Waiting for response... -
- ); + return
Waiting for response...
; } // Streaming text gets typewriter effect @@ -135,10 +126,9 @@ export const AssistantMessage: React.FC = ({ <> : undefined} > diff --git a/src/components/Messages/MessageWindow.tsx b/src/components/Messages/MessageWindow.tsx index e2e0586299..388d0744f0 100644 --- a/src/components/Messages/MessageWindow.tsx +++ b/src/components/Messages/MessageWindow.tsx @@ -1,27 +1,26 @@ -import type { ReactNode } from "react"; -import React, { useState, useMemo } from "react"; -import type { MuxMessage, DisplayedMessage } from "@/types/message"; -import { HeaderButton } from "../tools/shared/ToolPrimitives"; +import { cn } from "@/lib/utils"; +import type { DisplayedMessage, MuxMessage } from "@/types/message"; import { formatTimestamp } from "@/utils/ui/dateTime"; -import { TooltipWrapper, Tooltip } from "../Tooltip"; -import { KebabMenu, type KebabMenuItem } from "../KebabMenu"; +import { Code2Icon } from "lucide-react"; +import type { ReactNode } from "react"; +import React, { useMemo, useState } from "react"; +import { Tooltip, TooltipWrapper } from "../Tooltip"; +import { Button } from "../ui/button"; export interface ButtonConfig { label: string; onClick: () => void; + icon?: ReactNode; active?: boolean; disabled?: boolean; - emoji?: string; // Optional emoji that shows only on hover tooltip?: string; // Optional tooltip text } interface MessageWindowProps { label: ReactNode; - borderColor: string; - backgroundColor?: string; + variant?: "assistant" | "user"; message: MuxMessage | DisplayedMessage; buttons?: ButtonConfig[]; - kebabMenuItems?: KebabMenuItem[]; // Optional kebab menu items (provide empty array to use kebab with only Show JSON) children: ReactNode; className?: string; rightLabel?: ReactNode; @@ -30,13 +29,10 @@ interface MessageWindowProps { export const MessageWindow: React.FC = ({ label, - borderColor, - backgroundColor, + variant = "assistant", message, buttons = [], - kebabMenuItems = [], children, - className, rightLabel, backgroundEffect, }) => { @@ -52,113 +48,117 @@ export const MessageWindow: React.FC = ({ [timestamp] ); + const isLastPartOfMessage = useMemo(() => { + if ("isLastPartOfMessage" in message && message.isLastPartOfMessage && !message.isPartial) { + return true; + } + return false; + }, [message]); + + // We do not want to display these on every message, otherwise it spams the UI + // with buttons and timestamps + const showMetaRow = useMemo(() => { + return variant === "user" || isLastPartOfMessage; + }, [variant, isLastPartOfMessage]); + return (
- {backgroundEffect}
-
-
- {label} + {backgroundEffect} +
+
+ {showJson ? ( +
+                {JSON.stringify(message, null, 2)}
+              
+ ) : ( + children + )}
- {formattedTimestamp && ( - - {formattedTimestamp} - - )}
-
- {rightLabel} - {buttons.map((button, index) => - button.tooltip ? ( - - - {button.tooltip} - - ) : ( - - ) +
+ {showMetaRow && ( +
setShowJson(!showJson), - }, - ]} + data-message-meta + > +
+ {buttons.map((button, index) => ( + + ))} + , + active: showJson, + onClick: () => setShowJson(!showJson), + tooltip: showJson ? "Hide raw JSON" : "Show raw JSON", + }} /> - ) : ( - setShowJson(!showJson)}> - {showJson ? "Hide JSON" : "Show JSON"} - - )} +
+
+ {rightLabel} + {label && ( +
+ {label} +
+ )} + {formattedTimestamp && {formattedTimestamp}} +
-
-
- {showJson ? ( -
-            {JSON.stringify(message, null, 2)}
-          
- ) : ( - children - )} -
+ )}
); }; -// Button component that shows emoji only on hover -interface ButtonWithHoverEmojiProps { +interface IconActionButtonProps { button: ButtonConfig; - active?: boolean; - disabled?: boolean; } -const ButtonWithHoverEmoji: React.FC = ({ - button, - active, - disabled, -}) => { - const [isHovered, setIsHovered] = useState(false); - - return ( - = ({ button }) => { + const content = ( + ); + + if (button.tooltip || button.label) { + return ( + + {content} + {button.tooltip ?? button.label} + + ); + } + + return content; }; diff --git a/src/components/Messages/ModelDisplay.tsx b/src/components/Messages/ModelDisplay.tsx index c91ef90f41..86162e7e02 100644 --- a/src/components/Messages/ModelDisplay.tsx +++ b/src/components/Messages/ModelDisplay.tsx @@ -47,7 +47,7 @@ export const ModelDisplay: React.FC = ({ modelString, showToo {providerIcon} )} - {displayName} + {displayName} ); diff --git a/src/components/Messages/ReasoningMessage.tsx b/src/components/Messages/ReasoningMessage.tsx index f4b791a423..350edf0194 100644 --- a/src/components/Messages/ReasoningMessage.tsx +++ b/src/components/Messages/ReasoningMessage.tsx @@ -3,6 +3,8 @@ import type { DisplayedMessage } from "@/types/message"; import { MarkdownRenderer } from "./MarkdownRenderer"; import { TypewriterMarkdown } from "./TypewriterMarkdown"; import { cn } from "@/lib/utils"; +import { Shimmer } from "../ai-elements/shimmer"; +import { Lightbulb } from "lucide-react"; interface ReasoningMessageProps { message: DisplayedMessage & { type: "reasoning" }; @@ -47,17 +49,28 @@ export const ReasoningMessage: React.FC = ({ message, cla return (
-
- đź’­ - Thinking +
+ + + + + {isStreaming ? ( + Thinking... + ) : ( + "Thought..." + )} +
{!isStreaming && ( = ({ label: "Edit", onClick: handleEdit, disabled: isCompacting, + icon: , tooltip: isCompacting ? `Cannot edit while compacting (${formatKeybind(vimEnabled ? KEYBINDS.INTERRUPT_STREAM_VIM : KEYBINDS.INTERRUPT_STREAM_NORMAL)} to cancel)` : undefined, @@ -67,25 +68,21 @@ export const UserMessage: React.FC = ({ ] : []), { - label: copied ? "âś“ Copied" : "Copy", + label: copied ? "Copied" : "Copy", onClick: () => void copyToClipboard(content), + icon: copied ? : , }, ]; - // Currently no additional kebab items for user messages - // MessageWindow will add "Show JSON" to kebab menu automatically if kebabMenuItems is provided - const kebabMenuItems: KebabMenuItem[] = []; - // If it's a local command output, render with TerminalOutput if (isLocalCommandOutput) { return ( @@ -95,26 +92,25 @@ export const UserMessage: React.FC = ({ // Otherwise, render as normal user message return ( {content && ( -
+        
           {content}
         
)} {message.imageParts && message.imageParts.length > 0 && ( -
+
{message.imageParts.map((img, idx) => ( {`Attachment ))}
diff --git a/src/components/tools/shared/ToolPrimitives.tsx b/src/components/tools/shared/ToolPrimitives.tsx index 7276cb9ee1..995ea59fc9 100644 --- a/src/components/tools/shared/ToolPrimitives.tsx +++ b/src/components/tools/shared/ToolPrimitives.tsx @@ -13,7 +13,7 @@ interface ToolContainerProps extends React.HTMLAttributes { export const ToolContainer: React.FC = ({ expanded, className, ...props }) => (
{ } const transcript = page.getByRole("log", { name: "Conversation transcript" }); - const thinkingHeader = transcript.getByText("Thinking"); + const thinkingHeader = transcript.getByText("Thought..."); await expect(thinkingHeader).toBeVisible(); await thinkingHeader.click(); await expect(