diff --git a/src/workspaces/officerdev/src/apps/Chat/components/MessageBubble.tsx b/src/workspaces/officerdev/src/apps/Chat/components/MessageBubble.tsx index 4b16e31e..90df7636 100644 --- a/src/workspaces/officerdev/src/apps/Chat/components/MessageBubble.tsx +++ b/src/workspaces/officerdev/src/apps/Chat/components/MessageBubble.tsx @@ -4,6 +4,8 @@ import remarkGfm from 'remark-gfm'; import rehypeRaw from 'rehype-raw'; import rehypeSanitize, { defaultSchema } from 'rehype-sanitize'; import { Volume2, Loader2, Square, Clock, Check, X, CircleSlash } from 'lucide-react'; +import type { Tone } from '@/components/Data'; +import { toneText } from '@/components/Data'; import type { ChatMessage } from '../types'; import { ToolActivity } from './ToolActivity'; import { QuestionActivity } from './QuestionActivity'; @@ -126,11 +128,12 @@ export const MessageBubble = ({ message, onAnswer }: MessageBubbleProps) => { <> {metadata && }
-
+
{message.images?.map((img, i) => ( {img.filename} ))} -
{text}
+ {/* break-words, or a pasted URL or base64 blob pushes the bubble past max-w and out of the pane. */} +
{text}
@@ -174,7 +177,7 @@ export const MessageBubble = ({ message, onAnswer }: MessageBubbleProps) => { case 'result': return (
- + Done · ${message.cost.totalUSD.toFixed(3)} · {message.cost.inputTokens + message.cost.outputTokens} tokens
@@ -186,7 +189,7 @@ export const MessageBubble = ({ message, onAnswer }: MessageBubbleProps) => { case 'error': return (
-
+
{message.text}
@@ -206,24 +209,18 @@ export const MessageBubble = ({ message, onAnswer }: MessageBubbleProps) => { const TaskActivity = ({ message }: { message: Extract }) => { const { status, summary, description, taskType } = message; const Icon = status === 'completed' ? Check : status === 'failed' ? X : status === 'stopped' ? CircleSlash : Clock; - const tone = - status === 'completed' - ? 'text-green-600' - : status === 'failed' - ? 'text-red-600' - : status === 'stopped' - ? 'text-duck-dark/50' - : 'text-amber-500'; + const tone: Tone = + status === 'completed' ? 'success' : status === 'failed' ? 'danger' : status === 'stopped' ? 'neutral' : 'warning'; return ( -
- +
+
-
+
{description || 'Background task'} - {taskType && {taskType}} + {taskType && {taskType}}
-
+
{status ? summary || `Task ${status}.` : 'Running in the background…'}
@@ -231,21 +228,44 @@ const TaskActivity = ({ message }: { message: Extract ( + + {[0, 150, 300].map((delay) => ( + + ))} + +); + type StreamingBubbleProps = { text: string; }; export const StreamingBubble = ({ text }: StreamingBubbleProps) => { - if (!text) return null; return (
-
-
- - {injectImages(text)} - - -
+
+ {text ? ( +
+ + {injectImages(text)} + + +
+ ) : ( + + )}
); diff --git a/src/workspaces/officerdev/src/apps/Chat/components/MessageList.tsx b/src/workspaces/officerdev/src/apps/Chat/components/MessageList.tsx index 1d251be9..6818ce43 100644 --- a/src/workspaces/officerdev/src/apps/Chat/components/MessageList.tsx +++ b/src/workspaces/officerdev/src/apps/Chat/components/MessageList.tsx @@ -101,7 +101,8 @@ export const MessageList = ({ manager }: MessageListProps) => { {showJumpToBottom && ( diff --git a/src/workspaces/officerdev/src/apps/Chat/components/ModelSelector.tsx b/src/workspaces/officerdev/src/apps/Chat/components/ModelSelector.tsx index be5f042d..43f46a09 100644 --- a/src/workspaces/officerdev/src/apps/Chat/components/ModelSelector.tsx +++ b/src/workspaces/officerdev/src/apps/Chat/components/ModelSelector.tsx @@ -90,21 +90,26 @@ export function ModelSelector({ }; return ( -
+
{isLocked ? ( - + // A static label, not an action — `bg-muted` rather than a filled accent. It used to be + // `bg-duck-dark/80 text-white`, and --duck-dark is near-white in dark mode, so this was white + // on white for every session that had already started, i.e. almost always. + {activeProvider ? displayName(activeProvider) : 'Chat'} ) : ( -
+ // Fifteen providers can be configured and the names are long ("Google Vertex AI"), so the strip + // scrolls rather than crushing the model select beside it. +
{providers.map((provider) => (
)} -
+
{availableModels.find((m) => m.id === displayModel)?.reasoning && ( )} -
+
{providerModels.length > 0 ? (