mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-13 13:17:40 +02:00
[eric] ui: send-block banner is plain not red, compact button actually invalidates the stale estimate, chip tooltips show filename not temp path
This commit is contained in:
@@ -50,6 +50,8 @@ This app ships to non-developers. Anything a user sees has to read like a person
|
||||
- **Friendly without being cute.** Conversational, not chirpy. "Want me to shrink it down to a summary?" not "Whoops! That file is huge!".
|
||||
- **Minimalist by default. Less is more.** One short message, one subtle animation, one verb. Do NOT add rotating progress messages, multi-line status text, percentage counters, or step-by-step explainers unless the user explicitly needs them. A pulsing dot + "Shrinking" beats a 4-message carousel + spinner + progress bar every time. The user knows what they clicked; we just need to confirm we're alive.
|
||||
- **Animations are subtle.** Pulse, fade, soft scale (≤1.0× to 0.6×). No bounce, no flashing, no harsh blinking, no rotating spinners with multiple emoji. Easing: `ease-in-out`. Duration: 1-1.5s for ambient states (loading), 150-250ms for state changes (hover, mode flip).
|
||||
- **Don't expose absolute filesystem paths to users.** Tooltips, file chips, and labels should show only the file's basename (e.g. `llama2.pdf`), never the temp-dir path (`/var/folders/s7/.../self-swarm-uploads/llama2.pdf`). Users don't care where their file landed in temp, and a 200-char tooltip dangling over the chat input is ugly. If the user genuinely needs the path, expose it via a "copy path" action, not a hover tooltip.
|
||||
- **State changes from one button must invalidate downstream estimates.** If a button claims to shrink/clear/reset something the next user action depends on, you have to invalidate the cached estimate too. Example: clicking "Compact memory" calls `/compact` server-side, but the renderer's `tokens.input` was a snapshot from the previous round-trip — leaving it stale makes the next send re-fire the same "over context window" banner, looking like the button did nothing. Always pair an action with the redux update that its UX promise implies.
|
||||
|
||||
## Pitfalls
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ export const AttachmentChips: React.FC<Props> = ({
|
||||
return (
|
||||
<Tooltip
|
||||
key={`${cp.path}-${idx}`}
|
||||
title={copiedPathIdx === idx ? 'Copied!' : cp.path}
|
||||
title={copiedPathIdx === idx ? 'Copied!' : (cp.path.split('/').pop() || cp.path)}
|
||||
arrow
|
||||
placement="top"
|
||||
slotProps={{
|
||||
@@ -120,7 +120,7 @@ export const AttachmentChips: React.FC<Props> = ({
|
||||
sx: {
|
||||
fontFamily: c.font.mono,
|
||||
fontSize: '0.7rem',
|
||||
maxWidth: 420,
|
||||
maxWidth: 280,
|
||||
wordBreak: 'break-all',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -16,17 +16,18 @@ interface Props {
|
||||
}
|
||||
|
||||
export const SendBlockBanner: React.FC<Props> = ({ sendBlock, c, sessionId, setSendBlock, setContextPaths, setModelAnchor }) => {
|
||||
const fmt = (n: number) => n >= 1000 ? `${(n / 1000).toFixed(1)}K` : String(n);
|
||||
const over = sendBlock.estimate - sendBlock.window;
|
||||
const fmt = (n: number) => n >= 1000 ? `${(n / 1000).toFixed(0)}K` : String(n);
|
||||
return (
|
||||
<Box sx={{ mx: 1.5, mt: 1, mb: 0.5, p: 1.25, borderRadius: '10px', border: `1px solid ${c.status.error}`, bgcolor: `${c.status.error}10` }}>
|
||||
<Typography sx={{ fontSize: '0.78rem', fontWeight: 600, color: c.status.error, mb: 0.5 }}>
|
||||
This send would overflow the model's context window
|
||||
<Box sx={{
|
||||
mx: 1.5, mt: 1, mb: 0.5, px: 2, py: 1.5,
|
||||
borderRadius: '12px',
|
||||
bgcolor: c.bg.surface,
|
||||
border: `1px solid ${c.border.medium}`,
|
||||
}}>
|
||||
<Typography sx={{ fontSize: '0.9rem', color: c.text.primary, lineHeight: 1.5, mb: 1 }}>
|
||||
That's a lot to send at once. Pick one:
|
||||
</Typography>
|
||||
<Typography sx={{ fontSize: '0.72rem', color: c.text.secondary, mb: 0.75, fontVariantNumeric: 'tabular-nums' }}>
|
||||
~{fmt(sendBlock.estimate)} of {fmt(sendBlock.window)} tokens ({over > 0 ? `${fmt(over)} over` : 'at cap'}). History {fmt(sendBlock.history)} · Files {fmt(sendBlock.files)} · Tools/MCPs {fmt(sendBlock.framework)} · This message {fmt(sendBlock.prompt)}.
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap' }}>
|
||||
<Box sx={{ display: 'flex', gap: 0.75, flexWrap: 'wrap' }}>
|
||||
{sessionId && (
|
||||
<Box
|
||||
component="button"
|
||||
@@ -40,11 +41,13 @@ export const SendBlockBanner: React.FC<Props> = ({ sendBlock, c, sessionId, setS
|
||||
} catch (err) { console.error(err); }
|
||||
}}
|
||||
sx={{
|
||||
background: c.accent.primary, color: '#fff', border: 'none', borderRadius: '6px',
|
||||
px: 1, py: 0.5, fontSize: '0.72rem', cursor: 'pointer', '&:hover': { opacity: 0.9 },
|
||||
bgcolor: c.accent.primary, color: '#fff', border: 'none', borderRadius: '6px',
|
||||
px: 1.5, py: 0.7, fontSize: '0.82rem', fontWeight: 500, cursor: 'pointer',
|
||||
transition: 'background 0.15s ease',
|
||||
'&:hover': { bgcolor: c.accent.hover },
|
||||
}}
|
||||
>
|
||||
Compact memory
|
||||
Shrink history
|
||||
</Box>
|
||||
)}
|
||||
{sendBlock.largestFile && (
|
||||
@@ -56,21 +59,25 @@ export const SendBlockBanner: React.FC<Props> = ({ sendBlock, c, sessionId, setS
|
||||
setSendBlock(null);
|
||||
}}
|
||||
sx={{
|
||||
background: 'transparent', color: c.text.primary, border: `1px solid ${c.border.subtle}`,
|
||||
borderRadius: '6px', px: 1, py: 0.5, fontSize: '0.72rem', cursor: 'pointer',
|
||||
'&:hover': { background: c.bg.secondary },
|
||||
bgcolor: 'transparent', color: c.text.secondary,
|
||||
border: `1px solid ${c.border.medium}`, borderRadius: '6px',
|
||||
px: 1.5, py: 0.7, fontSize: '0.82rem', cursor: 'pointer',
|
||||
transition: 'background 0.15s ease, color 0.15s ease',
|
||||
'&:hover': { bgcolor: c.bg.secondary, color: c.text.primary },
|
||||
}}
|
||||
>
|
||||
Detach largest file (~{fmt(sendBlock.largestFile.tokens)})
|
||||
Remove biggest file
|
||||
</Box>
|
||||
)}
|
||||
<Box
|
||||
component="button"
|
||||
onClick={(e) => { setModelAnchor(e.currentTarget as HTMLElement); setSendBlock(null); }}
|
||||
sx={{
|
||||
background: 'transparent', color: c.text.primary, border: `1px solid ${c.border.subtle}`,
|
||||
borderRadius: '6px', px: 1, py: 0.5, fontSize: '0.72rem', cursor: 'pointer',
|
||||
'&:hover': { background: c.bg.secondary },
|
||||
bgcolor: 'transparent', color: c.text.secondary,
|
||||
border: `1px solid ${c.border.medium}`, borderRadius: '6px',
|
||||
px: 1.5, py: 0.7, fontSize: '0.82rem', cursor: 'pointer',
|
||||
transition: 'background 0.15s ease, color 0.15s ease',
|
||||
'&:hover': { bgcolor: c.bg.secondary, color: c.text.primary },
|
||||
}}
|
||||
>
|
||||
Switch model
|
||||
@@ -79,9 +86,10 @@ export const SendBlockBanner: React.FC<Props> = ({ sendBlock, c, sessionId, setS
|
||||
component="button"
|
||||
onClick={() => setSendBlock(null)}
|
||||
sx={{
|
||||
background: 'transparent', color: c.text.muted, border: 'none',
|
||||
borderRadius: '6px', px: 1, py: 0.5, fontSize: '0.72rem', cursor: 'pointer',
|
||||
'&:hover': { background: c.bg.secondary },
|
||||
bgcolor: 'transparent', color: c.text.muted, border: 'none',
|
||||
borderRadius: '6px', px: 1.5, py: 0.7, fontSize: '0.82rem', cursor: 'pointer',
|
||||
transition: 'color 0.15s ease',
|
||||
'&:hover': { color: c.text.secondary },
|
||||
}}
|
||||
>
|
||||
Dismiss
|
||||
|
||||
@@ -744,6 +744,14 @@ const agentsSlice = createSlice({
|
||||
const session = state.sessions[action.payload.sessionId];
|
||||
if (!session) return;
|
||||
session.compacted_through_msg_id = action.payload.throughMsgId;
|
||||
// The next preflight check looks at tokens.input to estimate "history used".
|
||||
// After a compaction the real number is unknown until the next turn round-trips,
|
||||
// but the OLD number is definitely wrong (it counts messages we just dropped).
|
||||
// Zero it so preflight falls back to the char/4 estimate of remaining content,
|
||||
// which is closer than holding the stale pre-compact value.
|
||||
if (action.payload.throughMsgId) {
|
||||
session.tokens = { input: 0, output: session.tokens?.output ?? 0 };
|
||||
}
|
||||
},
|
||||
|
||||
// Aux-LLM turn label; pill renderer prefers this over the static "Thinking..." verb.
|
||||
|
||||
Reference in New Issue
Block a user