Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Changed

- **One badge for every status and message role in the dashboard**
(`actionagent`). The agent cards' status pill and provider chip, the run
and sandbox session status pills (Agent Interactions, the sandbox runner)
and the message role pills in conversation streams all render through the
shared `Badge` (soft tint, strong mono text, radius 4) with one status
tone map (`statusTone`) and one role map (`roleBadge`), so an `active`
agent, a `complete` run and an `Assistant` turn look the same wherever
they appear and follow the dark theme, which the cards' Tailwind pills
did not. Two tones join the tokens for the system and developer roles.

## [1.9.3] - 2026-10-21

Releases `activeagent` and `actionagent` 1.9.3, with the matching browser
Expand Down
2 changes: 1 addition & 1 deletion actionagent/app/assets/builds/action_agent.css

Large diffs are not rendered by default.

120 changes: 60 additions & 60 deletions actionagent/app/assets/builds/action_agent.js

Large diffs are not rendered by default.

20 changes: 3 additions & 17 deletions actionagent/frontend/components/dashboard/AgentInteractions.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useState, useEffect, useRef } from 'react';
import { Badge, statusTone } from './primitives';
import AgentAvatar from '../AgentAvatar';
import { useTimeWindow } from '../../contexts/TimeWindowContext';
import TimeWindowSelector from './TimeWindowSelector';
Expand Down Expand Up @@ -252,17 +253,6 @@ export default function AgentInteractions({ agent, onBack }) {
return { cohorts, totals };
};

const getStatusColor = (status) => {
switch (status) {
case 'complete': return 'bg-green-100 text-green-700';
case 'failed': return 'bg-red-100 text-red-700';
case 'running': return 'bg-blue-100 text-blue-700';
case 'pending': return 'bg-yellow-100 text-yellow-700';
case 'cancelled': return 'bg-gray-100 text-gray-500';
default: return 'bg-gray-100 text-gray-500';
}
};

const formatDuration = (ms) => {
if (!ms) return '-';
if (ms < 1000) return `${ms}ms`;
Expand Down Expand Up @@ -406,9 +396,7 @@ export default function AgentInteractions({ agent, onBack }) {
}`}
>
<div className="flex items-center justify-between mb-2 gap-2">
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ${getStatusColor(execution.status)}`}>
{execution.status}
</span>
<Badge tone={statusTone(execution.status)}>{execution.status}</Badge>
<div className="flex items-center gap-2">
{/* The only real difference between these rows. */}
<span className={`px-1.5 py-0.5 rounded text-[10px] uppercase tracking-wide ${
Expand Down Expand Up @@ -534,9 +522,7 @@ export default function AgentInteractions({ agent, onBack }) {
</button>
</div>
{detailMode === 'run' && selectedRun && (
<span className={`px-3 py-1 rounded-full text-sm font-medium ${getStatusColor(selectedRun.status)}`}>
{selectedRun.status}
</span>
<Badge tone={statusTone(selectedRun.status)} size={12}>{selectedRun.status}</Badge>
)}
</div>

Expand Down
26 changes: 3 additions & 23 deletions actionagent/frontend/components/dashboard/AgentList.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useState } from 'react';
import AgentAvatar from '../AgentAvatar';
import AgentStatCard, { rateTone } from './AgentStatCard';
import { Badge, statusTone } from './primitives';
import { fmtPasses, fmtPercent } from '../../utils/evalFormat.mjs';

// Mirrors Api::AgentsController::LIST_SORTS. Ordering is applied server-side
Expand Down Expand Up @@ -53,15 +54,6 @@ export default function AgentList({
return date.toLocaleDateString();
};

const getStatusColor = (status) => {
switch (status) {
case 'active': return 'bg-green-100 text-green-700';
case 'draft': return 'bg-yellow-100 text-yellow-700';
case 'archived': return 'bg-gray-100 text-gray-500';
default: return 'bg-gray-100 text-gray-500';
}
};

const formatDuration = (ms) => {
if (ms == null) return '—';
if (ms < 1000) return `${Math.round(ms)}ms`;
Expand Down Expand Up @@ -193,11 +185,7 @@ export default function AgentList({
key={agent.id}
name={agent.name}
subtitle={agent.description || 'No description'}
badge={
<span className={`px-2 py-0.5 text-xs font-medium rounded-full ${getStatusColor(agent.status)}`}>
{agent.status}
</span>
}
badge={<Badge tone={statusTone(agent.status)}>{agent.status}</Badge>}
onClick={() => onSelect(agent)}
stats={[
{ label: `Runs ${stats.window_days || 30}d`, value: stats.runs ?? 0, title: runsTitle },
Expand Down Expand Up @@ -228,15 +216,7 @@ export default function AgentList({
footer={
<>
<span style={{ display: 'flex', alignItems: 'center', gap: '6px', minWidth: 0 }}>
<span style={{
padding: '2px 6px',
borderRadius: '4px',
background: 'rgba(128,128,128,0.15)',
whiteSpace: 'nowrap',
flexShrink: 0,
}}>
{agent.provider}
</span>
<Badge tone="muted" style={{ flexShrink: 0 }}>{agent.provider}</Badge>
{/* Model ids run long (meta-llama/llama-3.3-70b-instruct);
truncate rather than wrap the card to three lines. */}
<span
Expand Down
43 changes: 10 additions & 33 deletions actionagent/frontend/components/dashboard/InteractionStream.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useState } from 'react';
import { Badge, roleBadge, TONE } from './primitives';
import Markdown from './Markdown';
import GenerativeUI from './GenerativeUI';
import { ToolDetails } from './ToolRoster';
Expand Down Expand Up @@ -198,33 +199,13 @@ export const streamPreStyle = (darkMode) => ({
margin: 0,
});

export const roleBubble = (role, darkMode) => {
switch (role) {
case 'user':
return darkMode
? { background: 'rgba(59,130,246,0.15)', color: '#93c5fd', label: 'User' }
: { background: '#eff6ff', color: '#1d4ed8', label: 'User' };
case 'assistant':
return darkMode
? { background: 'rgba(239,68,68,0.12)', color: '#fca5a5', label: 'Assistant' }
: { background: '#fef2f2', color: '#b91c1c', label: 'Assistant' };
case 'tool':
return darkMode
? { background: 'rgba(245,158,11,0.12)', color: '#fcd34d', label: 'Tool' }
: { background: '#fffbeb', color: '#b45309', label: 'Tool' };
case 'system':
return darkMode
? { background: 'rgba(139,92,246,0.15)', color: '#c4b5fd', label: 'System' }
: { background: '#f5f3ff', color: '#6d28d9', label: 'System' };
case 'developer':
return darkMode
? { background: 'rgba(20,184,166,0.15)', color: '#5eead4', label: 'Dev' }
: { background: '#f0fdfa', color: '#0f766e', label: 'Dev' };
default:
return darkMode
? { background: 'rgba(255,255,255,0.08)', color: 'rgba(255,255,255,0.7)', label: role }
: { background: '#f3f4f6', color: '#374151', label: role };
}
// The role's badge colors as the shared tones resolve them (the theme is in
// the tokens, so `darkMode` no longer picks a palette; it stays in the
// signature for the callers that pass it).
export const roleBubble = (role, _darkMode) => {
const { tone, label } = roleBadge(role);
const t = TONE[tone] || TONE.muted;
return { background: t.soft, color: t.text, label, tone };
};

// Files a user message carried, as chips: image thumbnails from the
Expand Down Expand Up @@ -438,13 +419,9 @@ export default function InteractionStream({ messages, darkMode, tools, onUiActio
>
<span className="flex flex-col gap-1 flex-shrink-0 mt-0.5" style={{ minWidth: '72px' }}>
{(mergedCall ? [assistantTone, toolTone] : [bubble]).map((tone) => (
<span
key={tone.label}
className="px-2 py-0.5 rounded text-xs font-medium"
style={{ background: tone.background, color: tone.color, textAlign: 'center' }}
>
<Badge key={tone.label} tone={tone.tone} style={{ justifyContent: 'center' }}>
{tone.label}
</span>
</Badge>
))}
</span>
<div className="min-w-0 flex-1">
Expand Down
28 changes: 5 additions & 23 deletions actionagent/frontend/components/dashboard/SandboxRunner.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { Badge, statusTone } from './primitives';
import AgentAvatar from '../AgentAvatar';
import { useActionCable } from '../../hooks/useActionCable';
import { startCheckout } from '../../utils/checkout';
Expand Down Expand Up @@ -445,17 +446,6 @@ export default function SandboxRunner({ initialType = 'playwright_mcp', onClose,
return `${mins}:${secs.toString().padStart(2, '0')}`;
};

const getStatusColor = (status) => {
switch (status) {
case 'completed': return 'text-green-600 bg-green-100';
case 'failed': return 'text-red-600 bg-red-100';
case 'running': return 'text-blue-600 bg-blue-100';
case 'ready': return 'text-emerald-600 bg-emerald-100';
case 'expired': return 'text-orange-600 bg-orange-100';
default: return 'text-gray-600 bg-gray-100';
}
};

const canRun = session?.status === 'ready' &&
session?.runs_count < session?.max_runs &&
!isRunning;
Expand Down Expand Up @@ -501,9 +491,7 @@ export default function SandboxRunner({ initialType = 'playwright_mcp', onClose,
{/* Session Status */}
{session && (
<div className="flex items-center space-x-3 text-sm">
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusColor(session.status)}`}>
{session.status}
</span>
<Badge tone={statusTone(session.status)}>{session.status}</Badge>
<span className="text-gray-500">
{session.runs_count}/{session.max_runs} runs
</span>
Expand Down Expand Up @@ -578,9 +566,7 @@ export default function SandboxRunner({ initialType = 'playwright_mcp', onClose,
{comparisonMode && Object.keys(comparisonResults).length > 0 ? 'Comparison Results' : 'Output'}
</h3>
{currentRun && !comparisonMode && (
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ${getStatusColor(currentRun.status)}`}>
{currentRun.status}
</span>
<Badge tone={statusTone(currentRun.status)}>{currentRun.status}</Badge>
)}
{comparisonMode && runningProviders.length > 0 && (
<span className="text-xs text-blue-600">
Expand Down Expand Up @@ -615,9 +601,7 @@ export default function SandboxRunner({ initialType = 'playwright_mcp', onClose,
</div>
{result && (
<div className="flex items-center space-x-2">
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ${getStatusColor(result.status)}`}>
{result.status}
</span>
<Badge tone={statusTone(result.status)}>{result.status}</Badge>
{result.tokens && (
<span className="text-xs text-gray-500">{result.tokens} tokens</span>
)}
Expand Down Expand Up @@ -834,9 +818,7 @@ export default function SandboxRunner({ initialType = 'playwright_mcp', onClose,
onClick={() => setCurrentRun(run)}
>
<div className="flex items-center justify-between mb-1">
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ${getStatusColor(run.status || 'completed')}`}>
{run.status || 'completed'}
</span>
<Badge tone={statusTone(run.status || 'completed')}>{run.status || 'completed'}</Badge>
<span className="text-xs text-gray-400">
{run.tokens || 0} tokens
</span>
Expand Down
25 changes: 25 additions & 0 deletions actionagent/frontend/components/dashboard/primitives.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,33 @@ export const TONE = {
info: { strong: 'var(--color-info)', soft: 'var(--color-info-soft)', text: 'var(--color-info-text)' },
muted: { strong: 'var(--color-text-muted)', soft: 'var(--color-muted)', text: 'var(--color-text-secondary)' },
accent: { strong: 'var(--color-accent-ui)', soft: 'var(--color-accent-ui-tint)', text: 'var(--color-accent-ui)' },
system: { strong: 'var(--color-system)', soft: 'var(--color-system-soft)', text: 'var(--color-system-text)' },
dev: { strong: 'var(--color-dev)', soft: 'var(--color-dev-soft)', text: 'var(--color-dev-text)' },
};

// The tone a record's status badge takes, the same on an agent card, a
// run, a sandbox session or an evaluation: done is success, waiting is
// warning, in flight is info, broken is error, and the rest is muted.
const STATUS_TONES = {
active: 'success', complete: 'success', completed: 'success', ready: 'success', passed: 'success',
draft: 'warning', pending: 'warning', queued: 'warning', waiting: 'warning',
running: 'info', in_progress: 'info',
failed: 'error', error: 'error', expired: 'error',
archived: 'muted', cancelled: 'muted', canceled: 'muted', skipped: 'muted',
};
export const statusTone = (status) => STATUS_TONES[String(status || '').toLowerCase()] || 'muted';

// The tone and label of a message role's badge, wherever a conversation
// renders: the stream, a run's activity feed, a session preview.
export const ROLE_BADGES = {
user: { tone: 'info', label: 'User' },
assistant: { tone: 'accent', label: 'Assistant' },
tool: { tone: 'warning', label: 'Tool' },
system: { tone: 'system', label: 'System' },
developer: { tone: 'dev', label: 'Dev' },
};
export const roleBadge = (role) => ROLE_BADGES[role] || { tone: 'muted', label: role };

// The TUI glyph set. `chevron` rotates 90° when `open`.
export const GLYPH = {
object: '=',
Expand Down
Loading
Loading