mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
[eric] shell: uncrop the spawn-pill shadow, shortcut tooltip on the composer, always-visible glass title bubble (incl. fullscreen), and collapsed cards show a plain-text answer
This commit is contained in:
@@ -419,7 +419,8 @@ const DashboardToolbar = React.forwardRef<HTMLDivElement, Props>(
|
||||
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,
|
||||
}}
|
||||
|
||||
@@ -96,9 +96,9 @@ const DashboardOverlays: React.FC<DashboardOverlaysProps> = ({
|
||||
}) => {
|
||||
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 && (
|
||||
<Box sx={{ position: 'absolute', bottom: 16, left: '50%', transform: 'translateX(-50%)', zIndex: 10 }}>
|
||||
<Box sx={{ position: 'absolute', bottom: 30, left: '50%', transform: 'translateX(-50%)', zIndex: 10 }}>
|
||||
<DashboardToolbar
|
||||
ref={toolbarRef}
|
||||
inputOpen={toolbarOpen}
|
||||
|
||||
@@ -49,6 +49,7 @@ import { QuestionForm } from '@/app/pages/AgentChat/shell/ApprovalBar';
|
||||
import AgentChat from '@/app/pages/AgentChat/AgentChat';
|
||||
import { parseMcpToolName, getMcpShortAction } from '@/shared/mcpToolMeta';
|
||||
import { useClaudeTokens, DarkTokensScope } from '@/shared/styles/ThemeContext';
|
||||
import { GLASS_SURFACE, GLASS_SURFACE_BLUR, GLASS_SURFACE_TEXT } from '@/shared/styles/glassSurface';
|
||||
import { useDashboardActive } from '@/shared/hooks/useDashboardActive';
|
||||
import { useOverlayScrollPassthrough } from '../hooks/interaction/useOverlayScrollPassthrough';
|
||||
import { useStreamingMessage } from '@/shared/state/streamingSlice';
|
||||
@@ -712,6 +713,20 @@ const AgentCard: React.FC<Props> = ({
|
||||
[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<Props> = ({
|
||||
askPair={pillAskPair}
|
||||
sessionId={session.id}
|
||||
browserShot={browserShot}
|
||||
finalText={pillFinalText}
|
||||
selected={isSelected}
|
||||
highlighted={isHighlighted}
|
||||
/>
|
||||
@@ -1068,7 +1084,7 @@ const AgentCard: React.FC<Props> = ({
|
||||
...(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<Props> = ({
|
||||
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<Props> = ({
|
||||
'& 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<Props> = ({
|
||||
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<Props> = ({
|
||||
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 }),
|
||||
}}
|
||||
>
|
||||
<InlineEditableTitle
|
||||
value={displayChatTitle(session)}
|
||||
onCommit={(name) => 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' }}
|
||||
>
|
||||
<Typewriter
|
||||
value={displayChatTitle(session)}
|
||||
enabled={!!session.name && !isLegacyAutoName(session.name)}
|
||||
>
|
||||
{(t) => (
|
||||
<Typography sx={{ color: c.text.primary, fontWeight: 600, fontSize: '1rem', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
<Typography sx={{ color: titleColor, fontWeight: 600, fontSize: '1rem', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{t}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
@@ -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 (
|
||||
<Box
|
||||
@@ -175,6 +178,36 @@ function AgentNarratorPill({ label, running, todos, artifact, askPair, sessionId
|
||||
Thinking...
|
||||
</Typography>
|
||||
</Box>
|
||||
) : finalText ? (
|
||||
// Last resort, and the common one: a turn that just talked back. Without this a collapsed card answered "hi" with silence.
|
||||
<Box
|
||||
key={artifactKey}
|
||||
className="osw-artifact"
|
||||
sx={{
|
||||
maxWidth: 320,
|
||||
px: 1.5,
|
||||
py: 0.875,
|
||||
borderRadius: '14px',
|
||||
background: GLASS,
|
||||
backdropFilter: GLASS_BLUR,
|
||||
WebkitBackdropFilter: GLASS_BLUR,
|
||||
boxShadow: '0 6px 20px rgba(0,0,0,0.3)',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '0.75rem',
|
||||
color: 'rgba(255,255,255,0.82)',
|
||||
lineHeight: 1.45,
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 4,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{finalText}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : null}
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -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<HTMLDivElement | null>(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}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '0.8125rem',
|
||||
color: 'rgba(255,255,255,0.55)',
|
||||
fontWeight: 400,
|
||||
userSelect: 'none',
|
||||
mr: 1.5,
|
||||
}}
|
||||
>
|
||||
Ask me anything...
|
||||
</Typography>
|
||||
{/* Tooltip sits on the placeholder, not the whole pill, so it can't double up with the + and mic tooltips. */}
|
||||
<Tooltip title={`Ask me anything (${comboDisplay(newAgentShortcut || 'Meta+l')})`} placement="top" arrow>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '0.8125rem',
|
||||
color: 'rgba(255,255,255,0.55)',
|
||||
fontWeight: 400,
|
||||
userSelect: 'none',
|
||||
mr: 1.5,
|
||||
}}
|
||||
>
|
||||
Ask me anything...
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
<Box
|
||||
role="button"
|
||||
aria-label="Add to canvas"
|
||||
|
||||
@@ -15,6 +15,7 @@ import ArrowBackRoundedIcon from '@mui/icons-material/ArrowBackRounded';
|
||||
import ArrowUpwardRoundedIcon from '@mui/icons-material/ArrowUpwardRounded';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import { createDraftSession, launchAndSendFirstMessage, type AgentConfig } from '@/shared/state/agentsSlice';
|
||||
import { addBrowserCard } from '@/shared/state/dashboardLayoutSlice';
|
||||
import { getLastDashboardId } from '@/shared/lastDashboardId';
|
||||
import { API_BASE } from '@/shared/config';
|
||||
|
||||
@@ -198,7 +199,7 @@ const HelpPanel: React.FC<{ onClose: () => void }> = ({ onClose }) => {
|
||||
</Box>
|
||||
<ChevronRightRoundedIcon sx={{ fontSize: 16, color: 'rgba(255,255,255,0.4)' }} />
|
||||
</Box>
|
||||
<Box component="button" onClick={() => { openExternal(DISCORD_URL); onClose(); }} sx={rowSx}>
|
||||
<Box component="button" onClick={() => { dispatch(addBrowserCard({ url: DISCORD_URL })); onClose(); }} sx={rowSx}>
|
||||
<ForumOutlinedIcon sx={iconSx} />
|
||||
<Box sx={{ flex: 1, minWidth: 0 }}>
|
||||
<Typography sx={{ fontSize: '0.8125rem', fontWeight: 500 }}>Talk to the team</Typography>
|
||||
@@ -206,7 +207,7 @@ const HelpPanel: React.FC<{ onClose: () => void }> = ({ onClose }) => {
|
||||
</Box>
|
||||
<ArrowOutwardRoundedIcon sx={{ fontSize: 14, color: 'rgba(255,255,255,0.4)' }} />
|
||||
</Box>
|
||||
<Box component="button" onClick={() => { openExternal(DOCS_URL); onClose(); }} sx={rowSx}>
|
||||
<Box component="button" onClick={() => { dispatch(addBrowserCard({ url: DOCS_URL })); onClose(); }} sx={rowSx}>
|
||||
<MenuBookOutlinedIcon sx={iconSx} />
|
||||
<Typography sx={{ flex: 1, fontSize: '0.8125rem', fontWeight: 500 }}>Docs & shortcuts</Typography>
|
||||
<ArrowOutwardRoundedIcon sx={{ fontSize: 14, color: 'rgba(255,255,255,0.4)' }} />
|
||||
|
||||
@@ -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)';
|
||||
Reference in New Issue
Block a user