mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-07 02:07:45 +02:00
[eric] canvas: collapsed cards show live tool steps as a checklist, the transition phase between Thinking and the answer
This commit is contained in:
@@ -40,6 +40,7 @@ import AgentNarratorPill from '../desktop/AgentNarratorPill';
|
||||
import { openCardContextMenu, isNativeMenuTarget } from '../desktop/openCardContextMenu';
|
||||
import { agentCardMenuRows } from './agentCardMenuRows';
|
||||
import { extractLatestTodos } from '../desktop/agentTodos';
|
||||
import { extractLiveSteps } from '../desktop/agentLiveSteps';
|
||||
import { extractLatestShowUi, extractPendingAskUi, freezeIfDone } from '@/app/pages/AgentChat/tool-ui/showUiPayload';
|
||||
import { useDragEndBackstops } from '../hooks/interaction/useDragEndBackstops';
|
||||
import { useBrowserPillShot } from '../desktop/useBrowserPillShot';
|
||||
@@ -693,6 +694,10 @@ const AgentCard: React.FC<Props> = ({
|
||||
// Desktop-shell narrator pill: a collapsed card with nothing to ask renders as the minimal pill
|
||||
// (live turn label + plan checklist); approvals and drafts keep the full card so their UI has a home.
|
||||
const todos = useMemo(() => extractLatestTodos(session.messages || []), [session.messages]);
|
||||
const liveSteps = useMemo(
|
||||
() => (session.status === 'running' ? extractLiveSteps(session.messages || []) : null),
|
||||
[session.messages, session.status],
|
||||
);
|
||||
const pillArtifact = useMemo(() => {
|
||||
const artifact = extractLatestShowUi(session.messages || []);
|
||||
return artifact ? freezeIfDone(artifact, session.status === 'running') : null;
|
||||
@@ -1032,6 +1037,7 @@ const AgentCard: React.FC<Props> = ({
|
||||
label={pillLabel}
|
||||
running={pillRunning}
|
||||
todos={todos}
|
||||
liveSteps={liveSteps}
|
||||
artifact={pillArtifact}
|
||||
askPair={pillAskPair}
|
||||
sessionId={session.id}
|
||||
|
||||
@@ -10,11 +10,15 @@ import PillArtifactFrame from './PillArtifactFrame';
|
||||
import type { ToolPair } from '@/app/pages/AgentChat/tool-bubbles/ToolCallBubble';
|
||||
import { artifactName, type ShowUiPayload } from '@/app/pages/AgentChat/tool-ui/showUiPayload';
|
||||
import type { AgentTodoItem } from './agentTodos';
|
||||
import type { AgentLiveStep } from './agentLiveSteps';
|
||||
import { shimmerTextSx } from '@/app/pages/AgentChat/tool-bubbles/toolRowMotion';
|
||||
|
||||
interface AgentNarratorPillProps {
|
||||
label: string;
|
||||
running: boolean;
|
||||
todos: AgentTodoItem[] | null;
|
||||
/** Tool activity of the live turn, the transition phase between "Thinking" and the answer. */
|
||||
liveSteps: AgentLiveStep[] | null;
|
||||
artifact: ShowUiPayload | null;
|
||||
askPair?: ToolPair | null;
|
||||
sessionId?: string;
|
||||
@@ -29,14 +33,17 @@ 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, finalText, selected, highlighted }: AgentNarratorPillProps): React.ReactElement {
|
||||
/** Collapsed agent as the desktop narrator pill; below it, the best artifact wins: live question > widget > browser shot > plan > live steps > Thinking. */
|
||||
function AgentNarratorPill({ label, running, todos, liveSteps, 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;
|
||||
// Live tool steps window to the most recent, since earlier ones are history, not plan.
|
||||
const visibleSteps = running && !visibleTodos.length ? (liveSteps || []).slice(-MAX_VISIBLE_TODOS) : [];
|
||||
const earlierSteps = running && !visibleTodos.length ? Math.max(0, (liveSteps?.length || 0) - visibleSteps.length) : 0;
|
||||
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' : finalText ? 'final' : 'none';
|
||||
const artifactKey = liveAsk ? `ask-${liveAsk.id}` : artifact ? 'widget' : browserShot ? 'shot' : visibleTodos.length > 0 ? 'todos' : visibleSteps.length > 0 ? 'steps' : running ? 'thinking' : finalText ? 'final' : 'none';
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -159,6 +166,66 @@ function AgentNarratorPill({ label, running, todos, artifact, askPair, sessionId
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
) : visibleSteps.length > 0 ? (
|
||||
// The transition phase: real tool activity as a simple checklist while the turn works.
|
||||
<Box
|
||||
key={artifactKey}
|
||||
className="osw-artifact"
|
||||
sx={{
|
||||
borderRadius: '16px',
|
||||
background: GLASS,
|
||||
backdropFilter: GLASS_BLUR,
|
||||
WebkitBackdropFilter: GLASS_BLUR,
|
||||
boxShadow: '0 8px 24px rgba(0,0,0,0.32)',
|
||||
px: 1.75,
|
||||
py: 1.5,
|
||||
minWidth: 200,
|
||||
}}
|
||||
>
|
||||
{earlierSteps > 0 && (
|
||||
<Typography sx={{ fontSize: '0.75rem', color: 'rgba(255,255,255,0.4)', pl: '2px', pb: 0.5 }}>
|
||||
{earlierSteps} earlier step{earlierSteps === 1 ? '' : 's'}
|
||||
</Typography>
|
||||
)}
|
||||
<Box sx={{ position: 'relative' }}>
|
||||
{visibleSteps.length > 1 && (
|
||||
<Box sx={{ position: 'absolute', left: 10, top: 12, bottom: 12, width: '2px', background: 'rgba(255,255,255,0.18)' }} />
|
||||
)}
|
||||
{visibleSteps.map((step, i) => (
|
||||
<Box key={`${i}-${step.label.slice(0, 24)}`} sx={{ display: 'flex', alignItems: 'center', gap: 1.25, py: 0.75 }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: 22,
|
||||
height: 22,
|
||||
borderRadius: '50%',
|
||||
flexShrink: 0,
|
||||
zIndex: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: step.done ? 'rgba(255,255,255,0.85)' : 'rgba(255,255,255,0.28)',
|
||||
}}
|
||||
>
|
||||
{step.done && <CheckIcon sx={{ fontSize: 14, color: '#2a2a2a' }} />}
|
||||
</Box>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '0.8125rem',
|
||||
fontWeight: 500,
|
||||
color: 'rgba(255,255,255,0.92)',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
maxWidth: 260,
|
||||
...(step.done ? {} : shimmerTextSx('rgba(255,255,255,0.92)')),
|
||||
}}
|
||||
>
|
||||
{step.label}
|
||||
</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
) : running ? (
|
||||
<Box
|
||||
key={artifactKey}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { getToolLabelWithInput } from '@/app/pages/AgentChat/parsing/toolLabels';
|
||||
|
||||
export interface AgentLiveStep {
|
||||
label: string;
|
||||
done: boolean;
|
||||
}
|
||||
|
||||
// UI/meta tools are not "steps" a bystander cares about; the checklist is real work only.
|
||||
const HIDDEN_TOOLS = /(^|__)(ShowUI|AskUI|AskUserQuestion|TodoWrite|MCPSearch|MCPActivate)$/i;
|
||||
|
||||
/** The collapsed card's transition phase when the agent made no TodoWrite plan: the current turn's
|
||||
tool activity as a simple checklist, done steps checked, the live one last. */
|
||||
export function extractLiveSteps(messages: Array<{ role: string; content: unknown; hidden?: boolean }>): AgentLiveStep[] | null {
|
||||
let start = 0;
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
if (messages[i].role === 'user' && !messages[i].hidden) { start = i + 1; break; }
|
||||
}
|
||||
const steps: AgentLiveStep[] = [];
|
||||
for (let i = start; i < messages.length; i++) {
|
||||
const m = messages[i];
|
||||
if (m.role !== 'tool_call') continue;
|
||||
const body = (typeof m.content === 'object' && m.content !== null ? m.content : {}) as { tool?: unknown; input?: unknown };
|
||||
const tool = String(body.tool || '');
|
||||
if (!tool || HIDDEN_TOOLS.test(tool)) continue;
|
||||
const done = messages[i + 1]?.role === 'tool_result';
|
||||
const lbl = getToolLabelWithInput(tool, body.input, (m as { id?: string }).id);
|
||||
const label = done ? lbl.past : lbl.present;
|
||||
// Consecutive same-verb steps merge so "Read a file" x8 doesn't fill the card.
|
||||
const prev = steps[steps.length - 1];
|
||||
if (prev && prev.label === label) { prev.done = prev.done && done; continue; }
|
||||
steps.push({ label, done });
|
||||
}
|
||||
return steps.length ? steps : null;
|
||||
}
|
||||
Reference in New Issue
Block a user