[eric] canvas: tether labels wear the glass capsule, pill widgets go dark, sub-agents stop leaking their prompt as a title, stale plans yield to live steps

This commit is contained in:
ciregenz
2026-08-04 09:42:47 -07:00
parent fcbfc96464
commit 7664376397
5 changed files with 24 additions and 16 deletions
@@ -70,17 +70,16 @@ const TetherLayer: React.FC<TetherLayerProps> = ({ 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.
<g transform={`translate(${t.labelX},${t.labelY})`}>
<rect
x={-4}
y={-14}
width={t.label.length * 7.5 + 8}
height={20}
rx={4}
fill={c.bg.surface}
stroke={c.accent.primary}
strokeWidth={1}
opacity={0.95}
x={-6}
y={-15}
width={t.label.length * 7.5 + 12}
height={22}
rx={11}
fill="rgba(24,14,32,0.85)"
opacity={0.98}
/>
<text
x={t.label.length * 7.5 / 2}
@@ -89,7 +88,7 @@ const TetherLayer: React.FC<TetherLayerProps> = ({ tethers, c }) => {
fontSize={11}
fontWeight={600}
fontFamily="inherit"
fill={c.accent.primary}
fill="rgba(255,255,255,0.92)"
>
{t.label}
</text>
@@ -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 (
<Box
@@ -91,9 +95,9 @@ function AgentNarratorPill({ label, running, todos, liveSteps, artifact, askPair
<PillArtifactFrame key={artifactKey} name="question">
<AskUiBubble pair={liveAsk} sessionId={sessionId!} isPending suppressReveal />
</PillArtifactFrame>
) : artifact ? (
<PillArtifactFrame key={artifactKey} name={artifactName(artifact)}>
<ShowUiWidgetView payload={artifact} ambient />
) : shownArtifact ? (
<PillArtifactFrame key={artifactKey} name={artifactName(shownArtifact)}>
<ShowUiWidgetView payload={shownArtifact} ambient />
</PillArtifactFrame>
) : browserShot ? (
<Box
@@ -80,7 +80,8 @@ function PillArtifactFrame({ name, children }: Props): React.ReactElement {
return (
<Box
className="osw-artifact"
// The "dark" class scopes the vendored tool-ui theme: pill artifacts always sit on the dark glass surface, so a light widget card here read as a white slab.
className="osw-artifact dark"
onPointerDown={(e: React.PointerEvent) => e.stopPropagation()}
onClick={(e: React.MouseEvent) => e.stopPropagation()}
onDoubleClick={(e: React.MouseEvent) => e.stopPropagation()}
@@ -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];
@@ -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