This commit is contained in:
seb
2026-10-20 23:22:13 +02:00
parent 4f367ee943
commit 70c7d5eff2
2 changed files with 287 additions and 223 deletions

View File

@@ -719,7 +719,7 @@ function UsageChipView({ usage, isSummary = false }) {
fontSize: '0.68rem',
fontFamily: 'monospace',
color: isSummary ? 'text.primary' : 'text.secondary',
fontWeight: isSummary ? 600 : 400,
fontWeight: isSummary ? 400 : 400,
bgcolor: isSummary ? 'action.selected' : 'action.hover',
border: isSummary ? '1px solid' : 'none',
borderColor: isSummary ? 'divider' : 'transparent',
@@ -777,86 +777,159 @@ function UsageIconView({ usage }) {
);
}
export function TurnRow({ turn, isLive = false }) {
if (!turn) return null;
function turnHasIcons(turn) {
if (!turn) return false;
const hasReasonings = Boolean(
turn.reasonings &&
turn.reasonings.some((r) => Boolean(r.text && r.text.trim()))
);
const hasTools = Boolean(turn.tools && turn.tools.length > 0);
const hasContent = Boolean(turn.content);
const hasUsage = Boolean(turn.usage);
const hasContent = Boolean(turn.content);
const showHandshake = !hasReasonings && !hasTools && !hasContent && !hasUsage;
const showStatusLine = showHandshake || hasReasonings || hasTools || hasUsage;
return showHandshake || hasReasonings || hasTools || hasUsage;
}
export function TurnIcons({ turn }) {
if (!turn) return null;
const hasReasonings = Boolean(
turn.reasonings &&
turn.reasonings.some((r) => Boolean(r.text && r.text.trim()))
);
const hasTools = Boolean(turn.tools && turn.tools.length > 0);
const hasUsage = Boolean(turn.usage);
const hasContent = Boolean(turn.content);
const showHandshake = !hasReasonings && !hasTools && !hasContent && !hasUsage;
const showStatus = showHandshake || hasReasonings || hasTools || hasUsage;
if (!showStatus) return null;
return (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
px: 2,
py: 0.4,
gap: 0.75,
maxWidth: '100%',
display: 'inline-flex',
alignItems: 'center',
gap: 0.5,
}}
>
{/* Turn status line: left of the chat */}
{showStatusLine ? (
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
flexWrap: 'wrap',
minHeight: 24,
lineHeight: 1,
}}
>
{showHandshake ? <HandshakeIconView state={turn.handshake} /> : null}
{showHandshake ? <HandshakeIconView state={turn.handshake} /> : null}
{turn.reasonings?.map((r, i) => (
<BrainIconView key={r.id || i} reasoning={r} />
))}
{turn.reasonings?.map((r, i) => (
<BrainIconView key={r.id || i} reasoning={r} />
))}
{turn.tools?.map((t, i) => (
<WorkIconView key={t.id || i} tool={t} />
))}
{turn.tools?.map((t, i) => (
<WorkIconView key={t.id || i} tool={t} />
))}
{hasUsage ? (
<UsageIconView usage={turn.usage} />
) : null}
</Box>
) : null}
{/* Assistant message content for this turn (if any) */}
{hasContent ? (
<Box
sx={(theme) => ({
maxWidth: '85%',
px: 2,
py: 1.25,
borderRadius: '16px 16px 16px 4px',
bgcolor:
theme.palette.mode === 'dark'
? 'background.paper'
: SURFACE_RAISED,
color: theme.palette.text.primary,
boxShadow: SHADOW_BUBBLE_ASSISTANT,
border: `1px solid ${theme.palette.divider}`,
wordBreak: 'break-word',
fontSize: '0.875rem',
lineHeight: 1.5,
})}
>
<HealingMarkdown text={turn.content} streaming={isLive && !turn.sealed} />
</Box>
{hasUsage ? (
<UsageIconView usage={turn.usage} />
) : null}
</Box>
);
}
export function TurnsView({ turns = [], isLive = false }) {
if (!Array.isArray(turns) || turns.length === 0) return null;
// Segment turns into chunks where icons precede content
const segments = [];
let pendingTurns = [];
for (let i = 0; i < turns.length; i++) {
const turn = turns[i];
pendingTurns.push(turn);
if (turn.content) {
segments.push({
statusTurns: [...pendingTurns],
contentTurn: turn,
});
pendingTurns = [];
}
}
if (pendingTurns.length > 0) {
segments.push({
statusTurns: pendingTurns,
contentTurn: null,
});
}
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.75, width: '100%' }}>
{segments.map((seg, sIdx) => {
const hasAnyIcons = seg.statusTurns.some(turnHasIcons);
return (
<Box
key={sIdx}
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
px: 2,
py: 0.4,
gap: 0.75,
maxWidth: '100%',
}}
>
{/* All turn icons in ONE line */}
{hasAnyIcons ? (
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
flexWrap: 'wrap',
minHeight: 24,
lineHeight: 1,
}}
>
{seg.statusTurns.map((t, tIdx) => (
<TurnIcons key={t.id || tIdx} turn={t} />
))}
</Box>
) : null}
{/* Assistant message content for this segment (if any) */}
{seg.contentTurn?.content ? (
<Box
sx={(theme) => ({
maxWidth: '85%',
px: 2,
py: 1.25,
borderRadius: '16px 16px 16px 4px',
bgcolor:
theme.palette.mode === 'dark'
? 'background.paper'
: SURFACE_RAISED,
color: theme.palette.text.primary,
boxShadow: SHADOW_BUBBLE_ASSISTANT,
border: `1px solid ${theme.palette.divider}`,
wordBreak: 'break-word',
fontSize: '0.875rem',
lineHeight: 1.5,
})}
>
<HealingMarkdown
text={seg.contentTurn.content}
streaming={isLive && !seg.contentTurn.sealed}
/>
</Box>
) : null}
</Box>
);
})}
</Box>
);
}
export function TurnRow({ turn, isLive = false }) {
if (!turn) return null;
return <TurnsView turns={[turn]} isLive={isLive} />;
}
export function normalizeChatMessages(rawMessages = []) {
if (!Array.isArray(rawMessages) || rawMessages.length === 0) return [];
@@ -1063,9 +1136,7 @@ function MessageRow({ msg, showTime = true }) {
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.25 }}>
{msg.turns.map((t, idx) => (
<TurnRow key={t.id || idx} turn={t} isLive={false} />
))}
<TurnsView turns={msg.turns} isLive={false} />
{summaryUsage ? (
<Box sx={{ display: 'flex', alignItems: 'center', px: 2, py: 0.4 }}>
<UsageChipView
@@ -1309,9 +1380,7 @@ export default class ChatFlowList extends PureComponent {
{/* Live streaming turns */}
{activeTurns && activeTurns.length > 0 ? (
<>
{activeTurns.map((turn, idx) => (
<TurnRow key={turn.id || idx} turn={turn} isLive={true} />
))}
<TurnsView turns={activeTurns} isLive={true} />
{(() => {
const activeSummary = aggregateUsage(activeTurns);
if (!activeSummary) return null;