diff --git a/frontend/src/app/pages/Dashboard/DashboardToolbar.tsx b/frontend/src/app/pages/Dashboard/DashboardToolbar.tsx index ba36c4de..5f392a20 100644 --- a/frontend/src/app/pages/Dashboard/DashboardToolbar.tsx +++ b/frontend/src/app/pages/Dashboard/DashboardToolbar.tsx @@ -419,7 +419,8 @@ const DashboardToolbar = React.forwardRef( boxShadow: historyOpen || !isExpanded ? 'none' : '0 12px 32px rgba(0,0,0,0.4)', padding: isExpanded ? '6px' : '0px', userSelect: 'none' as const, - overflow: inputOpen || newAgentBounce || historyOpen ? 'visible' : 'hidden', + // Only the view picker needs clipping (it scrolls its own list); anything else clipped the collapsed spawn pill's drop shadow flat against its own border box. + overflow: viewPickerOpen ? 'hidden' : 'visible', // historyOpen: width owned by SchedulePopover; leave undefined so framer-motion measures intrinsic size. width: viewPickerOpen ? 580 : historyOpen ? undefined : isExpanded ? 540 : undefined, }} diff --git a/frontend/src/app/pages/Dashboard/canvas/DashboardOverlays.tsx b/frontend/src/app/pages/Dashboard/canvas/DashboardOverlays.tsx index d4978714..7c3c39b5 100644 --- a/frontend/src/app/pages/Dashboard/canvas/DashboardOverlays.tsx +++ b/frontend/src/app/pages/Dashboard/canvas/DashboardOverlays.tsx @@ -96,9 +96,9 @@ const DashboardOverlays: React.FC = ({ }) => { return ( <> - {/* Floating bottom toolbar (all floating chrome steps aside while anything is fullscreen) */} + {/* Floating bottom toolbar (all floating chrome steps aside while anything is fullscreen). Sits at 30 not 16 so the spawn pill's 26px shadow reach clears the canvas root's clip. */} {!anyFullscreen && ( - + = ({ [session.messages, session.status], ); const pillMode = !expanded && !hasPending && !isDraft && !tileZone; + // Glass bubble + fullscreen scrim are both dark in either theme, so the title goes light on them. + const titleColor = expanded ? GLASS_SURFACE_TEXT : c.text.primary; + // The answer a finished turn actually spoke, for pills with no widget/plan to show. Only the last assistant say, never a tool line. + const pillFinalText = useMemo(() => { + if (session.status === 'running') return null; + for (let i = session.messages.length - 1; i >= 0; i--) { + const m = session.messages[i]; + if (m.role === 'user') break; + if (m.role === 'assistant' && typeof m.content === 'string' && m.content.trim()) { + return m.content.trim().slice(0, 400); + } + } + return session.last_message_preview?.trim() || null; + }, [session.messages, session.status, session.last_message_preview]); const pillLabel = session.turn_label?.label || displayChatTitle(session); const pillRunning = session.status === 'running'; @@ -1049,6 +1064,7 @@ const AgentCard: React.FC = ({ askPair={pillAskPair} sessionId={session.id} browserShot={browserShot} + finalText={pillFinalText} selected={isSelected} highlighted={isHighlighted} /> @@ -1068,7 +1084,7 @@ const AgentCard: React.FC = ({ ...(expanded ? tiledStyle ? { - // Fullscreen/tiled: no room above the card, keep the inside hover scrim. + // Fullscreen/tiled: no room above the card, so the scrim rides on top. Never hidden: in fullscreen the title and the lights are the only way out. position: 'absolute', top: 0, left: 0, @@ -1077,9 +1093,7 @@ const AgentCard: React.FC = ({ px: 2, pt: 1.5, pb: 2, - opacity: 0, - transition: 'opacity 0.15s ease', - '&:hover': { opacity: 1 }, + opacity: 1, background: 'linear-gradient(to bottom, rgba(20,12,28,0.92) 0%, rgba(20,12,28,0.65) 60%, rgba(20,12,28,0) 100%)', borderRadius: '12px 12px 0 0', // Header text must read over the dark scrim regardless of app theme. @@ -1087,7 +1101,7 @@ const AgentCard: React.FC = ({ '& input': { color: 'rgba(255,255,255,0.92)' }, } : { - // On the canvas the title + lights pop up ABOVE the card, same as the minimized pill. + // On the canvas the title + lights pop up ABOVE the card, same as the minimized pill. Always visible: a hover-only name means you can't tell your agents apart at a glance. position: 'absolute', bottom: '100%', top: 'auto', @@ -1096,11 +1110,6 @@ const AgentCard: React.FC = ({ zIndex: 17, px: 0.25, pb: 0.75, - opacity: 0, - pointerEvents: 'none', - transition: 'opacity 0.15s ease, transform 0.15s ease', - transform: 'translateY(4px)', - '.osw-card:hover &': { opacity: 1, pointerEvents: 'auto', transform: 'none' }, } : { position: 'relative', @@ -1140,20 +1149,35 @@ const AgentCard: React.FC = ({ display: 'flex', alignItems: 'center', gap: 1, - borderRadius: 1, + // Expanded titles wear the same glass bubble as the collapsed pill; a bare label floating over the canvas read as a stray caption. + ...(expanded && !tiledStyle && { + alignSelf: 'flex-start', + flex: '0 1 auto', + // mr auto or the row's space-between flings the bubble to the far edge, away from the lights. + mr: 'auto', + maxWidth: '100%', + px: 1.25, + py: 0.375, + borderRadius: 999, + background: GLASS_SURFACE, + backdropFilter: GLASS_SURFACE_BLUR, + WebkitBackdropFilter: GLASS_SURFACE_BLUR, + boxShadow: '0 6px 20px rgba(0,0,0,0.3)', + }), + ...(!(expanded && !tiledStyle) && { borderRadius: 1 }), }} > dispatch(renameSession({ sessionId: session.id, name }))} - sx={{ flex: '0 1 auto', minWidth: 0, maxWidth: '100%', color: c.text.primary, fontWeight: 600, fontSize: '1rem' }} + sx={{ flex: '0 1 auto', minWidth: 0, maxWidth: '100%', color: titleColor, fontWeight: 600, fontSize: '1rem' }} > {(t) => ( - + {t} )} diff --git a/frontend/src/app/pages/Dashboard/desktop/AgentNarratorPill.tsx b/frontend/src/app/pages/Dashboard/desktop/AgentNarratorPill.tsx index 4b1fb9ae..5e75319b 100644 --- a/frontend/src/app/pages/Dashboard/desktop/AgentNarratorPill.tsx +++ b/frontend/src/app/pages/Dashboard/desktop/AgentNarratorPill.tsx @@ -3,6 +3,7 @@ import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; import CheckIcon from '@mui/icons-material/Check'; import DashboardGlyph from '../canvas/DashboardGlyph'; +import { GLASS_SURFACE, GLASS_SURFACE_BLUR } from '@/shared/styles/glassSurface'; import ShowUiWidgetView from '@/app/pages/AgentChat/tool-ui/ShowUiWidgetView'; import AskUiBubble from '@/app/pages/AgentChat/tool-ui/AskUiBubble'; import type { ToolPair } from '@/app/pages/AgentChat/tool-bubbles/ToolCallBubble'; @@ -17,22 +18,24 @@ interface AgentNarratorPillProps { askPair?: ToolPair | null; sessionId?: string; browserShot: string | null; + /** The turn's plain-text answer, shown when the turn produced no richer artifact. */ + finalText?: string | null; selected: boolean; highlighted: boolean; } -const GLASS = 'rgba(24,14,32,0.8)'; -const GLASS_BLUR = 'blur(18px) saturate(150%)'; +const GLASS = GLASS_SURFACE; +const GLASS_BLUR = GLASS_SURFACE_BLUR; const MAX_VISIBLE_TODOS = 4; /** Collapsed agent as the desktop narrator pill; below it, the best artifact wins: live question > widget > browser shot > plan > Thinking. */ -function AgentNarratorPill({ label, running, todos, artifact, askPair, sessionId, browserShot, selected, highlighted }: AgentNarratorPillProps): React.ReactElement { +function AgentNarratorPill({ label, running, todos, artifact, askPair, sessionId, browserShot, finalText, selected, highlighted }: AgentNarratorPillProps): React.ReactElement { const visibleTodos = (todos || []).slice(0, MAX_VISIBLE_TODOS); const hiddenCount = (todos?.length || 0) - visibleTodos.length; const ring = selected || highlighted ? { outline: '2px solid #3b82f6', outlineOffset: '2px' } : undefined; const liveAsk = askPair && sessionId ? askPair : null; // One key per ladder state so a state CHANGE remounts the artifact and replays the one-shot entrance; nothing loops. - const artifactKey = liveAsk ? `ask-${liveAsk.id}` : artifact ? 'widget' : browserShot ? 'shot' : visibleTodos.length > 0 ? 'todos' : running ? 'thinking' : 'none'; + const artifactKey = liveAsk ? `ask-${liveAsk.id}` : artifact ? 'widget' : browserShot ? 'shot' : visibleTodos.length > 0 ? 'todos' : running ? 'thinking' : finalText ? 'final' : 'none'; return ( + ) : finalText ? ( + // Last resort, and the common one: a turn that just talked back. Without this a collapsed card answered "hi" with silence. + + + {finalText} + + ) : null} ); diff --git a/frontend/src/app/pages/Dashboard/desktop/DesktopSpawnPill.tsx b/frontend/src/app/pages/Dashboard/desktop/DesktopSpawnPill.tsx index faffeaa4..066cc4da 100644 --- a/frontend/src/app/pages/Dashboard/desktop/DesktopSpawnPill.tsx +++ b/frontend/src/app/pages/Dashboard/desktop/DesktopSpawnPill.tsx @@ -8,6 +8,7 @@ import MicNoneOutlinedIcon from '@mui/icons-material/MicNoneOutlined'; import MicIcon from '@mui/icons-material/Mic'; import CircularProgress from '@mui/material/CircularProgress'; import { useVoice } from '@/shared/voice/voiceContext'; +import { useAppSelector } from '@/shared/hooks'; import GridViewRoundedIcon from '@mui/icons-material/GridViewRounded'; import StickyNote2OutlinedIcon from '@mui/icons-material/StickyNote2Outlined'; import HistoryRoundedIcon from '@mui/icons-material/HistoryRounded'; @@ -41,6 +42,7 @@ function DesktopSpawnPill({ onHistory, }: DesktopSpawnPillProps): React.ReactElement { const [menuOpen, setMenuOpen] = useState(false); + const newAgentShortcut = useAppSelector((s) => s.settings.data.new_agent_shortcut); const rootRef = useRef(null); const { state: voiceState, pct: voicePct, pressStart: voicePressStart, pressEnd: voicePressEnd } = useVoice(); const recording = voiceState === 'recording'; @@ -124,22 +126,26 @@ function DesktopSpawnPill({ background: 'rgba(22,12,34,0.66)', backdropFilter: 'blur(20px) saturate(160%)', WebkitBackdropFilter: 'blur(20px) saturate(160%)', - boxShadow: '0 8px 28px rgba(0,0,0,0.35)', + // Reach (offset + blur) stays under the 30px the canvas leaves below us, or the bottom edge shears the tail off. + boxShadow: '0 6px 20px rgba(0,0,0,0.35)', cursor: 'text', }} onClick={onOpenComposer} > - - Ask me anything... - + {/* Tooltip sits on the placeholder, not the whole pill, so it can't double up with the + and mic tooltips. */} + + + Ask me anything... + + void }> = ({ onClose }) => { - { openExternal(DISCORD_URL); onClose(); }} sx={rowSx}> + { dispatch(addBrowserCard({ url: DISCORD_URL })); onClose(); }} sx={rowSx}> Talk to the team @@ -206,7 +207,7 @@ const HelpPanel: React.FC<{ onClose: () => void }> = ({ onClose }) => { - { openExternal(DOCS_URL); onClose(); }} sx={rowSx}> + { dispatch(addBrowserCard({ url: DOCS_URL })); onClose(); }} sx={rowSx}> Docs & shortcuts diff --git a/frontend/src/shared/styles/glassSurface.ts b/frontend/src/shared/styles/glassSurface.ts new file mode 100644 index 00000000..ba62bf11 --- /dev/null +++ b/frontend/src/shared/styles/glassSurface.ts @@ -0,0 +1,5 @@ +// The shell's floating-chrome glass (narrator pill, card title bubbles). Deliberately dark in BOTH +// themes, so it is a fixed shell surface rather than a theme token: text on it is always light. +export const GLASS_SURFACE = 'rgba(24,14,32,0.8)'; +export const GLASS_SURFACE_BLUR = 'blur(18px) saturate(150%)'; +export const GLASS_SURFACE_TEXT = 'rgba(255,255,255,0.92)';