diff --git a/frontend/src/app/pages/Dashboard/canvas/TetherLayer.tsx b/frontend/src/app/pages/Dashboard/canvas/TetherLayer.tsx index aa72a56e..10321ce8 100644 --- a/frontend/src/app/pages/Dashboard/canvas/TetherLayer.tsx +++ b/frontend/src/app/pages/Dashboard/canvas/TetherLayer.tsx @@ -70,17 +70,16 @@ const TetherLayer: React.FC = ({ tethers, c }) => { markerEnd="url(#tether-arrow)" /> {t.label && ( + // Same glass capsule language as the narrator pills; the old white accent-bordered box read as a stray form element. = ({ tethers, c }) => { fontSize={11} fontWeight={600} fontFamily="inherit" - fill={c.accent.primary} + fill="rgba(255,255,255,0.92)" > {t.label} diff --git a/frontend/src/app/pages/Dashboard/desktop/AgentNarratorPill.tsx b/frontend/src/app/pages/Dashboard/desktop/AgentNarratorPill.tsx index 416b8187..d8e8200f 100644 --- a/frontend/src/app/pages/Dashboard/desktop/AgentNarratorPill.tsx +++ b/frontend/src/app/pages/Dashboard/desktop/AgentNarratorPill.tsx @@ -40,10 +40,14 @@ function AgentNarratorPill({ label, running, todos, liveSteps, artifact, askPair // 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; + // A plan/progress widget the agent posted once goes STALE while work continues; live steps outrank + // it until the turn ends. Answer-shaped widgets (weather, tables) still win the ladder. + const staleplan = running && artifact && /plan|progress/i.test(artifactName(artifact)) && visibleSteps.length > 0; + const shownArtifact = staleplan ? null : artifact; 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' : visibleSteps.length > 0 ? 'steps' : running ? 'thinking' : finalText ? 'final' : 'none'; + const artifactKey = liveAsk ? `ask-${liveAsk.id}` : shownArtifact ? 'widget' : browserShot ? 'shot' : visibleTodos.length > 0 ? 'todos' : visibleSteps.length > 0 ? 'steps' : running ? 'thinking' : finalText ? 'final' : 'none'; return ( - ) : artifact ? ( - - + ) : shownArtifact ? ( + + ) : browserShot ? ( e.stopPropagation()} onClick={(e: React.MouseEvent) => e.stopPropagation()} onDoubleClick={(e: React.MouseEvent) => e.stopPropagation()} diff --git a/frontend/src/app/pages/Dashboard/desktop/agentLiveSteps.ts b/frontend/src/app/pages/Dashboard/desktop/agentLiveSteps.ts index bb41c928..f975bdba 100644 --- a/frontend/src/app/pages/Dashboard/desktop/agentLiveSteps.ts +++ b/frontend/src/app/pages/Dashboard/desktop/agentLiveSteps.ts @@ -23,7 +23,9 @@ export function extractLiveSteps(messages: Array<{ role: string; content: unknow 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); + // MCP names arrive prefixed ("web__WebSearch"); the checklist speaks the bare action's language. + const bare = tool.replace(/^.*__/, ''); + const lbl = getToolLabelWithInput(bare, 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]; diff --git a/frontend/src/shared/state/sessionDisplay.ts b/frontend/src/shared/state/sessionDisplay.ts index c68c5af3..3b264916 100644 --- a/frontend/src/shared/state/sessionDisplay.ts +++ b/frontend/src/shared/state/sessionDisplay.ts @@ -35,6 +35,8 @@ export function displayChatTitle(session: AgentSession | null | undefined): stri if (session.name && !isLegacyAutoName(session.name)) { return session.name; } + // A spawned sub-agent's first message is its parent's raw instruction; titling the card with it reads as a leak. + if (session.parent_session_id) return 'Sub-agent'; const firstUserMsg = session.messages?.find((m) => m.role === 'user'); const firstUserContent = firstUserMsg && typeof firstUserMsg.content === 'string' ? firstUserMsg.content